Merge pull request #877 from murrayrm/oep_mhe-26Feb2023 · python-control/python-control@0422c82
1+# optestim_bench.py - benchmarks for optimal/moving horizon estimation
2+# RMM, 14 Mar 2023
3+#
4+# This benchmark tests the timing for the optimal estimation routines and
5+# is intended to be used for helping tune the performance of the functions
6+# used for optimization-based estimation.
7+8+import numpy as np
9+import math
10+import control as ct
11+import control.optimal as opt
12+13+minimizer_table = {
14+'default': (None, {}),
15+'trust': ('trust-constr', {}),
16+'trust_bigstep': ('trust-constr', {'finite_diff_rel_step': 0.01}),
17+'SLSQP': ('SLSQP', {}),
18+'SLSQP_bigstep': ('SLSQP', {'eps': 0.01}),
19+'COBYLA': ('COBYLA', {}),
20+}
21+22+# Table to turn on and off process disturbances and measurement noise
23+noise_table = {
24+'noisy': (1e-1, 1e-3),
25+'nodist': (0, 1e-3),
26+'nomeas': (1e-1, 0),
27+'clean': (0, 0)
28+}
29+30+31+# Assess performance as a function of optimization and integration methods
32+def time_oep_minimizer_methods(minimizer_name, noise_name, initial_guess):
33+# Use fixed system to avoid randome errors (was csys = ct.rss(4, 2, 5))
34+csys = ct.ss(
35+ [[-0.5, 1, 0, 0], [0, -1, 1, 0], [0, 0, -2, 1], [0, 0, 0, -3]], # A
36+ [[0, 0.1], [0, 0.1], [0, 0.1], [1, 0.1]], # B
37+ [[1, 0, 0, 0], [0, 0, 1, 0]], # C
38+0, dt=0)
39+# dsys = ct.c2d(csys, dt)
40+# sys = csys if dt == 0 else dsys
41+sys = csys
42+43+# Decide on process disturbances and measurement noise
44+dist_mag, meas_mag = noise_table[noise_name]
45+46+# Create disturbances and noise (fixed, to avoid random errors)
47+Rv = 0.1 * np.eye(1) # scalar disturbance
48+Rw = 0.01 * np.eye(sys.noutputs)
49+timepts = np.arange(0, 10.1, 1)
50+V = np.array(
51+ [0 if t % 2 == 1 else 1 if t % 4 == 0 else -1 for t in timepts]
52+ ).reshape(1, -1) * dist_mag
53+W = np.vstack([np.sin(2*timepts), np.cos(3*timepts)]) * meas_mag
54+55+# Generate system data
56+U = np.sin(timepts).reshape(1, -1)
57+res = ct.input_output_response(sys, timepts, [U, V])
58+Y = res.outputs + W
59+60+# Decide on the initial guess to use
61+if initial_guess == 'xhat':
62+initial_guess = (res.states, V*0)
63+elif initial_guess == 'both':
64+initial_guess = (res.states, V)
65+else:
66+initial_guess = None
67+68+69+# Set up optimal estimation function using Gaussian likelihoods for cost
70+traj_cost = opt.gaussian_likelihood_cost(sys, Rv, Rw)
71+init_cost = lambda xhat, x: (xhat - x) @ (xhat - x)
72+oep = opt.OptimalEstimationProblem(
73+sys, timepts, traj_cost, terminal_cost=init_cost)
74+75+# Noise and disturbances (the standard case)
76+est = oep.compute_estimate(Y, U, initial_guess=initial_guess)
77+assert est.success
78+np.testing.assert_allclose(
79+est.states[:, -1], res.states[:, -1], atol=1e-1, rtol=1e-2)
80+81+82+# Parameterize the test against different choices of integrator and minimizer
83+time_oep_minimizer_methods.param_names = ['minimizer', 'noise', 'initial']
84+time_oep_minimizer_methods.params = (
85+ ['default', 'trust', 'SLSQP', 'COBYLA'],
86+ ['noisy', 'nodist', 'nomeas', 'clean'],
87+ ['none', 'xhat', 'both'])