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'])