From 58d8cd6e4fe77fd5a18f91c791532e3e259b1949 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 20:09:23 +0100 Subject: [PATCH 01/87] round to nearest integer for default omega --- control/freqplot.py | 16 +++++++--------- control/tests/sisotool_test.py | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 881ec93dd..7225afe97 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -1326,7 +1326,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, features_ = np.concatenate((np.abs(sys.pole()), np.abs(sys.zero()))) # Get rid of poles and zeros at the origin - toreplace = features_ == 0.0 + toreplace = np.isclose(features_, 0.0) if np.any(toreplace): features_ = features_[~toreplace] elif sys.isdtime(strict=True): @@ -1339,7 +1339,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, # Get rid of poles and zeros on the real axis (imag==0) # * origin and real < 0 # * at 1.: would result in omega=0. (logaritmic plot!) - toreplace = (features_.imag == 0.0) & ( + toreplace = np.isclose(features_.imag, 0.0) & ( (features_.real <= 0.) | (np.abs(features_.real - 1.0) < 1.e-10)) if np.any(toreplace): @@ -1360,15 +1360,13 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, if Hz: features /= 2. * math.pi - features = np.log10(features) - lsp_min = np.floor(np.min(features) - feature_periphery_decades) - lsp_max = np.ceil(np.max(features) + feature_periphery_decades) + features = np.log10(features) + lsp_min = np.rint(np.min(features) - feature_periphery_decades) + lsp_max = np.rint(np.max(features) + feature_periphery_decades) + if Hz: lsp_min += np.log10(2. * math.pi) lsp_max += np.log10(2. * math.pi) - else: - features = np.log10(features) - lsp_min = np.floor(np.min(features) - feature_periphery_decades) - lsp_max = np.ceil(np.max(features) + feature_periphery_decades) + if freq_interesting: lsp_min = min(lsp_min, np.log10(min(freq_interesting))) lsp_max = max(lsp_max, np.log10(max(freq_interesting))) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 6b8c6d148..d5e9dd013 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -102,8 +102,8 @@ def test_sisotool(self, tsys): # Check if the bode_mag line has moved bode_mag_moved = np.array( - [674.0242, 667.8354, 661.7033, 655.6275, 649.6074, 643.6426, - 637.7324, 631.8765, 626.0742, 620.3252]) + [69.0065, 68.6749, 68.3448, 68.0161, 67.6889, 67.3631, 67.0388, + 66.7159, 66.3944, 66.0743]) assert_array_almost_equal(ax_mag.lines[0].get_data()[1][10:20], bode_mag_moved, 4) From 19801e6deac560e72ee9593f81be2d24e8df14e6 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 21:07:35 +0100 Subject: [PATCH 02/87] split up nyquist indent tests --- control/tests/nyquist_test.py | 36 ++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 4667c6219..c77d94c86 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -182,42 +182,56 @@ def test_nyquist_encirclements(): assert _Z(sys) == count + _P(sys) -def test_nyquist_indent(): +@pytest.fixture +def indentsys(): # FBS Figure 10.10 - s = ct.tf('s') - sys = 3 * (s+6)**2 / (s * (s+1)**2) # poles: [-1, -1, 0] + s = ct.tf('s') + return 3 * (s+6)**2 / (s * (s+1)**2) + +def test_nyquist_indent_default(indentsys): plt.figure(); - count = ct.nyquist_plot(sys) + count = ct.nyquist_plot(indentsys) plt.title("Pole at origin; indent_radius=default") - assert _Z(sys) == count + _P(sys) + assert _Z(indentsys) == count + _P(indentsys) + +def test_nyquist_indent_dont(indentsys): # first value of default omega vector was 0.1, replaced by 0. for contour # indent_radius is larger than 0.1 -> no extra quater circle around origin - count, contour = ct.nyquist_plot(sys, plot=False, indent_radius=.1007, + count, contour = ct.nyquist_plot(indentsys, + plot=False, + indent_radius=.1007, return_contour=True) np.testing.assert_allclose(contour[0], .1007+0.j) # second value of omega_vector is larger than indent_radius: not indented assert np.all(contour.real[2:] == 0.) + +def test_nyquist_indent_do(indentsys): plt.figure(); - count, contour = ct.nyquist_plot(sys, indent_radius=0.01, + count, contour = ct.nyquist_plot(indentsys, + indent_radius=0.01, return_contour=True) plt.title("Pole at origin; indent_radius=0.01; encirclements = %d" % count) - assert _Z(sys) == count + _P(sys) + assert _Z(indentsys) == count + _P(indentsys) # indent radius is smaller than the start of the default omega vector # check that a quarter circle around the pole at origin has been added. np.testing.assert_allclose(contour[:50].real**2 + contour[:50].imag**2, 0.01**2) + +def test_nyquist_indent_left(indentsys): plt.figure(); - count = ct.nyquist_plot(sys, indent_direction='left') + count = ct.nyquist_plot(indentsys, indent_direction='left') plt.title( "Pole at origin; indent_direction='left'; encirclements = %d" % count) - assert _Z(sys) == count + _P(sys, indent='left') + assert _Z(indentsys) == count + _P(indentsys, indent='left') + - # System with poles on the imaginary axis +def test_nyquist_indent_im(): + """Test system with poles on the imaginary axis.""" sys = ct.tf([1, 1], [1, 0, 1]) # Imaginary poles with standard indentation From c44b901d3af030187d4c2c75dbee75f2bedcf29a Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 21:08:06 +0100 Subject: [PATCH 03/87] passthrough Hz parameter for omega vector --- control/freqplot.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 7225afe97..18b9a4485 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -209,7 +209,7 @@ def bode_plot(syslist, omega=None, syslist = (syslist,) omega, omega_range_given = _determine_omega_vector( - syslist, omega, omega_limits, omega_num) + syslist, omega, omega_limits, omega_num, Hz=Hz) if plot: # Set up the axes with labels so that multiple calls to @@ -965,7 +965,7 @@ def gangof4_plot(P, C, omega=None, **kwargs): # Select a default range if none is provided # TODO: This needs to be made more intelligent if omega is None: - omega = _default_frequency_range((P, C, S)) + omega = _default_frequency_range((P, C, S), Hz=Hz) # Set up the axes with labels so that multiple calls to # gangof4_plot will superimpose the data. See details in bode_plot. @@ -1115,7 +1115,7 @@ def singular_values_plot(syslist, omega=None, syslist = (syslist,) omega, omega_range_given = _determine_omega_vector( - syslist, omega, omega_limits, omega_num) + syslist, omega, omega_limits, omega_num, Hz=Hz) omega = np.atleast_1d(omega) @@ -1210,7 +1210,8 @@ def singular_values_plot(syslist, omega=None, # Determine the frequency range to be used -def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num): +def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num, + Hz=None): """Determine the frequency range for a frequency-domain plot according to a standard logic. @@ -1236,6 +1237,10 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num): omega_num : int Number of points to be used for the frequency range (if the frequency range is not user-specified) + Hz : bool. optional + If True, the limits (first and last value) of the frequencies + are set to full decades in Hz so it fits plotting with logarithmic + scale in Hz otherwise in rad/s. Omega is always returned in rad/sec. Returns ------- @@ -1253,7 +1258,8 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num): omega_range_given = False # Select a default range if none is provided omega_out = _default_frequency_range(syslist, - number_of_samples=omega_num) + number_of_samples=omega_num, + Hz=Hz) else: omega_limits = np.asarray(omega_limits) if len(omega_limits) != 2: @@ -1280,7 +1286,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, ---------- syslist : list of LTI List of linear input/output systems (single system is OK) - Hz : bool + Hz : bool. optional If True, the limits (first and last value) of the frequencies are set to full decades in Hz so it fits plotting with logarithmic scale in Hz otherwise in rad/s. Omega is always returned in rad/sec. From 021372f4cd821907fee913ceef6d46af899a9bcd Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 21:37:10 +0100 Subject: [PATCH 04/87] docstring punctuation --- control/freqplot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 18b9a4485..a8324e06e 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -1237,7 +1237,7 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num, omega_num : int Number of points to be used for the frequency range (if the frequency range is not user-specified) - Hz : bool. optional + Hz : bool, optional If True, the limits (first and last value) of the frequencies are set to full decades in Hz so it fits plotting with logarithmic scale in Hz otherwise in rad/s. Omega is always returned in rad/sec. @@ -1286,7 +1286,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, ---------- syslist : list of LTI List of linear input/output systems (single system is OK) - Hz : bool. optional + Hz : bool, optional If True, the limits (first and last value) of the frequencies are set to full decades in Hz so it fits plotting with logarithmic scale in Hz otherwise in rad/s. Omega is always returned in rad/sec. From 25a6458734a46c680bef60d703ebd262b20f1732 Mon Sep 17 00:00:00 2001 From: Jonathan Pelham Date: Wed, 19 Jan 2022 17:00:47 +0000 Subject: [PATCH 05/87] added binder link --- README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 4010ecffe..a7d5ed77d 100644 --- a/README.rst +++ b/README.rst @@ -22,6 +22,18 @@ Python Control Systems Library The Python Control Systems Library is a Python module that implements basic operations for analysis and design of feedback control systems. + +Have a go now! +====== +Try out the examples in the examples folder using the binder service. + +.. image:: https://mybinder.org/badge_logo.svg + :target: https://mybinder.org/v2/gh/python-control/python-control/HEAD + + + + + Features -------- From 00606182b5d7f540589d738ce5fcbc97cfc7a6d9 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 24 Jan 2022 21:09:04 -0800 Subject: [PATCH 06/87] fix handling of endpoint in discrete optimal --- control/optimal.py | 15 +++++----- control/tests/optimal_test.py | 52 +++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/control/optimal.py b/control/optimal.py index dd09532c5..860f67582 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -58,6 +58,7 @@ class OptimalControlProblem(): extension of the time axis. log : bool, optional If `True`, turn on logging messages (using Python logging module). + Use ``logging.basicConfig`` to enable logging output (e.g., to a file). kwargs : dict, optional Additional parameters (passed to :func:`scipy.optimal.minimize`). @@ -95,7 +96,7 @@ class OptimalControlProblem(): trajectory generated by the proposed input. It does this by calling a user-defined function for the integral_cost given the current states and inputs at each point along the trajectory and then adding the value of a - user-defined terminal cost at the final pint in the trajectory. + user-defined terminal cost at the final point in the trajectory. The `_constraint_function` method evaluates the constraint functions along the trajectory generated by the proposed input. As in the case of the @@ -269,7 +270,6 @@ def _cost_function(self, coeffs): + str(states)) # Trajectory cost - # TODO: vectorize if ct.isctime(self.system): # Evaluate the costs costs = [self.integral_cost(states[:, i], inputs[:, i]) for @@ -279,6 +279,7 @@ def _cost_function(self, coeffs): dt = np.diff(self.timepts) # Integrate the cost + # TODO: vectorize cost = 0 for i in range(self.timepts.size-1): # Approximate the integral using trapezoidal rule @@ -288,8 +289,8 @@ def _cost_function(self, coeffs): # Sum the integral cost over the time (second) indices # cost += self.integral_cost(states[:,i], inputs[:,i]) cost = sum(map( - self.integral_cost, np.transpose(states), - np.transpose(inputs))) + self.integral_cost, np.transpose(states[:, :-1]), + np.transpose(inputs[:, :-1]))) # Terminal cost if self.terminal_cost is not None: @@ -661,8 +662,8 @@ def _output(t, x, u, params={}): return ct.NonlinearIOSystem( _update, _output, dt=dt, inputs=self.system.nstates, outputs=self.system.ninputs, - states=self.system.ninputs * - (self.timepts.size if self.basis is None else self.basis.N)) + states=self.system.ninputs * \ + (self.timepts.size if self.basis is None else self.basis.N)) # Compute the optimal trajectory from the current state def compute_trajectory( @@ -755,7 +756,7 @@ def compute_mpc(self, x, squeeze=None): """ res = self.compute_trajectory(x, squeeze=squeeze) - return inputs[:, 0] if res.success else None + return res.inputs[:, 0] # Optimal control result diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 528313e9d..10484795b 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -39,7 +39,8 @@ def test_finite_horizon_simple(): # Retrieve the full open-loop predictions res = opt.solve_ocp( - sys, time, x0, cost, constraints, squeeze=True) + sys, time, x0, cost, constraints, squeeze=True, + terminal_cost=cost) # include to match MPT3 formulation t, u_openloop = res.time, res.inputs np.testing.assert_almost_equal( u_openloop, [-1, -1, 0.1393, 0.3361, -5.204e-16], decimal=4) @@ -57,9 +58,7 @@ def test_finite_horizon_simple(): # # The next unit test is intended to confirm that a finite horizon # optimal control problem with terminal cost set to LQR "cost to go" -# gives the same answer as LQR. Unfortunately, it requires a discrete -# time LQR function which is not yet availbale => for now this just -# tests the interface a bit. +# gives the same answer as LQR. # @slycotonly def test_discrete_lqr(): @@ -76,35 +75,43 @@ def test_discrete_lqr(): # Include weights on states/inputs Q = np.eye(2) R = 1 - K, S, E = ct.lqr(A, B, Q, R) # note: *continuous* time LQR + K, S, E = ct.dlqr(A, B, Q, R) # Compute the integral and terminal cost integral_cost = opt.quadratic_cost(sys, Q, R) terminal_cost = opt.quadratic_cost(sys, S, None) - # Formulate finite horizon MPC problem + # Solve the LQR problem + lqr_sys = ct.ss2io(ct.ss(A - B @ K, B, C, D, 1)) + + # Generate a simulation of the LQR controller time = np.arange(0, 5, 1) x0 = np.array([1, 1]) + _, _, lqr_x = ct.input_output_response( + lqr_sys, time, 0, x0, return_x=True) + + # Use LQR input as initial guess to avoid convergence/precision issues + lqr_u = -K @ lqr_x[0:time.size] + + # Formulate the optimal control problem and compute optimal trajectory optctrl = opt.OptimalControlProblem( - sys, time, integral_cost, terminal_cost=terminal_cost) + sys, time, integral_cost, terminal_cost=terminal_cost, + initial_guess=lqr_u) res1 = optctrl.compute_trajectory(x0, return_states=True) - with pytest.xfail("discrete LQR not implemented"): - # Result should match LQR - K, S, E = ct.dlqr(A, B, Q, R) - lqr_sys = ct.ss2io(ct.ss(A - B @ K, B, C, D, 1)) - _, _, lqr_x = ct.input_output_response( - lqr_sys, time, 0, x0, return_x=True) - np.testing.assert_almost_equal(res1.states, lqr_x) + # Compare to make sure results are the same + np.testing.assert_almost_equal(res1.inputs, lqr_u[0]) + np.testing.assert_almost_equal(res1.states, lqr_x) # Add state and input constraints trajectory_constraints = [ - (sp.optimize.LinearConstraint, np.eye(3), [-10, -10, -1], [10, 10, 1]), + (sp.optimize.LinearConstraint, np.eye(3), [-5, -5, -.5], [5, 5, 0.5]), ] # Re-solve res2 = opt.solve_ocp( - sys, time, x0, integral_cost, constraints, terminal_cost=terminal_cost) + sys, time, x0, integral_cost, trajectory_constraints, + terminal_cost=terminal_cost, initial_guess=lqr_u) # Make sure we got a different solution assert np.any(np.abs(res1.inputs - res2.inputs) > 0.1) @@ -205,7 +212,9 @@ def test_constraint_specification(constraint_list): # Create a model predictive controller system time = np.arange(0, 5, 1) - optctrl = opt.OptimalControlProblem(sys, time, cost, constraints) + optctrl = opt.OptimalControlProblem( + sys, time, cost, constraints, + terminal_cost=cost) # include to match MPT3 formulation # Compute optimal control and compare against MPT3 solution x0 = [4, 0] @@ -223,7 +232,7 @@ def test_constraint_specification(constraint_list): ([[1, 0], [0, 1]], np.eye(2), np.eye(2), 0, 1), id = "discrete, dt=1"), pytest.param( - (np.zeros((2,2)), np.eye(2), np.eye(2), 0), + (np.zeros((2, 2)), np.eye(2), np.eye(2), 0), id = "continuous"), ]) def test_terminal_constraints(sys_args): @@ -274,8 +283,11 @@ def test_terminal_constraints(sys_args): # Re-run using a basis function and see if we get the same answer res = opt.solve_ocp(sys, time, x0, cost, terminal_constraints=final_point, - basis=flat.BezierFamily(4, Tf)) - np.testing.assert_almost_equal(res.inputs, u1, decimal=2) + basis=flat.BezierFamily(8, Tf)) + + # Final point doesn't affect cost => don't need to test + np.testing.assert_almost_equal( + res.inputs[:, :-1], u1[:, :-1], decimal=2) # Impose some cost on the state, which should change the path Q = np.eye(2) From 395dbbba75d1e9b862a57a1d942039840a9515e0 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 24 Jan 2022 21:12:47 -0800 Subject: [PATCH 07/87] check for unused keywords in OptimalControlProblem --- control/optimal.py | 4 ++++ control/tests/optimal_test.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/control/optimal.py b/control/optimal.py index 860f67582..1f17db0d6 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -134,6 +134,10 @@ def __init__( self.minimize_kwargs['options'] = kwargs.pop('minimize_options', {}) self.minimize_kwargs.update(kwargs.pop('minimize_kwargs', {})) + if len(kwargs) > 0: + raise ValueError( + f'unrecognized keyword(s): {list(kwargs.keys())}') + # Process trajectory constraints if isinstance(trajectory_constraints, tuple): self.trajectory_constraints = [trajectory_constraints] diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 10484795b..680911259 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -436,6 +436,11 @@ def test_ocp_argument_errors(): res = opt.solve_ocp( sys, time, x0, cost, constraints, initial_guess=np.zeros((4,1,1))) + # Unrecognized arguments + with pytest.raises(ValueError, match="unrecognized keyword"): + res = opt.solve_ocp( + sys, time, x0, cost, constraints, terminal_constraint=None) + def test_optimal_basis_simple(): sys = ct.ss2io(ct.ss([[1, 1], [0, 1]], [[1], [0.5]], np.eye(2), 0, 1)) From de4c75cdc938b65ee1af3a85051410f8643a559f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 25 Jan 2022 07:14:25 -0800 Subject: [PATCH 08/87] generate iosys warning if solve_ivp does not succeed --- control/iosys.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/control/iosys.py b/control/iosys.py index c8e921c90..46124e669 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1870,6 +1870,11 @@ def ivp_rhs(t, x): ivp_rhs, (T0, Tf), X0, t_eval=T, vectorized=False, **solve_ivp_kwargs) + if not soln.success or soln.status != 0: + # Something went wrong + warn("sp.integrate.solve_ivp failed") + print("Return bunch:", soln) + # Compute the output associated with the state (and use sys.out to # figure out the number of outputs just in case it wasn't specified) u = U[0] if len(U.shape) == 1 else U[:, 0] @@ -1886,7 +1891,7 @@ def ivp_rhs(t, x): "equally spaced.") # Make sure the sample time matches the given time - if (sys.dt is not True): + if sys.dt is not True: # Make sure that the time increment is a multiple of sampling time # TODO: add back functionality for undersampling From 93a5fae4aa3e000479a90be6d99e3471ca720e7f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 25 Jan 2022 07:15:57 -0800 Subject: [PATCH 09/87] remove one step delay in create_mpc_iosystem, discrete time only --- control/optimal.py | 72 +++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/control/optimal.py b/control/optimal.py index 1f17db0d6..f1c352263 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -639,35 +639,9 @@ def _print_statistics(self, reset=True): if reset: self._reset_statistics(self.log) - # Create an input/output system implementing an MPC controller - def _create_mpc_iosystem(self, dt=True): - """Create an I/O system implementing an MPC controller""" - def _update(t, x, u, params={}): - coeffs = x.reshape((self.system.ninputs, -1)) - if self.basis: - # Keep the coeffecients unchanged - # TODO: could compute input vector, shift, and re-project (?) - self.initial_guess = coeffs - else: - # Shift the basis elements by one time step - self.initial_guess = np.hstack( - [coeffs[:, 1:], coeffs[:, -1:]]).reshape(-1) - res = self.compute_trajectory(u, print_summary=False) - return res.inputs.reshape(-1) - - def _output(t, x, u, params={}): - if self.basis: - # TODO: compute inputs from basis elements - raise NotImplementedError("basis elements not implemented") - else: - inputs = x.reshape((self.system.ninputs, -1)) - return inputs[:, 0] - - return ct.NonlinearIOSystem( - _update, _output, dt=dt, - inputs=self.system.nstates, outputs=self.system.ninputs, - states=self.system.ninputs * \ - (self.timepts.size if self.basis is None else self.basis.N)) + # + # Optimal control computations + # # Compute the optimal trajectory from the current state def compute_trajectory( @@ -762,6 +736,41 @@ def compute_mpc(self, x, squeeze=None): res = self.compute_trajectory(x, squeeze=squeeze) return res.inputs[:, 0] + # Create an input/output system implementing an MPC controller + def create_mpc_iosystem(self): + """Create an I/O system implementing an MPC controller""" + # Check to make sure we are in discrete time + if self.system.dt == 0: + raise ControlNotImplemented( + "MPC for continuous time systems not implemented") + + def _update(t, x, u, params={}): + coeffs = x.reshape((self.system.ninputs, -1)) + if self.basis: + # Keep the coeffecients unchanged + # TODO: could compute input vector, shift, and re-project (?) + self.initial_guess = coeffs + else: + # Shift the basis elements by one time step + self.initial_guess = np.hstack( + [coeffs[:, 1:], coeffs[:, -1:]]).reshape(-1) + res = self.compute_trajectory(u, print_summary=False) + + # New state is the new input vector + return res.inputs.reshape(-1) + + def _output(t, x, u, params={}): + # Start with initial guess and recompute based on input state (u) + self.initial_guess = x + res = self.compute_trajectory(u, print_summary=False) + return res.inputs[:, 0] + + return ct.NonlinearIOSystem( + _update, _output, dt=self.system.dt, + inputs=self.system.nstates, outputs=self.system.ninputs, + states=self.system.ninputs * \ + (self.timepts.size if self.basis is None else self.basis.N)) + # Optimal control result class OptimalControlResult(sp.optimize.OptimizeResult): @@ -952,7 +961,7 @@ def solve_ocp( # Create a model predictive controller for an optimal control problem def create_mpc_iosystem( sys, horizon, cost, constraints=[], terminal_cost=None, - terminal_constraints=[], dt=True, log=False, **kwargs): + terminal_constraints=[], log=False, **kwargs): """Create a model predictive I/O control system This function creates an input/output system that implements a model @@ -1001,7 +1010,6 @@ def create_mpc_iosystem( :func:`OptimalControlProblem` for more information. """ - # Set up the optimal control problem ocp = OptimalControlProblem( sys, horizon, cost, trajectory_constraints=constraints, @@ -1009,7 +1017,7 @@ def create_mpc_iosystem( log=log, **kwargs) # Return an I/O system implementing the model predictive controller - return ocp._create_mpc_iosystem(dt=dt) + return ocp.create_mpc_iosystem() # From 5d572852cff0623646d3e183c8746dbe216bb52e Mon Sep 17 00:00:00 2001 From: Miroslav Fikar Date: Wed, 26 Jan 2022 11:38:37 +0100 Subject: [PATCH 10/87] fix in documentation of ss2tf --- control/xferfcn.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index 856b421ef..fd859f675 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1524,14 +1524,14 @@ def ss2tf(*args, **kwargs): The function accepts either 1 or 4 parameters: ``ss2tf(sys)`` - Convert a linear system into space system form. Always creates a - new system, even if sys is already a StateSpace object. + Convert a linear system from state space into transfer function form. Always creates a + new system. ``ss2tf(A, B, C, D)`` - Create a state space system from the matrices of its state and + Create a transfer function system from the matrices of its state and output equations. - For details see: :func:`ss` + For details see: :func:`tf` Parameters ---------- From 37d31480b6828ba282af1e959fc94eb68a610b52 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 29 Jan 2022 05:52:04 +0200 Subject: [PATCH 11/87] Interpret str-type args to `interconnect` as non-sequence If the arguments `inputs` or `outputs` (or their aliases `input` or `output`) are of type str, treat as a list containing that string. Fixes gh-692. --- control/iosys.py | 5 +++++ control/tests/interconnect_test.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/control/iosys.py b/control/iosys.py index c8e921c90..916fe9d6a 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -2519,6 +2519,11 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], # Use an empty connections list connections = [] + if isinstance(inputs, str): + inputs = [inputs] + if isinstance(outputs, str): + outputs = [outputs] + # If inplist/outlist is not present, try using inputs/outputs instead if not inplist and inputs is not None: inplist = list(inputs) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index c927bf0f6..dd31241e7 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -210,3 +210,23 @@ def test_interconnect_exceptions(): with pytest.raises(TypeError, match="unknown parameter"): sumblk = ct.summing_junction(input_count=2, output_count=2) + + +def test_string_inputoutput(): + # regression test for gh-692 + P1 = ct.rss(2, 1, 1) + P1_iosys = ct.LinearIOSystem(P1, inputs='u1', outputs='y1') + P2 = ct.rss(2, 1, 1) + P2_iosys = ct.LinearIOSystem(P2, inputs='y1', outputs='y2') + + P_s1 = ct.interconnect([P1_iosys, P2_iosys], inputs='u1', outputs=['y2']) + assert P_s1.input_index == {'u1' : 0} + + P_s2 = ct.interconnect([P1_iosys, P2_iosys], input='u1', outputs=['y2']) + assert P_s2.input_index == {'u1' : 0} + + P_s1 = ct.interconnect([P1_iosys, P2_iosys], inputs=['u1'], outputs='y2') + assert P_s1.output_index == {'y2' : 0} + + P_s2 = ct.interconnect([P1_iosys, P2_iosys], inputs=['u1'], output='y2') + assert P_s2.output_index == {'y2' : 0} From 7396f76db2dd5828339e43b1f34e3eddd620aff1 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 4 Feb 2022 21:27:37 -0800 Subject: [PATCH 12/87] add params to flat systems + updated keywords --- control/flatsys/flatsys.py | 48 +++++++++++++++++++++++--------------- control/flatsys/linflat.py | 4 ++-- control/flatsys/systraj.py | 6 +++-- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py index 9ea40f2fb..581d4d998 100644 --- a/control/flatsys/flatsys.py +++ b/control/flatsys/flatsys.py @@ -108,7 +108,7 @@ class FlatSystem(NonlinearIOSystem): ----- The class must implement two functions: - zflag = flatsys.foward(x, u) + zflag = flatsys.foward(x, u, params) This function computes the flag (derivatives) of the flat output. The inputs to this function are the state 'x' and inputs 'u' (both 1D arrays). The output should be a 2D array with the first @@ -116,7 +116,7 @@ class FlatSystem(NonlinearIOSystem): dimension of the length required to represent the full system dynamics (typically the number of states) - x, u = flatsys.reverse(zflag) + x, u = flatsys.reverse(zflag, params) This function system state and inputs give the the flag (derivatives) of the flat output. The input to this function is an 2D array whose first dimension is equal to the number of system inputs and whose @@ -244,8 +244,8 @@ def _basis_flag_matrix(sys, basis, flag, t, params={}): # Solve a point to point trajectory generation problem for a flat system def point_to_point( - sys, timepts, x0=0, u0=0, xf=0, uf=0, T0=0, basis=None, cost=None, - constraints=None, initial_guess=None, minimize_kwargs={}, **kwargs): + sys, timepts, x0=0, u0=0, xf=0, uf=0, T0=0, cost=None, basis=None, + trajectory_constraints=None, initial_guess=None, params=None, **kwargs): """Compute trajectory between an initial and final conditions. Compute a feasible trajectory for a differentially flat system between an @@ -284,7 +284,7 @@ def point_to_point( Function that returns the integral cost given the current state and input. Called as `cost(x, u)`. - constraints : list of tuples, optional + trajectory_constraints : list of tuples, optional List of constraints that should hold at each point in the time vector. Each element of the list should consist of a tuple with first element given by :class:`scipy.optimize.LinearConstraint` or @@ -337,8 +337,15 @@ def point_to_point( T0 = timepts[0] if len(timepts) > 1 else T0 # Process keyword arguments + if trajectory_constraints is None: + # Backwards compatibility + trajectory_constraints = kwargs.pop('constraints', None) + + minimize_kwargs = {} minimize_kwargs['method'] = kwargs.pop('minimize_method', None) minimize_kwargs['options'] = kwargs.pop('minimize_options', {}) + minimize_kwargs.update(kwargs.pop('minimize_kwargs', {})) + if kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) @@ -353,11 +360,14 @@ def point_to_point( # Make sure we have enough basis functions to solve the problem if basis.N * sys.ninputs < 2 * (sys.nstates + sys.ninputs): raise ValueError("basis set is too small") - elif (cost is not None or constraints is not None) and \ + elif (cost is not None or trajectory_constraints is not None) and \ basis.N * sys.ninputs == 2 * (sys.nstates + sys.ninputs): warnings.warn("minimal basis specified; optimization not possible") cost = None - constraints = None + trajectory_constraints = None + + # Figure out the parameters to use, if any + params = sys.params if params is None else params # # Map the initial and final conditions to flat output conditions @@ -366,8 +376,8 @@ def point_to_point( # and then evaluate this at the initial and final condition. # - zflag_T0 = sys.forward(x0, u0) - zflag_Tf = sys.forward(xf, uf) + zflag_T0 = sys.forward(x0, u0, params) + zflag_Tf = sys.forward(xf, uf, params) # # Compute the matrix constraints for initial and final conditions @@ -400,7 +410,7 @@ def point_to_point( # Start by solving the least squares problem alpha, residuals, rank, s = np.linalg.lstsq(M, Z, rcond=None) - if cost is not None or constraints is not None: + if cost is not None or trajectory_constraints is not None: # Search over the null space to minimize cost/satisfy constraints N = sp.linalg.null_space(M) @@ -418,7 +428,7 @@ def traj_cost(null_coeffs): zflag = (M_t @ coeffs).reshape(sys.ninputs, -1) # Find states and inputs at the time points - x, u = sys.reverse(zflag) + x, u = sys.reverse(zflag, params) # Evaluate the cost at this time point costval += cost(x, u) @@ -429,13 +439,13 @@ def traj_cost(null_coeffs): traj_cost = lambda coeffs: coeffs @ coeffs # Process the constraints we were given - traj_constraints = constraints - if constraints is None: + traj_constraints = trajectory_constraints + if traj_constraints is None: traj_constraints = [] - elif isinstance(constraints, tuple): + elif isinstance(traj_constraints, tuple): # TODO: Check to make sure this is really a constraint - traj_constraints = [constraints] - elif not isinstance(constraints, list): + traj_constraints = [traj_constraints] + elif not isinstance(traj_constraints, list): raise TypeError("trajectory constraints must be a list") # Process constraints @@ -456,7 +466,7 @@ def traj_const(null_coeffs): zflag = (M_t @ coeffs).reshape(sys.ninputs, -1) # Find states and inputs at the time points - states, inputs = sys.reverse(zflag) + states, inputs = sys.reverse(zflag, params) # Evaluate the constraint function along the trajectory for type, fun, lb, ub in traj_constraints: @@ -507,8 +517,8 @@ def traj_const(null_coeffs): # Transform the trajectory from flat outputs to states and inputs # - # Createa trajectory object to store the resul - systraj = SystemTrajectory(sys, basis) + # Create a trajectory object to store the result + systraj = SystemTrajectory(sys, basis, params=params) # Store the flag lengths and coefficients # TODO: make this more pythonic diff --git a/control/flatsys/linflat.py b/control/flatsys/linflat.py index 931446ca8..94523cc0b 100644 --- a/control/flatsys/linflat.py +++ b/control/flatsys/linflat.py @@ -113,7 +113,7 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, self.Cf = Cfz @ Tr # Compute the flat flag from the state (and input) - def forward(self, x, u): + def forward(self, x, u, params): """Compute the flat flag given the states and input. See :func:`control.flatsys.FlatSystem.forward` for more info. @@ -130,7 +130,7 @@ def forward(self, x, u): return zflag # Compute state and input from flat flag - def reverse(self, zflag): + def reverse(self, zflag, params): """Compute the states and input given the flat flag. See :func:`control.flatsys.FlatSystem.reverse` for more info. diff --git a/control/flatsys/systraj.py b/control/flatsys/systraj.py index c6ffb0867..5e390a7b5 100644 --- a/control/flatsys/systraj.py +++ b/control/flatsys/systraj.py @@ -62,7 +62,7 @@ class SystemTrajectory: """ - def __init__(self, sys, basis, coeffs=[], flaglen=[]): + def __init__(self, sys, basis, coeffs=[], flaglen=[], params=None): """Initilize a system trajectory object.""" self.nstates = sys.nstates self.ninputs = sys.ninputs @@ -70,6 +70,7 @@ def __init__(self, sys, basis, coeffs=[], flaglen=[]): self.basis = basis self.coeffs = list(coeffs) self.flaglen = list(flaglen) + self.params = sys.params if params is None else params # Evaluate the trajectory over a list of time points def eval(self, tlist): @@ -112,6 +113,7 @@ def eval(self, tlist): # Now copy the states and inputs # TODO: revisit order of list arguments - xd[:,tind], ud[:,tind] = self.system.reverse(zflag) + xd[:,tind], ud[:,tind] = \ + self.system.reverse(zflag, self.params) return xd, ud From 994fedc637304ca8f2379480feacc2ca483c68e6 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 28 Feb 2022 21:32:03 -0800 Subject: [PATCH 13/87] fix discrete time X0, U processing --- control/iosys.py | 3 ++- control/tests/iosys_test.py | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 46124e669..c3d8ebefb 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1820,6 +1820,7 @@ def input_output_response( legal_shapes = [(sys.ninputs, n_steps)] U = _check_convert_array(U, legal_shapes, 'Parameter ``U``: ', squeeze=False) + U = U.reshape(-1, n_steps) # Check to make sure this is not a static function nstates = _find_size(sys.nstates, X0) @@ -1908,7 +1909,7 @@ def ivp_rhs(t, x): # Compute the solution soln = sp.optimize.OptimizeResult() soln.t = T # Store the time vector directly - x = [float(x0) for x0 in X0] # State vector (store as floats) + x = X0 # Initilize state soln.y = [] # Solution, following scipy convention y = [] # System output for i in range(len(T)): diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 5fd83e946..d9a781936 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -731,6 +731,32 @@ def test_discrete(self, tsys): np.testing.assert_allclose(ios_t, lin_t,atol=0.002,rtol=0.) np.testing.assert_allclose(ios_y, lin_y,atol=0.002,rtol=0.) + def test_discrete_iosys(self, tsys): + """Create a discrete time system from scratch""" + linsys = ct.StateSpace( + [[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]], True) + + # Create nonlinear version of the same system + def nlsys_update(t, x, u, params): + A, B = params['A'], params['B'] + return A @ x + B @ u + def nlsys_output(t, x, u, params): + C = params['C'] + return C @ x + nlsys = ct.NonlinearIOSystem( + nlsys_update, nlsys_output, inputs=1, outputs=1, states=2, dt=True) + + # Set up parameters for simulation + T, U, X0 = tsys.T, tsys.U, tsys.X0 + + # Simulate and compare to LTI output + ios_t, ios_y = ios.input_output_response( + nlsys, T, U, X0, + params={'A': linsys.A, 'B': linsys.B, 'C': linsys.C}) + lin_t, lin_y = ct.forced_response(linsys, T, U, X0) + np.testing.assert_allclose(ios_t, lin_t,atol=0.002,rtol=0.) + np.testing.assert_allclose(ios_y, lin_y,atol=0.002,rtol=0.) + def test_find_eqpts(self, tsys): """Test find_eqpt function""" # Simple equilibrium point with no inputs @@ -1526,7 +1552,6 @@ def secord_update(t, x, u, params={}): """Second order system dynamics""" omega0 = params.get('omega0', 1.) zeta = params.get('zeta', 0.5) - u = np.array(u, ndmin=1) return np.array([ x[1], -2 * zeta * omega0 * x[1] - omega0*omega0 * x[0] + u[0] From d01a52cdce111a80020e113731f2c513613569b3 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 12 Mar 2022 07:57:33 -0800 Subject: [PATCH 14/87] fix optimal unit tests (matrix case) + PEP8 --- control/tests/iosys_test.py | 2 +- control/tests/optimal_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index d9a781936..7a7cc0f43 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -745,7 +745,7 @@ def nlsys_output(t, x, u, params): return C @ x nlsys = ct.NonlinearIOSystem( nlsys_update, nlsys_output, inputs=1, outputs=1, states=2, dt=True) - + # Set up parameters for simulation T, U, X0 = tsys.T, tsys.U, tsys.X0 diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 680911259..124cbc6dd 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -91,7 +91,7 @@ def test_discrete_lqr(): lqr_sys, time, 0, x0, return_x=True) # Use LQR input as initial guess to avoid convergence/precision issues - lqr_u = -K @ lqr_x[0:time.size] + lqr_u = np.array(-K @ lqr_x[0:time.size]) # convert from matrix # Formulate the optimal control problem and compute optimal trajectory optctrl = opt.OptimalControlProblem( From be660ceaf796f11bfb961ff494577393a8baa73c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 12 Mar 2022 09:16:24 -0800 Subject: [PATCH 15/87] default outfcn() bug fix + unit tests for flatsys --- control/flatsys/bezier.py | 2 +- control/flatsys/flatsys.py | 2 +- control/flatsys/poly.py | 2 +- control/tests/flatsys_test.py | 33 ++++++++++++++++++++++++++++++++- examples/kincar-flatsys.py | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/control/flatsys/bezier.py b/control/flatsys/bezier.py index 45a28995f..7e41c546e 100644 --- a/control/flatsys/bezier.py +++ b/control/flatsys/bezier.py @@ -55,7 +55,7 @@ class BezierFamily(BasisFamily): """ def __init__(self, N, T=1): """Create a polynomial basis of order N.""" - self.N = N # save number of basis functions + super(BezierFamily, self).__init__(N) self.T = T # save end of time interval # Compute the kth derivative of the ith basis function at time t diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py index 581d4d998..2f20aa1e9 100644 --- a/control/flatsys/flatsys.py +++ b/control/flatsys/flatsys.py @@ -216,7 +216,7 @@ def _flat_updfcn(self, t, x, u, params={}): def _flat_outfcn(self, t, x, u, params={}): # Return the flat output zflag = self.forward(x, u, params) - return np.array(zflag[:][0]) + return np.array([zflag[i][0] for i in range(len(zflag))]) # Utility function to compute flag matrix given a basis diff --git a/control/flatsys/poly.py b/control/flatsys/poly.py index 2d9f62455..08dcfb1c9 100644 --- a/control/flatsys/poly.py +++ b/control/flatsys/poly.py @@ -52,7 +52,7 @@ class PolyFamily(BasisFamily): """ def __init__(self, N): """Create a polynomial basis of order N.""" - self.N = N # save number of basis functions + super(PolyFamily, self).__init__(N) # Compute the kth derivative of the ith basis function at time t def eval_deriv(self, i, k, t): diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py index 6f4ef7cef..8b182a17a 100644 --- a/control/tests/flatsys_test.py +++ b/control/tests/flatsys_test.py @@ -113,8 +113,10 @@ def test_kinematic_car(self, vehicle_flat, poly): np.testing.assert_array_almost_equal(uf, u[:, 1]) # Simulate the system and make sure we stay close to desired traj - T = np.linspace(0, Tf, 500) + T = np.linspace(0, Tf, 100) xd, ud = traj.eval(T) + resp = ct.input_output_response(vehicle_flat, T, ud, x0) + np.testing.assert_array_almost_equal(resp.states, xd, decimal=2) # For SciPy 1.0+, integrate equations and compare to desired if StrictVersion(sp.__version__) >= "1.0": @@ -122,6 +124,35 @@ def test_kinematic_car(self, vehicle_flat, poly): vehicle_flat, T, ud, x0, return_x=True) np.testing.assert_allclose(x, xd, atol=0.01, rtol=0.01) + def test_flat_default_output(self, vehicle_flat): + # Construct a flat system with the default outputs + flatsys = fs.FlatSystem( + vehicle_flat.forward, vehicle_flat.reverse, vehicle_flat.updfcn, + inputs=vehicle_flat.ninputs, outputs=vehicle_flat.ninputs, + states=vehicle_flat.nstates) + + # Define the endpoints of the trajectory + x0 = [0., -2., 0.]; u0 = [10., 0.] + xf = [100., 2., 0.]; uf = [10., 0.] + Tf = 10 + + # Find trajectory between initial and final conditions + poly = fs.PolyFamily(6) + traj1 = fs.point_to_point(vehicle_flat, Tf, x0, u0, xf, uf, basis=poly) + traj2 = fs.point_to_point(flatsys, Tf, x0, u0, xf, uf, basis=poly) + + # Verify that the trajectory computation is correct + T = np.linspace(0, Tf, 10) + x1, u1 = traj1.eval(T) + x2, u2 = traj2.eval(T) + np.testing.assert_array_almost_equal(x1, x2) + np.testing.assert_array_almost_equal(u1, u2) + + # Run a simulation and verify that the outputs are correct + resp1 = ct.input_output_response(vehicle_flat, T, u1, x0) + resp2 = ct.input_output_response(flatsys, T, u1, x0) + np.testing.assert_array_almost_equal(resp1.outputs[0:2], resp2.outputs) + def test_flat_cost_constr(self): # Double integrator system sys = ct.ss([[0, 1], [0, 0]], [[0], [1]], [[1, 0]], 0) diff --git a/examples/kincar-flatsys.py b/examples/kincar-flatsys.py index ca2a946ed..967bdb310 100644 --- a/examples/kincar-flatsys.py +++ b/examples/kincar-flatsys.py @@ -109,7 +109,7 @@ def plot_results(t, x, ud): # Create differentially flat input/output system vehicle_flat = fs.FlatSystem( vehicle_flat_forward, vehicle_flat_reverse, vehicle_update, - inputs=('v', 'delta'), outputs=('x', 'y', 'theta'), + inputs=('v', 'delta'), outputs=('x', 'y'), states=('x', 'y', 'theta')) # Define the endpoints of the trajectory From f807b338bc770502bf997024f1f43b6575b9241f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 12 Mar 2022 10:16:03 -0800 Subject: [PATCH 16/87] updated optimal constraint handling + unit tests --- control/optimal.py | 33 ++++++++++++++---- control/tests/optimal_test.py | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/control/optimal.py b/control/optimal.py index f1c352263..493b6bc3d 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -146,6 +146,11 @@ def __init__( else: self.trajectory_constraints = trajectory_constraints + # Make sure that we recognize all of the constraint types + for ctype, fun, lb, ub in self.trajectory_constraints: + if not ctype in [opt.LinearConstraint, opt.NonlinearConstraint]: + raise TypeError(f"unknown constraint type {ctype}") + # Process terminal constraints if isinstance(terminal_constraints, tuple): self.terminal_constraints = [terminal_constraints] @@ -154,6 +159,11 @@ def __init__( else: self.terminal_constraints = terminal_constraints + # Make sure that we recognize all of the constraint types + for ctype, fun, lb, ub in self.terminal_constraints: + if not ctype in [opt.LinearConstraint, opt.NonlinearConstraint]: + raise TypeError(f"unknown constraint type {ctype}") + # # Compute and store constraints # @@ -401,7 +411,8 @@ def _constraint_function(self, coeffs): value.append(fun @ np.hstack([states[:, i], inputs[:, i]])) elif ctype == opt.NonlinearConstraint: value.append(fun(states[:, i], inputs[:, i])) - else: + else: # pragma: no cover + # Checked above => we should never get here raise TypeError(f"unknown constraint type {ctype}") # Evaluate the terminal constraint functions @@ -413,7 +424,8 @@ def _constraint_function(self, coeffs): value.append(fun @ np.hstack([states[:, i], inputs[:, i]])) elif ctype == opt.NonlinearConstraint: value.append(fun(states[:, i], inputs[:, i])) - else: + else: # pragma: no cover + # Checked above => we should never get here raise TypeError(f"unknown constraint type {ctype}") # Update statistics @@ -485,7 +497,8 @@ def _eqconst_function(self, coeffs): value.append(fun @ np.hstack([states[:, i], inputs[:, i]])) elif ctype == opt.NonlinearConstraint: value.append(fun(states[:, i], inputs[:, i])) - else: + else: # pragma: no cover + # Checked above => we should never get here raise TypeError(f"unknown constraint type {ctype}") # Evaluate the terminal constraint functions @@ -497,7 +510,8 @@ def _eqconst_function(self, coeffs): value.append(fun @ np.hstack([states[:, i], inputs[:, i]])) elif ctype == opt.NonlinearConstraint: value.append(fun(states[:, i], inputs[:, i])) - else: + else: # pragma: no cover + # Checked above => we should never get here raise TypeError("unknown constraint type {ctype}") # Update statistics @@ -844,7 +858,7 @@ def __init__( # Compute the input for a nonlinear, (constrained) optimal control problem def solve_ocp( - sys, horizon, X0, cost, constraints=[], terminal_cost=None, + sys, horizon, X0, cost, trajectory_constraints=[], terminal_cost=None, terminal_constraints=[], initial_guess=None, basis=None, squeeze=None, transpose=None, return_states=False, log=False, **kwargs): @@ -865,7 +879,7 @@ def solve_ocp( Function that returns the integral cost given the current state and input. Called as `cost(x, u)`. - constraints : list of tuples, optional + trajectory_constraints : list of tuples, optional List of constraints that should hold at each point in the time vector. Each element of the list should consist of a tuple with first element given by :meth:`scipy.optimize.LinearConstraint` or @@ -943,13 +957,18 @@ def solve_ocp( :func:`OptimalControlProblem` for more information. """ + # Process keyword arguments + if trajectory_constraints is None: + # Backwards compatibility + trajectory_constraints = kwargs.pop('constraints', None) + # Allow 'return_x` as a synonym for 'return_states' return_states = ct.config._get_param( 'optimal', 'return_x', kwargs, return_states, pop=True) # Set up the optimal control problem ocp = OptimalControlProblem( - sys, horizon, cost, trajectory_constraints=constraints, + sys, horizon, cost, trajectory_constraints=trajectory_constraints, terminal_cost=terminal_cost, terminal_constraints=terminal_constraints, initial_guess=initial_guess, basis=basis, log=log, **kwargs) diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 124cbc6dd..f059c4fc6 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -441,6 +441,17 @@ def test_ocp_argument_errors(): res = opt.solve_ocp( sys, time, x0, cost, constraints, terminal_constraint=None) + # Unrecognized trajectory constraint type + constraints = [(None, np.eye(3), [0, 0, 0], [0, 0, 0])] + with pytest.raises(TypeError, match="unknown constraint type"): + res = opt.solve_ocp( + sys, time, x0, cost, trajectory_constraints=constraints) + + # Unrecognized terminal constraint type + with pytest.raises(TypeError, match="unknown constraint type"): + res = opt.solve_ocp( + sys, time, x0, cost, terminal_constraints=constraints) + def test_optimal_basis_simple(): sys = ct.ss2io(ct.ss([[1, 1], [0, 1]], [[1], [0.5]], np.eye(2), 0, 1)) @@ -484,3 +495,57 @@ def test_optimal_basis_simple(): basis=flat.BezierFamily(4, Tf), return_x=True, log=True) assert res3.success np.testing.assert_almost_equal(res3.inputs, res1.inputs, decimal=3) + + +def test_equality_constraints(): + """Test out the ability to handle equality constraints""" + # Create the system (double integrator, continuous time) + sys = ct.ss2io(ct.ss(np.zeros((2, 2)), np.eye(2), np.eye(2), 0)) + + # Shortest path to a point is a line + Q = np.zeros((2, 2)) + R = np.eye(2) + cost = opt.quadratic_cost(sys, Q, R) + + # Set up the terminal constraint to be the origin + final_point = [opt.state_range_constraint(sys, [0, 0], [0, 0])] + + # Create the optimal control problem + time = np.arange(0, 3, 1) + optctrl = opt.OptimalControlProblem( + sys, time, cost, terminal_constraints=final_point) + + # Find a path to the origin + x0 = np.array([4, 3]) + res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) + t, u1, x1 = res.time, res.inputs, res.states + + # Bug prior to SciPy 1.6 will result in incorrect results + if NumpyVersion(sp.__version__) < '1.6.0': + pytest.xfail("SciPy 1.6 or higher required") + + np.testing.assert_almost_equal(x1[:,-1], 0, decimal=4) + + # Set up terminal constraints as a nonlinear constraint + def final_point_eval(x, u): + return x + final_point = [ + (sp.optimize.NonlinearConstraint, final_point_eval, [0, 0], [0, 0])] + + optctrl = opt.OptimalControlProblem( + sys, time, cost, terminal_constraints=final_point) + + # Find a path to the origin + x0 = np.array([4, 3]) + res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) + t, u2, x2 = res.time, res.inputs, res.states + np.testing.assert_almost_equal(x2[:,-1], 0, decimal=4) + np.testing.assert_almost_equal(u1, u2) + np.testing.assert_almost_equal(x1, x2) + + # Try passing and unknown constraint type + final_point = [(None, final_point_eval, [0, 0], [0, 0])] + with pytest.raises(TypeError, match="unknown constraint type"): + optctrl = opt.OptimalControlProblem( + sys, time, cost, terminal_constraints=final_point) + res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) From f712096d4ddb9ee62788abdb0c61e7d61f3c7c93 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 12 Mar 2022 22:38:48 -0800 Subject: [PATCH 17/87] ss, rss, drss return LinearIOSystem --- control/iosys.py | 100 +++++++++++++++++++++++++- control/matlab/__init__.py | 1 + control/matlab/wrappers.py | 2 +- control/sisotool.py | 2 +- control/statesp.py | 88 +---------------------- control/tests/statesp_test.py | 5 +- control/tests/type_conversion_test.py | 6 +- control/tests/xferfcn_test.py | 11 ++- 8 files changed, 112 insertions(+), 103 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 916fe9d6a..82688e02c 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -32,6 +32,7 @@ from warnings import warn from .statesp import StateSpace, tf2ss, _convert_to_statespace +from .statesp import _ss, _rss_generate from .xferfcn import TransferFunction from .timeresp import _check_convert_array, _process_time_response, \ TimeResponseData @@ -40,8 +41,8 @@ __all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem', 'InterconnectedSystem', 'LinearICSystem', 'input_output_response', - 'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect', - 'summing_junction'] + 'find_eqpt', 'linearize', 'ss', 'rss', 'drss', 'ss2io', 'tf2io', + 'interconnect', 'summing_junction'] # Define module default parameter values _iosys_defaults = { @@ -181,7 +182,8 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, nstates = 0 def __repr__(self): - return self.name if self.name is not None else str(type(self)) + return str(type(self)) + ": " + self.name if self.name is not None \ + else str(type(self)) def __str__(self): """String representation of an input/output system""" @@ -853,6 +855,10 @@ def _out(self, t, x, u): + self.D @ np.reshape(u, (-1, 1)) return np.array(y).reshape((-1,)) + def __str__(self): + return InputOutputSystem.__str__(self) + "\n\n" \ + + StateSpace.__str__(self) + class NonlinearIOSystem(InputOutputSystem): """Nonlinear I/O system. @@ -2261,6 +2267,94 @@ def _find_size(sysval, vecval): raise ValueError("Can't determine size of system component.") +# Define a state space object that is an I/O system +def ss(*args, **kwargs): + return LinearIOSystem(_ss(*args, **kwargs)) +ss.__doc__ = _ss.__doc__ + + +def rss(states=1, outputs=1, inputs=1, strictly_proper=False): + """ + Create a stable *continuous* random state space object. + + Parameters + ---------- + states : int + Number of state variables + outputs : int + Number of system outputs + inputs : int + Number of system inputs + strictly_proper : bool, optional + If set to 'True', returns a proper system (no direct term). + + Returns + ------- + sys : StateSpace + The randomly created linear system + + Raises + ------ + ValueError + if any input is not a positive integer + + See Also + -------- + drss + + Notes + ----- + If the number of states, inputs, or outputs is not specified, then the + missing numbers are assumed to be 1. The poles of the returned system + will always have a negative real part. + + """ + + return LinearIOSystem(_rss_generate( + states, inputs, outputs, 'c', strictly_proper=strictly_proper)) + + +def drss(states=1, outputs=1, inputs=1, strictly_proper=False): + """ + Create a stable *discrete* random state space object. + + Parameters + ---------- + states : int + Number of state variables + inputs : integer + Number of system inputs + outputs : int + Number of system outputs + strictly_proper: bool, optional + If set to 'True', returns a proper system (no direct term). + + Returns + ------- + sys : StateSpace + The randomly created linear system + + Raises + ------ + ValueError + if any input is not a positive integer + + See Also + -------- + rss + + Notes + ----- + If the number of states, inputs, or outputs is not specified, then the + missing numbers are assumed to be 1. The poles of the returned system + will always have a magnitude less than 1. + + """ + + return LinearIOSystem(_rss_generate( + states, inputs, outputs, 'd', strictly_proper=strictly_proper)) + + # Convert a state space system into an input/output system (wrapper) def ss2io(*args, **kwargs): return LinearIOSystem(*args, **kwargs) diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index 196a4a6c8..f10a76c54 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -62,6 +62,7 @@ # Control system library from ..statesp import * +from ..iosys import ss, rss, drss # moved from .statesp from ..xferfcn import * from ..lti import * from ..frdata import * diff --git a/control/matlab/wrappers.py b/control/matlab/wrappers.py index f7cbaea41..8eafdaad2 100644 --- a/control/matlab/wrappers.py +++ b/control/matlab/wrappers.py @@ -3,7 +3,7 @@ """ import numpy as np -from ..statesp import ss +from ..iosys import ss from ..xferfcn import tf from ..ctrlutil import issys from ..exception import ControlArgument diff --git a/control/sisotool.py b/control/sisotool.py index e6343c91e..b47eb7e40 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -5,7 +5,7 @@ from .timeresp import step_response from .lti import issiso, isdtime from .xferfcn import tf -from .statesp import ss +from .iosys import ss from .bdalg import append, connect from .iosys import tf2io, ss2io, summing_junction, interconnect from control.statesp import _convert_to_statespace, StateSpace diff --git a/control/statesp.py b/control/statesp.py index 0f1c560e2..62c96514c 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -62,8 +62,7 @@ from . import config from copy import deepcopy -__all__ = ['StateSpace', 'ss', 'rss', 'drss', 'tf2ss', 'ssdata'] - +__all__ = ['StateSpace', 'tf2ss', 'ssdata'] # Define module default parameter values _statesp_defaults = { @@ -1768,7 +1767,7 @@ def _mimo2simo(sys, input, warn_conversion=False): return sys -def ss(*args, **kwargs): +def _ss(*args, **kwargs): """ss(A, B, C, D[, dt]) Create a state space system. @@ -1932,89 +1931,6 @@ def tf2ss(*args): raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) -def rss(states=1, outputs=1, inputs=1, strictly_proper=False): - """ - Create a stable *continuous* random state space object. - - Parameters - ---------- - states : int - Number of state variables - outputs : int - Number of system outputs - inputs : int - Number of system inputs - strictly_proper : bool, optional - If set to 'True', returns a proper system (no direct term). - - Returns - ------- - sys : StateSpace - The randomly created linear system - - Raises - ------ - ValueError - if any input is not a positive integer - - See Also - -------- - drss - - Notes - ----- - If the number of states, inputs, or outputs is not specified, then the - missing numbers are assumed to be 1. The poles of the returned system - will always have a negative real part. - - """ - - return _rss_generate(states, inputs, outputs, 'c', - strictly_proper=strictly_proper) - - -def drss(states=1, outputs=1, inputs=1, strictly_proper=False): - """ - Create a stable *discrete* random state space object. - - Parameters - ---------- - states : int - Number of state variables - inputs : integer - Number of system inputs - outputs : int - Number of system outputs - strictly_proper: bool, optional - If set to 'True', returns a proper system (no direct term). - - - Returns - ------- - sys : StateSpace - The randomly created linear system - - Raises - ------ - ValueError - if any input is not a positive integer - - See Also - -------- - rss - - Notes - ----- - If the number of states, inputs, or outputs is not specified, then the - missing numbers are assumed to be 1. The poles of the returned system - will always have a magnitude less than 1. - - """ - - return _rss_generate(states, inputs, outputs, 'd', - strictly_proper=strictly_proper) - - def ssdata(sys): """ Return state space data objects for a system diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 78eacf857..be6cd9a6b 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -18,8 +18,9 @@ from control.config import defaults from control.dtime import sample_system from control.lti import evalfr -from control.statesp import (StateSpace, _convert_to_statespace, drss, - rss, ss, tf2ss, _statesp_defaults, _rss_generate) +from control.statesp import StateSpace, _convert_to_statespace, tf2ss, \ + _statesp_defaults, _rss_generate +from control.iosys import ss, rss, drss from control.tests.conftest import ismatarrayout, slycotonly from control.xferfcn import TransferFunction, ss2tf diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index dadcc587e..d8c2d2b71 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -59,7 +59,7 @@ def sys_dict(): rtype_list = ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'flt'] conversion_table = [ # op left ss tf frd lio ios arr flt - ('add', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), + ('add', 'ss', ['ss', 'ss', 'xrd', 'ss', 'ios', 'ss', 'ss' ]), ('add', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), ('add', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'xrd']), ('add', 'lio', ['lio', 'lio', 'xrd', 'lio', 'ios', 'lio', 'lio']), @@ -68,7 +68,7 @@ def sys_dict(): ('add', 'flt', ['ss', 'tf', 'xrd', 'lio', 'ios', 'arr', 'flt']), # op left ss tf frd lio ios arr flt - ('sub', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), + ('sub', 'ss', ['ss', 'ss', 'xrd', 'ss', 'ios', 'ss', 'ss' ]), ('sub', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), ('sub', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'xrd']), ('sub', 'lio', ['lio', 'lio', 'xrd', 'lio', 'ios', 'lio', 'lio']), @@ -77,7 +77,7 @@ def sys_dict(): ('sub', 'flt', ['ss', 'tf', 'xrd', 'lio', 'ios', 'arr', 'flt']), # op left ss tf frd lio ios arr flt - ('mul', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), + ('mul', 'ss', ['ss', 'ss', 'xrd', 'ss', 'ios', 'ss', 'ss' ]), ('mul', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), ('mul', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'frd']), ('mul', 'lio', ['lio', 'lio', 'xrd', 'lio', 'ios', 'lio', 'lio']), diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index bd073e0f3..7821ce54d 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -8,14 +8,11 @@ import operator import control as ct -from control.statesp import StateSpace, _convert_to_statespace, rss -from control.xferfcn import TransferFunction, _convert_to_transfer_function, \ - ss2tf -from control.lti import evalfr +from control import StateSpace, TransferFunction, rss, ss2tf, evalfr +from control import isctime, isdtime, sample_system, defaults +from control.statesp import _convert_to_statespace +from control.xferfcn import _convert_to_transfer_function from control.tests.conftest import slycotonly, nopython2, matrixfilter -from control.lti import isctime, isdtime -from control.dtime import sample_system -from control.config import defaults class TestXferFcn: From 4b0584d3474bc58023a76688d1af81de3b9e93f1 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 13 Mar 2022 17:18:04 -0700 Subject: [PATCH 18/87] create _NamedIOSystem, _NamedIOStateSystem parent classes --- control/iosys.py | 288 ++++++++++++++-------------------- control/lti.py | 3 +- control/namedio.py | 212 +++++++++++++++++++++++++ control/statesp.py | 97 ++---------- control/tests/iosys_test.py | 4 +- control/tests/namedio_test.py | 51 ++++++ control/timeresp.py | 3 +- control/xferfcn.py | 3 +- 8 files changed, 400 insertions(+), 261 deletions(-) create mode 100644 control/namedio.py create mode 100644 control/tests/namedio_test.py diff --git a/control/iosys.py b/control/iosys.py index 82688e02c..60698f88c 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -31,6 +31,7 @@ import copy from warnings import warn +from .namedio import _NamedIOStateObject, _process_signal_list from .statesp import StateSpace, tf2ss, _convert_to_statespace from .statesp import _ss, _rss_generate from .xferfcn import TransferFunction @@ -54,7 +55,7 @@ } -class InputOutputSystem(object): +class InputOutputSystem(_NamedIOStateObject): """A class for representing input/output systems. The InputOutputSystem class allows (possibly nonlinear) input/output @@ -125,14 +126,6 @@ class for a set of subclasses that are used to implement specific # Allow ndarray * InputOutputSystem to give IOSystem._rmul_() priority __array_priority__ = 12 # override ndarray, matrix, SS types - _idCounter = 0 - - def _name_or_default(self, name=None): - if name is None: - name = "sys[{}]".format(InputOutputSystem._idCounter) - InputOutputSystem._idCounter += 1 - return name - def __init__(self, inputs=None, outputs=None, states=None, params={}, name=None, **kwargs): """Create an input/output system. @@ -145,59 +138,19 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, :class:`~control.InterconnectedSystem`. """ - # Store the input arguments + # Store the system name, inputs, outputs, and states + _NamedIOStateObject.__init__( + self, inputs=inputs, outputs=outputs, states=states, name=name) # default parameters self.params = params.copy() - # timebase - self.dt = kwargs.get('dt', config.defaults['control.default_dt']) - # system name - self.name = self._name_or_default(name) - - # Parse and store the number of inputs, outputs, and states - self.set_inputs(inputs) - self.set_outputs(outputs) - self.set_states(states) - - # - # Class attributes - # - # These attributes are defined as class attributes so that they are - # documented properly. They are "overwritten" in __init__. - # - - #: Number of system inputs. - #: - #: :meta hide-value: - ninputs = 0 - - #: Number of system outputs. - #: - #: :meta hide-value: - noutputs = 0 - - #: Number of system states. - #: - #: :meta hide-value: - nstates = 0 - def __repr__(self): - return str(type(self)) + ": " + self.name if self.name is not None \ - else str(type(self)) + # timebase + self.dt = kwargs.pop('dt', config.defaults['control.default_dt']) - def __str__(self): - """String representation of an input/output system""" - str = "System: " + (self.name if self.name else "(None)") + "\n" - str += "Inputs (%s): " % self.ninputs - for key in self.input_index: - str += key + ", " - str += "\nOutputs (%s): " % self.noutputs - for key in self.output_index: - str += key + ", " - str += "\nStates (%s): " % self.nstates - for key in self.state_index: - str += key + ", " - return str + # Make sure there were no extraneous keyworks + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) def __mul__(sys2, sys1): """Multiply two input/output systems (series interconnection)""" @@ -395,34 +348,6 @@ def __neg__(sys): # Return the newly created system return newsys - def _isstatic(self): - """Check to see if a system is a static system (no states)""" - return self.nstates == 0 - - # Utility function to parse a list of signals - def _process_signal_list(self, signals, prefix='s'): - if signals is None: - # No information provided; try and make it up later - return None, {} - - elif isinstance(signals, int): - # Number of signals given; make up the names - return signals, {'%s[%d]' % (prefix, i): i for i in range(signals)} - - elif isinstance(signals, str): - # Single string given => single signal with given name - return 1, {signals: 0} - - elif all(isinstance(s, str) for s in signals): - # Use the list of strings as the signal names - return len(signals), {signals[i]: i for i in range(len(signals))} - - else: - raise TypeError("Can't parse signal list %s" % str(signals)) - - # Find a signal by name - def _find_signal(self, name, sigdict): return sigdict.get(name, None) - # Update parameters used for _rhs, _out (used by subclasses) def _update_params(self, params, warning=False): if warning: @@ -510,82 +435,6 @@ def output(self, t, x, u): """ return self._out(t, x, u) - def set_inputs(self, inputs, prefix='u'): - """Set the number/names of the system inputs. - - Parameters - ---------- - inputs : int, list of str, or None - Description of the system inputs. This can be given as an integer - count or as a list of strings that name the individual signals. - If an integer count is specified, the names of the signal will be - of the form `u[i]` (where the prefix `u` can be changed using the - optional prefix parameter). - prefix : string, optional - If `inputs` is an integer, create the names of the states using - the given prefix (default = 'u'). The names of the input will be - of the form `prefix[i]`. - - """ - self.ninputs, self.input_index = \ - self._process_signal_list(inputs, prefix=prefix) - - def set_outputs(self, outputs, prefix='y'): - """Set the number/names of the system outputs. - - Parameters - ---------- - outputs : int, list of str, or None - Description of the system outputs. This can be given as an integer - count or as a list of strings that name the individual signals. - If an integer count is specified, the names of the signal will be - of the form `u[i]` (where the prefix `u` can be changed using the - optional prefix parameter). - prefix : string, optional - If `outputs` is an integer, create the names of the states using - the given prefix (default = 'y'). The names of the input will be - of the form `prefix[i]`. - - """ - self.noutputs, self.output_index = \ - self._process_signal_list(outputs, prefix=prefix) - - def set_states(self, states, prefix='x'): - """Set the number/names of the system states. - - Parameters - ---------- - states : int, list of str, or None - Description of the system states. This can be given as an integer - count or as a list of strings that name the individual signals. - If an integer count is specified, the names of the signal will be - of the form `u[i]` (where the prefix `u` can be changed using the - optional prefix parameter). - prefix : string, optional - If `states` is an integer, create the names of the states using - the given prefix (default = 'x'). The names of the input will be - of the form `prefix[i]`. - - """ - self.nstates, self.state_index = \ - self._process_signal_list(states, prefix=prefix) - - def find_input(self, name): - """Find the index for an input given its name (`None` if not found)""" - return self.input_index.get(name, None) - - def find_output(self, name): - """Find the index for an output given its name (`None` if not found)""" - return self.output_index.get(name, None) - - def find_state(self, name): - """Find the index for a state given its name (`None` if not found)""" - return self.state_index.get(name, None) - - def issiso(self): - """Check to see if a system is single input, single output""" - return self.ninputs == 1 and self.noutputs == 1 - def feedback(self, other=1, sign=-1, params={}): """Feedback interconnection between two input/output systems @@ -801,6 +650,7 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, "or transfer function object") # Look for 'input' and 'output' parameter name variants + states = _parse_signal_parameter(states, 'state', kwargs) inputs = _parse_signal_parameter(inputs, 'input', kwargs) outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) @@ -814,15 +664,15 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, # Process input, output, state lists, if given # Make sure they match the size of the linear system - ninputs, self.input_index = self._process_signal_list( + ninputs, self.input_index = _process_signal_list( inputs if inputs is not None else linsys.ninputs, prefix='u') if ninputs is not None and linsys.ninputs != ninputs: raise ValueError("Wrong number/type of inputs given.") - noutputs, self.output_index = self._process_signal_list( + noutputs, self.output_index = _process_signal_list( outputs if outputs is not None else linsys.noutputs, prefix='y') if noutputs is not None and linsys.noutputs != noutputs: raise ValueError("Wrong number/type of outputs given.") - nstates, self.state_index = self._process_signal_list( + nstates, self.state_index = _process_signal_list( states if states is not None else linsys.nstates, prefix='x') if nstates is not None and linsys.nstates != nstates: raise ValueError("Wrong number/type of states given.") @@ -1110,12 +960,12 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[], # If input or output list was specified, update it if inputs is not None: nsignals, self.input_index = \ - self._process_signal_list(inputs, prefix='u') + _process_signal_list(inputs, prefix='u') if nsignals is not None and len(inplist) != nsignals: raise ValueError("Wrong number/type of inputs given.") if outputs is not None: nsignals, self.output_index = \ - self._process_signal_list(outputs, prefix='y') + _process_signal_list(outputs, prefix='y') if nsignals is not None and len(outlist) != nsignals: raise ValueError("Wrong number/type of outputs given.") @@ -2269,11 +2119,89 @@ def _find_size(sysval, vecval): # Define a state space object that is an I/O system def ss(*args, **kwargs): - return LinearIOSystem(_ss(*args, **kwargs)) -ss.__doc__ = _ss.__doc__ + """ss(A, B, C, D[, dt]) + + Create a state space system. + + The function accepts either 1, 4 or 5 parameters: + + ``ss(sys)`` + Convert a linear system into space system form. Always creates a + new system, even if sys is already a state space system. + + ``ss(A, B, C, D)`` + Create a state space system from the matrices of its state and + output equations: + + .. math:: + \\dot x = A \\cdot x + B \\cdot u + + y = C \\cdot x + D \\cdot u + + ``ss(A, B, C, D, dt)`` + Create a discrete-time state space system from the matrices of + its state and output equations: + .. math:: + x[k+1] = A \\cdot x[k] + B \\cdot u[k] + + y[k] = C \\cdot x[k] + D \\cdot u[ki] + + The matrices can be given as *array like* data types or strings. + Everything that the constructor of :class:`numpy.matrix` accepts is + permissible here too. + + Parameters + ---------- + sys : StateSpace or TransferFunction + A linear system. + A, B, C, D : array_like or string + System, control, output, and feed forward matrices. + dt : None, True or float, optional + System timebase. 0 (default) indicates continuous + time, True indicates discrete time with unspecified sampling + time, positive number is discrete time with specified + sampling time, None indicates unspecified timebase (either + continuous or discrete time). + inputs, outputs, states : str, or list of str, optional + List of strings that name the individual signals. If this parameter + is not given or given as `None`, the signal names will be of the + form `s[i]` (where `s` is one of `u`, `y`, or `x`). + name : string, optional + System name (used for specifying signals). If unspecified, a generic + name is generated with a unique integer id. + + Returns + ------- + out: :class:`LinearIOSystem` + Linear input/output system. + + Raises + ------ + ValueError + If matrix sizes are not self-consistent. + + See Also + -------- + tf + ss2tf + tf2ss + + Examples + -------- + >>> # Create a Linear I/O system object from from for matrices + >>> sys1 = ss([[1, -2], [3 -4]], [[5], [7]], [[6, 8]], [[9]]) + + >>> # Convert a TransferFunction to a StateSpace object. + >>> sys_tf = tf([2.], [1., 3]) + >>> sys2 = ss(sys_tf) -def rss(states=1, outputs=1, inputs=1, strictly_proper=False): + """ + sys = _ss(*args, keywords=kwargs) + return LinearIOSystem(sys, **kwargs) + + +def rss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs): """ Create a stable *continuous* random state space object. @@ -2309,12 +2237,18 @@ def rss(states=1, outputs=1, inputs=1, strictly_proper=False): will always have a negative real part. """ + # Process states, inputs, outputs (ignoring names) + nstates, _ = _process_signal_list(states) + ninputs, _ = _process_signal_list(inputs) + noutputs, _ = _process_signal_list(outputs) - return LinearIOSystem(_rss_generate( - states, inputs, outputs, 'c', strictly_proper=strictly_proper)) + sys = _rss_generate( + nstates, ninputs, noutputs, 'c', strictly_proper=strictly_proper) + return LinearIOSystem( + sys, states=states, inputs=inputs, outputs=outputs, **kwargs) -def drss(states=1, outputs=1, inputs=1, strictly_proper=False): +def drss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs): """ Create a stable *discrete* random state space object. @@ -2350,9 +2284,15 @@ def drss(states=1, outputs=1, inputs=1, strictly_proper=False): will always have a magnitude less than 1. """ + # Process states, inputs, outputs (ignoring names) + nstates, _ = _process_signal_list(states) + ninputs, _ = _process_signal_list(inputs) + noutputs, _ = _process_signal_list(outputs) - return LinearIOSystem(_rss_generate( - states, inputs, outputs, 'd', strictly_proper=strictly_proper)) + sys = _rss_generate( + nstates, ninputs, noutputs, 'd', strictly_proper=strictly_proper) + return LinearIOSystem( + sys, states=states, inputs=inputs, outputs=outputs, **kwargs) # Convert a state space system into an input/output system (wrapper) diff --git a/control/lti.py b/control/lti.py index b56c2bb44..3615a06c1 100644 --- a/control/lti.py +++ b/control/lti.py @@ -16,12 +16,13 @@ from numpy import absolute, real, angle, abs from warnings import warn from . import config +from .namedio import _NamedIOObject __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', 'isdtime', 'isctime', 'pole', 'zero', 'damp', 'evalfr', 'freqresp', 'dcgain'] -class LTI: +class LTI(_NamedIOObject): """LTI is a parent class to linear time-invariant (LTI) system objects. LTI is the parent to the StateSpace and TransferFunction child classes. It diff --git a/control/namedio.py b/control/namedio.py new file mode 100644 index 000000000..0eb189789 --- /dev/null +++ b/control/namedio.py @@ -0,0 +1,212 @@ +# namedio.py - internal named I/O object class +# RMM, 13 Mar 2022 +# +# This file implements the _NamedIOObject and _NamedIOStateObject classes, +# which are used as a parent classes for FrequencyResponseData, +# InputOutputSystem, LTI, TimeResponseData, and other similar classes to +# allow naming of signals. + +import numpy as np + +class _NamedIOObject(object): + _idCounter = 0 + + def _name_or_default(self, name=None): + if name is None: + name = "sys[{}]".format(_NamedIOObject._idCounter) + _NamedIOObject._idCounter += 1 + return name + + def __init__( + self, inputs=None, outputs=None, name=None): + + # system name + self.name = self._name_or_default(name) + + # Parse and store the number of inputs and outputs + self.set_inputs(inputs) + self.set_outputs(outputs) + + # + # Class attributes + # + # These attributes are defined as class attributes so that they are + # documented properly. They are "overwritten" in __init__. + # + + #: Number of system inputs. + #: + #: :meta hide-value: + ninputs = 0 + + #: Number of system outputs. + #: + #: :meta hide-value: + noutputs = 0 + + def __repr__(self): + return str(type(self)) + ": " + self.name if self.name is not None \ + else str(type(self)) + + def __str__(self): + """String representation of an input/output object""" + str = "Object: " + (self.name if self.name else "(None)") + "\n" + str += "Inputs (%s): " % self.ninputs + for key in self.input_index: + str += key + ", " + str += "\nOutputs (%s): " % self.noutputs + for key in self.output_index: + str += key + ", " + return str + + # Find a signal by name + def _find_signal(self, name, sigdict): + return sigdict.get(name, None) + + def set_inputs(self, inputs, prefix='u'): + """Set the number/names of the system inputs. + + Parameters + ---------- + inputs : int, list of str, or None + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `u[i]` (where the prefix `u` can be changed using the + optional prefix parameter). + prefix : string, optional + If `inputs` is an integer, create the names of the states using + the given prefix (default = 'u'). The names of the input will be + of the form `prefix[i]`. + + """ + self.ninputs, self.input_index = \ + _process_signal_list(inputs, prefix=prefix) + + def find_input(self, name): + """Find the index for an input given its name (`None` if not found)""" + return self.input_index.get(name, None) + + # Property for getting and setting list of input signals + input_list = property( + lambda self: list(self.input_index.keys()), # getter + set_inputs) # setter + + def set_outputs(self, outputs, prefix='y'): + """Set the number/names of the system outputs. + + Parameters + ---------- + outputs : int, list of str, or None + Description of the system outputs. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `u[i]` (where the prefix `u` can be changed using the + optional prefix parameter). + prefix : string, optional + If `outputs` is an integer, create the names of the states using + the given prefix (default = 'y'). The names of the input will be + of the form `prefix[i]`. + + """ + self.noutputs, self.output_index = \ + _process_signal_list(outputs, prefix=prefix) + + def find_output(self, name): + """Find the index for an output given its name (`None` if not found)""" + return self.output_index.get(name, None) + + # Property for getting and setting list of output signals + output_list = property( + lambda self: list(self.output_index.keys()), # getter + set_outputs) # setter + + def issiso(self): + """Check to see if a system is single input, single output""" + return self.ninputs == 1 and self.noutputs == 1 + + +class _NamedIOStateObject(_NamedIOObject): + def __init__( + self, inputs=None, outputs=None, states=None, name=None): + # Parse and store the system name, inputs, and outputs + _NamedIOObject.__init__( + self, inputs=inputs, outputs=outputs, name=name) + + # Parse and store the number of states + self.set_states(states) + + + # + # Class attributes + # + # These attributes are defined as class attributes so that they are + # documented properly. They are "overwritten" in __init__. + # + + #: Number of system states. + #: + #: :meta hide-value: + nstates = 0 + + def __str__(self): + """String representation of an input/output system""" + str = _NamedIOObject.__str__(self) + str += "\nStates (%s): " % self.nstates + for key in self.state_index: + str += key + ", " + return str + + def _isstatic(self): + """Check to see if a system is a static system (no states)""" + return self.nstates == 0 + + def set_states(self, states, prefix='x'): + """Set the number/names of the system states. + + Parameters + ---------- + states : int, list of str, or None + Description of the system states. This can be given as an integer + count or as a list of strings that name the individual signals. + If an integer count is specified, the names of the signal will be + of the form `u[i]` (where the prefix `u` can be changed using the + optional prefix parameter). + prefix : string, optional + If `states` is an integer, create the names of the states using + the given prefix (default = 'x'). The names of the input will be + of the form `prefix[i]`. + + """ + self.nstates, self.state_index = \ + _process_signal_list(states, prefix=prefix) + + def find_state(self, name): + """Find the index for a state given its name (`None` if not found)""" + return self.state_index.get(name, None) + + # Property for getting and setting list of state signals + state_list = property( + lambda self: list(self.state_index.keys()), # getter + set_states) # setter + +# Utility function to parse a list of signals +def _process_signal_list(signals, prefix='s'): + if signals is None: + # No information provided; try and make it up later + return None, {} + + elif isinstance(signals, (int, np.integer)): + # Number of signals given; make up the names + return signals, {'%s[%d]' % (prefix, i): i for i in range(signals)} + + elif isinstance(signals, str): + # Single string given => single signal with given name + return 1, {signals: 0} + + elif all(isinstance(s, str) for s in signals): + # Use the list of strings as the signal names + return len(signals), {signals[i]: i for i in range(len(signals))} + + else: + raise TypeError("Can't parse signal list %s" % str(signals)) diff --git a/control/statesp.py b/control/statesp.py index 62c96514c..0484cef17 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -59,6 +59,7 @@ from scipy.signal import StateSpace as signalStateSpace from warnings import warn from .lti import LTI, common_timebase, isdtime, _process_frequency_response +from .namedio import _NamedIOStateObject, _process_signal_list from . import config from copy import deepcopy @@ -152,7 +153,7 @@ def _f2s(f): return s -class StateSpace(LTI): +class StateSpace(LTI, _NamedIOStateObject): """StateSpace(A, B, C, D[, dt]) A class for representing state-space models. @@ -243,7 +244,7 @@ class StateSpace(LTI): # Allow ndarray * StateSpace to give StateSpace._rmul_() priority __array_priority__ = 11 # override ndarray and matrix types - def __init__(self, *args, **kwargs): + def __init__(self, *args, keywords=None, **kwargs): """StateSpace(A, B, C, D[, dt]) Construct a state space object. @@ -262,6 +263,10 @@ def __init__(self, *args, **kwargs): (default = False). """ + # Use keywords object if we received one (and pop keywords we use) + if keywords is None: + keywords = kwargs + # first get A, B, C, D matrices if len(args) == 4: # The user provided A, B, C, and D matrices. @@ -284,7 +289,7 @@ def __init__(self, *args, **kwargs): "Expected 1, 4, or 5 arguments; received %i." % len(args)) # Process keyword arguments - remove_useless_states = kwargs.get( + remove_useless_states = keywords.pop( 'remove_useless_states', config.defaults['statesp.remove_useless_states']) @@ -313,17 +318,18 @@ def __init__(self, *args, **kwargs): # now set dt if len(args) == 4: - if 'dt' in kwargs: - dt = kwargs['dt'] + if 'dt' in keywords: + dt = keywords.pop('dt') elif self._isstatic(): dt = None else: dt = config.defaults['control.default_dt'] elif len(args) == 5: dt = args[4] - if 'dt' in kwargs: + if 'dt' in keywords: warn("received multiple dt arguments, " "using positional arg dt = %s" % dt) + keywords.pop('dt') elif len(args) == 1: try: dt = args[0].dt @@ -1767,83 +1773,10 @@ def _mimo2simo(sys, input, warn_conversion=False): return sys -def _ss(*args, **kwargs): - """ss(A, B, C, D[, dt]) - - Create a state space system. - - The function accepts either 1, 4 or 5 parameters: - - ``ss(sys)`` - Convert a linear system into space system form. Always creates a - new system, even if sys is already a StateSpace object. - - ``ss(A, B, C, D)`` - Create a state space system from the matrices of its state and - output equations: - - .. math:: - \\dot x = A \\cdot x + B \\cdot u - - y = C \\cdot x + D \\cdot u - - ``ss(A, B, C, D, dt)`` - Create a discrete-time state space system from the matrices of - its state and output equations: - - .. math:: - x[k+1] = A \\cdot x[k] + B \\cdot u[k] - - y[k] = C \\cdot x[k] + D \\cdot u[ki] - - The matrices can be given as *array like* data types or strings. - Everything that the constructor of :class:`numpy.matrix` accepts is - permissible here too. - - Parameters - ---------- - sys: StateSpace or TransferFunction - A linear system - A: array_like or string - System matrix - B: array_like or string - Control matrix - C: array_like or string - Output matrix - D: array_like or string - Feed forward matrix - dt: If present, specifies the timebase of the system - - Returns - ------- - out: :class:`StateSpace` - The new linear system - - Raises - ------ - ValueError - if matrix sizes are not self-consistent - - See Also - -------- - StateSpace - tf - ss2tf - tf2ss - - Examples - -------- - >>> # Create a StateSpace object from four "matrices". - >>> sys1 = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") - - >>> # Convert a TransferFunction to a StateSpace object. - >>> sys_tf = tf([2.], [1., 3]) - >>> sys2 = ss(sys_tf) - - """ - +def _ss(*args, keywords=None, **kwargs): + """Internal function to create StateSpace system""" if len(args) == 4 or len(args) == 5: - return StateSpace(*args, **kwargs) + return StateSpace(*args, keywords=keywords, **kwargs) elif len(args) == 1: from .xferfcn import TransferFunction sys = args[0] diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 5fd83e946..1aac53005 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1016,7 +1016,7 @@ def test_sys_naming_convention(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.InputOutputSystem._idCounter = 0 + ct.namedio._NamedIOObject._idCounter = 0 sys = ct.LinearIOSystem(tsys.mimo_linsys1) assert sys.name == "sys[0]" @@ -1080,7 +1080,7 @@ def test_signals_naming_convention_0_8_4(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.InputOutputSystem._idCounter = 0 + ct.namedio._NamedIOObject._idCounter = 0 sys = ct.LinearIOSystem(tsys.mimo_linsys1) for statename in ["x[0]", "x[1]"]: assert statename in sys.state_index diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py new file mode 100644 index 000000000..48b4a3303 --- /dev/null +++ b/control/tests/namedio_test.py @@ -0,0 +1,51 @@ +"""namedio_test.py - test named input/output object operations + +RMM, 13 Mar 2022 + +This test suite checks to make sure that named input/output class +operations are working. It doesn't do exhaustive testing of +operations on input/output objects. Separate unit tests should be +created for that purpose. +""" + +import re + +import numpy as np +import control as ct +import pytest + +def test_named_ss(): + # Create a system to play with + sys = ct.rss(2, 2, 2) + assert sys.input_list == ['u[0]', 'u[1]'] + assert sys.output_list == ['y[0]', 'y[1]'] + assert sys.state_list == ['x[0]', 'x[1]'] + + # Get the state matrices for later use + A, B, C, D = sys.A, sys.B, sys.C, sys.D + + # Set up a named state space systems with default names + ct.namedio._NamedIOObject._idCounter = 0 + sys = ct.ss(A, B, C, D) + assert sys.name == 'sys[0]' + assert sys.input_list == ['u[0]', 'u[1]'] + assert sys.output_list == ['y[0]', 'y[1]'] + assert sys.state_list == ['x[0]', 'x[1]'] + + # Pass the names as arguments + sys = ct.ss( + A, B, C, D, name='system', + inputs=['u1', 'u2'], outputs=['y1', 'y2'], states=['x1', 'x2']) + assert sys.name == 'system' + assert ct.namedio._NamedIOObject._idCounter == 1 + assert sys.input_list == ['u1', 'u2'] + assert sys.output_list == ['y1', 'y2'] + assert sys.state_list == ['x1', 'x2'] + + # Do the same with rss + sys = ct.rss(['x1', 'x2', 'x3'], ['y1', 'y2'], 'u1', name='random') + assert sys.name == 'random' + assert ct.namedio._NamedIOObject._idCounter == 1 + assert sys.input_list == ['u1'] + assert sys.output_list == ['y1', 'y2'] + assert sys.state_list == ['x1', 'x2', 'x3'] diff --git a/control/timeresp.py b/control/timeresp.py index 3f3eacc27..998b5a1f9 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -80,6 +80,7 @@ from . import config from .lti import isctime, isdtime +from .namedio import _NamedIOStateObject from .statesp import StateSpace, _convert_to_statespace, _mimo2simo, _mimo2siso from .xferfcn import TransferFunction @@ -87,7 +88,7 @@ 'initial_response', 'impulse_response', 'TimeResponseData'] -class TimeResponseData(): +class TimeResponseData(_NamedIOStateObject): """A class for returning time responses. This class maintains and manipulates the data corresponding to the diff --git a/control/xferfcn.py b/control/xferfcn.py index fd859f675..96e0ce2db 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -243,7 +243,7 @@ def __init__(self, *args, **kwargs): if len(args) == 2: # no dt given in positional arguments if 'dt' in kwargs: - dt = kwargs['dt'] + dt = kwargs.pop('dt') elif self._isstatic(): dt = None else: @@ -253,6 +253,7 @@ def __init__(self, *args, **kwargs): if 'dt' in kwargs: warn('received multiple dt arguments, ' 'using positional arg dt=%s' % dt) + kwargs.pop('dt') elif len(args) == 1: # TODO: not sure this can ever happen since dt is always present try: From 2b13747f45c2bcd5109a1964b31ff3f4bc0342ef Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 14 Mar 2022 23:03:21 -0700 Subject: [PATCH 19/87] add create_statefbk_iosystem + unit tests --- control/iosys.py | 13 ++- control/lti.py | 3 +- control/namedio.py | 31 +++--- control/statefbk.py | 191 ++++++++++++++++++++++++++++++++- control/statesp.py | 4 +- control/tests/iosys_test.py | 4 +- control/tests/namedio_test.py | 30 +++--- control/tests/statefbk_test.py | 111 +++++++++++++++++++ control/timeresp.py | 3 +- 9 files changed, 344 insertions(+), 46 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 60698f88c..142fdf0cc 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -31,7 +31,7 @@ import copy from warnings import warn -from .namedio import _NamedIOStateObject, _process_signal_list +from .namedio import _NamedIOStateSystem, _process_signal_list from .statesp import StateSpace, tf2ss, _convert_to_statespace from .statesp import _ss, _rss_generate from .xferfcn import TransferFunction @@ -55,7 +55,7 @@ } -class InputOutputSystem(_NamedIOStateObject): +class InputOutputSystem(_NamedIOStateSystem): """A class for representing input/output systems. The InputOutputSystem class allows (possibly nonlinear) input/output @@ -139,7 +139,7 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, """ # Store the system name, inputs, outputs, and states - _NamedIOStateObject.__init__( + _NamedIOStateSystem.__init__( self, inputs=inputs, outputs=outputs, states=states, name=name) # default parameters @@ -886,7 +886,7 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[], # Look for 'input' and 'output' parameter name variants inputs = _parse_signal_parameter(inputs, 'input', kwargs) - outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) + outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) # Convert input and output names to lists if they aren't already if not isinstance(inplist, (list, tuple)): @@ -2526,9 +2526,8 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], raise ValueError('check_unused is False, but either ' + 'ignore_inputs or ignore_outputs non-empty') - if (connections is False - and not inplist and not outlist - and not inputs and not outputs): + if connections is False and not inplist and not outlist \ + and not inputs and not outputs: # user has disabled auto-connect, and supplied neither input # nor output mappings; assume they know what they're doing check_unused = False diff --git a/control/lti.py b/control/lti.py index 3615a06c1..b56c2bb44 100644 --- a/control/lti.py +++ b/control/lti.py @@ -16,13 +16,12 @@ from numpy import absolute, real, angle, abs from warnings import warn from . import config -from .namedio import _NamedIOObject __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', 'isdtime', 'isctime', 'pole', 'zero', 'damp', 'evalfr', 'freqresp', 'dcgain'] -class LTI(_NamedIOObject): +class LTI: """LTI is a parent class to linear time-invariant (LTI) system objects. LTI is the parent to the StateSpace and TransferFunction child classes. It diff --git a/control/namedio.py b/control/namedio.py index 0eb189789..aca5edc5a 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -1,20 +1,21 @@ # namedio.py - internal named I/O object class # RMM, 13 Mar 2022 # -# This file implements the _NamedIOObject and _NamedIOStateObject classes, +# This file implements the _NamedIOSystem and _NamedIOStateSystem classes, # which are used as a parent classes for FrequencyResponseData, # InputOutputSystem, LTI, TimeResponseData, and other similar classes to # allow naming of signals. import numpy as np -class _NamedIOObject(object): + +class _NamedIOSystem(object): _idCounter = 0 def _name_or_default(self, name=None): if name is None: - name = "sys[{}]".format(_NamedIOObject._idCounter) - _NamedIOObject._idCounter += 1 + name = "sys[{}]".format(_NamedIOSystem._idCounter) + _NamedIOSystem._idCounter += 1 return name def __init__( @@ -38,7 +39,7 @@ def __init__( #: #: :meta hide-value: ninputs = 0 - + #: Number of system outputs. #: #: :meta hide-value: @@ -88,7 +89,7 @@ def find_input(self, name): return self.input_index.get(name, None) # Property for getting and setting list of input signals - input_list = property( + input_labels = property( lambda self: list(self.input_index.keys()), # getter set_inputs) # setter @@ -117,7 +118,7 @@ def find_output(self, name): return self.output_index.get(name, None) # Property for getting and setting list of output signals - output_list = property( + output_labels = property( lambda self: list(self.output_index.keys()), # getter set_outputs) # setter @@ -125,18 +126,17 @@ def issiso(self): """Check to see if a system is single input, single output""" return self.ninputs == 1 and self.noutputs == 1 - -class _NamedIOStateObject(_NamedIOObject): + +class _NamedIOStateSystem(_NamedIOSystem): def __init__( self, inputs=None, outputs=None, states=None, name=None): # Parse and store the system name, inputs, and outputs - _NamedIOObject.__init__( + _NamedIOSystem.__init__( self, inputs=inputs, outputs=outputs, name=name) - + # Parse and store the number of states self.set_states(states) - # # Class attributes # @@ -151,12 +151,12 @@ def __init__( def __str__(self): """String representation of an input/output system""" - str = _NamedIOObject.__str__(self) + str = _NamedIOSystem.__str__(self) str += "\nStates (%s): " % self.nstates for key in self.state_index: str += key + ", " return str - + def _isstatic(self): """Check to see if a system is a static system (no states)""" return self.nstates == 0 @@ -186,10 +186,11 @@ def find_state(self, name): return self.state_index.get(name, None) # Property for getting and setting list of state signals - state_list = property( + state_labels = property( lambda self: list(self.state_index.keys()), # getter set_states) # setter + # Utility function to parse a list of signals def _process_signal_list(signals, prefix='s'): if signals is None: diff --git a/control/statefbk.py b/control/statefbk.py index ef16cbfff..935b15ff9 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -46,6 +46,8 @@ from .mateqn import care, dare, _check_shape from .statesp import StateSpace, _ssmatrix, _convert_to_statespace from .lti import LTI, isdtime, isctime +from .iosys import InputOutputSystem, NonlinearIOSystem, LinearIOSystem, \ + interconnect, ss from .exception import ControlSlycot, ControlArgument, ControlDimension, \ ControlNotImplemented @@ -69,7 +71,7 @@ def sb03md(n, C, A, U, dico, job='X',fact='N',trana='N',ldwork=None): __all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr', 'lqe', - 'dlqr', 'dlqe', 'acker'] + 'dlqr', 'dlqe', 'acker', 'create_statefbk_iosystem'] # Pole placement @@ -785,6 +787,193 @@ def dlqr(*args, **keywords): return _ssmatrix(K), _ssmatrix(S), E +# Function to create an I/O sytems representing a state feedback controller +def create_statefbk_iosystem( + sys, K, integral_action=None, xd_labels='xd[{i}]', ud_labels='ud[{i}]', + estimator=None, type='linear'): + """Create an I/O system using a (full) state feedback controller + + This function creates an input/output system that implements a + state feedback controller of the form + + u = ud - K_p (x - xd) - K_i integral(C x - C x_d) + + It can be called in the form + + ctrl, clsys = ct.create_statefbk_iosystem(sys, K) + + where ``sys`` is the process dynamics and ``K`` is the state (+ integral) + feedback gain (eg, from LQR). The function returns the controller + ``ctrl`` and the closed loop systems ``clsys``, both as I/O systems. + + Parameters + ---------- + sys : InputOutputSystem + The I/O system that represents the process dynamics. If no estimator + is given, the output of this system should represent the full state. + + K : ndarray + The state feedback gain. This matrix defines the gains to be + applied to the system. If ``integral_action`` is None, then the + dimensions of this array should be (sys.ninputs, sys.nstates). If + `integral action` is set to a matrix or a function, then additional + columns represent the gains of the integral states of the + controller. + + xd_labels, ud_labels : str or list of str, optional + Set the name of the signals to use for the desired state and inputs. + If a single string is specified, it should be a format string using + the variable ``i`` as an index. Otherwise, a list of strings matching + the size of xd and ud, respectively, should be used. Default is + ``'xd[{i}]'`` for xd_labels and ``'xd[{i}]'`` for ud_labels. + + integral_action : None, ndarray, or func, optional + If this keyword is specified, the controller can include integral + action in addition to state feedback. If ``integral_action`` is an + ndarray, it will be multiplied by the current and desired state to + generate the error for the internal integrator states of the control + law. If ``integral_action`` is a function ``h``, that function will + be called with the signature h(t, x, u, params) to obtain the + outputs that should be integrated. The number of outputs that are + to be integrated must match the number of additional columns in the + ``K`` matrix. + + estimator : InputOutputSystem, optional + If an estimator is provided, using the states of the estimator as + the system inputs for the controller. + + type : 'nonlinear' or 'linear', optional + Set the type of controller to create. The default is a linear + controller implementing the LQR regulator. If the type is 'nonlinear', + a :class:NonlinearIOSystem is created instead, with the gain ``K`` as + a parameter (allowing modifications of the gain at runtime). + + Returns + ------- + ctrl : InputOutputSystem + Input/output system representing the controller. This system takes + as inputs the desired state xd, the desired input ud, and the system + state x. It outputs the controller action u according to the + formula u = ud - K(x - xd). If the keyword `integral_action` is + specified, then an additional set of integrators is included in the + control system (with the gain matrix K having the integral gains + appended after the state gains). + + clsys : InputOutputSystem + Input/output system representing the closed loop system. This + systems takes as inputs the desired trajectory (xd, ud) and outputs + the system state x and the applied input u (vertically stacked). + + """ + # Make sure that we were passed an I/O system as an input + if not isinstance(sys, InputOutputSystem): + raise ControlArgument("Input system must be I/O system") + + # See whether we were give an estimator + if estimator is not None: + # Check to make sure the estimator is the right size + if estimator.noutputs != sys.nstates: + raise ControlArgument("Estimator output size must match state") + elif sys.noutputs != sys.nstates: + # If no estimator, make sure that the system has all states as outputs + # TODO: check to make sure output map is the identity + raise ControlArgument("System output must be the full state") + else: + # Use the system directly instead of an estimator + estimator = sys + + # See whether we should implement integral action + nintegrators = 0 + if integral_action is not None: + if not isinstance(integral_action, np.ndarray): + raise ControlArgument("Integral action must pass an array") + elif integral_action.shape[1] != sys.nstates: + raise ControlArgument( + "Integral gain output size must match system input size") + else: + nintegrators = integral_action.shape[0] + C = integral_action + else: + # Create a C matrix with no outputs, just in case update gets called + C = np.zeros((0, sys.nstates)) + + # Check to make sure that state feedback has the right shape + if not isinstance(K, np.ndarray) or \ + K.shape != (sys.ninputs, estimator.noutputs + nintegrators): + raise ControlArgument( + f'Control gain must be an array of size {sys.ninputs}' + f'x {sys.nstates}' + + (f'+{nintegrators}' if nintegrators > 0 else '')) + + # Figure out the labels to use + if isinstance(xd_labels, str): + # Gnerate the list of labels using the argument as a format string + xd_labels = [xd_labels.format(i=i) for i in range(sys.nstates)] + + if isinstance(ud_labels, str): + # Gnerate the list of labels using the argument as a format string + ud_labels = [ud_labels.format(i=i) for i in range(sys.ninputs)] + + # Define the controller system + if type == 'nonlinear': + # Create an I/O system for the state feedback gains + def _control_update(t, x, inputs, params): + # Split input into desired state, nominal input, and current state + xd_vec = inputs[0:sys.nstates] + x_vec = inputs[-estimator.nstates:] + + # Compute the integral error in the xy coordinates + return C @ x_vec - C @ xd_vec + + def _control_output(t, e, z, params): + K = params.get('K') + + # Split input into desired state, nominal input, and current state + xd_vec = z[0:sys.nstates] + ud_vec = z[sys.nstates:sys.nstates + sys.ninputs] + x_vec = z[-sys.nstates:] + + # Compute the control law + u = ud_vec - K[:, 0:sys.nstates] @ (x_vec - xd_vec) + if nintegrators > 0: + u -= K[:, sys.nstates:] @ e + + return u + + ctrl = NonlinearIOSystem( + _control_update, _control_output, name='control', + inputs=xd_labels + ud_labels + estimator.output_labels, + outputs=list(sys.input_index.keys()), params={'K': K}, + states=nintegrators) + + elif type == 'linear' or type is None: + # Create the matrices implementing the controller + A_lqr = np.zeros((C.shape[0], C.shape[0])) + B_lqr = np.hstack([-C, np.zeros((C.shape[0], sys.ninputs)), C]) + C_lqr = -K[:, sys.nstates:] + D_lqr = np.hstack([ + K[:, 0:sys.nstates], np.eye(sys.ninputs), -K[:, 0:sys.nstates] + ]) + + ctrl = ss( + A_lqr, B_lqr, C_lqr, D_lqr, dt=sys.dt, name='control', + inputs=xd_labels + ud_labels + estimator.output_labels, + outputs=list(sys.input_index.keys()), states=nintegrators) + + else: + raise ControlArgument(f"unknown type '{type}'") + + # Define the closed loop system + closed = interconnect( + [sys, ctrl] if estimator == sys else [sys, ctrl, estimator], + name=sys.name + "_" + ctrl.name, + inplist=xd_labels + ud_labels, inputs=xd_labels + ud_labels, + outlist=sys.output_labels + sys.input_labels, + outputs=sys.output_labels + sys.input_labels + ) + return ctrl, closed + + def ctrb(A, B): """Controllabilty matrix diff --git a/control/statesp.py b/control/statesp.py index 0484cef17..c847180a1 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -59,7 +59,7 @@ from scipy.signal import StateSpace as signalStateSpace from warnings import warn from .lti import LTI, common_timebase, isdtime, _process_frequency_response -from .namedio import _NamedIOStateObject, _process_signal_list +from .namedio import _NamedIOStateSystem, _process_signal_list from . import config from copy import deepcopy @@ -153,7 +153,7 @@ def _f2s(f): return s -class StateSpace(LTI, _NamedIOStateObject): +class StateSpace(LTI, _NamedIOStateSystem): """StateSpace(A, B, C, D[, dt]) A class for representing state-space models. diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 1aac53005..d8fcc7e56 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1016,7 +1016,7 @@ def test_sys_naming_convention(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.namedio._NamedIOObject._idCounter = 0 + ct.namedio._NamedIOSystem._idCounter = 0 sys = ct.LinearIOSystem(tsys.mimo_linsys1) assert sys.name == "sys[0]" @@ -1080,7 +1080,7 @@ def test_signals_naming_convention_0_8_4(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.namedio._NamedIOObject._idCounter = 0 + ct.namedio._NamedIOSystem._idCounter = 0 sys = ct.LinearIOSystem(tsys.mimo_linsys1) for statename in ["x[0]", "x[1]"]: assert statename in sys.state_index diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 48b4a3303..9278136b5 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -17,35 +17,35 @@ def test_named_ss(): # Create a system to play with sys = ct.rss(2, 2, 2) - assert sys.input_list == ['u[0]', 'u[1]'] - assert sys.output_list == ['y[0]', 'y[1]'] - assert sys.state_list == ['x[0]', 'x[1]'] + assert sys.input_labels == ['u[0]', 'u[1]'] + assert sys.output_labels == ['y[0]', 'y[1]'] + assert sys.state_labels == ['x[0]', 'x[1]'] # Get the state matrices for later use A, B, C, D = sys.A, sys.B, sys.C, sys.D # Set up a named state space systems with default names - ct.namedio._NamedIOObject._idCounter = 0 + ct.namedio._NamedIOSystem._idCounter = 0 sys = ct.ss(A, B, C, D) assert sys.name == 'sys[0]' - assert sys.input_list == ['u[0]', 'u[1]'] - assert sys.output_list == ['y[0]', 'y[1]'] - assert sys.state_list == ['x[0]', 'x[1]'] + assert sys.input_labels == ['u[0]', 'u[1]'] + assert sys.output_labels == ['y[0]', 'y[1]'] + assert sys.state_labels == ['x[0]', 'x[1]'] # Pass the names as arguments sys = ct.ss( A, B, C, D, name='system', inputs=['u1', 'u2'], outputs=['y1', 'y2'], states=['x1', 'x2']) assert sys.name == 'system' - assert ct.namedio._NamedIOObject._idCounter == 1 - assert sys.input_list == ['u1', 'u2'] - assert sys.output_list == ['y1', 'y2'] - assert sys.state_list == ['x1', 'x2'] + assert ct.namedio._NamedIOSystem._idCounter == 1 + assert sys.input_labels == ['u1', 'u2'] + assert sys.output_labels == ['y1', 'y2'] + assert sys.state_labels == ['x1', 'x2'] # Do the same with rss sys = ct.rss(['x1', 'x2', 'x3'], ['y1', 'y2'], 'u1', name='random') assert sys.name == 'random' - assert ct.namedio._NamedIOObject._idCounter == 1 - assert sys.input_list == ['u1'] - assert sys.output_list == ['y1', 'y2'] - assert sys.state_list == ['x1', 'x2', 'x3'] + assert ct.namedio._NamedIOSystem._idCounter == 1 + assert sys.input_labels == ['u1'] + assert sys.output_labels == ['y1', 'y2'] + assert sys.state_labels == ['x1', 'x2', 'x3'] diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 73410312f..ecbf5a050 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -616,3 +616,114 @@ def test_lqe_discrete(self): # Calling dlqe() with a continuous time system should raise an error with pytest.raises(ControlArgument, match="called with a continuous"): K, S, E = ct.dlqe(csys, Q, R) + + @pytest.mark.parametrize( + 'nstates, noutputs, ninputs, nintegrators, type', + [(2, 0, 1, 0, None), + (2, 1, 1, 0, None), + (4, 0, 2, 0, None), + (4, 3, 2, 0, None), + (2, 0, 1, 1, None), + (4, 0, 2, 2, None), + (4, 3, 2, 2, None), + (2, 0, 1, 0, 'nonlinear'), + (4, 0, 2, 2, 'nonlinear'), + (4, 3, 2, 2, 'nonlinear'), + ]) + def test_lqr_iosys(self, nstates, ninputs, noutputs, nintegrators, type): + # Create the system to be controlled (and estimator) + # TODO: make sure it is controllable? + if noutputs == 0: + # Create a system with full state output + sys = ct.rss(nstates, nstates, ninputs, strictly_proper=True) + sys.C = np.eye(nstates) + est = None + + else: + # Create a system with of the desired size + sys = ct.rss(nstates, noutputs, ninputs, strictly_proper=True) + + # Create an estimator with different signal names + L, _, _ = ct.lqe( + sys.A, sys.B, sys.C, np.eye(ninputs), np.eye(noutputs)) + est = ss( + sys.A - L @ sys.C, np.hstack([L, sys.B]), np.eye(nstates), 0, + inputs=sys.output_labels + sys.input_labels, + outputs=[f'xhat[{i}]' for i in range(nstates)]) + + # Decide whether to include integral action + if nintegrators: + # Choose the first 'n' outputs as integral terms + C_int = np.eye(nintegrators, nstates) + + # Set up an augmented system for LQR computation + # TODO: move this computation into LQR + A_aug = np.block([ + [sys.A, np.zeros((sys.nstates, nintegrators))], + [C_int, np.zeros((nintegrators, nintegrators))] + ]) + B_aug = np.vstack([sys.B, np.zeros((nintegrators, ninputs))]) + C_aug = np.hstack([sys.C, np.zeros((sys.C.shape[0], nintegrators))]) + aug = ss(A_aug, B_aug, C_aug, 0) + else: + C_int = np.zeros((0, nstates)) + aug = sys + + # Design an LQR controller + K, _, _ = ct.lqr(aug, np.eye(nstates + nintegrators), np.eye(ninputs)) + Kp, Ki = K[:, :nstates], K[:, nstates:] + + # Create an I/O system for the controller + ctrl, clsys = ct.create_statefbk_iosystem( + sys, K, integral_action=C_int, estimator=est, type=type) + + # If we used a nonlinear controller, linearize it for testing + if type == 'nonlinear': + clsys = clsys.linearize(0, 0) + + # Make sure the linear system elements are correct + if noutputs == 0: + # No estimator + Ac = np.block([ + [sys.A - sys.B @ Kp, -sys.B @ Ki], + [C_int, np.zeros((nintegrators, nintegrators))] + ]) + Bc = np.block([ + [sys.B @ Kp, sys.B], + [-C_int, np.zeros((nintegrators, ninputs))] + ]) + Cc = np.block([ + [np.eye(nstates), np.zeros((nstates, nintegrators))], + [-Kp, -Ki] + ]) + Dc = np.block([ + [np.zeros((nstates, nstates + ninputs))], + [Kp, np.eye(ninputs)] + ]) + else: + # Estimator + Be1, Be2 = est.B[:, :noutputs], est.B[:, noutputs:] + Ac = np.block([ + [sys.A, -sys.B @ Ki, -sys.B @ Kp], + [np.zeros((nintegrators, nstates + nintegrators)), C_int], + [Be1 @ sys.C, -Be2 @ Ki, est.A - Be2 @ Kp] + ]) + Bc = np.block([ + [sys.B @ Kp, sys.B], + [-C_int, np.zeros((nintegrators, ninputs))], + [Be2 @ Kp, Be2] + ]) + Cc = np.block([ + [sys.C, np.zeros((noutputs, nintegrators + nstates))], + [np.zeros_like(Kp), -Ki, -Kp] + ]) + Dc = np.block([ + [np.zeros((noutputs, nstates + ninputs))], + [Kp, np.eye(ninputs)] + ]) + + # Check to make sure everything matches + np.testing.assert_array_almost_equal(clsys.A, Ac) + np.testing.assert_array_almost_equal(clsys.B, Bc) + np.testing.assert_array_almost_equal(clsys.C, Cc) + np.testing.assert_array_almost_equal(clsys.D, Dc) diff --git a/control/timeresp.py b/control/timeresp.py index 998b5a1f9..e2ce822f6 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -80,7 +80,6 @@ from . import config from .lti import isctime, isdtime -from .namedio import _NamedIOStateObject from .statesp import StateSpace, _convert_to_statespace, _mimo2simo, _mimo2siso from .xferfcn import TransferFunction @@ -88,7 +87,7 @@ 'initial_response', 'impulse_response', 'TimeResponseData'] -class TimeResponseData(_NamedIOStateObject): +class TimeResponseData: """A class for returning time responses. This class maintains and manipulates the data corresponding to the From 213905ca1bf134ef0147da6d5d4cb87c728e66ca Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 15 Mar 2022 17:39:10 -0700 Subject: [PATCH 20/87] add integral_action option to lqr/dlqr --- control/statefbk.py | 93 +++++++++++++++++++++++++++----- control/tests/statefbk_test.py | 99 ++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 13 deletions(-) diff --git a/control/statefbk.py b/control/statefbk.py index 935b15ff9..82643cd96 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -578,7 +578,7 @@ def acker(A, B, poles): return _ssmatrix(K) -def lqr(*args, **keywords): +def lqr(*args, **kwargs): """lqr(A, B, Q, R[, N]) Linear quadratic regulator design @@ -646,18 +646,15 @@ def lqr(*args, **keywords): # Process the arguments and figure out what inputs we received # - # Get the method to use (if specified as a keyword) - method = keywords.get('method', None) + # If we were passed a discrete time system as the first arg, use dlqr() + if isinstance(args[0], LTI) and isdtime(args[0], strict=True): + # Call dlqr + return dlqr(*args, **kwargs) # Get the system description if (len(args) < 3): raise ControlArgument("not enough input arguments") - # If we were passed a discrete time system as the first arg, use dlqr() - if isinstance(args[0], LTI) and isdtime(args[0], strict=True): - # Call dlqr - return dlqr(*args, **keywords) - # If we were passed a state space system, use that to get system matrices if isinstance(args[0], StateSpace): A = np.array(args[0].A, ndmin=2, dtype=float) @@ -682,12 +679,47 @@ def lqr(*args, **keywords): else: N = None + # + # Process keywords + # + + # Get the method to use (if specified as a keyword) + method = kwargs.pop('method', None) + + # See if we should augment the controller with integral feedback + integral_action = kwargs.pop('integral_action', None) + if integral_action is not None: + # Figure out the size of the system + nstates = A.shape[0] + ninputs = B.shape[1] + + # Make sure that the integral action argument is the right type + if not isinstance(integral_action, np.ndarray): + raise ControlArgument("Integral action must pass an array") + elif integral_action.shape[1] != nstates: + raise ControlArgument( + "Integral gain output size must match system input size") + + # Process the states to be integrated + nintegrators = integral_action.shape[0] + C = integral_action + + # Augment the system with integrators + A = np.block([ + [A, np.zeros((nstates, nintegrators))], + [C, np.zeros((nintegrators, nintegrators))] + ]) + B = np.vstack([B, np.zeros((nintegrators, ninputs))]) + + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + # Compute the result (dimension and symmetry checking done in care()) X, L, G = care(A, B, Q, R, N, None, method=method, S_s="N") return G, X, L -def dlqr(*args, **keywords): +def dlqr(*args, **kwargs): """dlqr(A, B, Q, R[, N]) Discrete-time linear quadratic regulator design @@ -747,9 +779,6 @@ def dlqr(*args, **keywords): # Process the arguments and figure out what inputs we received # - # Get the method to use (if specified as a keyword) - method = keywords.get('method', None) - # Get the system description if (len(args) < 3): raise ControlArgument("not enough input arguments") @@ -782,6 +811,39 @@ def dlqr(*args, **keywords): else: N = np.zeros((Q.shape[0], R.shape[1])) + # + # Process keywords + # + + # Get the method to use (if specified as a keyword) + method = kwargs.pop('method', None) + + # See if we should augment the controller with integral feedback + integral_action = kwargs.pop('integral_action', None) + if integral_action is not None: + # Figure out the size of the system + nstates = A.shape[0] + ninputs = B.shape[1] + + if not isinstance(integral_action, np.ndarray): + raise ControlArgument("Integral action must pass an array") + elif integral_action.shape[1] != nstates: + raise ControlArgument( + "Integral gain output size must match system input size") + else: + nintegrators = integral_action.shape[0] + C = integral_action + + # Augment the system with integrators + A = np.block([ + [A, np.zeros((nstates, nintegrators))], + [C, np.eye(nintegrators)] + ]) + B = np.vstack([B, np.zeros((nintegrators, ninputs))]) + + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + # Compute the result (dimension and symmetry checking done in dare()) S, E, K = dare(A, B, Q, R, N, method=method, S_s="N") return _ssmatrix(K), _ssmatrix(S), E @@ -948,7 +1010,12 @@ def _control_output(t, e, z, params): elif type == 'linear' or type is None: # Create the matrices implementing the controller - A_lqr = np.zeros((C.shape[0], C.shape[0])) + if isctime(sys): + # Continuous time: integrator + A_lqr = np.zeros((C.shape[0], C.shape[0])) + else: + # Discrete time: summer + A_lqr = np.eye(C.shape[0]) B_lqr = np.hstack([-C, np.zeros((C.shape[0], sys.ninputs)), C]) C_lqr = -K[:, sys.nstates:] D_lqr = np.hstack([ diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index ecbf5a050..d3ac2a632 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -727,3 +727,102 @@ def test_lqr_iosys(self, nstates, ninputs, noutputs, nintegrators, type): np.testing.assert_array_almost_equal(clsys.B, Bc) np.testing.assert_array_almost_equal(clsys.C, Cc) np.testing.assert_array_almost_equal(clsys.D, Dc) + + def test_lqr_integral_continuous(self): + # Generate a continuous time system for testing + sys = ct.rss(4, 4, 2, strictly_proper=True) + sys.C = np.eye(4) # reset output to be full state + C_int = np.eye(2, 4) # integrate outputs for first two states + nintegrators = C_int.shape[0] + + # Generate a controller with integral action + K, _, _ = ct.lqr( + sys, np.eye(sys.nstates + nintegrators), np.eye(sys.ninputs), + integral_action=C_int) + Kp, Ki = K[:, :sys.nstates], K[:, sys.nstates:] + + # Create an I/O system for the controller + ctrl, clsys = ct.create_statefbk_iosystem( + sys, K, integral_action=C_int) + + # Construct the state space matrices for the controller + # Controller inputs = xd, ud, x + # Controller state = z (integral of x-xd) + # Controller output = ud - Kp(x - xd) - Ki z + A_ctrl = np.zeros((nintegrators, nintegrators)) + B_ctrl = np.block([ + [-C_int, np.zeros((nintegrators, sys.ninputs)), C_int] + ]) + C_ctrl = -K[:, sys.nstates:] + D_ctrl = np.block([[Kp, np.eye(nintegrators), -Kp]]) + + # Check to make sure everything matches + np.testing.assert_array_almost_equal(ctrl.A, A_ctrl) + np.testing.assert_array_almost_equal(ctrl.B, B_ctrl) + np.testing.assert_array_almost_equal(ctrl.C, C_ctrl) + np.testing.assert_array_almost_equal(ctrl.D, D_ctrl) + + # Construct the state space matrices for the closed loop system + A_clsys = np.block([ + [sys.A - sys.B @ Kp, -sys.B @ Ki], + [C_int, np.zeros((nintegrators, nintegrators))] + ]) + B_clsys = np.block([ + [sys.B @ Kp, sys.B], + [-C_int, np.zeros((nintegrators, sys.ninputs))] + ]) + C_clsys = np.block([ + [np.eye(sys.nstates), np.zeros((sys.nstates, nintegrators))], + [-Kp, -Ki] + ]) + D_clsys = np.block([ + [np.zeros((sys.nstates, sys.nstates + sys.ninputs))], + [Kp, np.eye(sys.ninputs)] + ]) + + # Check to make sure closed loop matches + np.testing.assert_array_almost_equal(clsys.A, A_clsys) + np.testing.assert_array_almost_equal(clsys.B, B_clsys) + np.testing.assert_array_almost_equal(clsys.C, C_clsys) + np.testing.assert_array_almost_equal(clsys.D, D_clsys) + + # Check the poles of the closed loop system + assert all(np.real(clsys.pole()) < 0) + + # Make sure controller infinite zero frequency gain + if slycot_check(): + ctrl_tf = tf(ctrl) + assert abs(ctrl_tf(1e-9)[0][0]) > 1e6 + assert abs(ctrl_tf(1e-9)[1][1]) > 1e6 + + def test_lqr_integral_discrete(self): + # Generate a discrete time system for testing + sys = ct.drss(4, 4, 2, strictly_proper=True) + sys.C = np.eye(4) # reset output to be full state + C_int = np.eye(2, 4) # integrate outputs for first two states + nintegrators = C_int.shape[0] + + # Generate a controller with integral action + K, _, _ = ct.lqr( + sys, np.eye(sys.nstates + nintegrators), np.eye(sys.ninputs), + integral_action=C_int) + Kp, Ki = K[:, :sys.nstates], K[:, sys.nstates:] + + # Create an I/O system for the controller + ctrl, clsys = ct.create_statefbk_iosystem( + sys, K, integral_action=C_int) + + # Construct the state space matrices by hand + A_ctrl = np.eye(nintegrators) + B_ctrl = np.block([ + [-C_int, np.zeros((nintegrators, sys.ninputs)), C_int] + ]) + C_ctrl = -K[:, sys.nstates:] + D_ctrl = np.block([[Kp, np.eye(nintegrators), -Kp]]) + + # Check to make sure everything matches + assert ct.isdtime(clsys) + np.testing.assert_array_almost_equal(ctrl.A, A_ctrl) + np.testing.assert_array_almost_equal(ctrl.B, B_ctrl) + np.testing.assert_array_almost_equal(ctrl.C, C_ctrl) + np.testing.assert_array_almost_equal(ctrl.D, D_ctrl) From ca77cca687af1e79bc05bf7c27d31f4f87da30a4 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 15 Mar 2022 21:26:53 -0700 Subject: [PATCH 21/87] update create_statefbk_iosys/lqr/dlqr errors + unit tests --- control/statefbk.py | 6 ++-- control/tests/statefbk_test.py | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/control/statefbk.py b/control/statefbk.py index 82643cd96..c4a1cb92e 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -698,7 +698,7 @@ def lqr(*args, **kwargs): raise ControlArgument("Integral action must pass an array") elif integral_action.shape[1] != nstates: raise ControlArgument( - "Integral gain output size must match system input size") + "Integral gain size must match system state size") # Process the states to be integrated nintegrators = integral_action.shape[0] @@ -829,7 +829,7 @@ def dlqr(*args, **kwargs): raise ControlArgument("Integral action must pass an array") elif integral_action.shape[1] != nstates: raise ControlArgument( - "Integral gain output size must match system input size") + "Integral gain size must match system state size") else: nintegrators = integral_action.shape[0] C = integral_action @@ -951,7 +951,7 @@ def create_statefbk_iosystem( raise ControlArgument("Integral action must pass an array") elif integral_action.shape[1] != sys.nstates: raise ControlArgument( - "Integral gain output size must match system input size") + "Integral gain size must match system state size") else: nintegrators = integral_action.shape[0] C = integral_action diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index d3ac2a632..10ae85a78 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -826,3 +826,62 @@ def test_lqr_integral_discrete(self): np.testing.assert_array_almost_equal(ctrl.B, B_ctrl) np.testing.assert_array_almost_equal(ctrl.C, C_ctrl) np.testing.assert_array_almost_equal(ctrl.D, D_ctrl) + + @pytest.mark.parametrize( + "rss_fun, lqr_fun", + [(ct.rss, lqr), (ct.drss, dlqr)]) + def test_lqr_errors(self, rss_fun, lqr_fun): + # Generate a discrete time system for testing + sys = rss_fun(4, 4, 2, strictly_proper=True) + + with pytest.raises(ControlArgument, match="must pass an array"): + K, _, _ = lqr_fun( + sys, np.eye(sys.nstates), np.eye(sys.ninputs), + integral_action="invalid argument") + + with pytest.raises(ControlArgument, match="gain size must match"): + C_int = np.eye(2, 3) + K, _, _ = lqr_fun( + sys, np.eye(sys.nstates), np.eye(sys.ninputs), + integral_action=C_int) + + with pytest.raises(TypeError, match="unrecognized keywords"): + K, _, _ = lqr_fun( + sys, np.eye(sys.nstates), np.eye(sys.ninputs), + integrator=None) + + def test_statefbk_errors(self): + sys = ct.rss(4, 4, 2, strictly_proper=True) + K, _, _ = ct.lqr(sys, np.eye(sys.nstates), np.eye(sys.ninputs)) + + with pytest.raises(ControlArgument, match="must be I/O system"): + sys_tf = ct.tf([1], [1, 1]) + ctrl, clsys = ct.create_statefbk_iosystem(sys_tf, K) + + with pytest.raises(ControlArgument, match="output size must match"): + est = ct.rss(3, 3, 2) + ctrl, clsys = ct.create_statefbk_iosystem(sys, K, estimator=est) + + with pytest.raises(ControlArgument, match="must be the full state"): + sys_nf = ct.rss(4, 3, 2, strictly_proper=True) + ctrl, clsys = ct.create_statefbk_iosystem(sys_nf, K) + + with pytest.raises(ControlArgument, match="gain must be an array"): + ctrl, clsys = ct.create_statefbk_iosystem(sys, "bad argument") + + with pytest.raises(ControlArgument, match="unknown type"): + ctrl, clsys = ct.create_statefbk_iosystem(sys, K, type=1) + + # Errors involving integral action + C_int = np.eye(2, 4) + K_int, _, _ = ct.lqr( + sys, np.eye(sys.nstates + C_int.shape[0]), np.eye(sys.ninputs), + integral_action=C_int) + + with pytest.raises(ControlArgument, match="must pass an array"): + ctrl, clsys = ct.create_statefbk_iosystem( + sys, K_int, integral_action="bad argument") + + with pytest.raises(ControlArgument, match="must be an array of size"): + ctrl, clsys = ct.create_statefbk_iosystem( + sys, K, integral_action=C_int) From effd6b6b60d6ec22e20d82d9afa45b7f81803cfa Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 15 Mar 2022 22:02:40 -0700 Subject: [PATCH 22/87] add documentation on integral action to lqr/dqlr --- control/statefbk.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/control/statefbk.py b/control/statefbk.py index c4a1cb92e..a866af725 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -608,6 +608,13 @@ def lqr(*args, **kwargs): State and input weight matrices N : 2D array, optional Cross weight matrix + integral_action : ndarray, optional + If this keyword is specified, the controller includes integral action + in addition to state feedback. The value of the `integral_action`` + keyword should be an ndarray that will be multiplied by the current to + generate the error for the internal integrator states of the control + law. The number of outputs that are to be integrated must match the + number of additional rows and columns in the ``Q`` matrix. method : str, optional Set the method used for computing the result. Current methods are 'slycot' and 'scipy'. If set to None (default), try 'slycot' first @@ -750,6 +757,17 @@ def dlqr(*args, **kwargs): State and input weight matrices N : 2D array, optional Cross weight matrix + integral_action : ndarray, optional + If this keyword is specified, the controller includes integral action + in addition to state feedback. The value of the `integral_action`` + keyword should be an ndarray that will be multiplied by the current to + generate the error for the internal integrator states of the control + law. The number of outputs that are to be integrated must match the + number of additional rows and columns in the ``Q`` matrix. + method : str, optional + Set the method used for computing the result. Current methods are + 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + and then 'scipy'. Returns ------- From 316945dcf5a2ddf3e7243b1849997331f4aec369 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 19 Mar 2022 22:56:59 -0700 Subject: [PATCH 23/87] use super() for LTI functions --- control/frdata.py | 2 +- control/namedio.py | 3 +-- control/statesp.py | 3 +-- control/xferfcn.py | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index a80208963..c43a241e4 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -204,7 +204,7 @@ def __init__(self, *args, **kwargs): w=1.0/(absolute(self.fresp[i, j, :]) + 0.001), s=0.0) else: self.ifunc = None - LTI.__init__(self, self.fresp.shape[1], self.fresp.shape[0]) + super().__init__(self.fresp.shape[1], self.fresp.shape[0]) def __str__(self): """String representation of the transfer function.""" diff --git a/control/namedio.py b/control/namedio.py index aca5edc5a..4ea82d819 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -131,8 +131,7 @@ class _NamedIOStateSystem(_NamedIOSystem): def __init__( self, inputs=None, outputs=None, states=None, name=None): # Parse and store the system name, inputs, and outputs - _NamedIOSystem.__init__( - self, inputs=inputs, outputs=outputs, name=name) + super().__init__(inputs=inputs, outputs=outputs, name=name) # Parse and store the number of states self.set_states(states) diff --git a/control/statesp.py b/control/statesp.py index c847180a1..36682532c 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -309,8 +309,7 @@ def __init__(self, *args, keywords=None, **kwargs): D = np.zeros((C.shape[0], B.shape[1])) D = _ssmatrix(D) - # TODO: use super here? - LTI.__init__(self, inputs=D.shape[1], outputs=D.shape[0]) + super().__init__(inputs=D.shape[1], outputs=D.shape[0]) self.A = A self.B = B self.C = C diff --git a/control/xferfcn.py b/control/xferfcn.py index 96e0ce2db..df1b6d404 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -233,7 +233,7 @@ def __init__(self, *args, **kwargs): if zeronum: den[i][j] = ones(1) - LTI.__init__(self, inputs, outputs) + super().__init__(inputs, outputs) self.num = num self.den = den From 79d11935799b5bfacb5abab3048dc6dd3b93faaa Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 19 Mar 2022 11:20:01 -0700 Subject: [PATCH 24/87] add not implemented test/fix for continuous MPC --- control/tests/optimal_test.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index f059c4fc6..270a22131 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -117,7 +117,7 @@ def test_discrete_lqr(): assert np.any(np.abs(res1.inputs - res2.inputs) > 0.1) -def test_mpc_iosystem(): +def test_mpc_iosystem_aircraft(): # model of an aircraft discretized with 0.2s sampling time # Source: https://www.mpt3.org/UI/RegulationProblem A = [[0.99, 0.01, 0.18, -0.09, 0], @@ -171,6 +171,21 @@ def test_mpc_iosystem(): xout[0:sys.nstates, -1], xd, atol=0.1, rtol=0.01) +def test_mpc_iosystem_continuous(): + # Create a random state space system + sys = ct.rss(2, 1, 1) + T, _ = ct.step_response(sys) + + # provide penalties on the system signals + Q = np.eye(sys.nstates) + R = np.eye(sys.ninputs) + cost = opt.quadratic_cost(sys, Q, R) + + # Continuous time MPC controller not implemented + with pytest.raises(NotImplementedError): + ctrl = opt.create_mpc_iosystem(sys, T, cost) + + # Test various constraint combinations; need to use a somewhat convoluted # parametrization due to the need to define sys instead the test function @pytest.mark.parametrize("constraint_list", [ From 19c1d582a95b019cdd137b898cf25a61f2efbe2b Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 19 Mar 2022 17:29:16 -0700 Subject: [PATCH 25/87] optimal improvements: defaults, docs, t_eval, cost * Added config.defaults['optimal'] * Improved keyword handling (including unknown check) * Allow t_eval in input_output_response to chnage timepts * Save cost in optimal control result * Fixed doc/optimal.rst example (generates working solution) * Updated optimal docstrings and user documentation * Added optimization tips to doc/optimal.rst * Fixed user documentation warnings/errors (not related to optimal) --- control/config.py | 3 + control/iosys.py | 126 +++++++++++++++++++++------------- control/optimal.py | 69 ++++++++++++++++--- control/statefbk.py | 2 +- control/tests/optimal_test.py | 59 ++++++++++++++++ doc/control.rst | 2 +- doc/examples.rst | 1 + doc/optimal.rst | 76 +++++++++++++++----- doc/steering-optimal.png | Bin 39597 -> 29001 bytes doc/steering-optimal.py | 1 + doc/steering-optimal.rst | 14 ++++ 11 files changed, 276 insertions(+), 77 deletions(-) create mode 120000 doc/steering-optimal.py create mode 100644 doc/steering-optimal.rst diff --git a/control/config.py b/control/config.py index afd7615ca..8acdf28e2 100644 --- a/control/config.py +++ b/control/config.py @@ -103,6 +103,9 @@ def reset_defaults(): from .iosys import _iosys_defaults defaults.update(_iosys_defaults) + from .optimal import _optimal_defaults + defaults.update(_optimal_defaults) + def _get_param(module, param, argval=None, defval=None, pop=False, last=False): """Return the default value for a configuration option. diff --git a/control/iosys.py b/control/iosys.py index 2f63a7e8b..d5a7b755b 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1571,7 +1571,7 @@ def __init__(self, io_sys, ss_sys=None): def input_output_response( sys, T, U=0., X0=0, params={}, transpose=False, return_x=False, squeeze=None, - solve_ivp_kwargs={}, **kwargs): + solve_ivp_kwargs={}, t_eval='T', **kwargs): """Compute the output response of a system to a given input. Simulate a dynamical system with a given input and return its output @@ -1654,50 +1654,57 @@ def input_output_response( if kwargs.get('solve_ivp_method', None): if kwargs.get('method', None): raise ValueError("ivp_method specified more than once") - solve_ivp_kwargs['method'] = kwargs['solve_ivp_method'] + solve_ivp_kwargs['method'] = kwargs.pop('solve_ivp_method') # Set the default method to 'RK45' if solve_ivp_kwargs.get('method', None) is None: solve_ivp_kwargs['method'] = 'RK45' + # Make sure all input arguments got parsed + if kwargs: + raise TypeError("unknown parameters %s" % kwargs) + # Sanity checking on the input if not isinstance(sys, InputOutputSystem): raise TypeError("System of type ", type(sys), " not valid") # Compute the time interval and number of steps T0, Tf = T[0], T[-1] - n_steps = len(T) + ntimepts = len(T) + + # Figure out simulation times (t_eval) + if solve_ivp_kwargs.get('t_eval'): + if t_eval == 'T': + # Override the default with the solve_ivp keyword + t_eval = solve_ivp_kwargs.pop('t_eval') + else: + raise ValueError("t_eval specified more than once") + if isinstance(t_eval, str) and t_eval == 'T': + # Use the input time points as the output time points + t_eval = T # Check and convert the input, if needed # TODO: improve MIMO ninputs check (choose from U) if sys.ninputs is None or sys.ninputs == 1: - legal_shapes = [(n_steps,), (1, n_steps)] + legal_shapes = [(ntimepts,), (1, ntimepts)] else: - legal_shapes = [(sys.ninputs, n_steps)] + legal_shapes = [(sys.ninputs, ntimepts)] U = _check_convert_array(U, legal_shapes, 'Parameter ``U``: ', squeeze=False) - U = U.reshape(-1, n_steps) - - # Check to make sure this is not a static function - nstates = _find_size(sys.nstates, X0) - if nstates == 0: - # No states => map input to output - u = U[0] if len(U.shape) == 1 else U[:, 0] - y = np.zeros((np.shape(sys._out(T[0], X0, u))[0], len(T))) - for i in range(len(T)): - u = U[i] if len(U.shape) == 1 else U[:, i] - y[:, i] = sys._out(T[i], [], u) - return TimeResponseData( - T, y, None, U, issiso=sys.issiso(), - output_labels=sys.output_index, input_labels=sys.input_index, - transpose=transpose, return_x=return_x, squeeze=squeeze) + U = U.reshape(-1, ntimepts) + ninputs = U.shape[0] # create X0 if not given, test if X0 has correct shape + nstates = _find_size(sys.nstates, X0) X0 = _check_convert_array(X0, [(nstates,), (nstates, 1)], 'Parameter ``X0``: ', squeeze=True) - # Update the parameter values - sys._update_params(params) + # Figure out the number of outputs + if sys.noutputs is None: + # Evaluate the output function to find number of outputs + noutputs = np.shape(sys._out(T[0], X0, U[:, 0]))[0] + else: + noutputs = sys.noutputs # # Define a function to evaluate the input at an arbitrary time @@ -1714,6 +1721,31 @@ def ufun(t): dt = (t - T[idx-1]) / (T[idx] - T[idx-1]) return U[..., idx-1] * (1. - dt) + U[..., idx] * dt + # Check to make sure this is not a static function + if nstates == 0: # No states => map input to output + # Make sure the user gave a time vector for evaluation (or 'T') + if t_eval is None: + # User overrode t_eval with None, but didn't give us the times... + warn("t_eval set to None, but no dynamics; using T instead") + t_eval = T + + # Allocate space for the inputs and outputs + u = np.zeros((ninputs, len(t_eval))) + y = np.zeros((noutputs, len(t_eval))) + + # Compute the input and output at each point in time + for i, t in enumerate(t_eval): + u[:, i] = ufun(t) + y[:, i] = sys._out(t, [], u[:, i]) + + return TimeResponseData( + t_eval, y, None, u, issiso=sys.issiso(), + output_labels=sys.output_index, input_labels=sys.input_index, + transpose=transpose, return_x=return_x, squeeze=squeeze) + + # Update the parameter values + sys._update_params(params) + # Create a lambda function for the right hand side def ivp_rhs(t, x): return sys._rhs(t, x, ufun(t)) @@ -1724,27 +1756,27 @@ def ivp_rhs(t, x): raise NameError("scipy.integrate.solve_ivp not found; " "use SciPy 1.0 or greater") soln = sp.integrate.solve_ivp( - ivp_rhs, (T0, Tf), X0, t_eval=T, + ivp_rhs, (T0, Tf), X0, t_eval=t_eval, vectorized=False, **solve_ivp_kwargs) + if not soln.success: + raise RuntimeError("solve_ivp failed: " + soln.message) - if not soln.success or soln.status != 0: - # Something went wrong - warn("sp.integrate.solve_ivp failed") - print("Return bunch:", soln) - - # Compute the output associated with the state (and use sys.out to - # figure out the number of outputs just in case it wasn't specified) - u = U[0] if len(U.shape) == 1 else U[:, 0] - y = np.zeros((np.shape(sys._out(T[0], X0, u))[0], len(T))) - for i in range(len(T)): - u = U[i] if len(U.shape) == 1 else U[:, i] - y[:, i] = sys._out(T[i], soln.y[:, i], u) + # Compute inputs and outputs for each time point + u = np.zeros((ninputs, len(soln.t))) + y = np.zeros((noutputs, len(soln.t))) + for i, t in enumerate(soln.t): + u[:, i] = ufun(t) + y[:, i] = sys._out(t, soln.y[:, i], u[:, i]) elif isdtime(sys): + # If t_eval was not specified, use the sampling time + if t_eval is None: + t_eval = np.arange(T[0], T[1] + sys.dt, sys.dt) + # Make sure the time vector is uniformly spaced - dt = T[1] - T[0] - if not np.allclose(T[1:] - T[:-1], dt): - raise ValueError("Parameter ``T``: time values must be " + dt = t_eval[1] - t_eval[0] + if not np.allclose(t_eval[1:] - t_eval[:-1], dt): + raise ValueError("Parameter ``t_eval``: time values must be " "equally spaced.") # Make sure the sample time matches the given time @@ -1764,21 +1796,23 @@ def ivp_rhs(t, x): # Compute the solution soln = sp.optimize.OptimizeResult() - soln.t = T # Store the time vector directly - x = X0 # Initilize state + soln.t = t_eval # Store the time vector directly + x = np.array(X0) # State vector (store as floats) soln.y = [] # Solution, following scipy convention - y = [] # System output - for i in range(len(T)): - # Store the current state and output + u, y = [], [] # System input, output + for t in t_eval: + # Store the current input, state, and output soln.y.append(x) - y.append(sys._out(T[i], x, ufun(T[i]))) + u.append(ufun(t)) + y.append(sys._out(t, x, u[-1])) # Update the state for the next iteration - x = sys._rhs(T[i], x, ufun(T[i])) + x = sys._rhs(t, x, u[-1]) # Convert output to numpy arrays soln.y = np.transpose(np.array(soln.y)) y = np.transpose(np.array(y)) + u = np.transpose(np.array(u)) # Mark solution as successful soln.success = True # No way to fail @@ -1787,7 +1821,7 @@ def ivp_rhs(t, x): raise TypeError("Can't determine system type") return TimeResponseData( - soln.t, y, soln.y, U, issiso=sys.issiso(), + soln.t, y, soln.y, u, issiso=sys.issiso(), output_labels=sys.output_index, input_labels=sys.input_index, state_labels=sys.state_index, transpose=transpose, return_x=return_x, squeeze=squeeze) diff --git a/control/optimal.py b/control/optimal.py index 493b6bc3d..9dc8b225b 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -17,9 +17,19 @@ import time from .timeresp import TimeResponseData +from . import config __all__ = ['find_optimal_input'] +# Define module default parameter values +_optimal_defaults = { + 'optimal.minimize_method': None, + 'optimal.minimize_options': {}, + 'optimal.minimize_kwargs': {}, + 'optimal.solve_ivp_method': None, + 'optimal.solve_ivp_options': {}, +} + class OptimalControlProblem(): """Description of a finite horizon, optimal control problem. @@ -110,6 +120,10 @@ class OptimalControlProblem(): values of the input at the specified times (using linear interpolation for continuous systems). + The default values for ``minimize_method``, ``minimize_options``, + ``minimize_kwargs``, ``solve_ivp_method``, and ``solve_ivp_options`` can + be set using config.defaults['optimal.']. + """ def __init__( self, sys, timepts, integral_cost, trajectory_constraints=[], @@ -126,13 +140,22 @@ def __init__( # Process keyword arguments self.solve_ivp_kwargs = {} - self.solve_ivp_kwargs['method'] = kwargs.pop('solve_ivp_method', None) - self.solve_ivp_kwargs.update(kwargs.pop('solve_ivp_kwargs', {})) + self.solve_ivp_kwargs['method'] = kwargs.pop( + 'solve_ivp_method', config.defaults['optimal.solve_ivp_method']) + self.solve_ivp_kwargs.update(kwargs.pop( + 'solve_ivp_kwargs', config.defaults['optimal.solve_ivp_options'])) self.minimize_kwargs = {} - self.minimize_kwargs['method'] = kwargs.pop('minimize_method', None) - self.minimize_kwargs['options'] = kwargs.pop('minimize_options', {}) - self.minimize_kwargs.update(kwargs.pop('minimize_kwargs', {})) + self.minimize_kwargs['method'] = kwargs.pop( + 'minimize_method', config.defaults['optimal.minimize_method']) + self.minimize_kwargs['options'] = kwargs.pop( + 'minimize_options', config.defaults['optimal.minimize_options']) + self.minimize_kwargs.update(kwargs.pop( + 'minimize_kwargs', config.defaults['optimal.minimize_kwargs'])) + + # Make sure all input arguments got parsed + if kwargs: + raise TypeError("unknown parameters %s" % kwargs) if len(kwargs) > 0: raise ValueError( @@ -271,9 +294,10 @@ def _cost_function(self, coeffs): logging.debug("initial input[0:3] =\n" + str(inputs[:, 0:3])) # Simulate the system to get the state + # TODO: try calling solve_ivp directly for better speed? _, _, states = ct.input_output_response( self.system, self.timepts, inputs, x, return_x=True, - solve_ivp_kwargs=self.solve_ivp_kwargs) + solve_ivp_kwargs=self.solve_ivp_kwargs, t_eval=self.timepts) self.system_simulations += 1 self.last_x = x self.last_coeffs = coeffs @@ -393,7 +417,7 @@ def _constraint_function(self, coeffs): # Simulate the system to get the state _, _, states = ct.input_output_response( self.system, self.timepts, inputs, x, return_x=True, - solve_ivp_kwargs=self.solve_ivp_kwargs) + solve_ivp_kwargs=self.solve_ivp_kwargs, t_eval=self.timepts) self.system_simulations += 1 self.last_x = x self.last_coeffs = coeffs @@ -475,7 +499,7 @@ def _eqconst_function(self, coeffs): # Simulate the system to get the state _, _, states = ct.input_output_response( self.system, self.timepts, inputs, x, return_x=True, - solve_ivp_kwargs=self.solve_ivp_kwargs) + solve_ivp_kwargs=self.solve_ivp_kwargs, t_eval=self.timepts) self.system_simulations += 1 self.last_x = x self.last_coeffs = coeffs @@ -548,7 +572,7 @@ def _process_initial_guess(self, initial_guess): initial_guess = np.atleast_1d(initial_guess) # See whether we got entire guess or just first time point - if len(initial_guess.shape) == 1: + if initial_guess.ndim == 1: # Broadcast inputs to entire time vector try: initial_guess = np.broadcast_to( @@ -804,6 +828,15 @@ class OptimalControlResult(sp.optimize.OptimizeResult): Whether or not the optimizer exited successful. problem : OptimalControlProblem Optimal control problem that generated this solution. + cost : float + Final cost of the return solution. + system_simulations, {cost, constraint, eqconst}_evaluations : int + Number of system simulations and evaluations of the cost function, + (inequality) constraint function, and equality constraint function + performed during the optimzation. + {cost, constraint, eqconst}_process_time : float + If logging was enabled, the amount of time spent evaluating the cost + and constraint functions. """ def __init__( @@ -833,15 +866,19 @@ def __init__( "unable to solve optimal control problem\n" "scipy.optimize.minimize returned " + res.message, UserWarning) + # Save the final cost + self.cost = res.fun + # Optionally print summary information if print_summary: ocp._print_statistics() + print("* Final cost:", self.cost) if return_states and inputs.shape[1] == ocp.timepts.shape[0]: # Simulate the system if we need the state back _, _, states = ct.input_output_response( ocp.system, ocp.timepts, inputs, ocp.x, return_x=True, - solve_ivp_kwargs=ocp.solve_ivp_kwargs) + solve_ivp_kwargs=ocp.solve_ivp_kwargs, t_eval=ocp.timepts) ocp.system_simulations += 1 else: states = None @@ -858,7 +895,7 @@ def __init__( # Compute the input for a nonlinear, (constrained) optimal control problem def solve_ocp( - sys, horizon, X0, cost, trajectory_constraints=[], terminal_cost=None, + sys, horizon, X0, cost, trajectory_constraints=None, terminal_cost=None, terminal_constraints=[], initial_guess=None, basis=None, squeeze=None, transpose=None, return_states=False, log=False, **kwargs): @@ -966,6 +1003,16 @@ def solve_ocp( return_states = ct.config._get_param( 'optimal', 'return_x', kwargs, return_states, pop=True) + # Process terminal constraints keyword + if constraints is None: + constraints = kwargs.pop('trajectory_constraints', []) + + # Process (legacy) method keyword + if kwargs.get('method'): + if kwargs.get('minimize_method'): + raise ValueError("'minimize_method' specified more than once") + kwargs['minimize_method'] = kwargs.pop('method') + # Set up the optimal control problem ocp = OptimalControlProblem( sys, horizon, cost, trajectory_constraints=trajectory_constraints, diff --git a/control/statefbk.py b/control/statefbk.py index a866af725..6598eeeb8 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -734,7 +734,7 @@ def dlqr(*args, **kwargs): The dlqr() function computes the optimal state feedback controller u[n] = - K x[n] that minimizes the quadratic cost - .. math:: J = \\Sum_0^\\infty (x[n]' Q x[n] + u[n]' R u[n] + 2 x[n]' N u[n]) + .. math:: J = \\sum_0^\\infty (x[n]' Q x[n] + u[n]' R u[n] + 2 x[n]' N u[n]) The function can be called with either 3, 4, or 5 arguments: diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 270a22131..53d8fe8e4 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -45,6 +45,9 @@ def test_finite_horizon_simple(): np.testing.assert_almost_equal( u_openloop, [-1, -1, 0.1393, 0.3361, -5.204e-16], decimal=4) + # Make sure the final cost is correct + assert math.isclose(res.cost, 32.4898, rel_tol=1e-5) + # Convert controller to an explicit form (not implemented yet) # mpc_explicit = opt.explicit_mpc(); @@ -564,3 +567,59 @@ def final_point_eval(x, u): optctrl = opt.OptimalControlProblem( sys, time, cost, terminal_constraints=final_point) res = optctrl.compute_trajectory(x0, squeeze=True, return_x=True) + + +def test_optimal_doc(): + """Test optimal control problem from documentation""" + def vehicle_update(t, x, u, params): + # Get the parameters for the model + l = params.get('wheelbase', 3.) # vehicle wheelbase + phimax = params.get('maxsteer', 0.5) # max steering angle (rad) + + # Saturate the steering input + phi = np.clip(u[1], -phimax, phimax) + + # Return the derivative of the state + return np.array([ + np.cos(x[2]) * u[0], # xdot = cos(theta) v + np.sin(x[2]) * u[0], # ydot = sin(theta) v + (u[0] / l) * np.tan(phi) # thdot = v/l tan(phi) + ]) + + def vehicle_output(t, x, u, params): + return x # return x, y, theta (full state) + + # Define the vehicle steering dynamics as an input/output system + vehicle = ct.NonlinearIOSystem( + vehicle_update, vehicle_output, states=3, name='vehicle', + inputs=('v', 'phi'), outputs=('x', 'y', 'theta')) + + # Define the initial and final points and time interval + x0 = [0., -2., 0.]; u0 = [10., 0.] + xf = [100., 2., 0.]; uf = [10., 0.] + Tf = 10 + + # Define the cost functions + Q = np.diag([0, 0, 0.1]) # don't turn too sharply + R = np.diag([1, 1]) # keep inputs small + P = np.diag([1000, 1000, 1000]) # get close to final point + traj_cost = opt.quadratic_cost(vehicle, Q, R, x0=xf, u0=uf) + term_cost = opt.quadratic_cost(vehicle, P, 0, x0=xf) + + # Define the constraints + constraints = [ opt.input_range_constraint(vehicle, [8, -0.1], [12, 0.1]) ] + + # Solve the optimal control problem + horizon = np.linspace(0, Tf, 3, endpoint=True) + result = opt.solve_ocp( + vehicle, horizon, x0, traj_cost, constraints, + terminal_cost= term_cost, initial_guess=u0) + + # Make sure the resulting trajectory generate a good solution + resp = ct.input_output_response( + vehicle, horizon, result.inputs, x0, + t_eval=np.linspace(0, Tf, 10)) + t, y = resp + assert (y[0, -1] - xf[0]) / xf[0] < 0.01 + assert (y[1, -1] - xf[1]) / xf[1] < 0.01 + assert y[2, -1] < 0.1 diff --git a/doc/control.rst b/doc/control.rst index 87c1151eb..8bd6f7a32 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -114,7 +114,7 @@ Control system synthesis lqe mixsyn place - rlocus_pid_designer + rootlocus_pid_designer Model simplification tools ========================== diff --git a/doc/examples.rst b/doc/examples.rst index 91476bc9d..89a2b16a1 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -29,6 +29,7 @@ other sources. robust_mimo cruise-control steering-gainsched + steering-optimal kincar-flatsys Jupyter notebooks diff --git a/doc/optimal.rst b/doc/optimal.rst index e173e430b..8da08e7af 100644 --- a/doc/optimal.rst +++ b/doc/optimal.rst @@ -79,7 +79,7 @@ Every :math:`\Delta T` seconds, an optimal control problem is solved over a :math:`T` second horizon, starting from the current state. The first :math:`\Delta T` seconds of the optimal control :math:`u_T^{\*}(\cdot; x(t))` is then applied to the system. If we let :math:`x_T^{\*}(\cdot; -x(t))` represent the optimal trajectory starting from :math:`x(t)`$ then the +x(t))` represent the optimal trajectory starting from :math:`x(t)` then the system state evolves from :math:`x(t)` at current time :math:`t` to :math:`x_T^{*}(\delta T, x(t))` at the next sample time :math:`t + \Delta T`, assuming no model uncertainty. @@ -219,9 +219,11 @@ with a starting and ending velocity of 10 m/s:: To set up the optimal control problem we design a cost function that penalizes the state and input using quadratic cost functions:: - Q = np.diag([0.1, 10, .1]) # keep lateral error low - R = np.eye(2) * 0.1 - cost = opt.quadratic_cost(vehicle, Q, R, x0=xf, u0=uf) + Q = np.diag([0, 0, 0.1]) # don't turn too sharply + R = np.diag([1, 1]) # keep inputs small + P = np.diag([1000, 1000, 1000]) # get close to final point + traj_cost = opt.quadratic_cost(vehicle, Q, R, x0=xf, u0=uf) + term_cost = opt.quadratic_cost(vehicle, P, 0, x0=xf) We also constraint the maximum turning rate to 0.1 radians (about 6 degees) and constrain the velocity to be in the range of 9 m/s to 11 m/s:: @@ -230,20 +232,19 @@ and constrain the velocity to be in the range of 9 m/s to 11 m/s:: Finally, we solve for the optimal inputs:: - horizon = np.linspace(0, Tf, 20, endpoint=True) - bend_left = [10, 0.01] # slight left veer - + horizon = np.linspace(0, Tf, 3, endpoint=True) result = opt.solve_ocp( - vehicle, horizon, x0, cost, constraints, initial_guess=bend_left, - options={'eps': 0.01}) # set step size for gradient calculation - - # Extract the results - u = result.inputs - t, y = ct.input_output_response(vehicle, horizon, u, x0) + vehicle, horizon, x0, traj_cost, constraints, + terminal_cost=term_cost, initial_guess=u0) Plotting the results:: - # Plot the results + # Simulate the system dynamics (open loop) + resp = ct.input_output_response( + vehicle, horizon, result.inputs, x0, + t_eval=np.linspace(0, Tf, 100)) + t, y, u = resp.time, resp.outputs, resp.inputs + plt.subplot(3, 1, 1) plt.plot(y[0], y[1]) plt.plot(x0[0], x0[1], 'ro', xf[0], xf[1], 'ro') @@ -252,15 +253,13 @@ Plotting the results:: plt.subplot(3, 1, 2) plt.plot(t, u[0]) - plt.axis([0, 10, 8.5, 11.5]) - plt.plot([0, 10], [9, 9], 'k--', [0, 10], [11, 11], 'k--') + plt.axis([0, 10, 9.9, 10.1]) plt.xlabel("t [sec]") plt.ylabel("u1 [m/s]") plt.subplot(3, 1, 3) plt.plot(t, u[1]) - plt.axis([0, 10, -0.15, 0.15]) - plt.plot([0, 10], [-0.1, -0.1], 'k--', [0, 10], [0.1, 0.1], 'k--') + plt.axis([0, 10, -0.01, 0.01]) plt.xlabel("t [sec]") plt.ylabel("u2 [rad/s]") @@ -272,6 +271,47 @@ yields .. image:: steering-optimal.png +Optimization Tips +================= + +The python-control optimization module makes use of the SciPy optimization +toolbox and it can sometimes be tricky to get the optimization to converge. +If you are getting errors when solving optimal control problems or your +solutions do not seem close to optimal, here are a few things to try: + +* Less is more: try using a smaller number of time points in your + optimiation. The default optimal control problem formulation uses the + value of the inputs at each time point as a free variable and this can + generate a large number of parameters quickly. Often you can find very + good solutions with a small number of free variables (the example above + uses 3 time points for 2 inputs, so a total of 6 optimization variables). + Note that you can "resample" the optimal trajectory by running a + simulation of the sytem and using the `t_eval` keyword in + `input_output_response` (as done above). + +* Use a smooth basis: as an alternative to parameterizing the optimal + control inputs using the value of the control at the listed time points, + you can specify a set of basis functions using the `basis` keyword in + :func:`~control.solve_ocp` and then parameterize the controller by linear + combination of the basis functions. The :mod:`!control.flatsys` module + defines several sets of basis functions that can be used. + +* Tweak the optimizer: by using the `minimize_method`, `minimize_options`, + and `minimize_kwargs` keywords in :func:`~control.solve_ocp`, you can + choose the SciPy optimization function that you use and set many + parameters. See :func:`scipy.optimize.minimize` for more information on + the optimzers that are available and the options and keywords that they + accept. + +* Walk before you run: try setting up a simpler version of the optimization, + remove constraints or simplifying the cost to get a simple version of the + problem working and then add complexity. Sometimes this can help you find + the right set of options or identify situations in which you are being too + aggressive in what your are trying to get the system to do. + +See :ref:`steering-optimal` for some examples of different problem +formulations. + Module classes and functions ============================ diff --git a/doc/steering-optimal.png b/doc/steering-optimal.png index 6ff50c0f423ca3c58abffeb34f6be372333e1659..f847923b5f8f86f6901d2d60c95d714f36289f18 100644 GIT binary patch literal 29001 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 QSVPJVePg{G9h<=a0(3Y8y8r+H literal 39597 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%0Ai;&5OpPwz%1`&WDgSb!13RxYKY3lsg!f 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 y?1%qb+7 Date: Sat, 19 Mar 2022 22:09:03 -0700 Subject: [PATCH 26/87] unit test for nonuniform sampling in simulations --- control/tests/iosys_test.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index bdc5ba31a..e76a6be55 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1648,7 +1648,9 @@ def test_interconnect_unused_output(): outputs=['u'], name='k') - with pytest.warns(UserWarning, match=r"Unused output\(s\) in InterconnectedSystem:") as record: + with pytest.warns( + UserWarning, + match=r"Unused output\(s\) in InterconnectedSystem:") as record: h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y']) @@ -1679,13 +1681,17 @@ def test_interconnect_unused_output(): pytest.fail(f'Unexpected warning: {r.message}') # warn if explicity ignored output in fact used - with pytest.warns(UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"): + with pytest.warns( + UserWarning, + match=r"Output\(s\) specified as ignored is \(are\) used:"): h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y'], ignore_outputs=['dy','u']) - with pytest.warns(UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"): + with pytest.warns( + UserWarning, + match=r"Output\(s\) specified as ignored is \(are\) used:"): h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y'], @@ -1697,3 +1703,25 @@ def test_interconnect_unused_output(): inputs=['r'], outputs=['y'], ignore_outputs=['v']) + +def test_nonuniform_timepts(): + """Test non-uniform time points for simulations""" + sys = ct.LinearIOSystem(ct.rss(2, 1, 1)) + + # Start with a uniform set of times + unifpts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + uniform = [1, 2, 3, 2, 1, -1, -3, -5, -7, -3, 1] + t_unif, y_unif = ct.input_output_response(sys, unifpts, uniform) + + # Create a non-uniform set of inputs + noufpts = [0, 2, 4, 8, 10] + nonunif = [1, 3, 1, -7, 1] + t_nouf, y_nouf = ct.input_output_response(sys, noufpts, nonunif) + + # Make sure the outputs agree at common times + np.testing.assert_almost_equal(y_unif[noufpts], y_nouf, decimal=6) + + # Resimulate using a new set of evaluation points + t_even, y_even = ct.input_output_response( + sys, noufpts, nonunif, t_eval=unifpts) + np.testing.assert_almost_equal(y_unif, y_even, decimal=6) From f3d46bcc9c7be4d34357d227d592c2afdbe2caa0 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 22 Mar 2022 22:18:52 -0700 Subject: [PATCH 27/87] rebase cleanup --- control/optimal.py | 15 ++++----------- control/tests/optimal_test.py | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/control/optimal.py b/control/optimal.py index 9dc8b225b..cb9f57ded 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -16,8 +16,9 @@ import logging import time -from .timeresp import TimeResponseData from . import config +from .exception import ControlNotImplemented +from .timeresp import TimeResponseData __all__ = ['find_optimal_input'] @@ -155,11 +156,7 @@ def __init__( # Make sure all input arguments got parsed if kwargs: - raise TypeError("unknown parameters %s" % kwargs) - - if len(kwargs) > 0: - raise ValueError( - f'unrecognized keyword(s): {list(kwargs.keys())}') + raise TypeError("unrecognized keyword(s): ", str(kwargs)) # Process trajectory constraints if isinstance(trajectory_constraints, tuple): @@ -997,16 +994,12 @@ def solve_ocp( # Process keyword arguments if trajectory_constraints is None: # Backwards compatibility - trajectory_constraints = kwargs.pop('constraints', None) + trajectory_constraints = kwargs.pop('constraints', []) # Allow 'return_x` as a synonym for 'return_states' return_states = ct.config._get_param( 'optimal', 'return_x', kwargs, return_states, pop=True) - # Process terminal constraints keyword - if constraints is None: - constraints = kwargs.pop('trajectory_constraints', []) - # Process (legacy) method keyword if kwargs.get('method'): if kwargs.get('minimize_method'): diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index 53d8fe8e4..1aa307b60 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -455,7 +455,7 @@ def test_ocp_argument_errors(): sys, time, x0, cost, constraints, initial_guess=np.zeros((4,1,1))) # Unrecognized arguments - with pytest.raises(ValueError, match="unrecognized keyword"): + with pytest.raises(TypeError, match="unrecognized keyword"): res = opt.solve_ocp( sys, time, x0, cost, constraints, terminal_constraint=None) From 8b6bfd199de4a0f820be577496b7392560ececb7 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 06:20:19 -0700 Subject: [PATCH 28/87] check for unused keywords --- control/frdata.py | 6 +++++- control/freqplot.py | 6 ++---- control/iosys.py | 9 +++++---- control/optimal.py | 2 +- control/pzmap.py | 6 +++++- control/rlocus.py | 4 ++++ control/tests/interconnect_test.py | 10 +++++----- control/tests/trdata_test.py | 2 +- control/timeresp.py | 6 +++--- 9 files changed, 31 insertions(+), 20 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index c43a241e4..19e865821 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -150,7 +150,11 @@ def __init__(self, *args, **kwargs): """ # TODO: discrete-time FRD systems? - smooth = kwargs.get('smooth', False) + smooth = kwargs.pop('smooth', False) + + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) if len(args) == 2: if not isinstance(args[0], FRD) and isinstance(args[0], LTI): diff --git a/control/freqplot.py b/control/freqplot.py index a8324e06e..7a1243c6c 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -221,12 +221,10 @@ def bode_plot(syslist, omega=None, # Get the current figure if 'sisotool' in kwargs: - fig = kwargs['fig'] + fig = kwargs.pop('fig') ax_mag = fig.axes[0] ax_phase = fig.axes[2] - sisotool = kwargs['sisotool'] - del kwargs['fig'] - del kwargs['sisotool'] + sisotool = kwargs.pop('sisotool') else: fig = plt.gcf() ax_mag = None diff --git a/control/iosys.py b/control/iosys.py index d5a7b755b..5f606ee93 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -148,7 +148,7 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, # timebase self.dt = kwargs.pop('dt', config.defaults['control.default_dt']) - # Make sure there were no extraneous keyworks + # Make sure there were no extraneous keywords if kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) @@ -790,9 +790,9 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, params=params, dt=dt, name=name ) - # Make sure all input arguments got parsed + # Make sure there were no extraneous keywords if kwargs: - raise TypeError("unknown parameters %s" % kwargs) + raise TypeError("unrecognized keywords: ", str(kwargs)) # Check to make sure arguments are consistent if updfcn is None: @@ -2134,7 +2134,8 @@ def _parse_signal_parameter(value, name, kwargs, end=False): value = kwargs.pop(name) if end and kwargs: - raise TypeError("unknown parameters %s" % kwargs) + raise TypeError("unrecognized keywords: ", str(kwargs)) + return value diff --git a/control/optimal.py b/control/optimal.py index cb9f57ded..aea9b02b8 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -154,7 +154,7 @@ def __init__( self.minimize_kwargs.update(kwargs.pop( 'minimize_kwargs', config.defaults['optimal.minimize_kwargs'])) - # Make sure all input arguments got parsed + # Make sure there were no extraneous keywords if kwargs: raise TypeError("unrecognized keyword(s): ", str(kwargs)) diff --git a/control/pzmap.py b/control/pzmap.py index ae8db1241..7d3836d7f 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -91,7 +91,11 @@ def pzmap(sys, plot=None, grid=None, title='Pole Zero Map', **kwargs): import warnings warnings.warn("'Plot' keyword is deprecated in pzmap; use 'plot'", FutureWarning) - plot = kwargs['Plot'] + plot = kwargs.pop('Plot') + + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) # Get parameter values plot = config._get_param('pzmap', 'plot', plot, True) diff --git a/control/rlocus.py b/control/rlocus.py index 23122fe72..5cf7983a3 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -168,6 +168,10 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, # Check for sisotool mode sisotool = False if 'sisotool' not in kwargs else True + # Make sure there were no extraneous keywords + if not sisotool and kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + # Create the Plot if plot: if sisotool: diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index dd31241e7..3b99adc6e 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -188,19 +188,19 @@ def test_interconnect_exceptions(): # Unrecognized arguments # LinearIOSystem - with pytest.raises(TypeError, match="unknown parameter"): + with pytest.raises(TypeError, match="unrecognized keyword"): P = ct.LinearIOSystem(ct.rss(2, 1, 1), output_name='y') # Interconnect - with pytest.raises(TypeError, match="unknown parameter"): + with pytest.raises(TypeError, match="unrecognized keyword"): T = ct.interconnect((P, C, sumblk), input_name='r', output='y') # Interconnected system - with pytest.raises(TypeError, match="unknown parameter"): + with pytest.raises(TypeError, match="unrecognized keyword"): T = ct.InterconnectedSystem((P, C, sumblk), input_name='r', output='y') # NonlinearIOSytem - with pytest.raises(TypeError, match="unknown parameter"): + with pytest.raises(TypeError, match="unrecognized keyword"): nlios = ct.NonlinearIOSystem( None, lambda t, x, u, params: u*u, input_count=1, output_count=1) @@ -208,7 +208,7 @@ def test_interconnect_exceptions(): with pytest.raises(TypeError, match="input specification is required"): sumblk = ct.summing_junction() - with pytest.raises(TypeError, match="unknown parameter"): + with pytest.raises(TypeError, match="unrecognized keyword"): sumblk = ct.summing_junction(input_count=2, output_count=2) diff --git a/control/tests/trdata_test.py b/control/tests/trdata_test.py index fcd8676e9..734d35599 100644 --- a/control/tests/trdata_test.py +++ b/control/tests/trdata_test.py @@ -208,7 +208,7 @@ def test_response_copy(): assert response.input_labels == ['u'] # Unknown keyword - with pytest.raises(ValueError, match="Unknown parameter(s)*"): + with pytest.raises(TypeError, match="unrecognized keywords"): response_bad_kw = response_mimo(input=0) diff --git a/control/timeresp.py b/control/timeresp.py index e2ce822f6..bf826b539 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -480,9 +480,9 @@ def __call__(self, **kwargs): response.state_labels = _process_labels( state_labels, "state", response.nstates) - # Make sure no unknown keywords were passed - if len(kwargs) != 0: - raise ValueError("Unknown parameter(s) %s" % kwargs) + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) return response From 3656968a55ea50f9c103c51f14b9aa789fde9938 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 06:24:48 -0700 Subject: [PATCH 29/87] remove_useless -> remove_useless_states in iosys --- control/iosys.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 5f606ee93..c2fe62e40 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -560,7 +560,7 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, # Create the state space system linsys = LinearIOSystem( - StateSpace(A, B, C, D, self.dt, remove_useless=False), + StateSpace(A, B, C, D, self.dt, remove_useless_states=False), name=name, **kwargs) # Set the names the system, inputs, outputs, and states @@ -660,7 +660,7 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, states=linsys.nstates, params={}, dt=linsys.dt, name=name) # Initalize additional state space variables - StateSpace.__init__(self, linsys, remove_useless=False) + StateSpace.__init__(self, linsys, remove_useless_states=False) # Process input, output, state lists, if given # Make sure they match the size of the linear system @@ -1551,7 +1551,7 @@ def __init__(self, io_sys, ss_sys=None): io_sys.nstates != ss_sys.nstates: raise ValueError("System dimensions for first and second " "arguments must match.") - StateSpace.__init__(self, ss_sys, remove_useless=False) + StateSpace.__init__(self, ss_sys, remove_useless_states=False) else: raise TypeError("Second argument must be a state space system.") From ceddc6f0e60262958a60b31cb709e39860480076 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 06:33:35 -0700 Subject: [PATCH 30/87] add test for extraneous keywords in iosys --- control/iosys.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index c2fe62e40..196b8968b 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1656,14 +1656,14 @@ def input_output_response( raise ValueError("ivp_method specified more than once") solve_ivp_kwargs['method'] = kwargs.pop('solve_ivp_method') + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + # Set the default method to 'RK45' if solve_ivp_kwargs.get('method', None) is None: solve_ivp_kwargs['method'] = 'RK45' - # Make sure all input arguments got parsed - if kwargs: - raise TypeError("unknown parameters %s" % kwargs) - # Sanity checking on the input if not isinstance(sys, InputOutputSystem): raise TypeError("System of type ", type(sys), " not valid") From 97a0a14b9341c0b2c5b2f2957d83aca3852b6794 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 06:48:03 -0700 Subject: [PATCH 31/87] add test for extraneous keywords in xferfcn --- control/xferfcn.py | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index df1b6d404..76d704afc 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -265,6 +265,10 @@ def __init__(self, *args, **kwargs): dt = config.defaults['control.default_dt'] self.dt = dt + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + # # Class attributes # @@ -1297,7 +1301,7 @@ def _add_siso(num1, den1, num2, den2): return num, den -def _convert_to_transfer_function(sys, **kw): +def _convert_to_transfer_function(sys, inputs=1, outputs=1): """Convert a system to transfer function form (if needed). If sys is already a transfer function, then it is returned. If sys is a @@ -1324,13 +1328,9 @@ def _convert_to_transfer_function(sys, **kw): from .statesp import StateSpace if isinstance(sys, TransferFunction): - if len(kw): - raise TypeError("If sys is a TransferFunction, " + - "_convertToTransferFunction cannot take keywords.") - return sys - elif isinstance(sys, StateSpace): + elif isinstance(sys, StateSpace): if 0 == sys.nstates: # Slycot doesn't like static SS->TF conversion, so handle # it first. Can't join this with the no-Slycot branch, @@ -1341,14 +1341,9 @@ def _convert_to_transfer_function(sys, **kw): for i in range(sys.noutputs)] else: try: - from slycot import tb04ad - if len(kw): - raise TypeError( - "If sys is a StateSpace, " + - "_convertToTransferFunction cannot take keywords.") - # Use Slycot to make the transformation # Make sure to convert system matrices to numpy arrays + from slycot import tb04ad tfout = tb04ad( sys.nstates, sys.ninputs, sys.noutputs, array(sys.A), array(sys.B), array(sys.C), array(sys.D), tol1=0.0) @@ -1381,15 +1376,6 @@ def _convert_to_transfer_function(sys, **kw): return TransferFunction(num, den, sys.dt) elif isinstance(sys, (int, float, complex, np.number)): - if "inputs" in kw: - inputs = kw["inputs"] - else: - inputs = 1 - if "outputs" in kw: - outputs = kw["outputs"] - else: - outputs = 1 - num = [[[sys] for j in range(inputs)] for i in range(outputs)] den = [[[1] for j in range(inputs)] for i in range(outputs)] @@ -1498,6 +1484,10 @@ def tf(*args, **kwargs): if len(args) == 2 or len(args) == 3: return TransferFunction(*args, **kwargs) elif len(args) == 1: + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + # Look for special cases defining differential/delay operator if args[0] == 's': return TransferFunction.s @@ -1525,8 +1515,8 @@ def ss2tf(*args, **kwargs): The function accepts either 1 or 4 parameters: ``ss2tf(sys)`` - Convert a linear system from state space into transfer function form. Always creates a - new system. + Convert a linear system from state space into transfer function + form. Always creates a new system. ``ss2tf(A, B, C, D)`` Create a transfer function system from the matrices of its state and From 57e3751ffabc5385898eaaaa6576431906ccbd3a Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 07:03:17 -0700 Subject: [PATCH 32/87] add test for extraneous keywords in statesp --- control/statesp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/control/statesp.py b/control/statesp.py index 36682532c..960fdc157 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1776,7 +1776,12 @@ def _ss(*args, keywords=None, **kwargs): """Internal function to create StateSpace system""" if len(args) == 4 or len(args) == 5: return StateSpace(*args, keywords=keywords, **kwargs) + elif len(args) == 1: + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + from .xferfcn import TransferFunction sys = args[0] if isinstance(sys, StateSpace): From a86d33fa0b1f817b776d5d3bb357e919e27898cd Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 22:36:30 -0700 Subject: [PATCH 33/87] add kwargs_test + checks for unrecognized keywords --- control/iosys.py | 10 ++- control/statefbk.py | 18 ++-- control/statesp.py | 4 + control/tests/frd_test.py | 6 ++ control/tests/iosys_test.py | 13 ++- control/tests/kwargs_test.py | 168 +++++++++++++++++++++++++++++++++++ control/xferfcn.py | 6 +- 7 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 control/tests/kwargs_test.py diff --git a/control/iosys.py b/control/iosys.py index 196b8968b..eeb483227 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -2238,7 +2238,15 @@ def ss(*args, **kwargs): >>> sys2 = ss(sys_tf) """ - sys = _ss(*args, keywords=kwargs) + # Extract the keyword arguments needed for StateSpace (via _ss) + ss_kwlist = ('dt', 'remove_useless_states') + ss_kwargs = {} + for kw in ss_kwlist: + if kw in kwargs: + ss_kwargs[kw] = kwargs.pop(kw) + + # Create the statespace system and then convert to I/O system + sys = _ss(*args, keywords=ss_kwargs) return LinearIOSystem(sys, **kwargs) diff --git a/control/statefbk.py b/control/statefbk.py index 6598eeeb8..099baa225 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -261,7 +261,7 @@ def place_varga(A, B, p, dtime=False, alpha=None): # contributed by Sawyer B. Fuller -def lqe(*args, **keywords): +def lqe(*args, method=None): """lqe(A, G, C, QN, RN, [, NN]) Linear quadratic estimator design (Kalman filter) for continuous-time @@ -356,18 +356,15 @@ def lqe(*args, **keywords): # Process the arguments and figure out what inputs we received # - # Get the method to use (if specified as a keyword) - method = keywords.get('method', None) + # If we were passed a discrete time system as the first arg, use dlqe() + if isinstance(args[0], LTI) and isdtime(args[0], strict=True): + # Call dlqe + return dlqe(*args, method=method) # Get the system description if (len(args) < 3): raise ControlArgument("not enough input arguments") - # If we were passed a discrete time system as the first arg, use dlqe() - if isinstance(args[0], LTI) and isdtime(args[0], strict=True): - # Call dlqe - return dlqe(*args, **keywords) - # If we were passed a state space system, use that to get system matrices if isinstance(args[0], StateSpace): A = np.array(args[0].A, ndmin=2, dtype=float) @@ -409,7 +406,7 @@ def lqe(*args, **keywords): # contributed by Sawyer B. Fuller -def dlqe(*args, **keywords): +def dlqe(*args, method=None): """dlqe(A, G, C, QN, RN, [, N]) Linear quadratic estimator design (Kalman filter) for discrete-time @@ -480,9 +477,6 @@ def dlqe(*args, **keywords): # Process the arguments and figure out what inputs we received # - # Get the method to use (if specified as a keyword) - method = keywords.get('method', None) - # Get the system description if (len(args) < 3): raise ControlArgument("not enough input arguments") diff --git a/control/statesp.py b/control/statesp.py index 960fdc157..435ff702f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -340,6 +340,10 @@ def __init__(self, *args, keywords=None, **kwargs): self.dt = dt self.nstates = A.shape[1] + # Make sure there were no extraneous keywords + if keywords: + raise TypeError("unrecognized keywords: ", str(keywords)) + if 0 == self.nstates: # static gain # matrix's default "empty" shape is 1x0 diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index c63a4c02b..af7d18bc1 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -472,3 +472,9 @@ def test_repr_str(self): 10.000 0.2 +4j 100.000 0.1 +6j""" assert str(sysm) == refm + + def test_unrecognized_keyword(self): + h = TransferFunction([1], [1, 2, 2]) + omega = np.logspace(-1, 2, 10) + with pytest.raises(TypeError, match="unrecognized keyword"): + frd = FRD(h, omega, unknown=None) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index e76a6be55..93ce5df65 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1580,7 +1580,8 @@ def test_interconnect_unused_input(): outputs=['u'], name='k') - with pytest.warns(UserWarning, match=r"Unused input\(s\) in InterconnectedSystem"): + with pytest.warns( + UserWarning, match=r"Unused input\(s\) in InterconnectedSystem"): h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y']) @@ -1611,13 +1612,19 @@ def test_interconnect_unused_input(): # warn if explicity ignored input in fact used - with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record: + with pytest.warns( + UserWarning, + match=r"Input\(s\) specified as ignored is \(are\) used:") \ + as record: h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y'], ignore_inputs=['u','n']) - with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record: + with pytest.warns( + UserWarning, + match=r"Input\(s\) specified as ignored is \(are\) used:") \ + as record: h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y'], diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py new file mode 100644 index 000000000..089d910c8 --- /dev/null +++ b/control/tests/kwargs_test.py @@ -0,0 +1,168 @@ +# kwargs_test.py - test for uncrecognized keywords +# RMM, 20 Mar 2022 +# +# Allowing unrecognized keywords to be passed to a function without +# generating and error message can generate annoying bugs, since you +# sometimes think you are telling the function to do something and actually +# you have a misspelling or other error and your input is being ignored. +# +# This unit test looks through all functions in the package for any that +# allow kwargs as part of the function signature and makes sure that there +# is a unit test that checks for unrecognized keywords. + +import inspect +import pytest +import warnings + +import control +import control.flatsys + +# List of all of the test modules where kwarg unit tests are defined +import control.tests.flatsys_test as flatsys_test +import control.tests.frd_test as frd_test +import control.tests.interconnect_test as interconnect_test +import control.tests.statefbk_test as statefbk_test +import control.tests.trdata_test as trdata_test + + +@pytest.mark.parametrize("module, prefix", [ + (control, ""), (control.flatsys, "flatsys.") +]) +def test_kwarg_search(module, prefix): + # Look through every object in the package + for name, obj in inspect.getmembers(module): + # Skip anything that is outside of this module + if inspect.getmodule(obj) is not None and \ + not inspect.getmodule(obj).__name__.startswith('control'): + # Skip anything that isn't part of the control package + continue + + # Look for functions with keyword arguments + if inspect.isfunction(obj): + # Get the signature for the function + sig = inspect.signature(obj) + + # See if there is a variable keyword argument + for argname, par in sig.parameters.items(): + if par.kind == inspect.Parameter.VAR_KEYWORD: + # Make sure there is a unit test defined + assert prefix + name in kwarg_unittest + + # Make sure there is a unit test + if not hasattr(kwarg_unittest[prefix + name], '__call__'): + warnings.warn("No unit test defined for '%s'" + % prefix + name) + + # Look for classes and then check member functions + if inspect.isclass(obj): + test_kwarg_search(obj, prefix + obj.__name__ + '.') + + +# Create a SISO system for use in parameterized tests +sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) + + +# Parameterized tests for looking for unrecognized keyword errors +@pytest.mark.parametrize("function, args, kwargs", [ + [control.dlqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], + [control.drss, (2, 1, 1), {}], + [control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}], + [control.lqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], + [control.pzmap, (sys,), {}], + [control.rlocus, (control.tf([1], [1, 1]), ), {}], + [control.root_locus, (control.tf([1], [1, 1]), ), {}], + [control.rss, (2, 1, 1), {}], + [control.ss, (0, 0, 0, 0), {'dt': 1}], + [control.ss2io, (sys,), {}], + [control.summing_junction, (2,), {}], + [control.tf, ([1], [1, 1]), {}], + [control.tf2io, (control.tf([1], [1, 1]),), {}], + [control.InputOutputSystem, (1, 1, 1), {}], + [control.StateSpace, ([[-1, 0], [0, -1]], [[1], [1]], [[1, 1]], 0), {}], + [control.TransferFunction, ([1], [1, 1]), {}], +]) +def test_unrecognized_kwargs(function, args, kwargs): + # Call the function normally and make sure it works + function(*args, **kwargs) + + # Now add an unrecognized keyword and make sure there is an error + with pytest.raises(TypeError, match="unrecognized keyword"): + function(*args, **kwargs, unknown=None) + + +# Parameterized tests for looking for keyword errors handled by matplotlib +@pytest.mark.parametrize("function, args, kwargs", [ + [control.bode, (sys, ), {}], + [control.bode_plot, (sys, ), {}], + [control.gangof4, (sys, sys), {}], + [control.gangof4_plot, (sys, sys), {}], + [control.nyquist, (sys, ), {}], + [control.nyquist_plot, (sys, ), {}], +]) +def test_matplotlib_kwargs(function, args, kwargs): + # Call the function normally and make sure it works + function(*args, **kwargs) + + # Now add an unrecognized keyword and make sure there is an error + with pytest.raises(AttributeError, match="has no property"): + function(*args, **kwargs, unknown=None) + + +# +# List of all unit tests that check for unrecognized keywords +# +# Every function that accepts variable keyword arguments (**kwargs) should +# have an entry in this table, to make sure that nothing is missing. This +# will also force people who add new functions to put in an appropriate unit +# test. +# + +kwarg_unittest = { + 'bode': test_matplotlib_kwargs, + 'bode_plot': test_matplotlib_kwargs, + 'describing_function_plot': None, + 'dlqr': statefbk_test.TestStatefbk.test_lqr_errors, + 'drss': test_unrecognized_kwargs, + 'find_eqpt': None, + 'gangof4': test_matplotlib_kwargs, + 'gangof4_plot': test_matplotlib_kwargs, + 'input_output_response': test_unrecognized_kwargs, + 'interconnect': interconnect_test.test_interconnect_exceptions, + 'linearize': None, + 'lqr': statefbk_test.TestStatefbk.test_lqr_errors, + 'nyquist': test_matplotlib_kwargs, + 'nyquist_plot': test_matplotlib_kwargs, + 'pzmap': None, + 'rlocus': test_unrecognized_kwargs, + 'root_locus': test_unrecognized_kwargs, + 'rss': test_unrecognized_kwargs, + 'set_defaults': None, + 'singular_values_plot': None, + 'ss': test_unrecognized_kwargs, + 'ss2io': test_unrecognized_kwargs, + 'ss2tf': test_unrecognized_kwargs, + 'summing_junction': interconnect_test.test_interconnect_exceptions, + 'tf': test_unrecognized_kwargs, + 'tf2io' : test_unrecognized_kwargs, + 'flatsys.point_to_point': + flatsys_test.TestFlatSys.test_point_to_point_errors, + 'FrequencyResponseData.__init__': + frd_test.TestFRD.test_unrecognized_keyword, + 'InputOutputSystem.__init__': None, + 'InputOutputSystem.linearize': None, + 'InterconnectedSystem.__init__': + interconnect_test.test_interconnect_exceptions, + 'InterconnectedSystem.linearize': None, + 'LinearICSystem.linearize': None, + 'LinearIOSystem.__init__': + interconnect_test.test_interconnect_exceptions, + 'LinearIOSystem.linearize': None, + 'NonlinearIOSystem.__init__': + interconnect_test.test_interconnect_exceptions, + 'NonlinearIOSystem.linearize': None, + 'StateSpace.__init__': None, + 'TimeResponseData.__call__': trdata_test.test_response_copy, + 'TransferFunction.__init__': None, + 'flatsys.FlatSystem.linearize': None, + 'flatsys.LinearFlatSystem.linearize': None, +} diff --git a/control/xferfcn.py b/control/xferfcn.py index 76d704afc..a171a1143 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1574,7 +1574,11 @@ def ss2tf(*args, **kwargs): # Assume we were given the A, B, C, D matrix and (optional) dt return _convert_to_transfer_function(StateSpace(*args, **kwargs)) - elif len(args) == 1: + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + + if len(args) == 1: sys = args[0] if isinstance(sys, StateSpace): return _convert_to_transfer_function(sys) From 26303ff99234096ee4833cecf9fceb2ae9090159 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 23:06:21 -0700 Subject: [PATCH 34/87] move kwarg lists inside function to fix initialization issue --- control/tests/kwargs_test.py | 100 ++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 089d910c8..5275dbd2d 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -58,54 +58,58 @@ def test_kwarg_search(module, prefix): test_kwarg_search(obj, prefix + obj.__name__ + '.') -# Create a SISO system for use in parameterized tests -sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) - - -# Parameterized tests for looking for unrecognized keyword errors -@pytest.mark.parametrize("function, args, kwargs", [ - [control.dlqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], - [control.drss, (2, 1, 1), {}], - [control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}], - [control.lqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], - [control.pzmap, (sys,), {}], - [control.rlocus, (control.tf([1], [1, 1]), ), {}], - [control.root_locus, (control.tf([1], [1, 1]), ), {}], - [control.rss, (2, 1, 1), {}], - [control.ss, (0, 0, 0, 0), {'dt': 1}], - [control.ss2io, (sys,), {}], - [control.summing_junction, (2,), {}], - [control.tf, ([1], [1, 1]), {}], - [control.tf2io, (control.tf([1], [1, 1]),), {}], - [control.InputOutputSystem, (1, 1, 1), {}], - [control.StateSpace, ([[-1, 0], [0, -1]], [[1], [1]], [[1, 1]], 0), {}], - [control.TransferFunction, ([1], [1, 1]), {}], -]) -def test_unrecognized_kwargs(function, args, kwargs): - # Call the function normally and make sure it works - function(*args, **kwargs) - - # Now add an unrecognized keyword and make sure there is an error - with pytest.raises(TypeError, match="unrecognized keyword"): - function(*args, **kwargs, unknown=None) - - -# Parameterized tests for looking for keyword errors handled by matplotlib -@pytest.mark.parametrize("function, args, kwargs", [ - [control.bode, (sys, ), {}], - [control.bode_plot, (sys, ), {}], - [control.gangof4, (sys, sys), {}], - [control.gangof4_plot, (sys, sys), {}], - [control.nyquist, (sys, ), {}], - [control.nyquist_plot, (sys, ), {}], -]) -def test_matplotlib_kwargs(function, args, kwargs): - # Call the function normally and make sure it works - function(*args, **kwargs) - - # Now add an unrecognized keyword and make sure there is an error - with pytest.raises(AttributeError, match="has no property"): - function(*args, **kwargs, unknown=None) +def test_unrecognized_kwargs(): + # Create a SISO system for use in parameterized tests + sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) + + table = [ + [control.dlqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], + [control.drss, (2, 1, 1), {}], + [control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}], + [control.lqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], + [control.pzmap, (sys,), {}], + [control.rlocus, (control.tf([1], [1, 1]), ), {}], + [control.root_locus, (control.tf([1], [1, 1]), ), {}], + [control.rss, (2, 1, 1), {}], + [control.ss, (0, 0, 0, 0), {'dt': 1}], + [control.ss2io, (sys,), {}], + [control.summing_junction, (2,), {}], + [control.tf, ([1], [1, 1]), {}], + [control.tf2io, (control.tf([1], [1, 1]),), {}], + [control.InputOutputSystem, (1, 1, 1), {}], + [control.StateSpace, ([[-1, 0], [0, -1]], [[1], [1]], [[1, 1]], 0), {}], + [control.TransferFunction, ([1], [1, 1]), {}], + ] + + for function, args, kwargs in table: + # Call the function normally and make sure it works + function(*args, **kwargs) + + # Now add an unrecognized keyword and make sure there is an error + with pytest.raises(TypeError, match="unrecognized keyword"): + function(*args, **kwargs, unknown=None) + + +def test_matplotlib_kwargs(): + # Create a SISO system for use in parameterized tests + sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) + + table = [ + [control.bode, (sys, ), {}], + [control.bode_plot, (sys, ), {}], + [control.gangof4, (sys, sys), {}], + [control.gangof4_plot, (sys, sys), {}], + [control.nyquist, (sys, ), {}], + [control.nyquist_plot, (sys, ), {}], + ] + + for function, args, kwargs in table: + # Call the function normally and make sure it works + function(*args, **kwargs) + + # Now add an unrecognized keyword and make sure there is an error + with pytest.raises(AttributeError, match="has no property"): + function(*args, **kwargs, unknown=None) # From 87cb31a5b4dccdc3541c0c02672e9aaae96ef82c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 21 Mar 2022 21:20:08 -0700 Subject: [PATCH 35/87] add missing kwarg tests + small bug fixes --- control/config.py | 5 +- control/iosys.py | 10 ++-- control/tests/config_test.py | 8 ++-- control/tests/kwargs_test.py | 88 ++++++++++++++++++++++-------------- 4 files changed, 68 insertions(+), 43 deletions(-) diff --git a/control/config.py b/control/config.py index 8acdf28e2..605fbcb23 100644 --- a/control/config.py +++ b/control/config.py @@ -73,6 +73,9 @@ def set_defaults(module, **keywords): if not isinstance(module, str): raise ValueError("module must be a string") for key, val in keywords.items(): + keyname = module + '.' + key + if keyname not in defaults and f"deprecated.{keyname}" not in defaults: + raise TypeError(f"unrecognized keyword: {key}") defaults[module + '.' + key] = val @@ -289,6 +292,6 @@ def use_legacy_defaults(version): set_defaults('control', squeeze_time_response=True) # switched mirror_style of nyquist from '-' to '--' - set_defaults('nyqist', mirror_style='-') + set_defaults('nyquist', mirror_style='-') return (major, minor, patch) diff --git a/control/iosys.py b/control/iosys.py index eeb483227..ccfdba2ca 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1829,7 +1829,7 @@ def ivp_rhs(t, x): def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={}, iu=None, iy=None, ix=None, idx=None, dx0=None, - return_y=False, return_result=False, **kw): + return_y=False, return_result=False): """Find the equilibrium point for an input/output system. Returns the value of an equilibrium point given the initial state and @@ -1933,7 +1933,7 @@ def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={}, # Take u0 as fixed and minimize over x # TODO: update to allow discrete time systems def ode_rhs(z): return sys._rhs(t, z, u0) - result = root(ode_rhs, x0, **kw) + result = root(ode_rhs, x0) z = (result.x, u0, sys._out(t, result.x, u0)) else: # Take y0 as fixed and minimize over x and u @@ -1944,7 +1944,7 @@ def rootfun(z): return np.concatenate( (sys._rhs(t, x, u), sys._out(t, x, u) - y0), axis=0) z0 = np.concatenate((x0, u0), axis=0) # Put variables together - result = root(rootfun, z0, **kw) # Find the eq point + result = root(rootfun, z0) # Find the eq point x, u = np.split(result.x, [nstates]) # Split result back in two z = (x, u, sys._out(t, x, u)) @@ -2056,7 +2056,7 @@ def rootfun(z): z0 = np.concatenate((x[state_vars], u[input_vars]), axis=0) # Finally, call the root finding function - result = root(rootfun, z0, **kw) + result = root(rootfun, z0) # Extract out the results and insert into x and u x[state_vars] = result.x[:nstate_vars] @@ -2135,7 +2135,7 @@ def _parse_signal_parameter(value, name, kwargs, end=False): if end and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) - + return value diff --git a/control/tests/config_test.py b/control/tests/config_test.py index e198254bf..b495a0f6f 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -23,10 +23,10 @@ class TestConfig: sys = ct.tf([10], [1, 2, 1]) def test_set_defaults(self): - ct.config.set_defaults('config', test1=1, test2=2, test3=None) - assert ct.config.defaults['config.test1'] == 1 - assert ct.config.defaults['config.test2'] == 2 - assert ct.config.defaults['config.test3'] is None + ct.config.set_defaults('freqplot', dB=1, deg=2, Hz=None) + assert ct.config.defaults['freqplot.dB'] == 1 + assert ct.config.defaults['freqplot.deg'] == 2 + assert ct.config.defaults['freqplot.Hz'] is None @mplcleanup def test_get_param(self): diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 5275dbd2d..0502114dc 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -13,6 +13,7 @@ import inspect import pytest import warnings +import matplotlib.pyplot as plt import control import control.flatsys @@ -36,28 +37,45 @@ def test_kwarg_search(module, prefix): not inspect.getmodule(obj).__name__.startswith('control'): # Skip anything that isn't part of the control package continue - - # Look for functions with keyword arguments - if inspect.isfunction(obj): - # Get the signature for the function - sig = inspect.signature(obj) - - # See if there is a variable keyword argument - for argname, par in sig.parameters.items(): - if par.kind == inspect.Parameter.VAR_KEYWORD: - # Make sure there is a unit test defined - assert prefix + name in kwarg_unittest - - # Make sure there is a unit test - if not hasattr(kwarg_unittest[prefix + name], '__call__'): - warnings.warn("No unit test defined for '%s'" - % prefix + name) + + # Only look for functions with keyword arguments + if not inspect.isfunction(obj): + continue + + # Get the signature for the function + sig = inspect.signature(obj) + + # Skip anything that is inherited + if inspect.isclass(module) and obj.__name__ not in module.__dict__: + continue + + # See if there is a variable keyword argument + for argname, par in sig.parameters.items(): + if not par.kind == inspect.Parameter.VAR_KEYWORD: + continue + + # Make sure there is a unit test defined + assert prefix + name in kwarg_unittest + + # Make sure there is a unit test + if not hasattr(kwarg_unittest[prefix + name], '__call__'): + warnings.warn("No unit test defined for '%s'" % prefix + name) + source = None + else: + source = inspect.getsource(kwarg_unittest[prefix + name]) + + # Make sure the unit test looks for unrecognized keyword + if source and source.find('unrecognized keyword') < 0: + warnings.warn( + f"'unrecognized keyword' not found in unit test " + f"for {name}") # Look for classes and then check member functions if inspect.isclass(obj): test_kwarg_search(obj, prefix + obj.__name__ + '.') +@pytest.mark.usefixtures('editsdefaults') def test_unrecognized_kwargs(): # Create a SISO system for use in parameterized tests sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) @@ -67,16 +85,20 @@ def test_unrecognized_kwargs(): [control.drss, (2, 1, 1), {}], [control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}], [control.lqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], + [control.linearize, (sys, 0, 0), {}], [control.pzmap, (sys,), {}], [control.rlocus, (control.tf([1], [1, 1]), ), {}], [control.root_locus, (control.tf([1], [1, 1]), ), {}], [control.rss, (2, 1, 1), {}], + [control.set_defaults, ('control',), {'default_dt': True}], [control.ss, (0, 0, 0, 0), {'dt': 1}], [control.ss2io, (sys,), {}], + [control.ss2tf, (sys,), {}], [control.summing_junction, (2,), {}], [control.tf, ([1], [1, 1]), {}], [control.tf2io, (control.tf([1], [1, 1]),), {}], [control.InputOutputSystem, (1, 1, 1), {}], + [control.InputOutputSystem.linearize, (sys, 0, 0), {}], [control.StateSpace, ([[-1, 0], [0, -1]], [[1], [1]], [[1, 1]], 0), {}], [control.TransferFunction, ([1], [1, 1]), {}], ] @@ -97,10 +119,13 @@ def test_matplotlib_kwargs(): table = [ [control.bode, (sys, ), {}], [control.bode_plot, (sys, ), {}], + [control.describing_function_plot, + (sys, control.descfcn.saturation_nonlinearity(1), [1, 2, 3, 4]), {}], [control.gangof4, (sys, sys), {}], [control.gangof4_plot, (sys, sys), {}], [control.nyquist, (sys, ), {}], [control.nyquist_plot, (sys, ), {}], + [control.singular_values_plot, (sys, ), {}], ] for function, args, kwargs in table: @@ -110,7 +135,11 @@ def test_matplotlib_kwargs(): # Now add an unrecognized keyword and make sure there is an error with pytest.raises(AttributeError, match="has no property"): function(*args, **kwargs, unknown=None) - + + # If we opened any figures, close them + if plt.gca(): + plt.close('all') + # # List of all unit tests that check for unrecognized keywords @@ -124,24 +153,23 @@ def test_matplotlib_kwargs(): kwarg_unittest = { 'bode': test_matplotlib_kwargs, 'bode_plot': test_matplotlib_kwargs, - 'describing_function_plot': None, + 'describing_function_plot': test_matplotlib_kwargs, 'dlqr': statefbk_test.TestStatefbk.test_lqr_errors, 'drss': test_unrecognized_kwargs, - 'find_eqpt': None, 'gangof4': test_matplotlib_kwargs, 'gangof4_plot': test_matplotlib_kwargs, 'input_output_response': test_unrecognized_kwargs, 'interconnect': interconnect_test.test_interconnect_exceptions, - 'linearize': None, + 'linearize': test_unrecognized_kwargs, 'lqr': statefbk_test.TestStatefbk.test_lqr_errors, 'nyquist': test_matplotlib_kwargs, 'nyquist_plot': test_matplotlib_kwargs, - 'pzmap': None, + 'pzmap': test_matplotlib_kwargs, 'rlocus': test_unrecognized_kwargs, 'root_locus': test_unrecognized_kwargs, 'rss': test_unrecognized_kwargs, - 'set_defaults': None, - 'singular_values_plot': None, + 'set_defaults': test_unrecognized_kwargs, + 'singular_values_plot': test_matplotlib_kwargs, 'ss': test_unrecognized_kwargs, 'ss2io': test_unrecognized_kwargs, 'ss2tf': test_unrecognized_kwargs, @@ -152,21 +180,15 @@ def test_matplotlib_kwargs(): flatsys_test.TestFlatSys.test_point_to_point_errors, 'FrequencyResponseData.__init__': frd_test.TestFRD.test_unrecognized_keyword, - 'InputOutputSystem.__init__': None, - 'InputOutputSystem.linearize': None, + 'InputOutputSystem.__init__': test_unrecognized_kwargs, + 'InputOutputSystem.linearize': test_unrecognized_kwargs, 'InterconnectedSystem.__init__': interconnect_test.test_interconnect_exceptions, - 'InterconnectedSystem.linearize': None, - 'LinearICSystem.linearize': None, 'LinearIOSystem.__init__': interconnect_test.test_interconnect_exceptions, - 'LinearIOSystem.linearize': None, 'NonlinearIOSystem.__init__': interconnect_test.test_interconnect_exceptions, - 'NonlinearIOSystem.linearize': None, - 'StateSpace.__init__': None, + 'StateSpace.__init__': test_unrecognized_kwargs, 'TimeResponseData.__call__': trdata_test.test_response_copy, - 'TransferFunction.__init__': None, - 'flatsys.FlatSystem.linearize': None, - 'flatsys.LinearFlatSystem.linearize': None, + 'TransferFunction.__init__': test_unrecognized_kwargs, } From 4c197caa9e3c5347aef6807fa64b3b908f966c8e Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 12 Mar 2022 13:15:09 -0800 Subject: [PATCH 36/87] add not implemented test/fix for continuous MPC --- control/optimal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/optimal.py b/control/optimal.py index aea9b02b8..da1bdcb8e 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -776,7 +776,7 @@ def create_mpc_iosystem(self): """Create an I/O system implementing an MPC controller""" # Check to make sure we are in discrete time if self.system.dt == 0: - raise ControlNotImplemented( + raise ct.ControlNotImplemented( "MPC for continuous time systems not implemented") def _update(t, x, u, params={}): From b1b3ad58b3d75d62974c8c504e35298548aca427 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 23 Mar 2022 07:45:02 -0700 Subject: [PATCH 37/87] create _NamedIOSystem, _NamedIOStateSystem parent classes --- control/lti.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/control/lti.py b/control/lti.py index b56c2bb44..174a7a3f8 100644 --- a/control/lti.py +++ b/control/lti.py @@ -16,12 +16,13 @@ from numpy import absolute, real, angle, abs from warnings import warn from . import config +from .namedio import _NamedIOSystem __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', 'isdtime', 'isctime', 'pole', 'zero', 'damp', 'evalfr', 'freqresp', 'dcgain'] -class LTI: +class LTI(_NamedIOSystem): """LTI is a parent class to linear time-invariant (LTI) system objects. LTI is the parent to the StateSpace and TransferFunction child classes. It From 3de4956c3deb3b72287c8dc3e8988fcb5756bd2c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 16 Mar 2022 16:59:28 -0700 Subject: [PATCH 38/87] initial creation of stochsys module + create_estimator_iosystem --- control/__init__.py | 1 + control/statefbk.py | 268 +------------------- control/stochsys.py | 449 +++++++++++++++++++++++++++++++++ control/tests/statefbk_test.py | 116 +-------- control/tests/stochsys_test.py | 187 ++++++++++++++ 5 files changed, 643 insertions(+), 378 deletions(-) create mode 100644 control/stochsys.py create mode 100644 control/tests/stochsys_test.py diff --git a/control/__init__.py b/control/__init__.py index 57f2d2690..386fa91c1 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -61,6 +61,7 @@ from .rlocus import * from .statefbk import * from .statesp import * +from .stochsys import * from .timeresp import * from .xferfcn import * from .ctrlutil import * diff --git a/control/statefbk.py b/control/statefbk.py index 099baa225..0aaf49f61 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -70,8 +70,8 @@ def sb03md(n, C, A, U, dico, job='X',fact='N',trana='N',ldwork=None): sb03od = None -__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr', 'lqe', - 'dlqr', 'dlqe', 'acker', 'create_statefbk_iosystem'] +__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr', + 'dlqr', 'acker', 'create_statefbk_iosystem'] # Pole placement @@ -260,270 +260,6 @@ def place_varga(A, B, p, dtime=False, alpha=None): return _ssmatrix(-F) -# contributed by Sawyer B. Fuller -def lqe(*args, method=None): - """lqe(A, G, C, QN, RN, [, NN]) - - Linear quadratic estimator design (Kalman filter) for continuous-time - systems. Given the system - - .. math:: - - x &= Ax + Bu + Gw \\\\ - y &= Cx + Du + v - - with unbiased process noise w and measurement noise v with covariances - - .. math:: E{ww'} = QN, E{vv'} = RN, E{wv'} = NN - - The lqe() function computes the observer gain matrix L such that the - stationary (non-time-varying) Kalman filter - - .. math:: x_e = A x_e + B u + L(y - C x_e - D u) - - produces a state estimate x_e that minimizes the expected squared error - using the sensor measurements y. The noise cross-correlation `NN` is - set to zero when omitted. - - The function can be called with either 3, 4, 5, or 6 arguments: - - * ``L, P, E = lqe(sys, QN, RN)`` - * ``L, P, E = lqe(sys, QN, RN, NN)`` - * ``L, P, E = lqe(A, G, C, QN, RN)`` - * ``L, P, E = lqe(A, G, C, QN, RN, NN)`` - - where `sys` is an `LTI` object, and `A`, `G`, `C`, `QN`, `RN`, and `NN` - are 2D arrays or matrices of appropriate dimension. - - Parameters - ---------- - A, G, C : 2D array_like - Dynamics, process noise (disturbance), and output matrices - sys : LTI (StateSpace or TransferFunction) - Linear I/O system, with the process noise input taken as the system - input. - QN, RN : 2D array_like - Process and sensor noise covariance matrices - NN : 2D array, optional - Cross covariance matrix. Not currently implemented. - method : str, optional - Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first - and then 'scipy'. - - Returns - ------- - L : 2D array (or matrix) - Kalman estimator gain - P : 2D array (or matrix) - Solution to Riccati equation - - .. math:: - - A P + P A^T - (P C^T + G N) R^{-1} (C P + N^T G^T) + G Q G^T = 0 - - E : 1D array - Eigenvalues of estimator poles eig(A - L C) - - Notes - ----- - 1. If the first argument is an LTI object, then this object will be used - to define the dynamics, noise and output matrices. Furthermore, if - the LTI object corresponds to a discrete time system, the ``dlqe()`` - function will be called. - - 2. The return type for 2D arrays depends on the default class set for - state space operations. See :func:`~control.use_numpy_matrix`. - - Examples - -------- - >>> L, P, E = lqe(A, G, C, QN, RN) - >>> L, P, E = lqe(A, G, C, Q, RN, NN) - - See Also - -------- - lqr, dlqe, dlqr - - """ - - # TODO: incorporate cross-covariance NN, something like this, - # which doesn't work for some reason - # if NN is None: - # NN = np.zeros(QN.size(0),RN.size(1)) - # NG = G @ NN - - # - # Process the arguments and figure out what inputs we received - # - - # If we were passed a discrete time system as the first arg, use dlqe() - if isinstance(args[0], LTI) and isdtime(args[0], strict=True): - # Call dlqe - return dlqe(*args, method=method) - - # Get the system description - if (len(args) < 3): - raise ControlArgument("not enough input arguments") - - # If we were passed a state space system, use that to get system matrices - if isinstance(args[0], StateSpace): - A = np.array(args[0].A, ndmin=2, dtype=float) - G = np.array(args[0].B, ndmin=2, dtype=float) - C = np.array(args[0].C, ndmin=2, dtype=float) - index = 1 - - elif isinstance(args[0], LTI): - # Don't allow other types of LTI systems - raise ControlArgument("LTI system must be in state space form") - - else: - # Arguments should be A and B matrices - A = np.array(args[0], ndmin=2, dtype=float) - G = np.array(args[1], ndmin=2, dtype=float) - C = np.array(args[2], ndmin=2, dtype=float) - index = 3 - - # Get the weighting matrices (converting to matrices, if needed) - QN = np.array(args[index], ndmin=2, dtype=float) - RN = np.array(args[index+1], ndmin=2, dtype=float) - - # Get the cross-covariance matrix, if given - if (len(args) > index + 2): - NN = np.array(args[index+2], ndmin=2, dtype=float) - raise ControlNotImplemented("cross-covariance not implemented") - - else: - # For future use (not currently used below) - NN = np.zeros((QN.shape[0], RN.shape[1])) - - # Check dimensions of G (needed before calling care()) - _check_shape("QN", QN, G.shape[1], G.shape[1]) - - # Compute the result (dimension and symmetry checking done in care()) - P, E, LT = care(A.T, C.T, G @ QN @ G.T, RN, method=method, - B_s="C", Q_s="QN", R_s="RN", S_s="NN") - return _ssmatrix(LT.T), _ssmatrix(P), E - - -# contributed by Sawyer B. Fuller -def dlqe(*args, method=None): - """dlqe(A, G, C, QN, RN, [, N]) - - Linear quadratic estimator design (Kalman filter) for discrete-time - systems. Given the system - - .. math:: - - x[n+1] &= Ax[n] + Bu[n] + Gw[n] \\\\ - y[n] &= Cx[n] + Du[n] + v[n] - - with unbiased process noise w and measurement noise v with covariances - - .. math:: E{ww'} = QN, E{vv'} = RN, E{wv'} = NN - - The dlqe() function computes the observer gain matrix L such that the - stationary (non-time-varying) Kalman filter - - .. math:: x_e[n+1] = A x_e[n] + B u[n] + L(y[n] - C x_e[n] - D u[n]) - - produces a state estimate x_e[n] that minimizes the expected squared error - using the sensor measurements y. The noise cross-correlation `NN` is - set to zero when omitted. - - Parameters - ---------- - A, G : 2D array_like - Dynamics and noise input matrices - QN, RN : 2D array_like - Process and sensor noise covariance matrices - NN : 2D array, optional - Cross covariance matrix (not yet supported) - method : str, optional - Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first - and then 'scipy'. - - Returns - ------- - L : 2D array (or matrix) - Kalman estimator gain - P : 2D array (or matrix) - Solution to Riccati equation - - .. math:: - - A P + P A^T - (P C^T + G N) R^{-1} (C P + N^T G^T) + G Q G^T = 0 - - E : 1D array - Eigenvalues of estimator poles eig(A - L C) - - Notes - ----- - The return type for 2D arrays depends on the default class set for - state space operations. See :func:`~control.use_numpy_matrix`. - - Examples - -------- - >>> L, P, E = dlqe(A, G, C, QN, RN) - >>> L, P, E = dlqe(A, G, C, QN, RN, NN) - - See Also - -------- - dlqr, lqe, lqr - - """ - - # - # Process the arguments and figure out what inputs we received - # - - # Get the system description - if (len(args) < 3): - raise ControlArgument("not enough input arguments") - - # If we were passed a continus time system as the first arg, raise error - if isinstance(args[0], LTI) and isctime(args[0], strict=True): - raise ControlArgument("dlqr() called with a continuous time system") - - # If we were passed a state space system, use that to get system matrices - if isinstance(args[0], StateSpace): - A = np.array(args[0].A, ndmin=2, dtype=float) - G = np.array(args[0].B, ndmin=2, dtype=float) - C = np.array(args[0].C, ndmin=2, dtype=float) - index = 1 - - elif isinstance(args[0], LTI): - # Don't allow other types of LTI systems - raise ControlArgument("LTI system must be in state space form") - - else: - # Arguments should be A and B matrices - A = np.array(args[0], ndmin=2, dtype=float) - G = np.array(args[1], ndmin=2, dtype=float) - C = np.array(args[2], ndmin=2, dtype=float) - index = 3 - - # Get the weighting matrices (converting to matrices, if needed) - QN = np.array(args[index], ndmin=2, dtype=float) - RN = np.array(args[index+1], ndmin=2, dtype=float) - - # TODO: incorporate cross-covariance NN, something like this, - # which doesn't work for some reason - # if NN is None: - # NN = np.zeros(QN.size(0),RN.size(1)) - # NG = G @ NN - if len(args) > index + 2: - NN = np.array(args[index+2], ndmin=2, dtype=float) - raise ControlNotImplemented("cross-covariance not yet implememented") - - # Check dimensions of G (needed before calling care()) - _check_shape("QN", QN, G.shape[1], G.shape[1]) - - # Compute the result (dimension and symmetry checking done in dare()) - P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method, - B_s="C", Q_s="QN", R_s="RN", S_s="NN") - return _ssmatrix(LT.T), _ssmatrix(P), E - # Contributed by Roberto Bucher def acker(A, B, poles): """Pole placement using Ackermann method diff --git a/control/stochsys.py b/control/stochsys.py new file mode 100644 index 000000000..3dc82e32f --- /dev/null +++ b/control/stochsys.py @@ -0,0 +1,449 @@ +# stochsys.py - stochastic systems module +# RMM, 16 Mar 2022 +# +# This module contains functions that are intended to be used for analysis +# and design of stochastic control systems, mainly involving Kalman +# filtering and its variants. +# + +"""The :mod:`~control.stochsys` module contains functions for analyzing and +designing stochastic (control) systems, including white noise processes and +Kalman filtering. + +""" + +__license__ = "BSD" +__maintainer__ = "Richard Murray" +__email__ = "murray@cds.caltech.edu" + +import numpy as np + +from .iosys import InputOutputSystem, NonlinearIOSystem +from .lti import LTI, isctime, isdtime +from .mateqn import care, dare, _check_shape +from .statesp import StateSpace, _ssmatrix +from .exception import ControlArgument, ControlNotImplemented + +__all__ = ['lqe','dlqe', 'create_estimator_iosystem'] + + +# contributed by Sawyer B. Fuller +def lqe(*args, **keywords): + """lqe(A, G, C, QN, RN, [, NN]) + + Linear quadratic estimator design (Kalman filter) for continuous-time + systems. Given the system + + .. math:: + + x &= Ax + Bu + Gw \\\\ + y &= Cx + Du + v + + with unbiased process noise w and measurement noise v with covariances + + .. math:: E{ww'} = QN, E{vv'} = RN, E{wv'} = NN + + The lqe() function computes the observer gain matrix L such that the + stationary (non-time-varying) Kalman filter + + .. math:: x_e = A x_e + B u + L(y - C x_e - D u) + + produces a state estimate x_e that minimizes the expected squared error + using the sensor measurements y. The noise cross-correlation `NN` is + set to zero when omitted. + + The function can be called with either 3, 4, 5, or 6 arguments: + + * ``L, P, E = lqe(sys, QN, RN)`` + * ``L, P, E = lqe(sys, QN, RN, NN)`` + * ``L, P, E = lqe(A, G, C, QN, RN)`` + * ``L, P, E = lqe(A, G, C, QN, RN, NN)`` + + where `sys` is an `LTI` object, and `A`, `G`, `C`, `QN`, `RN`, and `NN` + are 2D arrays or matrices of appropriate dimension. + + Parameters + ---------- + A, G, C : 2D array_like + Dynamics, process noise (disturbance), and output matrices + sys : LTI (StateSpace or TransferFunction) + Linear I/O system, with the process noise input taken as the system + input. + QN, RN : 2D array_like + Process and sensor noise covariance matrices + NN : 2D array, optional + Cross covariance matrix. Not currently implemented. + method : str, optional + Set the method used for computing the result. Current methods are + 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + and then 'scipy'. + + Returns + ------- + L : 2D array (or matrix) + Kalman estimator gain + P : 2D array (or matrix) + Solution to Riccati equation + + .. math:: + + A P + P A^T - (P C^T + G N) R^{-1} (C P + N^T G^T) + G Q G^T = 0 + + E : 1D array + Eigenvalues of estimator poles eig(A - L C) + + Notes + ----- + 1. If the first argument is an LTI object, then this object will be used + to define the dynamics, noise and output matrices. Furthermore, if + the LTI object corresponds to a discrete time system, the ``dlqe()`` + function will be called. + + 2. The return type for 2D arrays depends on the default class set for + state space operations. See :func:`~control.use_numpy_matrix`. + + Examples + -------- + >>> L, P, E = lqe(A, G, C, QN, RN) + >>> L, P, E = lqe(A, G, C, Q, RN, NN) + + See Also + -------- + lqr, dlqe, dlqr + + """ + + # TODO: incorporate cross-covariance NN, something like this, + # which doesn't work for some reason + # if NN is None: + # NN = np.zeros(QN.size(0),RN.size(1)) + # NG = G @ NN + + # + # Process the arguments and figure out what inputs we received + # + + # Get the method to use (if specified as a keyword) + method = keywords.get('method', None) + + # Get the system description + if (len(args) < 3): + raise ControlArgument("not enough input arguments") + + # If we were passed a discrete time system as the first arg, use dlqe() + if isinstance(args[0], LTI) and isdtime(args[0], strict=True): + # Call dlqe + return dlqe(*args, **keywords) + + # If we were passed a state space system, use that to get system matrices + if isinstance(args[0], StateSpace): + A = np.array(args[0].A, ndmin=2, dtype=float) + G = np.array(args[0].B, ndmin=2, dtype=float) + C = np.array(args[0].C, ndmin=2, dtype=float) + index = 1 + + elif isinstance(args[0], LTI): + # Don't allow other types of LTI systems + raise ControlArgument("LTI system must be in state space form") + + else: + # Arguments should be A and B matrices + A = np.array(args[0], ndmin=2, dtype=float) + G = np.array(args[1], ndmin=2, dtype=float) + C = np.array(args[2], ndmin=2, dtype=float) + index = 3 + + # Get the weighting matrices (converting to matrices, if needed) + QN = np.array(args[index], ndmin=2, dtype=float) + RN = np.array(args[index+1], ndmin=2, dtype=float) + + # Get the cross-covariance matrix, if given + if (len(args) > index + 2): + NN = np.array(args[index+2], ndmin=2, dtype=float) + raise ControlNotImplemented("cross-covariance not implemented") + + else: + # For future use (not currently used below) + NN = np.zeros((QN.shape[0], RN.shape[1])) + + # Check dimensions of G (needed before calling care()) + _check_shape("QN", QN, G.shape[1], G.shape[1]) + + # Compute the result (dimension and symmetry checking done in care()) + P, E, LT = care(A.T, C.T, G @ QN @ G.T, RN, method=method, + B_s="C", Q_s="QN", R_s="RN", S_s="NN") + return _ssmatrix(LT.T), _ssmatrix(P), E + + +# contributed by Sawyer B. Fuller +def dlqe(*args, **keywords): + """dlqe(A, G, C, QN, RN, [, N]) + + Linear quadratic estimator design (Kalman filter) for discrete-time + systems. Given the system + + .. math:: + + x[n+1] &= Ax[n] + Bu[n] + Gw[n] \\\\ + y[n] &= Cx[n] + Du[n] + v[n] + + with unbiased process noise w and measurement noise v with covariances + + .. math:: E{ww'} = QN, E{vv'} = RN, E{wv'} = NN + + The dlqe() function computes the observer gain matrix L such that the + stationary (non-time-varying) Kalman filter + + .. math:: x_e[n+1] = A x_e[n] + B u[n] + L(y[n] - C x_e[n] - D u[n]) + + produces a state estimate x_e[n] that minimizes the expected squared error + using the sensor measurements y. The noise cross-correlation `NN` is + set to zero when omitted. + + Parameters + ---------- + A, G : 2D array_like + Dynamics and noise input matrices + QN, RN : 2D array_like + Process and sensor noise covariance matrices + NN : 2D array, optional + Cross covariance matrix (not yet supported) + method : str, optional + Set the method used for computing the result. Current methods are + 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + and then 'scipy'. + + Returns + ------- + L : 2D array (or matrix) + Kalman estimator gain + P : 2D array (or matrix) + Solution to Riccati equation + + .. math:: + + A P + P A^T - (P C^T + G N) R^{-1} (C P + N^T G^T) + G Q G^T = 0 + + E : 1D array + Eigenvalues of estimator poles eig(A - L C) + + Notes + ----- + The return type for 2D arrays depends on the default class set for + state space operations. See :func:`~control.use_numpy_matrix`. + + Examples + -------- + >>> L, P, E = dlqe(A, G, C, QN, RN) + >>> L, P, E = dlqe(A, G, C, QN, RN, NN) + + See Also + -------- + dlqr, lqe, lqr + + """ + + # + # Process the arguments and figure out what inputs we received + # + + # Get the method to use (if specified as a keyword) + method = keywords.get('method', None) + + # Get the system description + if (len(args) < 3): + raise ControlArgument("not enough input arguments") + + # If we were passed a continus time system as the first arg, raise error + if isinstance(args[0], LTI) and isctime(args[0], strict=True): + raise ControlArgument("dlqr() called with a continuous time system") + + # If we were passed a state space system, use that to get system matrices + if isinstance(args[0], StateSpace): + A = np.array(args[0].A, ndmin=2, dtype=float) + G = np.array(args[0].B, ndmin=2, dtype=float) + C = np.array(args[0].C, ndmin=2, dtype=float) + index = 1 + + elif isinstance(args[0], LTI): + # Don't allow other types of LTI systems + raise ControlArgument("LTI system must be in state space form") + + else: + # Arguments should be A and B matrices + A = np.array(args[0], ndmin=2, dtype=float) + G = np.array(args[1], ndmin=2, dtype=float) + C = np.array(args[2], ndmin=2, dtype=float) + index = 3 + + # Get the weighting matrices (converting to matrices, if needed) + QN = np.array(args[index], ndmin=2, dtype=float) + RN = np.array(args[index+1], ndmin=2, dtype=float) + + # TODO: incorporate cross-covariance NN, something like this, + # which doesn't work for some reason + # if NN is None: + # NN = np.zeros(QN.size(0),RN.size(1)) + # NG = G @ NN + if len(args) > index + 2: + NN = np.array(args[index+2], ndmin=2, dtype=float) + raise ControlNotImplemented("cross-covariance not yet implememented") + + # Check dimensions of G (needed before calling care()) + _check_shape("QN", QN, G.shape[1], G.shape[1]) + + # Compute the result (dimension and symmetry checking done in dare()) + P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method, + B_s="C", Q_s="QN", R_s="RN", S_s="NN") + return _ssmatrix(LT.T), _ssmatrix(P), E + + +# Function to create an estimator +def create_estimator_iosystem( + sys, QN, RN, P0=None, G=None, C=None, + state_labels='xhat[{i}]', output_labels='xhat[{i}]', + covariance_labels='P[{i},{j}]'): + """Create an I/O system implementing a linqear quadratic estimator + + This function creates an input/output system that implements a + state estimator of the form + + xhat[k + 1] = A x[k] + B u[k] - L (C xhat[k] - y[k]) + P[k + 1] = A P A^T + F QN F^T - A P C^T Reps^{-1} C P A + L = A P C^T Reps^{-1} + + where Reps = RN + C P C^T. It can be called in the form + + estim = ct.create_estimator_iosystem(sys, QN, RN) + + where ``sys`` is the process dynamics and QN and RN are the covariance of + the disturbance noise and sensor noise. The function returns the + estimator ``estim`` as I/O systems. + + Parameters + ---------- + sys : InputOutputSystem + The I/O system that represents the process dynamics. If no estimator + is given, the output of this system should represent the full state. + QN, RN : ndarray + Process and sensor noise covariance matrices. + P0 : ndarray, optional + Initial covariance matrix. If not specified, defaults to the steady + state covariance. + G : ndarray, optional + Disturbance matrix describing how the disturbances enters the + dynamics. Defaults to sys.B. + C : ndarray, optional + If the system has all full states output, define the measured values + to be used by the estimator. Otherwise, use the system output as the + measured values. + {state, covariance, output}_labels : str or list of str, optional + Set the name of the signals to use for the internal state, covariance, + and output (state estimate). If a single string is specified, it + should be a format string using the variable ``i`` as an index (or + ``i`` and ``j`` for covariance). Otherwise, a list of strings + matching the size of the respective signal should be used. Default is + ``'xhat[{i}]'`` for state and output labels and ``'P[{i},{j}]'`` for + covariance labels. + + Returns + ------- + estim : InputOutputSystem + Input/output system representing the estimator. This system takes the + system input and output and generates the estimated state. + + Notes + ----- + This function can be used with the ``create_statefbk_iosystem()`` function + to create a closed loop, output-feedback, state space controller: + + K, _, _ = ct.lqr(sys, Q, R) + est = ct.create_estimator_iosystem(sys, QN, RN, P0) + ctrl, clsys = ct.create_statefbk_iosystem(sys, K, estimator=est) + + """ + + # Make sure that we were passed an I/O system as an input + if not isinstance(sys, InputOutputSystem): + raise ControlArgument("Input system must be I/O system") + + # Extract the matrices that we need for easy reference + A, B = sys.A, sys.B + + # Set the disturbance and output matrices + G = sys.B if G is None else G + if C is not None: + # Make sure that we have the full system output + if not np.array_equal(sys.C, np.eye(sys.nstates)): + raise ValueError("System output must be full state") + + # Make sure that the output matches the size of RN + if C.shape[0] != RN.shape[0]: + raise ValueError("System output is the wrong size for C") + else: + # Use the system outputs as the sensor outputs + C = sys.C + + # Initialize the covariance matrix + if P0 is None: + # Initalize P0 to the steady state value + _, P0, _ = lqe(A, G, C, QN, RN) + + # Figure out the labels to use + if isinstance(state_labels, str): + # Generate the list of labels using the argument as a format string + state_labels = [state_labels.format(i=i) for i in range(sys.nstates)] + + if isinstance(covariance_labels, str): + # Generate the list of labels using the argument as a format string + covariance_labels = [ + covariance_labels.format(i=i, j=j) \ + for i in range(sys.nstates) for j in range(sys.nstates)] + + if isinstance(output_labels, str): + # Generate the list of labels using the argument as a format string + output_labels = [output_labels.format(i=i) for i in range(sys.nstates)] + + if isctime(sys): + raise NotImplementedError("continuous time not yet implemented") + + else: + # Create an I/O system for the state feedback gains + def _estim_update(t, x, u, params): + # See if we are estimating or predicting + correct = params.get('correct', True) + + # Get the state of the estimator + x = np.array(x) # bug fix for python-control 0.9.1 + xhat = x[0:sys.nstates] + P = x[sys.nstates:].reshape(sys.nstates, sys.nstates) + + # Extract the inputs to the estimator + y = u[0:C.shape[0]] + u = u[C.shape[0]:] + + # Compute the optimal again + Reps_inv = np.linalg.inv(RN + C @ P @ C.T) + L = A @ P @ C.T @ Reps_inv + + # Update the state estimate + dxhat = A @ xhat + B @ u # prediction + if correct: + dxhat -= L @ (C @ xhat - y) # correction + + # Update the covariance + dP = A @ P @ A.T + G @ QN @ G.T + if correct: + dP -= A @ P @ C.T @ Reps_inv @ C @ P @ A.T + + # Return the update + return np.hstack([dxhat, dP.reshape(-1)]) + + def _estim_output(t, x, u, params): + return x[0:sys.nstates] + + # Define the estimator system + return NonlinearIOSystem( + _estim_update, _estim_output, states=state_labels + covariance_labels, + inputs=sys.output_labels + sys.input_labels, outputs=output_labels, + dt=sys.dt) diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 10ae85a78..9f04b3723 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -7,12 +7,12 @@ import pytest import control as ct -from control import lqe, pole, rss, ss, tf +from control import lqe, dlqe, pole, rss, ss, tf from control.exception import ControlDimension, ControlSlycot, \ ControlArgument, slycot_check from control.mateqn import care, dare from control.statefbk import (ctrb, obsv, place, place_varga, lqr, dlqr, - lqe, dlqe, gram, acker) + gram, acker) from control.tests.conftest import (slycotonly, check_deprecated_matrix, ismatarrayout, asmatarrayout) @@ -440,82 +440,6 @@ def testDLQR_warning(self): with pytest.warns(UserWarning): (K, S, E) = dlqr(A, B, Q, R, N) - def check_LQE(self, L, P, poles, G, QN, RN): - P_expected = asmatarrayout(np.sqrt(G @ QN @ G @ RN)) - L_expected = asmatarrayout(P_expected / RN) - poles_expected = -np.squeeze(np.asarray(L_expected)) - np.testing.assert_array_almost_equal(P, P_expected) - np.testing.assert_array_almost_equal(L, L_expected) - np.testing.assert_array_almost_equal(poles, poles_expected) - - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) - def test_LQE(self, matarrayin, method): - if method == 'slycot' and not slycot_check(): - return - - A, G, C, QN, RN = (matarrayin([[X]]) for X in [0., .1, 1., 10., 2.]) - L, P, poles = lqe(A, G, C, QN, RN, method=method) - self.check_LQE(L, P, poles, G, QN, RN) - - @pytest.mark.parametrize("cdlqe", [lqe, dlqe]) - def test_lqe_call_format(self, cdlqe): - # Create a random state space system for testing - sys = rss(4, 3, 2) - sys.dt = None # treat as either continuous or discrete time - - # Covariance matrices - Q = np.eye(sys.ninputs) - R = np.eye(sys.noutputs) - N = np.zeros((sys.ninputs, sys.noutputs)) - - # Standard calling format - Lref, Pref, Eref = cdlqe(sys.A, sys.B, sys.C, Q, R) - - # Call with system instead of matricees - L, P, E = cdlqe(sys, Q, R) - np.testing.assert_array_almost_equal(Lref, L) - np.testing.assert_array_almost_equal(Pref, P) - np.testing.assert_array_almost_equal(Eref, E) - - # Make sure we get an error if we specify N - with pytest.raises(ct.ControlNotImplemented): - L, P, E = cdlqe(sys, Q, R, N) - - # Inconsistent system dimensions - with pytest.raises(ct.ControlDimension, match="Incompatible"): - L, P, E = cdlqe(sys.A, sys.C, sys.B, Q, R) - - # Incorrect covariance matrix dimensions - with pytest.raises(ct.ControlDimension, match="Incompatible"): - L, P, E = cdlqe(sys.A, sys.B, sys.C, R, Q) - - # Too few input arguments - with pytest.raises(ct.ControlArgument, match="not enough input"): - L, P, E = cdlqe(sys.A, sys.C) - - # First argument is the wrong type (use SISO for non-slycot tests) - sys_tf = tf(rss(3, 1, 1)) - sys_tf.dt = None # treat as either continuous or discrete time - with pytest.raises(ct.ControlArgument, match="LTI system must be"): - L, P, E = cdlqe(sys_tf, Q, R) - - def check_DLQE(self, L, P, poles, G, QN, RN): - P_expected = asmatarrayout(G.dot(QN).dot(G)) - L_expected = asmatarrayout(0) - poles_expected = -np.squeeze(np.asarray(L_expected)) - np.testing.assert_array_almost_equal(P, P_expected) - np.testing.assert_array_almost_equal(L, L_expected) - np.testing.assert_array_almost_equal(poles, poles_expected) - - @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) - def test_DLQE(self, matarrayin, method): - if method == 'slycot' and not slycot_check(): - return - - A, G, C, QN, RN = (matarrayin([[X]]) for X in [0., .1, 1., 10., 2.]) - L, P, poles = dlqe(A, G, C, QN, RN, method=method) - self.check_DLQE(L, P, poles, G, QN, RN) - def test_care(self, matarrayin): """Test stabilizing and anti-stabilizing feedback, continuous""" A = matarrayin(np.diag([1, -1])) @@ -584,39 +508,6 @@ def test_lqr_discrete(self): with pytest.raises(ControlArgument, match="dsys must be discrete"): K, S, E = ct.dlqr(csys, Q, R) - def test_lqe_discrete(self): - """Test overloading of lqe operator for discrete time systems""" - csys = ct.rss(2, 1, 1) - dsys = ct.drss(2, 1, 1) - Q = np.eye(1) - R = np.eye(1) - - # Calling with a system versus explicit A, B should be the sam - K_csys, S_csys, E_csys = ct.lqe(csys, Q, R) - K_expl, S_expl, E_expl = ct.lqe(csys.A, csys.B, csys.C, Q, R) - np.testing.assert_almost_equal(K_csys, K_expl) - np.testing.assert_almost_equal(S_csys, S_expl) - np.testing.assert_almost_equal(E_csys, E_expl) - - # Calling lqe() with a discrete time system should call dlqe() - K_lqe, S_lqe, E_lqe = ct.lqe(dsys, Q, R) - K_dlqe, S_dlqe, E_dlqe = ct.dlqe(dsys, Q, R) - np.testing.assert_almost_equal(K_lqe, K_dlqe) - np.testing.assert_almost_equal(S_lqe, S_dlqe) - np.testing.assert_almost_equal(E_lqe, E_dlqe) - - # Calling lqe() with no timebase should call lqe() - asys = ct.ss(csys.A, csys.B, csys.C, csys.D, dt=None) - K_asys, S_asys, E_asys = ct.lqe(asys, Q, R) - K_expl, S_expl, E_expl = ct.lqe(csys.A, csys.B, csys.C, Q, R) - np.testing.assert_almost_equal(K_asys, K_expl) - np.testing.assert_almost_equal(S_asys, S_expl) - np.testing.assert_almost_equal(E_asys, E_expl) - - # Calling dlqe() with a continuous time system should raise an error - with pytest.raises(ControlArgument, match="called with a continuous"): - K, S, E = ct.dlqe(csys, Q, R) - @pytest.mark.parametrize( 'nstates, noutputs, ninputs, nintegrators, type', [(2, 0, 1, 0, None), @@ -630,7 +521,8 @@ def test_lqe_discrete(self): (4, 0, 2, 2, 'nonlinear'), (4, 3, 2, 2, 'nonlinear'), ]) - def test_lqr_iosys(self, nstates, ninputs, noutputs, nintegrators, type): + def test_statefbk_iosys( + self, nstates, ninputs, noutputs, nintegrators, type): # Create the system to be controlled (and estimator) # TODO: make sure it is controllable? if noutputs == 0: diff --git a/control/tests/stochsys_test.py b/control/tests/stochsys_test.py new file mode 100644 index 000000000..a8319fd2d --- /dev/null +++ b/control/tests/stochsys_test.py @@ -0,0 +1,187 @@ +# stochsys_test.py - test stochastic system operations +# RMM, 16 Mar 2022 + +import numpy as np +import pytest +from control.tests.conftest import asmatarrayout + +import control as ct +from control import lqe, dlqe, rss, drss, tf, ss, ControlArgument, slycot_check + +# Utility function to check LQE answer +def check_LQE(L, P, poles, G, QN, RN): + P_expected = asmatarrayout(np.sqrt(G @ QN @ G @ RN)) + L_expected = asmatarrayout(P_expected / RN) + poles_expected = -np.squeeze(np.asarray(L_expected)) + np.testing.assert_array_almost_equal(P, P_expected) + np.testing.assert_array_almost_equal(L, L_expected) + np.testing.assert_array_almost_equal(poles, poles_expected) + +# Utility function to check discrete LQE solutions +def check_DLQE(L, P, poles, G, QN, RN): + P_expected = asmatarrayout(G.dot(QN).dot(G)) + L_expected = asmatarrayout(0) + poles_expected = -np.squeeze(np.asarray(L_expected)) + np.testing.assert_array_almost_equal(P, P_expected) + np.testing.assert_array_almost_equal(L, L_expected) + np.testing.assert_array_almost_equal(poles, poles_expected) + +@pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) +def test_LQE(matarrayin, method): + if method == 'slycot' and not slycot_check(): + return + + A, G, C, QN, RN = (matarrayin([[X]]) for X in [0., .1, 1., 10., 2.]) + L, P, poles = lqe(A, G, C, QN, RN, method=method) + check_LQE(L, P, poles, G, QN, RN) + +@pytest.mark.parametrize("cdlqe", [lqe, dlqe]) +def test_lqe_call_format(cdlqe): + # Create a random state space system for testing + sys = rss(4, 3, 2) + sys.dt = None # treat as either continuous or discrete time + + # Covariance matrices + Q = np.eye(sys.ninputs) + R = np.eye(sys.noutputs) + N = np.zeros((sys.ninputs, sys.noutputs)) + + # Standard calling format + Lref, Pref, Eref = cdlqe(sys.A, sys.B, sys.C, Q, R) + + # Call with system instead of matricees + L, P, E = cdlqe(sys, Q, R) + np.testing.assert_array_almost_equal(Lref, L) + np.testing.assert_array_almost_equal(Pref, P) + np.testing.assert_array_almost_equal(Eref, E) + + # Make sure we get an error if we specify N + with pytest.raises(ct.ControlNotImplemented): + L, P, E = cdlqe(sys, Q, R, N) + + # Inconsistent system dimensions + with pytest.raises(ct.ControlDimension, match="Incompatible"): + L, P, E = cdlqe(sys.A, sys.C, sys.B, Q, R) + + # Incorrect covariance matrix dimensions + with pytest.raises(ct.ControlDimension, match="Incompatible"): + L, P, E = cdlqe(sys.A, sys.B, sys.C, R, Q) + + # Too few input arguments + with pytest.raises(ct.ControlArgument, match="not enough input"): + L, P, E = cdlqe(sys.A, sys.C) + + # First argument is the wrong type (use SISO for non-slycot tests) + sys_tf = tf(rss(3, 1, 1)) + sys_tf.dt = None # treat as either continuous or discrete time + with pytest.raises(ct.ControlArgument, match="LTI system must be"): + L, P, E = cdlqe(sys_tf, Q, R) + +@pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) +def test_DLQE(matarrayin, method): + if method == 'slycot' and not slycot_check(): + return + + A, G, C, QN, RN = (matarrayin([[X]]) for X in [0., .1, 1., 10., 2.]) + L, P, poles = dlqe(A, G, C, QN, RN, method=method) + check_DLQE(L, P, poles, G, QN, RN) + +def test_lqe_discrete(): + """Test overloading of lqe operator for discrete time systems""" + csys = ct.rss(2, 1, 1) + dsys = ct.drss(2, 1, 1) + Q = np.eye(1) + R = np.eye(1) + + # Calling with a system versus explicit A, B should be the sam + K_csys, S_csys, E_csys = ct.lqe(csys, Q, R) + K_expl, S_expl, E_expl = ct.lqe(csys.A, csys.B, csys.C, Q, R) + np.testing.assert_almost_equal(K_csys, K_expl) + np.testing.assert_almost_equal(S_csys, S_expl) + np.testing.assert_almost_equal(E_csys, E_expl) + + # Calling lqe() with a discrete time system should call dlqe() + K_lqe, S_lqe, E_lqe = ct.lqe(dsys, Q, R) + K_dlqe, S_dlqe, E_dlqe = ct.dlqe(dsys, Q, R) + np.testing.assert_almost_equal(K_lqe, K_dlqe) + np.testing.assert_almost_equal(S_lqe, S_dlqe) + np.testing.assert_almost_equal(E_lqe, E_dlqe) + + # Calling lqe() with no timebase should call lqe() + asys = ct.ss(csys.A, csys.B, csys.C, csys.D, dt=None) + K_asys, S_asys, E_asys = ct.lqe(asys, Q, R) + K_expl, S_expl, E_expl = ct.lqe(csys.A, csys.B, csys.C, Q, R) + np.testing.assert_almost_equal(K_asys, K_expl) + np.testing.assert_almost_equal(S_asys, S_expl) + np.testing.assert_almost_equal(E_asys, E_expl) + + # Calling dlqe() with a continuous time system should raise an error + with pytest.raises(ControlArgument, match="called with a continuous"): + K, S, E = ct.dlqe(csys, Q, R) + +def test_estimator_iosys(): + sys = ct.drss(4, 2, 2, strictly_proper=True) + + Q, R = np.eye(sys.nstates), np.eye(sys.ninputs) + K, _, _ = ct.dlqr(sys, Q, R) + + P0 = np.eye(sys.nstates) + QN = np.eye(sys.ninputs) + RN = np.eye(sys.noutputs) + estim = ct.create_estimator_iosystem(sys, QN, RN, P0) + + ctrl, clsys = ct.create_statefbk_iosystem(sys, K, estimator=estim) + + # Extract the elements of the estimator + est = estim.linearize(0, 0) + Be1 = est.B[:sys.nstates, :sys.noutputs] + Be2 = est.B[:sys.nstates, sys.noutputs:] + A_clchk = np.block([ + [sys.A, -sys.B @ K], + [Be1 @ sys.C, est.A[:sys.nstates, :sys.nstates] - Be2 @ K] + ]) + B_clchk = np.block([ + [sys.B @ K, sys.B], + [Be2 @ K, Be2] + ]) + C_clchk = np.block([ + [sys.C, np.zeros((sys.noutputs, sys.nstates))], + [np.zeros_like(K), -K] + ]) + D_clchk = np.block([ + [np.zeros((sys.noutputs, sys.nstates + sys.ninputs))], + [K, np.eye(sys.ninputs)] + ]) + + # Check to make sure everything matches + cls = clsys.linearize(0, 0) + nstates = sys.nstates + np.testing.assert_array_almost_equal(cls.A[:2*nstates, :2*nstates], A_clchk) + np.testing.assert_array_almost_equal(cls.B[:2*nstates, :], B_clchk) + np.testing.assert_array_almost_equal(cls.C[:, :2*nstates], C_clchk) + np.testing.assert_array_almost_equal(cls.D, D_clchk) + + +def test_estimator_errors(): + sys = ct.drss(4, 2, 2, strictly_proper=True) + P0 = np.eye(sys.nstates) + QN = np.eye(sys.ninputs) + RN = np.eye(sys.noutputs) + + with pytest.raises(ct.ControlArgument, match="Input system must be I/O"): + sys_tf = ct.tf([1], [1, 1], dt=True) + estim = ct.create_estimator_iosystem(sys_tf, QN, RN) + + with pytest.raises(NotImplementedError, match="continuous time not"): + sys_ct = ct.rss(4, 2, 2, strictly_proper=True) + estim = ct.create_estimator_iosystem(sys_ct, QN, RN) + + with pytest.raises(ValueError, match="output must be full state"): + C = np.eye(2, 4) + estim = ct.create_estimator_iosystem(sys, QN, RN, C=C) + + with pytest.raises(ValueError, match="output is the wrong size"): + sys_fs = ct.drss(4, 4, 2, strictly_proper=True) + sys_fs.C = np.eye(4) + C = np.eye(1, 4) + estim = ct.create_estimator_iosystem(sys_fs, QN, RN, C=C) From dff520652618c207b6cc6cae6c3dba926315cba5 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 16 Mar 2022 22:58:34 -0700 Subject: [PATCH 39/87] allow legacy matrix representation --- control/stochsys.py | 93 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/control/stochsys.py b/control/stochsys.py index 3dc82e32f..d6745447e 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -17,6 +17,8 @@ __email__ = "murray@cds.caltech.edu" import numpy as np +import scipy as sp +from math import sqrt from .iosys import InputOutputSystem, NonlinearIOSystem from .lti import LTI, isctime, isdtime @@ -24,7 +26,8 @@ from .statesp import StateSpace, _ssmatrix from .exception import ControlArgument, ControlNotImplemented -__all__ = ['lqe','dlqe', 'create_estimator_iosystem'] +__all__ = ['lqe','dlqe', 'create_estimator_iosystem', 'white_noise', + 'correlation'] # contributed by Sawyer B. Fuller @@ -409,18 +412,18 @@ def create_estimator_iosystem( else: # Create an I/O system for the state feedback gains + # Note: reshape various vectors into column vectors for legacy matrix def _estim_update(t, x, u, params): # See if we are estimating or predicting correct = params.get('correct', True) - # Get the state of the estimator - x = np.array(x) # bug fix for python-control 0.9.1 - xhat = x[0:sys.nstates] + # Get the state of the estimator + xhat = x[0:sys.nstates].reshape(-1, 1) P = x[sys.nstates:].reshape(sys.nstates, sys.nstates) # Extract the inputs to the estimator - y = u[0:C.shape[0]] - u = u[C.shape[0]:] + y = u[0:C.shape[0]].reshape(-1, 1) + u = u[C.shape[0]:].reshape(-1, 1) # Compute the optimal again Reps_inv = np.linalg.inv(RN + C @ P @ C.T) @@ -437,7 +440,7 @@ def _estim_update(t, x, u, params): dP -= A @ P @ C.T @ Reps_inv @ C @ P @ A.T # Return the update - return np.hstack([dxhat, dP.reshape(-1)]) + return np.hstack([dxhat.reshape(-1), dP.reshape(-1)]) def _estim_output(t, x, u, params): return x[0:sys.nstates] @@ -447,3 +450,79 @@ def _estim_output(t, x, u, params): _estim_update, _estim_output, states=state_labels + covariance_labels, inputs=sys.output_labels + sys.input_labels, outputs=output_labels, dt=sys.dt) + + +def white_noise(T, Q, dt=0): + """Generate a white noise signal with specified intensity. + + This function generates a (multi-variable) white noise signal of + specified intensity as either a sampled continous time signal or a + discrete time signal. A white noise signal along a 1D array + of linearly spaced set of times T can be computing using + + V = ct.white_noise(T, Q, dt) + + where Q is a positive definite matrix providing the noise intensity. + + In continuous time, the white noise signal is scaled such that the + integral of the covariance over a sample period is Q, thus approximating + a white noise signal. In discrete time, the white noise signal has + covariance Q at each point in time (without any scaling based on the + sample time). + + """ + # Check the shape of the input arguments + if len(T.shape) != 1: + raise ValueError("Time vector T must be 1D") + if len(Q.shape) != 2 or Q.shape[0] != Q.shape[1]: + raise ValueError("Covariance matrix Q must be square") + + # Figure out the time increment + if dt != 0: + # Discrete time system => white noise is not scaled + dt = 1 + else: + dt = T[1] - T[0] + + # Make sure data points are equally spaced + if not np.allclose(np.diff(T), T[1] - T[0]): + raise ValueError("Time values must be equally spaced.") + + # Generate independent white noise sources for each input + W = np.array([ + np.random.normal(0, 1/sqrt(dt), T.size) for i in range(Q.shape[0])]) + + # Return a linear combination of the noise sources + return sp.linalg.sqrtm(Q) @ W + +def correlation(T, X, Y=None, dt=0, squeeze=True): + T = np.atleast_1d(T) + X = np.atleast_2d(X) + Y = np.atleast_2d(Y) if Y is not None else X + + # Check the shape of the input arguments + if len(T.shape) != 1: + raise ValueError("Time vector T must be 1D") + if len(X.shape) != 2 or len(Y.shape) != 2: + raise ValueError("Signals X and Y must be 2D arrays") + if T.shape[0] != X.shape[1] or T.shape[0] != Y.shape[1]: + raise ValueError("Signals X and Y must have same length as T") + + # Figure out the time increment + if dt != 0: + raise NotImplementedError("Discrete time systems not yet supported") + else: + dt = T[1] - T[0] + + # Make sure data points are equally spaced + if not np.allclose(np.diff(T), T[1] - T[0]): + raise ValueError("Time values must be equally spaced.") + + # Compute the correlation matrix + R = np.array( + [[sp.signal.correlate(X[i], Y[j]) + for i in range(X.shape[0])] for j in range(Y.shape[0])] + ) * dt / (T[-1] - T[0]) + tau = sp.signal.correlation_lags(len(X[0]), len(Y[0])) * dt + + return tau, R.squeeze() if squeeze else R From 536b97e654a135126755ddaf3b146d869474bb67 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 17 Mar 2022 17:34:05 -0700 Subject: [PATCH 40/87] stochsys ipynb examples + labeling fixes --- control/stochsys.py | 15 +- examples/kincar-fusion.ipynb | 525 +++++++++++++++++++++++++++++++++ examples/pvtol-outputfbk.ipynb | 478 ++++++++++++++++++++++++++++++ examples/pvtol.py | 315 ++++++++++++++++++++ examples/vehicle.py | 111 +++++++ 5 files changed, 1440 insertions(+), 4 deletions(-) create mode 100644 examples/kincar-fusion.ipynb create mode 100644 examples/pvtol-outputfbk.ipynb create mode 100644 examples/pvtol.py create mode 100644 examples/vehicle.py diff --git a/control/stochsys.py b/control/stochsys.py index d6745447e..e99e4e87e 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -305,7 +305,7 @@ def dlqe(*args, **keywords): def create_estimator_iosystem( sys, QN, RN, P0=None, G=None, C=None, state_labels='xhat[{i}]', output_labels='xhat[{i}]', - covariance_labels='P[{i},{j}]'): + covariance_labels='P[{i},{j}]', sensor_labels=None): """Create an I/O system implementing a linqear quadratic estimator This function creates an input/output system that implements a @@ -386,6 +386,8 @@ def create_estimator_iosystem( else: # Use the system outputs as the sensor outputs C = sys.C + if sensor_labels is None: + sensor_labels = sys.output_labels # Initialize the covariance matrix if P0 is None: @@ -407,12 +409,17 @@ def create_estimator_iosystem( # Generate the list of labels using the argument as a format string output_labels = [output_labels.format(i=i) for i in range(sys.nstates)] + sensor_labels = 'y[{i}]' if sensor_labels is None else sensor_labels + if isinstance(sensor_labels, str): + # Generate the list of labels using the argument as a format string + sensor_labels = [sensor_labels.format(i=i) for i in range(C.shape[0])] + if isctime(sys): raise NotImplementedError("continuous time not yet implemented") else: # Create an I/O system for the state feedback gains - # Note: reshape various vectors into column vectors for legacy matrix + # Note: reshape vectors into column vectors for legacy np.matrix def _estim_update(t, x, u, params): # See if we are estimating or predicting correct = params.get('correct', True) @@ -448,8 +455,8 @@ def _estim_output(t, x, u, params): # Define the estimator system return NonlinearIOSystem( _estim_update, _estim_output, states=state_labels + covariance_labels, - inputs=sys.output_labels + sys.input_labels, outputs=output_labels, - dt=sys.dt) + inputs=sensor_labels + sys.input_labels, + outputs=output_labels, dt=sys.dt) def white_noise(T, Q, dt=0): diff --git a/examples/kincar-fusion.ipynb b/examples/kincar-fusion.ipynb new file mode 100644 index 000000000..04a1a968d --- /dev/null +++ b/examples/kincar-fusion.ipynb @@ -0,0 +1,525 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "eec23018", + "metadata": {}, + "source": [ + "# Kinematic car sensor fusion example\n", + "RMM, 24 Feb 2022\n", + "\n", + "In this example we work through estimation of the state of a car changing\n", + "lanes with two different sensors available: one with good longitudinal accuracy\n", + "and the other with good lateral accuracy.\n", + "\n", + "All calculations are done in discrete time, using both the form of the Kalman\n", + "filter in Theorem 7.2 and the predictor corrector form." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "107a6613", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import scipy as sp\n", + "import matplotlib.pyplot as plt\n", + "import control as ct\n", + "import control.optimal as opt\n", + "import control.flatsys as fs\n", + "\n", + "# Define line styles\n", + "ebarstyle = {'elinewidth': 0.5, 'capsize': 2}\n", + "xdstyle = {'color': 'k', 'linestyle': '--', 'linewidth': 0.5, \n", + " 'marker': '+', 'markersize': 4}" + ] + }, + { + "cell_type": "markdown", + "id": "ea8807a4", + "metadata": {}, + "source": [ + "## System definition\n", + "\n", + "### Continuous time model" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a04106f8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object: vehicle\n", + "Inputs (2): v, delta, \n", + "Outputs (3): x, y, theta, \n", + "States (3): x, y, theta, \n" + ] + } + ], + "source": [ + "# Vehicle steering dynamics\n", + "#\n", + "# System state: x, y, theta\n", + "# System input: v, phi\n", + "# System output: x, y\n", + "# System parameters: wheelbase, maxsteer\n", + "#\n", + "from vehicle import vehicle, plot_lanechange\n", + "print(vehicle)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "69c048ed", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Generate a trajectory for the vehicle\n", + "# Define the endpoints of the trajectory\n", + "x0 = [0., -2., 0.]; u0 = [10., 0.]\n", + "xf = [40., 2., 0.]; uf = [10., 0.]\n", + "Tf = 4\n", + "\n", + "# Find a trajectory between the initial condition and the final condition\n", + "traj = fs.point_to_point(vehicle, Tf, x0, u0, xf, uf, basis=fs.PolyFamily(6))\n", + "\n", + "# Create the desired trajectory between the initial and final condition\n", + "Ts = 0.1\n", + "# Ts = 0.5\n", + "T = np.arange(0, Tf + Ts, Ts)\n", + "xd, ud = traj.eval(T)\n", + "\n", + "plot_lanechange(T, xd, ud)" + ] + }, + { + "cell_type": "markdown", + "id": "aeeaa39e", + "metadata": {}, + "source": [ + "### Discrete time system model\n", + "\n", + "For the model that we use for the Kalman filter, we take a simple discretization using the approximation that $\\dot x = (x[k+1] - x[k])/T_s$ where $T_s$ is the sampling time." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2469c60e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object: sys[2]\n", + "Inputs (2): u[0], u[1], \n", + "Outputs (3): y[0], y[1], y[2], \n", + "States (3): x[0], x[1], x[2], \n", + "\n", + "A = [[ 1.0000000e+00 0.0000000e+00 -5.0004445e-07]\n", + " [ 0.0000000e+00 1.0000000e+00 1.0000000e+00]\n", + " [ 0.0000000e+00 0.0000000e+00 1.0000000e+00]]\n", + "\n", + "B = [[0.1 0. ]\n", + " [0. 0. ]\n", + " [0. 0.33333333]]\n", + "\n", + "C = [[1. 0. 0.]\n", + " [0. 1. 0.]\n", + " [0. 0. 1.]]\n", + "\n", + "D = [[0. 0.]\n", + " [0. 0.]\n", + " [0. 0.]]\n", + "\n", + "dt = 0.1\n", + "\n" + ] + } + ], + "source": [ + "#\n", + "# Create a discrete time, linear model\n", + "#\n", + "\n", + "# Linearize about the starting point\n", + "linsys = ct.linearize(vehicle, x0, u0)\n", + "\n", + "# Create a discrete time model by hand\n", + "Ad = np.eye(linsys.nstates) + linsys.A * Ts\n", + "Bd = linsys.B * Ts\n", + "discsys = ct.LinearIOSystem(ct.ss(Ad, Bd, np.eye(linsys.nstates), 0, dt=Ts))\n", + "print(discsys)" + ] + }, + { + "cell_type": "markdown", + "id": "084c5ae8", + "metadata": {}, + "source": [ + "### Sensor model\n", + "\n", + "We assume that we have two sensors: one with good longitudinal accuracy and the other with good lateral accuracy. For each sensor we define the map from the state space to the sensor outputs, the covariance matrix for the measurements, and a white noise signal (now in discrete time)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0a19d109", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAagAAAEYCAYAAAAJeGK1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAABZuklEQVR4nO3dd3iUVfbA8e9JgwQIgdBb6E2agqCCgIoVBbtiWTuoq7LqT7EtSXTXrquoa+/YWAVFRJr0DqF3CAQIAVJI75k5vz9miAkkkMAkM8j5PM88mXnLfU9eZU5uee8VVcUYY4zxNX7eDsAYY4wpiyUoY4wxPskSlDHGGJ9kCcoYY4xPsgRljDHGJ1mCMsYY45MsQRljjPFJlqCMOUEiMkBEFotIuogcEpFFInK2t+OqKBFZLiIdRKStiKw6Yt9DIrJSRPJF5AsvhWhOcwHeDsCYU5GIhAJTgAeACUAQcD6Q7824DhMRAURVneXsDwQigB3A9cCqIw5JAP4FXAoEV2GoxpTLalDGnJiOAKr6nao6VDVXVWeo6rrDB4jI3SKyWURSRWS6iESU2Kcicr+IbHfvf8+dVBCR9iIyz10zSxaRH0qcd56IrHDvWyEi55XYN1dE/i0ii4AcoO0x4u8GbFLXVDJ9OCJBqepEVf0ZSDmZm2TMybAEZcyJ2QY4RORLEblcROqV3CkiVwPPANcCDYEFwHdHlHElcDbQE7gRV20F4AVgBlAPaAG84y6zPvAbMA4IB94EfhOR8BJl3g6MBOoAu48MWkTuEpE0YBFwrvv948ArIpImIm0qeyOMqSqWoIw5AaqaAQwAFPgYSBKRySLS2H3IKOAlVd2sqkXAi0CvkrUo4GVVTVPVPcAcoJd7eyGu5rdmqpqnqgvd24cC21X1a1UtUtXvgC3AVSXK/EJVN7r3F5YR9+eqGgbEAOcAPYANQKiqhqnqrpO7M8Z4jiUoY06QO/ncqaotcDWZNQPecu+OAN5210rSgEOAAM1LFHGgxPscoLb7/ZPuY5eLyEYRudu9vRlH14p2H1Hm3vLiFZH67njSgfOAucBWoBOQKiL/ON7vbEx1sgRljAeo6hbgC1yJClyJYpS7VnL4FayqiytQ1gFVvU9Vm+Gqif1XRNrjGrgQccThrYB9JU8/RrmH3LWnUcAn7vfTgKvc8b1VgV/VmGpjCcqYEyAinUXkcRFp4f7cEhgBLHUf8gHwtIic4d5fV0RuqGDZNxwuF0jFlXQcwFSgo4jcIiIBInIT0BXXaMLK6M2fgyLOxNXcd2QMASJSE/AH/EWkpojYqF9TrSxBGXNiMoF+wDIRycaVmDbgGnCAqk4CXgG+F5EM977LK1j22e5ys4DJwGhV3aWqKbgGVjyOa3Tdk8CVqppcydh7A6vcgyscqppaxjHPAbnAU8Bt7vfPVfI6xpwUsQULjTHG+CKrQRljjPFJlqCMMcb4JEtQxhhjfJIlKGOMMT7ptBg22qBBA23durW3wzDGGFOGmJiYZFVteOT20yJBtW7dmpUrV3o7DGOMMWUQkaPmjQRr4jPGGOOjLEEZY8xpqMjh5K7PlzN944HjH+wlp0UTnzHGmNI27c9gztYkVu9No3dEPRrUruHtkI5iNShjjDkNLd3pWosyO7+I6F83eTmash03Qbmn6D/eK6waYjXGGOMhy3Yeom2DWjx8YQd+XZvAH5sPejuko1SkiS/B/ZJjHOOPa9p/Y4wxx7H3UA6/rkvgvvPbEuhf/Q1ZDqeyfNchruzZjPsHteO3dft57ucN9G1Tnzo1A6s9nvJU5M5sVtW2qtqmvBeumZWNMcZUwHM/b+DVaVt5f26sV66/KSGDzPwizmlbn6AAP165vgcHM/J4ZdqWSpWzIu4Qr07bQlVNOl6RBHWuh445JhFpKSJzRGSzexXR0WUcM1hE0kVkjfs19mSva4wx1WnZzhTmbUuiUZ0ajPtjOxsT0qs9hsP9T/3ahAPQq2UYd/Vvw/ile1gRd6hCZczblsTtny5j2oYDZOQVVUmcx01QqpoHICJ9RGSSiKwSkXUisl5E1pU85iQVAY+rahfgHODvItK1jOMWqGov9+t5D1zXGGOqhary6vStNA6twS8P9aderSAen7CWgiJntcaxbFcKbRrUokndmsXbHr+kIy3qBTPmp3XkFTqOef60Dfu598sVtGlQmwn3n0vd4KppFqxM4+c3wOfAdcBVuBZOu8pTgajqflVd5X6fCWwGmnuqfGOM8bbZWxKJ2Z3KIxd1oGndYF68pjtbDmTy7uzt1RaDw6ks23WIc9rWL7U9JCiAF6/pzs6kbN6dvaPc83+KiefBb1bRvXldvr/vnCodnl6ZBJWkqpPdK3vuPvyqiqBEpDWupaiXlbH7XBFZKyK/H15O2xhjfJ3Tqbw2fSutw0O4sU9LAC7u2phrz2rOe3NjWRefVi1xbN6fQWZeUXHzXkkDOzbkurNa8MG8WDbvzzhq/1dL4nj8f2s5t104X9/Tj7ohVTugojIJKlJEPhGRESJy7eGXpwMSkdrAT8A/VPXIO7QKiFDVnsA7wM/HKGekiKwUkZVJSUmeDtMYYyrl13UJbDmQyaMXdyw1ci/yqjNoUNvV1He8pjVPKO5/OqIGddg/r+xCWEggY35aR5Hjz6bH9+bsYOwvGxnSpTGf3nE2tWpU/TwPlUlQdwG9gMtwNe0dbubzGBEJxJWcvlHViUfuV9UMVc1yv58KBIpIg7LKUtWPVLWPqvZp2PCoSXKNMabaFDqcvDlzG52b1OGqHs1K7asbHMgr1/Vge2IWb82q+qa+pTsP0To8hKZ1g8vcHxYSRNSwM1gXn87ni+JQVV6ZtoXXpm9leK9mvH/bWdQM9K/yOKFyCaqn+wv/DlW9y/2621OBiIgAn+Ia1v5mOcc0cR+HiPTFFb8NcTfG+LQJK/eyOyWHJy7thJ/f0Y+UDu7UiJvPbslH82NZtSfVo9eOiooqfu96/imFc9qGl7n/sKHdmzKkS2PemLmVc294gPfnxjKibyvevLHXUc9tlXW+p1QmQS0tZ1Sdp/QHbgcuLDGM/AoRuV9E7ncfcz2wQUTWAuOAm7WqBuAbY7xmV3I2D4yPYeuBzGq/9oZ96Vz21nxmbvLMzAp5hQ7G/bGd3hH1uLBzo1L7Sn65Pzu0C03rBvN/E9aSW+A4an9ZKpIcoqOji99v3p9BRl4RfdvUw+Fw4HA4iI6OJi8vD4fDQW5uLtnZ2TidTsZc2Ap/RyHLfvqQv/VuSPSVncjKzCA9PZ3CwkLS0tLIyckpVb6nSUW/30VkM9AO2AXk45pZQlW1R5VF5yF9+vRRWw/KnO7yCh38Z9Y26oUE0aNFXbo3r+tTswYctjg2mQfGryI9t5BhPZsxbsSZ1Xbt/CIHV72zkG0Hs/ATePnaHtx4dstyj4+KijpmkoiKiqLZhbfz4tQtfD/ynFI1l+zsbGrXrk16ejo7duwgPDycWau289QXM7nnxmE0SVrBo48+yqpVq/jll1+46KKL2LVrF7GxsYwaNYqPPvqI6Oholi5dyu+//84VV1zBunXriI+PZ+TIkdxyyy3Mmzev+HqjRo0iM6Qpvy3fyuyPX+DxB+9l7ty5xfv//ve/c8YZZ5Cbm8tNN93EXXfdxcyZM4v3jx49mk6dOuHn58ell17K/fffz/Tp04v3R0ZGnnBtSkRiVLXPUdsrkaAiytpeVSP5PMkSlDHw5I9rmbAyvvizCLRtUIueLcLo3qIuPVqEcUaz0GrrXyjLd8v38M+fN9CmQS06NK7NrE2JLHvmIurVCqqW678ybQvvz43lvVvO4unn/klG12t48rJOPDCoHe7ehVJEhIKCAnbv3o2fnx/Z2dmsXLmSQYMGMWnSJP7v//6Pzv/4kqDY+fzrvuGoKqtWrSItLY233367uJybb76Z559/Hj8/P16fupHvvvuW9CU/FO8/8ss/KiqqVM3lWMlBRIpnerjvq5VsO5jJvCcuKHP/8c4/kf0VUV6CQlX/8q/evXurMaezH5bv0YgxU/S1aVs0OTNPZ285qG/P2qb3fLFc+/xrpkaMmaIRY6Zou6d/0yvenq+/r99frfEVOZz6/K8bNWLMFP3bp8s0PbdAN+9P14gxU/STBTur5JqRkZGlPq/afUjbPDVFn/jfGnU6nQrodWM/1Rajv9fLRz6jn376mS5cuFAjIyN15MiRChS/rr76al2xYoXu27dPd+3apc8++2yp/Q88OqbMGFxfwaVl5RXq+a/M1vNfmV3m/uOdX97v6XA4tUfUdH3yf2uPeR/KO/9E91cEsFLL+O4+bg1KRFap6lkne4w3WQ3KnM42JqRz7X8X06d1Pb66ux/+R3TSqyoHM/JZG5/Guvg0/ticyJYDmTwwuB3/d0mno473tMy8QkZ/v4bZWxK5q39rnr2iCwHujvir31tEdn4RMx4dWGYN5ljKa34rKioiNzeX0NBQxo8fz5AhQxj3zrv8b2M6wS3OoM6ab1i0cEHx8QNuuI+4JoO59pxOvHbjmQQF/Nl1f6zaQ3JWPg3r1OTB8TG8d2vZX4/lxbhsZwo3f7yUVrt+Y973/0VViU/NZdvBTLYcyGTbwUy2HsikYPkPzP72vQrdj40J6Qwdt5A3b+zJtWe1qNA51aW8GlRFBrJ3OTylUXllA3VPODJjTiM5BUU88t1qHrqwA71ahlX59dJzC3lg/CrqhQTx9s1nlplsRIQmdWvSpG4TLj2jCY9c1IHoXzfx/txYNuxL5+2bz6R+FTWx7T2Uw71frmRHUhb/vqYbt/Yr3ZMwom9Lxvy0nlV7UukdUfq5nfK+3FWV7OxsoqOj6dChA+eccw6TJk2ioKCAm266iXvv/bPv5bbbbuO5554j5NwRFBTt4tN7+nJ+h1HF9+XwX/L/nRvLa9O3kpq3kg9uO4uQINdXZ2RkZLm/23/nxBLWfwSPXtyx3GPKa5br1zacu85rw2c6lOHvLiQ2KZus/D/nu2seFkxocCA7W13Bgu1JnN/h+I/SLNt5qLjsU0VFalBl9j0dwaGq8cc/zDusBmV8xeeLdhH96yZuPrslL19XteOLVJWRX8cwZ0siP4w6l94R9Sp1/g8r9vDPnzfSsE4NPry9N92aH//vUFVl68FMVsSl0rhODTo1qUPLeiFlDq2O2X2IkV/FUOhw8v5tvenf/uhHGrPzi+j771lc3r0pr9/Qs9Q+EWHBggU0btyYefPmsWfPHkaNGsUtt9zC/Pnzi4977rnneOGFF44q+3ACWhF3iBs/XMItfVvx72u6F+8/MgF+v3wPz0xaT48WYXx+59nH7Bfbl5bLBa/N5eozm/Hq9T3LPe5YcgscjPx6JQVFTjo3qUPHJnXo3KQOHRrXIbRmIHmFDq4Yt4BCh5Pp/xhYnDTLM/KrlWw5kMn8Jy845nHecMI1KD0FBkEYcyoocjj5dOEuAOZsTXS1sVey2aoyPpy/k5mbDjL2yq6VTk4AN53dis5NQrl/fAzXvb+Yf1/Tnet7H900lFfoYElsCn9sOcicLUnsS8sttT840J+OjWvTqUkdOjUJpVPjOiSk5fLczxtoXi+YT+7oQ7uGtY8qNyoqimeffZaL29Xix6mzqb1lCvfedQe33nprcQI6//zzeeKJJ3jllVeK7+XhkWvH67yPjIwkp6CIJ/63luZhwTx9RZejrl/SzX1bUa9WEA9/t5rrP1jMV/f0o3nYnw+7qipJmflsTMjgyyVxAIweUn7t6XiCg/z5+p5+5e6vGejvGmX44RL+M3Mbzw4t/ykgp1NZHneIS7o2PuF4vKHq56owxgAwdcMB4lNzuaRrY2ZsOsjm/Zl0bRZaJddaujOFV6dtYWiPptzVv/UJl9OzZRi/PjyAh79dzf/9by3r4tN4bmhXkrPymb0lkdlbElkcm0xeoZOQIH/6t2/AQxe2p3+7BqRk55fqM5m9JbF4FGHawm+4/G+P8P5tZxEWEkRBQQHbtm0jPDycCRMmkJ6eTnR0NP7+/vQ642x+DAmnxYDLaNGiRaUS0LFERUURNXkjcSk5fHffOdSuwNQ9l57RhK/v7su9X63k+vcX8+iQjuxKyWZjQgabEtJJziooPvaxizuWSmBVoW+b+tzarxWfLtzFlT2a0bOcZuOtBzNJyyksc/49X1bhYeanMmviM96mqlz17kJyChx8e+85nPPSHzxxaSf+fkF7j18rMSOPK8YtJDQ4gMkPDajQF+/xFDmcvDp9Kx/N30n9WkEcynZ9EbesH8xFnRtzQedG9GtTv3iIenn9Q8lZ+cyP2cR1g89iT/w+Pv7wA9q2bUvnzp3Zs2cPF1xwAe+8806pJrmxY8eyvP4Q/P1gysPnF28/3jNIx7MkNoURHy/lzvNaEzWscvNOb96fwR2fLScxM58AP6FD4zqc0SyUM5qF0rVpKF2ahRJaTc+YZeQVcvGb86gXEsSvDw8oc4Xew03LC8dcQIt6IdUSV2Wc9HNQpzJLUMbbFu9I5pZPlvHytd25uW8rho5bQK2gACbcf9JrfZZS6HBy68fLWL8vnV8e6k/HxnU8Wv5v6/bzy5p99GntmhWhXcPa5T4fpKosWbKE7du307dvX7799lvi4uL4+uuvi4+r6PM7Xy2JY+wvG5ny8IDj9oXtS8vl59X7aNugFue0DS+zryg7v4hL35pPgJ8wdfT5x+2/KUt6biH7UnNp16gWNQK89+wYwIyNBxj5dUy5f/SM+nolm/ZnsODJC70Q3fGdzCi+wwXUwLUWVOuS56ktGmjMcX04fycNatfg6jNdS5xd0KkR78+LJT2n0KNLFrw2fSvL4w7x1k29PJ6cAIb2aMrQHk2Jioqi/cAowFU7zM/PZ9asWXz99ddMmDABcCWYO+64g5deeommTZvy/POur4qvvvqqQg93lmyiG96rOS9O3cx3y/eUGshQUnZ+Ee/PjeXjBTvJL7EAYJemoZzbNpxz24XTt0196gYH8uLUzexLy+V/o849oeQErkleq2qhvsq65IwmXNG9CW//sZ3LuzWhbYk+Pad7/aeLu5xa/U9QuT6oX4B0IAbXVEfGmArYvD+DeduSeOLSTsVNYBd0bsi7c3awYEcSVx4xu3VlHczIY3FsMgu2JTNx9T5uO6dVcSL0NFXl0KFDREdHM2TIEGJjY9mxYwePPPIIDRo04PPPP+eHH3446f4hKD1IoW5wIFd0b8ovaxJ4dmiXUknF6VR+XBXPa9O3kpSZz/BezXjs4o4kZ+WzJDaFJTtT+GbZbj5btAs/gc5NQtm0P4P7zm9Dn9ZlLzlxKooadgYLtyfz9MT1fHffOcUjJ7cluvufTqHh5YdVJkG1UNXLqiwSY/6iPp6/k5Agf24r8YxPr5b1qBscyJwtlU9Q6TmFLNmZwuLYZBbtSCY2KRuAsJBAbuzTgn9eefJzOh/u30lISGDhwoX07t2bCRMmMHv2bGbNmgW4RtBFRkYW9xeVXNamIgMUKuvms1sxcdU+flu3nxvcC/4t3ZnCC1M2sTEhgzNbhfHh7b05q5VrxGJEeC16R9TnoQs7kFfoYM3eNBbHprA0NoVz2tbn8Us6VToGX9aoTk2eHdqFMT+t54eVexnRtxUAS2Pd6z+1OfWScWXm4vsIeEdV11dtSJ5nfVDGWxLSchn46hxuPzeCyKtKd8Q//N1qlsSmsPyZi8p8TqikHYlZ/BgTz+LYZDbsS8epruHbfdvUp3/7cM5r14CuTUOPW85hZQ0wiI+PZ+nSpZx33nk0b96c8ePH06tXL5xOJ126dCEg4M+/Zz0x/1plqSpD3pxHWEgQb97Yk5embmHaxgM0q1uTMZd3ZljPZlU6bP9UoKrc+omrD3LWY4NoHFqT+7+OYUNCOgvH+Gb/E3igDwoYANwpIqfcbObGeMvni3ahwD0D2hy174JODfl1bQIbEzLo3qL8jn+HU7n1k6Ucyi7gzJb1eOSiDvRv34CeLcJKTbtTGdHR0QwePJjGjRvz7bffcsEFF6CqzJo1ixtuuAFwzbJQ3iCGijTReZqIcPPZrfj31M0MeXMegf5+/N8lHbn3/LZeneDWl4gIL17TnUvfms/YXzbw/q29WbYrhYtOwf4nqFyCurzKojDmLygjr5Dvlu9laPemZQ7tHdixISKuh3aPlaCW7kzhYEY+795yZoWaA4+sHWVlZbFv3z4SEhKIjIxkwQLXPHMXXHBBqSY6gIsuuogPPvjguDWkqlyk7liu692Cb5fvoU9EPZ64tBONQmt6JQ5f1rpBLR69uCMv/76Fd2bvIDWn8JRs3oNKJChV3S0iPYHDDyIsUNW1ngxGRC4D3gb8gU9U9eUj9ot7/xVADnCnqq7yZAzGeMq3y/aQlV/EyIFty9zfoHYNerQIY87WRB65qEO55Uxek0CtIH+GVOCv4MLCQqKjoznrrLNYvnw5o0aN4ttvv2XgwIEMGjSoeAYGTwxi8Ib6tYKY83+DvR2Gz7t3QBt+XZvAf2ZtAyi1DtWppMLtAyIyGvgGaOR+jReRhz0ViIj4A+/hqql1BUaUsYLv5UAH92sk8L6nrm+MJxUUOfl80S76tw8/5nM7gzs2ZM3etOIHX4+UX+Tg9w37ufSMJqUeggVXf0NhYSGTJk3ipZde4pFHHiEoyPXMz/Dhw/H396dly5aMGTOGc889Fz+/P/+5V8UgBuM7Avz9eOW6Hvj7Cc3DgmlZ3/cezq2IyjTx3QP0U9VsABF5BVgCvOOhWPoCO1R1p7v874HhwKYSxwwHvnKvH7JURMJEpKmq7vdQDMZ4xC9r9nEwI/+4E4Ve0LkRb/+xnQXbkxje6+ih4fO2JpGRV8RVvZpRVFTEH3/8QXR0NBdddBEzZsxgxIgR9OrVi2HDhuHv78+4ceMqNIDBEtBfX7fmdfn31d2oEXhi/ZS+oDKRC+Ao8dnh3uYpzYG9JT7Hu7dV9hgARGSkiKwUkZVJSUkeDNOYY1NVPl6wk85N6jCww9EzdJfUo3ldwmsFMWdLYvG2qKgoCgoKWL58OR/9NB3ZtZSZX41jzJgxXHaZ60mPgQMH4u/vT9euXWnTpg3+/n8OEvDV5jlT/W7u24przvSttZ8qozIJ6nNgmYhEiUgUsBT41IOxlJXsjvwzsCLHuDaqfqSqfVS1T8nnM8xfV05BEYUO5/EPrGJztyax7WAWIwe2LXPYc8nai5+fMLB9ONMXr2L3nr288847REdHk5SUxO74BNak1eDWETfy4r//xRtvvFFcM1LVcmtBVjsyfxUVTlCq+iZwN3AISAXuUtW3PBhLPNCyxOcWQMIJHGNOQ3tSchj46hwem+DRcTsn5MP5sTStW5OrepY94i46OprMzEyef/553n//fZo5EkjauYnn/vUKjzzyCAAtWrTgp+nzKQqqc1TTn9WQzOmiUo2TqhqjquNU9W1VXe3hWFYAHUSkjYgEATcDk484ZjLwN3E5B0i3/ieTnlPIXV8sJzmrgCnrEtiRmOW1WNbuTWPaV+9wz4A2BPr7kZaWRnJyMq+88go333xzcY0qNDSUwsJCHnjgAe679hJCu1/ImTc8UqqGFHLuCJrVrUnvVqXXcrIakjldHHeQhIgsVNUBIpJJ6ea0ww/qemRBG1UtEpGHgOm4hpl/pqobReR+9/4PgKm4hpjvwDXM/C5PXNucugqKnNw/PoY9h3L4761n8egPa/hofuwJr2IKruSQkVtEcnY+yZn5pGQXkJyVT3JWASlZ+TQJrcmFXRrRtWloqSa8wsJCXv9pIemLvqMdf+eDD2aQnZ3NAw88wIMPPkidOnX4/vvvjxrEEBYSxJmt6jF3ayKPXdyRyMhIUrMLmL8tiXsGtKnw7BDG/NVUZEXdAe6fnp8a+ehrTcWVhEpu+6DEewX+XtVxmFODqvLspPUs2ZnCf27qyRXdm7IkNoXvV+zhsYs70aRuxR/iXLM3jchfNnAwI5+U7HwKHUd3bYpA7tLvqdnvZl6fsYWw/EQaZW7j7ltuZP3sSSxbEcPsmdMAuHDggErNwnBBp4a8PmMbSZn5REVF8c2y3RQ5lWG9Tm4iWWNOZZV5DuqVimwzvmXLgQyiJm9k1qaD1T53WlX779xY/hcTz+iLOhSPVLrv/LY4nMpni3ZVqqyP5seyMykbv9X/454BbXluaBfeuqkX4+/px9RHBvDBJaE83Gg7B+eO5zpdxA3hB+jSPJzdYWfy9MwDTPIbQNHFT9L+md+Ayg9iGNypEQDztrlGnE5ek0C7hrXo2rRqVtw15lRQmT6oi8vYdlpOf6SqFDqcZOcXkZpdwMGMPPan5/pUAohPzeGxCWu4/O0FfLkkjnu/Wskdn6/wav+MJ/26NoHXpm/l6l7N+MeQP2dhaBUewpU9mvHN0t2k5xRWqKy0nAJmbUrkut4tWPLjh/SSOPx3zKOl8wDTv3qbjD2bmfTDeB5/7FEA3nz1JWrn7OP7J65m3cs38OXdfbmhTwtU4e7+bU5oEMMZzUJpVKcGc7Ymsj89l+Vxhxjeq/lpP/mpOb0ddzZzEXkAeBBoh6vv5/C/mDrAIlW9tUoj9ICTmc3879+sYvWeVAocTvKLnBQUOSlwOCnrtkUPO4M7zmt9csGepNTsAt6bs4OvluwGgbvOa82957dl8toE3pq5jdxCB3ec15rRQzpU25LUAClZ+exMziav0EFugYPcQgf5hU5yCx3kFTrIK3TSs2VdBnZoeNw+l5jdhxjx8TJ6tqjL+Hv7HbWa6caEdIaOW1jm6qKH56krLCwkJyeHX3/9lZj9eXz02VfkbFlQfNzTTz/Niy++eNS1q3IW7yf+t5bpGw/wwOD2vDJtC3P/bzCtG9SqkmsZ40tOZjbzb4DfgReBp3APjgAyVTXVo1H6oE5N6hAc5E9QgB9B/n7UCPArfh8U8Ofr80VxTFwV77UElVvg4LNFu/hgbizZBUVcd1YLHr24I83CggHXbNrDezXj9elb+WzRLn5evY8nLu3EDX1a4l9FnfCqyqo9aXy1JI6p6/eX2a9zpIjwEG7t14obercsc6nu3SnZ3PdVDM3q1uSj2/sclZwOJ6CBHRvy+aJd3N2/NXnZmSQlJfHrr78SHR1Nw4YNSUlJYfTo0Zx99tn8b8ZBBoz8F9P+cT5+fn5em6Pugs6N+F9MPP+du4MeLepacjKnvYrUoA6P4ssCSj4F6dFRfFWpOtaD+nBeLC/9voX5T1xAq/Dqm/fK6VR+WLmX/8zcRmJmPkO6NObJyzodc7nv9fHpRP+6kZW7U+nWPJSoq87w6MqieYUOJq9J4KulcWzYl0GdGgFc17sFgzs1pFaNAIID/akZ6EfNQH9qBvoTHOiPv58wY9NBxi/ZzfK4QwQF+HFVj2bcfm4EPVvURURIzymk19X3UXfALUx6sD9tSnyBqyqJiYk0adKEyZMnM3HGPGY6unN23ipGXNafmJgYXn311eLjDw9giE3K4qI35vHMFZ0ZObBdmeskVZeMvELOfH4mDqfy3NAu3Ht+2ZPMGvNXU14NqsILFp7KqiNBxafmMOCVOTx5WSceHNz++Cd4yMu/b+GDebH0jqjHU5d35uwKJhpVZfLaBF6auoUDGXkM79WMZ4d2oVGdE1++YO+hHMYv3c0PK/eSllNIx8a1+du5rbnmzObUqlHxaR+3HMhg/NLdTFq1j+wCB92b1+W2c1oxafU+fhh1HlOWbMCZtJMzzjiDn3/+mdzcXBITExk3blxxGWPHjmVNo0tIzS5g9uODCPB3dbce2UT36jTX/Vv69EU+sXTDTR8uYXncIZY+fRGNfSAeY6pDeQkKVf3Lv3r37q3V4er3Furlb82v9HlLYpP1xambNLegqFLnLdiWpBFjpuiYH9eq0+ms9HVVVbPyCvW1aVu0w7NT9c7Plp1QGaqqP6+O19ZPTdG2T/+m93+9UhfvSK50TJGRkaqq6nA4dM+ePbpt52595KUPtO0ld2idPsMVV9OyAnrvvfdqdnb2UWW4/pd2+X19gkaMmaKT1+w76hqqqkUOp57z4iy94yR+b09btCNJP54f6+0wjKlWwEot47u7wl/ywA1AHff7fwITgbMqer43X9WVoD6eH6sRY6ZobGJmhc9ZtzdNu/zzd40YM0VveH+xpuUUVOi85Mw87fOvmXrRG3M1J79yia0s0ZM3aodnp55wWZe/NV8veXOe7kvNKfeYkskhKytLExMTdcWKFTpu3Djds2ePAjpu3Dhdu3atfvTRR7p9+3ZNTU3VoqIiXRqbrDM3HiiVgI53jSKHUy94bY5e8fb8MpPlwu1JRyUwY0z1Ky9BVWaY+T9VNVNEBgCXAF9i6zGVMrRHUwB+W1ex2Zf2pORw1xfLqRcSxAvDz2D13lRu+nAJiRl5xzxPVXnix3Wk5xbyzogzCQ46+eWuB3VqSEGRk2W7Uso9pry+mU0JGWzan0Hwhok0CwvG6XRy4MABdu3axbp16xg3bhwbN24kOjqaF154gbi4ON577z1iY2Np0qQJe/fupVWrVgA88sgjTJw4kfvuu4/27dsTFhaGv78//dqGM6Rr40qtY+TvJ4wa1JaNCRks3JF81LE/xcRTp2YAF3c9NZfDNuYvr6ysVdYLWO3++RJwS8ltvv6qrhqUqur17y/SS96cd9zjUrLydfBrc7Rn9HTdftBV41qwLUm7/vN37f/yH7ozKavccz9buFMjxkzRLxbt8lTYmltQpPXPv0WjJ28s9xhAk5OTNTc3V2fMmKHTpk3TFStW6OCbH9AWd7yhgI4dO1b37dun7733ni5atEgTExP1iSeeKNU8V7KWc2T5npZXWKR9/z1Tb/l4SantmXmF2vm53/Wpn9Z5/JrGmMrBA018U4APgVggDKgBrK3o+d58VWeC+mLRLo0YM0W3Hcgotb3kl3JOfpFe/d5C7fjsVF2xK6XU/rV7U/Ws52foWc/P0HV70446f8O+NO3wzFS954vlpZqtyvvSL2+/w+HQtLQ0zcrK0lWrVun27dsV0I7XjNa9e/fq888/r//5z39048aNOmjQoFIJ5oknntD58+drXFyc5hUUatMLbvNIAjre73CiPpi7QyPGTNG1e1OLt01YsUcjxkzRlXEpVXJNY0zFeSJBhQDXAh3cn5sCl1T0fG++TjZBVebL/2B6rrZ+aoq+MWNrqWMOfzkXFjn0ni9WaOunpujv6/cftV9VNTYxU8976Q/t8uwUnb/1oKanpyug+5NStN+TX+qZz/6kC5bG6OzZszU1NVV/+uknBXT58uX66quv6tq1a/Wdd97RyMhI3bdvn0ZGRiqg06dP18jISN2yZYu+8cYb+u677+qBAwf0pptuKpVgRj/xVJm/Z1kJZtamAxoxZorOqGT/UHXKyC3QbpHT9IHxK4u33fThYh382pwTHlxijPGc8hJUhcf+qmqOiMQCl4rIpcACVZ1R4bbEU1h0dDT+/v7ceeedfPrpp/Tq1YugoCCWL1/ObbfdRnR0NMHBwVx33XWMHz+elll1+Pzb1aQvLCQjI4O33noLcA1xbtm1N/ndr+H6M+qy9McVzPw8kzfffLN4/6BBg3jhhRe4ud4uxs1fzLCZn5K2fBIATRuGE9zxXL7471sEaAFFIrz66qu89NJLAPTt25enn36abt260aNHD8DVJxMdHQ3ApZdeSmRkJJ06daJTp07Fv9/3339fPMt2xJgp9L2me5n3oaz+nx9j4gmvFcTgTg0r1T9UnerUDOT2cyJ4f14su5KzCfATlu48xOMXd7SphIzxZWVlrbJewGhgA/C8+7UeeLii53vzdaI1qMM1D8ppuipv/9dL4jRizBTduC+9+FhA3529XSPGTNGXpm4+6lqUUftIyynQ699fpK2fmqKARoyZoq/8fvS55Z1fmf2qqmPHjtVzX5ylo75aedxjVVVTs/O1wzNTNWryhgod702JGXna4dmp+tRP6/TtWds0YswU3Xvo6GHqxpjqhwea+NYBtUp8rgWsq+j5xyn7NWCL+xqTgLByjotzJ8Y15f1CZb1Otomvsl/+yZl52uapKfrqtD+TyQ33PaoRY6bo6O9WqcNxdLNSec1fuQVFes8XK7Ru/xE6/N2FWlDkKPO4yvZBlWfMj2u129hp5V6npK8Wu/rbNuxLq1DZ3vb0xHXa4Zmpes6Ls/TmD5cc/wRjTLUo7/u8MsPMBXCU+Ozgz4ljT9ZMoJuq9gC2AU8f49gLVLWXlvXUcRU5XtPVkfvDa9fgvHYNmLJuP6rK/G1JrGowhP7tw3n1+p5lToZaXvNXzUB/PrjtLN5/82U+vL03gf5l/yc7XvNZRZvXBnVsSGZ+EWv2ph332B9j4uncpA5nNKtbobK9beT5bSlyOtmfnsd1vVt4OxxjzHFUJkF9DiwTkSgRiQaWAp96IghVnaGqRe6PSwGf+vY4kS//K3s0ZXdKDt+v2MsD42Po0LgOH9zWm6CAytxylwB/P0b0bVUtU9+c174B/n7CfPe6ROXZkZjJ2vh0rj+FvuhbN6jFFd2bUrtGAJd1a+LtcIwxx1Hhb0tVfRPXEuuHgBTgLlV9qwpiuhvX7OllhgHMEJEYERl5rEJEZKSIrBSRlUlJx/6yrQqXdWtCgJ/w9MT1hIUE8cVdZ1OnGpe3OFF1gwM5s2VY8cJ55fkxZh/+fsLwXs2rKTLPePm6Hkx5eAC1KzE3oDHGOyqzom5NYDBwATAIGOzeVtHzZ4nIhjJew0sc8yxQhGuJj7L0V9WzcC2U+HcRGVje9VT1I1Xto6p9GjZsWNEwPSYsJIgLOzcitGYAX9x19ik18efAjg1Zvy+dlKz8Mvc7nMqk1fEM7tiQhnVqVHN0J6d2jQBbxsKYU0Rl/oz8CsgEDk8ZPQL4GtccfcelqkOOtV9E7gCuBC5yd5qVVUaC+2eiiEwC+gLzKxS9F/znpl4UOpyEhRy9rpEvG9SxIW/O3MbCHcll1pAW7kjmYEY+kVedOs17xphTT2USVCdV7Vni8xwRWeuJIETkMmAMMEhVc8o5phbgp675AGvhmg/weU9cv6pUZokJX9KteV3qhQQyb1tSmQnqp5h46gYHclGXRl6IzhhzuqhMj/1qETnn8AcR6Qcs8lAc7+JaQn6miKwRkQ/c12gmIlPdxzQGFrqT4nLgN1Wd5qHrmxL8/YTzOzRk/rZknM7SldmMvEKmbzzAsJ7NjlrN1hhjPKkyf+L3A/4mInvcn1sBm0VkPa7ngHqcaBCqWuYKf+4mvSvc73cCPcs6znjewI4Nmbw2gc0HMkoNI/9t3X7yi5w2TNsYU+Uqk6Auq7IojM8Z2KEBAPO2JZVKUD/FxNO+UW16tjg1nn0yxpy6KjPMfPexXlUZpKl+jUJr0qVpaKnnoXYlZ7NydyrXndXC5rAzxlS5yj81ak4bAzs2IGZ3Kln5rmeoJ66Kx0/gmjNPrWefjDGnJktQplyDOjak0KEsiU3B6VQmrtrHgA4NaVL31Hmmyxhz6qrMg7qzRMQGKZxG+kTUJyTIn/nbkli6M4V9ablcd5bVnowx1aMygySeBP4jIruBZ1R1fxXFZHxEUIAf57ULZ962JLILiqhTI4BLz7A57Iwx1aMygyRWqeqFuJZ+nyYikSISXHWhGV8wsGND9hzK4de1CQzt0ZSagfbskzGmelSqD0pcQ7e2Au8DDwPbReT2qgjM+IaBHVzzGBY69JSaudwYc+qrTB/UQmAf8B+gOXAnrslj+4rIR1URnPG+1g1qEREeQuvwEHpH1PN2OMaY00hl+qDuBzaWMZHrwyKy2YMxGR/z7oiz8PPDnn0yxlSrCicoVd1wjN1DPRCL8VHdbdYIY4wXeOQ5KPc8ecYYY4zHSDlLL/2liEgScDLTMTUAkj0UTlWxGD3DYvQMi9EzTpcYI1T1qJVlT4sEdbJEZKWq9vF2HMdiMXqGxegZFqNnnO4x2lRHxhhjfJIlKGOMMT7JElTFnArPeVmMnmExeobF6BmndYzWB2WMMcYnWQ3KGGOMT7IEZYwxxidZgjoOEblMRLaKyA4Recrb8ZRFROJEZL2IrBGRld6OB0BEPhORRBHZUGJbfRGZKSLb3T+9OrlfOTFGicg+971cIyJXeDnGliIyR0Q2i8hGERnt3u4z9/IYMfrEvRSRmiKyXETWuuOLdm/3mXt4nDh94j6WiNNfRFaLyBT35yq7j9YHdQwi4g9sAy4G4oEVwAhV3eTVwI4gInFAH1X1mQf6RGQgkAV8pard3NteBQ6p6svuZF9PVcf4WIxRQJaqvu6tuEoSkaZAU1VdJSJ1gBjgalyTNfvEvTxGjDfiA/fSvQpDLVXNEpFAYCEwGrgWH7mHx4nzMnzgPh4mIo8BfYBQVb2yKv9dWw3q2PoCO1R1p6oWAN8Dw70c0ylBVecDh47YPBz40v3+S1xfYl5TTow+RVX3q+oq9/tMYDOu1QR85l4eI0afoC5Z7o+B7pfiQ/cQjhmnzxCRFrjmXv2kxOYqu4+WoI6tObC3xOd4fOgfXgkKzBCRGBEZ6e1gjqHx4ZWY3T8beTme8jwkIuvcTYA+s8aIiLQGzgSW4aP38ogYwUfupbtZag2QCMxUVZ+8h+XECT5yH4G3cK2u7iyxrcruoyWoYytrfQmf+ovGrb+qngVcDvzd3XRlTsz7QDugF7AfeMOr0biJSG3gJ+Afqprh7XjKUkaMPnMvVdWhqr2AFrjWsOvmrViOpZw4feI+isiVQKKqxlTXNS1BHVs80LLE5xZAgpdiKZeqJrh/JgKTcDVN+qKD7v6Kw/0WiV6O5yiqetD9JeEEPsYH7qW7P+In4BtVneje7FP3sqwYffFeqmoaMBdXv45P3cOSSsbpQ/exPzDM3ef9PXChiIynCu+jJahjWwF0EJE2IhIE3AxM9nJMpYhILXfHNCJSC7gEONbaXd40GbjD/f4O4BcvxlKmw//Q3K7By/fS3XH+KbBZVd8ssctn7mV5MfrKvRSRhiIS5n4fDAwBtuBD9xDKj9NX7qOqPq2qLVS1Na7vwtmqehtVeB8rs6LuaUdVi0TkIWA64A98pqobvRzWkRoDk1zfEQQA36rqNO+GBCLyHTAYaCAi8UAk8DIwQUTuAfYAN3gvwnJjHCwivXA15cYBo7wVn1t/4HZgvbtvAuAZfOtelhfjCB+5l02BL92jcv2ACao6RUSW4Dv3EMqP82sfuY/lqbL/F22YuTHGGJ9kTXzGGGN8kiUoY4wxPskSlDHGGJ9kCcoYY4xPsgRljDHGJ1mCMsYY45MsQRljjPFJlqCM8VEislhEwkTkwSO3e6j81iKSW+Lh2oqcE+xek6hARBp4Ig5jymMJyhgfparnAWHAg2Vs95RY9+SkFY0p1328z81Jaf56LEEZUwniWjn2Yvf7f4nIuCP2txaRLSLypXt5hB9FJMS97zER2eB+/cO9rZaI/CauVVQ3iMhNJcrKwjWNTDt3reW1Ets5RpmtxbW67cfiWpl1hntut+P9bodj/8Rd3jciMkREFolrtVSvT/ZqTi82F58xlRMJPC8ijXCtezSsjGM6Afeo6iIR+Qx4UETmAHcB/XAt47JMROYBbYEEVR0KICJ1jyjrKaBbWbUcEeldTpmpQAdcqz/fJyITgOuA8RX4/drjmkttJK7Jkm8BBrh/z2fw8qJ+5vRiNShjKsG9Cq8AjwE3q6qjjMP2quoi9/vxuL7gBwCTVDXbvWrqROB8YD0wREReEZHzVTW9EuGUVybALlVd434fA7SuYJm7VHW9e2mHjcAf6pqwc30lyjDGIyxBGVMJItId16zT+e7lzcty5AzMStmLX6Kq24DeuBLASyIytjLhHGNffon3DireWlLyPGeJz85KlGGMR1iCMqaC3OvyfAMMB7JF5NJyDm0lIue6348AFgLzgatFJMS9btc1wAIRaQbkqOp44HXgrCPKygTqlHOdMss8wV/PGJ9jCcqYCnAPdJgIPK6qm4EXgKhyDt8M3CEi64D6wPuqugr4AlgOLAM+UdXVQHdguXuo97PAv0oWpKopwCL3oIXXjthXXpnG/CXYelDGeJCItAamqGo3b8dyPCcTq3vZ7z6qmuzpuIw5zGpQxpy+HEDdE3lQFwjE1S9lTJWxGpQxxhifZDUoY4wxPskSlDHGGJ9kCcoYY4xPsgRljDHGJ1mCMsYY45MsQRljjPFJlqCMMcb4JEtQxhhjfJIlKGOMMT7JEpQxxhifZAnKGGOMT7IEZYwxxidZgjLGGOOTLEEZc4JEZICILBaRdBE5JCKLRORsb8dVUSKyXEQ6iEhbEVlVYnsNEflURHaLSKaIrBaRy70Zqzk9WYIy5gSISCgwBXgH16q5zYFoIN+bcR0mLuX++xaRQCAC2AH0BlaV2B0A7AUGAXWBfwIT3AscGlNtLEEZc2I6Aqjqd6rqUNVcVZ2hqusOHyAid4vIZhFJFZHpIhJRYp+KyP0ist29/z0REfe+9iIyz10zSxaRH0qcd56IrHDvWyEi55XYN1dE/i0ii4AcoO0x4u8GbFLXgnB9KJGgVDVbVaNUNU5Vnao6BdiFK5EZU20sQRlzYrYBDhH5UkQuF5F6JXeKyNXAM8C1QENgAfDdEWVcCZwN9ARuBC51b38BmAHUA1rgqqUhIvWB34BxQDjwJvCbiISXKPN2YCRQB9h9ZNAicpeIpAGLgHPd7x8HXhGRNBFpU8Y5jXEl5I3HuynGeJIlKGNOgKpmAAMABT4GkkRksvvLHGAU8JKqblbVIuBFoFfJWhTwsqqmqeoeYA7Qy729EFfzWzNVzVPVhe7tQ4Htqvq1qhap6nfAFuCqEmV+oaob3fsLy4j7c1UNA2KAc4AewAYgVFXDVHVXyePdTYHfAF+q6pbK3yljTpwlKGNOkDv53KmqLXA1mTUD3nLvjgDedtdK0oBDgODqqzrsQIn3OUBt9/sn3ccuF5GNInK3e3szjq4V7T6izL3lxSsi9d3xpAPnAXOBrUAnIFVE/nHE8X7A10AB8FB55RpTVSxBGeMB7trFF7gSFbgSxSh3reTwK1hVF1egrAOqep+qNsNVE/uviLQHEnAlvpJaAftKnn6Mcg+5a0+jgE/c76cBV7nje+vwse7+sE+BxsB1ZdXGjKlqlqCMOQEi0llEHheRFu7PLYERwFL3IR8AT4vIGe79dUXkhgqWfcPhcoFUXEnHAUwFOorILSISICI3AV1xjSasjJKj9s7E1dx3pPeBLriSV24lyzfGIyxBGXNiMoF+wDIRycaVmDbgGnCAqk4CXgG+F5EM976KPkt0trvcLGAyMFpVd6lqCq6BFY8DKbiaAq9U1eRKxt4bWOUeXOFQ1dSSO939ZKNw9YkdEJEs9+vWSl7HmJMirlGmxhhjjG+xGpQxxhifZAnKGGOMT7IEZYwxxidZgjLGGOOTArwdQHVo0KCBtm7d2tthGGOMKUNMTEyyqjY8cvtpkaBat27NypUrvR2GMcaYMojIUfNGwinYxCciLUVkjnuW6I0iMtrbMRljzF9ZfpGDmZsOUt2PJZ1yCQooAh5X1S64Jrv8u4h09XJMxhjzlzV5TQL3fbWSYe8uYu7WxGpLVKdcglLV/aq6yv0+E9hM6ckyjTGm2nyyYCfvzdnBr2sTWBefRnpO5actTMzI48vFcfy6NoHluw4Rl5xNboGjeH9UVNQxz6/q/esnf8LrN/QkNaeAOz9fwU0fLmX5rkPHPMcTTuk+KPcKn2cCy7wcijHmNORwKt8u38POpOxS20NrBhARXotW9UNoFR7Cip8+5LExz9IqPISmdYPx95NSx19z76Psa3vlUeXXqRlAw1qBzImOJqHp+dQP9qN5w/oUpu6jbctmBBRkkZm8n+joaFq1akVoaChdu3Zl2rRp9O/fn+3bt7Njxw6io6MBaNmyJWeccQbTpk3jiiuuYN26dcTHxxfvb9++Pa1atWL27NlcffXVLFq0iKSkJF544Xn0+WiG9WzGDyv28PYf27nxwyUM7NiQpy7rTNdmoVVyfz021ZF7MbXjcapqmoeuVxuYB/xbVSeWsX8kroXbaNWqVe/du8vsgzPGmJNS6HByx0NPUNTr+lK1iga1a1CnZgDxqTnseHEorZ78FS3IJTDQn7o5CTQLDyW8BqTvXMMvn/6H/reMpl/rMPpeOJQff/iWhm26Mn/qRDYunFZcZq3OAwi78F4KEncRWL85GSsnk7Xqz7mCO19yC0NuH039mtA8vC6zv/+AHz56q3h/ZGRkqdpSVFRUcXICeODRMdx8/+OkZOeTklXA9x+8yR/fvle8v+WQv1H73BHklKjddWhUm5mPDTqpeygiMara56jtHkxQebiWA5BjHOavqq08cK1AXDM4T1fVN493fJ8+fdRG8RljyuNwKhm5haTmFJCWW0haTgFpOYWk5hSSnlPAz5+9Tc/h95GWU0B2fhH1awXRoHYNGtSuQcM6Nbizfxt+X76FTdti2ZQTwq//+w5HYAi1Dm1jz9Kpxdfp3vd8rn78VVaviiErKJwNc34meeEPxfuPTCCHiQiqiqqSnlvIwYx8DmbkkZjp+vnQhR24/+uVHMzI42BGPkmZ+RQ4nMXn737lSiLGTCG0ZgCNQ2vSsE4NCoqcpGQXkJyVz4boy4gYc/Sk+IH+QnitGix7dgi3fbKUBrVrEF4riPDin0F0aRpKs7Dgk7r/5SUoTzbxbVbVM48TxOqTvUiJdWo2VyQ5GWNOH06nkplXRFpuAak5fyaatBzX5/TDSSinsFQiysgrpLy/1UUg7of3kTOvIzA3mXrhDVn86//Iyi0kNWEnWWtcNZzL+3YmpMsgwi9/mKCuF+JXoxYiQ4gY9GBxgnj7zrO5oHMjuPEcd+k3k5bzFfVq1TjmwIPIyEh3LEJYSBBhIUF0alKneH9SZCRRt/Uu/qyqpOUUcjAzj8SMfN459A8GX9qJRHcCS8zMo2agP92a1yW8VhAtbn+I26/p7k687gRUO4g6NQIQEaIKIom6p9/J/cc5AZ6sQdVU1byTPaYC1xkALADWA4f/RHhGVaeWd47VoIz561NVBr42h72Hyl++KrRmAGEhQdQLCaSu+2dYcCBLf/yQm+9/jHohQdQNCeRQ/E7279zKprUxvP/eO8XnDx8+nM8++wyn00l4eDhOhdScAhrWqcn8bYkkZ+WTnFlAUlY+yZn5JGbms3BHMmkLvyFsgGu1ks8PJ6kSoqKijjtQ4a+sypv4SlyoD/AsrpU/A3A1+amq9vDohSrBEpQxp4cvF8fxwbxY9qe7/g6+pGtjRg1qx4QP3+TfL0QT4F964HJhYSHTpk1j2LBhLFy4kOnTp3PNNdeQkpJC8+bN6dy5MyJS3MRWnookmA370nl/XixPXdaZlvVDTvp3/SupzgS1FXiC0jUcVNVroxQsQRlz+ihyOJm1+SBfLI5j6c5D1AjwY9u/r2DZ1niStq8hODiYlJQU1q1bR25uLm+88UbxueX1AZ3uNZyqVp0JaqGqDvBooSfJEpQxf21HJpCCggJWr17Nux9/zvhPPyze3ua8K1n227c0DKtT6vzj1ZBM1aqOQRKHRYrIJ8AfQP7hjWUNBTfGmCMlpOXy2IQ1HMouIMDPj0B/IcDfjwA/IcBf/tzm50eAvxDo78db0dFkdriMmN/GU7tuPSI69yQ7NYn2l9/DsDNvZ/JDA4gYM4WercKoUbPmUdc8PAjB+JaqqEGNBzoDG/mziU9V9W6PXqgSrAZlzKkhr9DBjR8uYWdSNgPaN6DI6aTQoaz6+SO6DL0Hh1MpcjjJy8sjccsK9q2cQdLaucXnNxo4gkYX3EGRQylyOilyKt2a1cUZM4H/vvESEeG1vPfLmXJVZxPfelXt7tFCT5IlKGN8n6ryf/9bx0+r4vn4b324uGvj4n0iQkJCAjNmzGDbtm088sgjxMXF0bt3bwICAqyJ7hRXXoKqirn4ltrkrcaYyvpycRw/rYrnH0M6cHHXxmzYsIErrrgC16OP0KxZM3bu3Mm///1vGjduTL9+/QgIcPVSWBPdX1NV1KA2A+2AXbj6oGyYuTHmmO555Enm1OzPWfXy6ZS1lquvvpr09HTOPPNMwsPDrYb0F1edgyQuq4IyjTF/MarKqvUbmbwxlc/eeY2O1+Tz309foEHd6/HzK924YzWk05PHa1C+yGpQxnjX4WHgTqeTxYsXs2nLVnbm1eKD98aRvmVJ8XHlPYdk/tqqvAYlIqtU9ayTPcYY89eiqkRHR1NYWMhdd93NjDVx/JHenH2ZDi5/9E2eurwzvVrVsyY8cxRPNvF1EZF1x9gvQF0PXs8Y46OysrJwOBwMHTqURYsWAfDiiy/yQ0wCRb2up2vTWrx4Q2cGdmiAiFgTnimTJyeLjajAYQ5VjffIBSvBmviMqZgih5OcQge5BQ5yChzkFBSVeO8gt7DI9bPktoIiZo5/lzMuuYWD++JITTpI0p7tNO57JYUBtcgtdLAm8lIixkyhZf1g/u+STlzVoxl+fsdamcecTqq8ic+bc+0ZY05cQlouN3yw5Kg1hCrCvyiPwp3L2DvpQw5kF9Gy1yCadh9A296DCAnyJzjIn5Agf+rf8iD3Xduda89qQVBAVTzdYv6KTukl340xJ69ucCBXdG/C9sQsEtJy2ZeaS3aJFVMPE4GW9UJoW78GRbtW0rCWPwc3LuG7Sd8AsOv3T/lb3xZE3X/70Re5+r2jtxlzHDaKzxhTyuFVW+NTc9nnTlifv/MqiaGdObBhEXXOvAJHdipBTTrw/PBu3HFea3tOyZyU6nwOyhhzChMRggL8SM0uYMXWfXzzyTts+f1Lmj/4JeGDbqdHy/qc0zacfm3rM7BDQ8CeUzJVoypmkqgBXAe0pkQCVNXnPXqhSrAalDHH9uw/x3L53x5h6c4UFm6OZ9nCOeTs24qok/Rlfy5E8PSz/+TFf3ntn7L5i6rOGtQvQDoQQ4nlNowxviOnoIiY3aks3ZnC0p2HmPivF/gqvTOZKybSuUcfRt9zK+e2a0DviHrUrmGTsRrvqIoE1UJVbbojY3zIkQlp7d40CgsLSJ39CZmrfgMg/p1b3TWkp44635rwjDdURYJaLCLdVXV9FZRtjKmArQcyidmdyqb96WzYl8H87/9LaP9b8BNo5TzAjd3aEb/kD0a9N5b+50w5bg3Jph8y3lAVCWoAcKeI+Mxs5sacThLScrn0rfnFnwP9hdSF3/LU366ifaNaOApCGTbsXGrcPrj4GKshGV9UFYMkypxRwpsP8togCXO6+WPzQX5ZtZuvXvw/0jctKN4+7K5HePf1l2hZP8SL0RlTWrWtqOu+WE/gfPfHBaq61uMXqQRLUOZ0smbNGtatW0dERAQ1agbj16gd/do24MLX5xCblA1Ap8Z1GNK1ERd1aUyvFmE27ZDxqmobxScio4H7gMNjU8eLyEeq+o6nr2WMcXnyySdp1aoVQ4YMYfv27dxwww0EBwcX74+MjCTq8cHsSs7mj80HmbX5IB/M28l7c2JpULsGF3VuxEVdGjGgQwNCguzxSOMbqqKJbx1wrqpmuz/XApZ4sg9KRC4D3gb8gU9U9eVjHW81KPNXpKpMnz6dHTt28PDDDxMXF0dEREXmbHZJyylg3rYkZm46yLytSWTmF1EjwI/+7RswpEtjLurSiMahNavwNzDGpTqfgxKg5EReDvc2zxQu4g+8B1wMxAMrRGSyqm7y1DWM8WXx8fF8+eWXDBs2jB9//JFPP/0UgNatW/PcP8fy1LP/pKDISaHDSYHDSaFDXe/d24o/O5zUDPTnsm5NGNihIYtik5m9OZHZW1wvJkGPFnW5oXcLbjsnAhFrBjTVqypqUI8BdwCT3JuuBr5Q1bc8VP65QJSqXur+/DSAqr5U3jlWgzKnkkKHk4MZeSSk5bkmb03L5fsP3sAvrBkHDuwnrNsg1L8mzqBa7oTjZPMLlxMxZkqVxNO+UW1mPjrQEpSpMtVWg1LVN0VkHtAfV83pLlVd7cFLNAf2lvgcD/TzYPnGVLsVcYd4ddoW4lNzOZiRh9P9d2Nh2gGy1s0kY8kPDB77A+efdzm1a/gT6O9HYIAfQf5+BPoLc2/9O0OHdCDQ/89tgQF+JT7/ua2G+9zD24r3Bxzx2b3fEpPxlirpDVXVGFxTHVWFsv61HFUNFJGRwEiAVq1aVVEoxpy8vEIHj01YQ0GRkwHtG9I8rCZb5v2MMzsN/8B8PlryAwBzn7+JQZGRZT40++zQd6s5amOqnscSlIgsVNUBIpJJ6YRx+EHdUA9dKh5oWeJzCyDhyINU9SPgI3A18Xno2sZ43KcLd7Hul0/46Pl/sGrGt/QbNox7xzxAaKjrn8yH77xpc+GZ05InV9Qd4P5Zx1NllmMF0EFE2gD7gJuBW6r4msZUiX2HsvjX62+Tsfg7hvf/jBsu6IOf39ErztpMD+Z05PG1l0XklYpsO1GqWgQ8BEwHNgMTVHWjp8o3pqrl5uYSGxvLmDFjuOTaWzgw1dU8FxwczPPPl72Uhc2FZ05HVTGKb5WqnnXEtnXenIvPRvEZb4uMjGT06NF88c0PrN8eR/ehd7L5YDYzNx3kgcHteOryLtaEZ05bVT6KT0QeAB4E2rkf1j08mKEOsMhT1zHmVKCq7EvLZU7MFrbsT+fF55/nhwPh5NVrByGtmDt3J20b1GJE35Y8dEF7cq0Jz5ijeKwGJSKhQH3gReAp3IMjgExVTfXIRU6Q1aBMVXI6ld2HctiwL50NCems3ZnI2m07SYpdT/am+eTt+nNA6/C7HuG5sZF0aRpKrRo2pZAxUD3PQU11j+IbBlxZ+triyVF8xnhddn4RMzcdZPLaBJbvOsTeWV8S3LonwfUaExg7n0GXD2PQiNF0az6Wzk3qEBwUYE14xlRSVYziq+2pMo3xJQVFTuZtS+KXNfuYtfkgeYVOwp2pnBlawMbF3/HiyOHcddM1hNS886hzbRSeMZVXJctt+Bpr4jMnyuFUlu86xOS1+5i6/gDpuYWESh7NU9cw6pZr+PiVsUyd+lvx8ZHlPEhrjClfdS63cQMwTVUzReSfwJnAv1R1laevZUxVUFU27MvglzX7+HVdAgcz8slaNJ5zzu7NNRH16NepBZ063kXbtm25eqBr/jt7kNYYz6uKXtp/qur/RGQAcAnwOvA+Nl+e8XE7k7KYvDaByWsS2JmcTaC/0DUgidYpq5iw8HvGvvEoZ599dplz01kTnjGe5/EHdflzqY2hwPuq+gsQVAXXMeakZeUXcc09/2DYuwu58I15vDVrG2GBhfROmcWzXdJpk76aCZ+MA6Bfv35ER0eXWY416xnjeVXxoO4UXFMQDQF6A7nAclXt6dELVYL1QZkj5Rc5+HrJbt6ZvYN1UZdy8Su/0yZ3O0Fpu4l67mlq1apFzZp/LtZnTXjGVJ3y+qCqogZ1I65piC5T1TRcz0Y9UQXXMabSnE5l8toEhrw5j0cfeZh1UZcCMHPM5TRxJvPOf14nPDy8VHICa8IzxhuqZBSfiPQEznd/XKCqaz1+kUqwGtTpLSoqiqioKJbEJvP0J1PYHJdAk8A8ruzWmLGj76VmzZpWOzLGi6pzFN9o4D5gonvTeBH5SFXf8fS1jKmI6Ohofl1/gAMNzyYkNZbXHrydW8/vjL+fa7CD1Y6M8U1V0Qe1DjhXVbPdn2sBS2yyWFMdCh1O1u05xPx1sbz21APs3/LnYs6X/+1hJn7yH2oG+nsxQmPMkaqtBoVrDj5Hic8Oyl4F15iTlpiRx+NPPUfbC28kZts+ViycQ35GMqH9rqPlrS9xTduG/PfW3iRn5hFeu4a3wzXGVEJVJKjPgWUiMglXYhoOfFoF1zGnmfTcQtbHp7M2Po118WksXbWB+M0rOTT9PcI3J3PmoCv4+wP30zuiHmdFhNG0bjAADbdFWnIy5hTk8QSlqm+KyFxggHvTXaq6+hinGHOUvEIHGxPSWbs3nY/eepmQc0YQezCdotT9ZG+aS9s+g8iLmcyhxdMASJn7Jf0HtWbsVTceVZY9o2TMqakqBknUBAbjGsXnBPxFZLOq5nn6WuavocjhZOvBTNbFp7MuPo21e9PZejCTIoeDwqQ49v/0ET2KlM4NG3DF9SMIq30x+zML2dyrN6FXPM7K5y62UXjG/AVVRRPfV0AmMM79eQTwNXBDFVzLnIL2pOSwak+qu6kunXnfvUft825xJRlHIVnrZyH+geTtWU/2xtkArPvlY3b3H8HWuXuOKq/5hbdX969gjKkGVZGgOh0xa8QcEfHqc1DGN+QUFPHH5kQe/s7V4quqFKUmkLLgW6R+KwoSdxHa+0qC2/UlILQBtXtcTIMrH2P3K1cSMcY1KWvr8BC6NA2la9NQujQNpUuzUJrVvcKbv5YxpopURYJaLSLnqOpSABHphy35ftoocjiZszWJ7YmZJKTlsj8tj3nf/5fa540gNTMHLcwnZ/syilITcBbmkrlyMgDJv7zM4Jsf5NYbz6NZWDBN69akWVgwDWrX4IWakURFDfXyb2aMqW5V8RzUZqATcLgtphWwGVd/lHrjeSh7DqpqRUVF8cxzY5m0Op7/zo1ld0oOqkpYcAD+e2NY/dlz3PvGBPavX8glQ4fTs2tH2japT+PQmgQF+Nk8d8ac5qrzOajLqqBM46PyCh1ER0fzu5zNrnVLiagfzL29I8jas4m8tGze/uxtAD55/EYiIyN55PoLjyrDZnIwxpTFVtT1kg370vkxJp6wkEDObRtOr1Zh1AjwzRkODs9lB1BQUEBycjITJ//G+J+ns2z6T8XHDb3hNn786uOjJlq1GpIx5ljKq0FZgqpGhQ4n0zce4MvFcayISyUowI9ChxNVqBnox9mt63Nuu3DOa9eAbs1CCfD3IzOvkFV70ggJ8qdWUAC1awRQu2YAtWr4eyyhlUxA4Bq8kJKSQm5uLnFxcQwcOJC4uDi++OILAmvX41DDnkxavouiWuFc0LkxX97dD6fTWeZCfmWVb4wxJf0lEpSIvAZcBRQAsbgeAk473nneTlDJWfl8v3wP45fu4UBGHq3qh/C3cyO4oU9LUFi2K4XFsSksiU1h68FMAOrUCKBvm/osjztEZl5RmeUG+gu1awRQq0ZAqZ+u9/7UrhFI7Rr+zP72PUY88Lhre1AAzcKCad+gJunp6WRmZtKuXTs2bNjA7NmzSUlJ4Z577mHSpEls2LCBjz/+uPh6fa4dSUrHYfj7CcN7Nee+89vSqUkdS0DGmJNSbQlKRGYBj1fFEhsicgkwW1WLROQVAFUdc7zzvJWg1sWn8cXiOKas3U+Bw8n5HRpw53mtGdypUfFM2lC6hpGUmc/Sna6EtXRnCruSs0lb+A1hA249qvzGoTXo2yaclT99SPdh95JdUERmTj5Z+UUkx+8iKzuHgoBg9n04ksa3vExR2kGK0g5Qu9elhMUvoqkzhVlTJhaXFxkZWSrROJ3KvG1JXNilMRFjplCnRgC3nNOKu85rQ5O6NY+KxxhjTkR1JqizgNeB3cAzqrrfoxf48zrXANer6tHf3Ec42QQ14KYHOOvqkVzWrQmDOzUkJKj02JJSfTRFTn7fsJ8vF8cVN8013DaZT8e9SvtGtcv7XVBVVJX8/Hz8/f1JSkoiOzubgNr1adusAQ+Mm0jMjgNs37SWkPZ9yd48H0QI6XAuCR+P4q4XPuLs5iEk7tnBqFGjWLBgAdOnT+fTT/+cBvHh/3uae0c/ycq4VD6av5MDGXn0ahnGLw8N4L9zdhAU4EcN9yunwMH4pbvZnphF0YofiIqK4qazW1KnZuAJ30djjClLtTfxich1wFhc60K9qqq5Hi7/V+AHVR1/vGNPJkE5nU78/f1pMfQhHI06UrBjKeedP4i2wXkE5yXx8IMP0Lx5c14f9x77pBE/TJyMs3lPQvMP0rl2Pq8+9xid27Xmiy++oF27dsyaNYvhw4ezdOlSfvjhB+bNm1d8rb/97W906dKFa6+9ltWrVzNhwgQmTvyzhvPEE0/wwCOPsisDVu3N4JUXXyB5/jfF++v2H0HYgFtpVrcmT1/RhaHdm+LnJ2UOUsgvcvBjTDwfztvJuskfl1lD69ykDqMGteXKHs0I9K+KxZeNMaaaE5S4esvPwDVh7L+APOBpVf26AufOApqUsetZVf3FfcyzQB/gWi3nFxCRkcBIgFatWvXevXt3pX+PqKgooqOjiz/f/fATNL/ob0zfeICDGflkLPqW1IXfFu+v238Ew+/+B3ee15pBHRvy/PPRpc4/sgmtRKzHHOV2rP0FRU5qBPrz9MR1fLus9DRA4bWCiPnnxcftI3I6lQKHk/xCJ/lFDvKLnDicSkR4SLkDH4wxxlPKS1DFTUueegELgQRgJvACcCXQHngH+MgD5d8BLAFCKnpO79699WS4btOfHA6nrow7pP+aslH7v/yHAhr5ywbdkZhZofOPFBkZ6bH9DodTl8Qm69XvLdQpaxOOeZ4xxvgCYKWW9X1f1saTeQHdcNfMyti3+STLvgzYBDSszHknm6COlSCcTqeOHTv2hM83xpjTXXkJqlqHmYtIW1XdeRLn7wBqACnuTUtV9f7jneftYebGGGPKV51THZXrZJKT+/z2norFGGOMbzulHtQ9USKShGvY+4lqACR7KJyqYPGdHF+PD3w/Rovv5Jzu8UWoasMjN54WCepkicjKsqqfvsLiOzm+Hh/4fowW38mx+MpmD7cYY4zxSZagjDHG+CRLUBXzkbcDOA6L7+T4enzg+zFafCfH4iuD9UEZY4zxSVaDMsYY45MsQRljjPFJlqCOQUQuE5GtIrJDRJ7ydjxlEZE4EVkvImtExOvTZYjIZyKSKCIbSmyrLyIzRWS7+2c9H4svSkT2ue/hGhG5wovxtRSROSKyWUQ2isho93afuIfHiM8n7qGI1BSR5SKy1h1ftHu7r9y/8uLziftXIk5/EVktIlPcn71y/6wPqhwi4g9sAy4G4oEVwAhV3eTVwI4gInFAH1X1iYf8RGQgkAV8pard3NteBQ6p6svuRF9PK7DQZDXGFwVkqerr3oipJBFpCjRV1VUiUgeIAa4G7sQH7uEx4rsRH7iH7pUUaqlqlogE4pq8ejRwLb5x/8qL7zJ84P4dJiKP4VoxIlRVr/TWv2GrQZWvL7BDVXeqagHwPTDcyzH5PFWdDxw6YvNw4Ev3+y9xfaF5RTnx+QxV3a+qq9zvM4HNQHN85B4eIz6f4J57NMv9MdD9Unzn/pUXn88QkRbAUOCTEpu9cv8sQZWvObC3xOd4fOgfYgkKzBCRGPcaWL6osbpXVnb/bOTleMrykIisczcBeq0JsiQRaQ2cCSzDB+/hEfGBj9xDd/PUGiARmKmqPnX/yokPfOT+AW8BTwLOEtu8cv8sQZWvrJX6fOovHbf+qnoWcDnwd3cTlqmc94F2QC9gP/CGV6MBRKQ28BPwD1XN8HY8RyojPp+5h6rqUNVeQAugr4h081YsZSknPp+4fyJyJZCoqjHeuP6RLEGVLx5oWeJzC1wLMfoUVU1w/0wEJuFqmvQ1B919F4f7MBK9HE8pqnrQ/aXhBD7Gy/fQ3TfxE/CNqk50b/aZe1hWfL52D90xpQFzcfXv+Mz9O6xkfD50//oDw9x9298DF4rIeLx0/yxBlW8F0EFE2ohIEHAzMNnLMZUiIrXcHdWISC3gEmDDsc/yism4VkLG/fMXL8ZylMP/8NyuwYv30N2J/imuxT3fLLHLJ+5hefH5yj0UkYYiEuZ+HwwMAbbgO/evzPh85f6p6tOq2kJVW+P6zputqrfhpftXretBnUpUtUhEHgKmA/7AZ6q60cthHakxMMn1nUEA8K2qTvNmQCLyHTAYaCAi8UAk8DIwQUTuAfYAN/hYfINFpBeuJtw4YJS34sP1F+ztwHp3PwXAM/jOPSwvvhE+cg+bAl+6R+H6ARNUdYqILME37l958X3tI/evPF75/8+GmRtjjPFJ1sRnjDHGJ1mCMsYY45MsQRljjPFJlqCMMcb4JEtQxhhjfJIlKGOMMT7JEpQxxhifZAnKGB8lIotFJExEHjxyu4fKby0iuSUeuK3IOcHu9YoKRKSBJ+IwpjyWoIzxUap6HhAGPFjGdk+JdU9cWtGYct3H+9y8lOavxxKUMZUgrtVkL3a//5eIjDtif2sR2SIiX7qXTvhRRELc+x4TkQ3u1z/c22qJyG/iWmF1g4jcVKKsLFxTzLRz11peK7GdY5TZWlwr3n4srlVbZ7jnfTve73Y49k/c5X0jIkNEZJG4VlL1+gSw5vRic/EZUzmRwPMi0gjXWkjDyjimE3CPqi4Skc+AB0VkDnAX0A/XUi7LRGQe0BZIUNWhACJS94iyngK6lVXLEZHe5ZSZCnTAtQL0fSIyAbgOGF+B3689rnnWRuKaMPkWYID793wGLy42aU4/VoMyphLcK/IK8Bhws6o6yjhsr6oucr8fj+sLfgAwSVWz3SuqTgTOB9YDQ0TkFRE5X1XTKxFOeWUC7FLVNe73MUDrCpa5S1XXu5d92Aj8oa4JO9dXogxjPMISlDGVICLdcc1Ine9e8rwsR87ArJS9ACaqug3ojSsBvCQiYysTzjH25Zd476DirSUlz3OW+OysRBnGeIQlKGMqyL1mzzfAcCBbRC4t59BWInKu+/0IYCEwH7haRELca3ddAywQkWZAjqqOB14HzjqirEygTjnXKbPME/z1jPE5lqCMqQD3QIeJwOOquhl4AYgq5/DNwB0isg6oD7yvqquAL4DlwDLgE1VdDXQHlruHej8L/KtkQaqaAixyD1p47Yh95ZVpzF+CrQdljAeJSGtgiqp283Ysx3MysbqXBO+jqsmejsuYw6wGZczpywHUPZEHdYFAXP1SxlQZq0EZY4zxSVaDMsYY45MsQRljjPFJlqCMMcb4JEtQxhhjfJIlKGOMMT7JEpQxxhifZAnKGGOMT/p/ppNUCHS06N0AAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Sensor #1: longitudinal\n", + "C_lon = np.eye(2, discsys.nstates)\n", + "Rw_lon = np.diag([0.1 ** 2, 1 ** 2])\n", + "W_lon = ct.white_noise(T, Rw_lon, dt=Ts)\n", + "\n", + "# Sensor #2: lateral\n", + "C_lat = np.eye(2, discsys.nstates)\n", + "Rw_lat = np.diag([1 ** 2, 0.1 ** 2])\n", + "W_lat = ct.white_noise(T, Rw_lat, dt=Ts)\n", + "\n", + "# Plot the noisy signals\n", + "plt.subplot(2, 1, 1)\n", + "Y = xd[0:2] + W_lon\n", + "plt.plot(Y[0], Y[1])\n", + "plt.plot(xd[0], xd[1], **xdstyle)\n", + "plt.xlabel(\"$x$ position [m]\")\n", + "plt.ylabel(\"$y$ position [m]\")\n", + "plt.title(\"Sensor #1\")\n", + " \n", + "plt.subplot(2, 1, 2)\n", + "Y = xd[0:2] + W_lat\n", + "plt.plot(Y[0], Y[1])\n", + "plt.plot(xd[0], xd[1], **xdstyle)\n", + "plt.xlabel(\"$x$ position [m]\")\n", + "plt.ylabel(\"$y$ position [m]\")\n", + "plt.title(\"Sensor #2\")\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "c3fa1a3d", + "metadata": {}, + "source": [ + "## Linear Quadratic Estimator" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "993601a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object: sys[3]\n", + "Inputs (6): y[0], y[1], y[2], y[3], u[0], u[1], \n", + "Outputs (3): xhat[0], xhat[1], xhat[2], \n", + "States (12): xhat[0], xhat[1], xhat[2], P[0,0], P[0,1], P[0,2], P[1,0], P[1,1], P[1,2], P[2,0], P[2,1], P[2,2], \n" + ] + } + ], + "source": [ + "#\n", + "# Create an estimator for the system\n", + "#\n", + "\n", + "# Disturbance and initial condition model\n", + "Rv = np.diag([0.1, 0.01]) * Ts\n", + "# Rv = np.diag([10, 0.1]) * Ts # No input data\n", + "# \n", + "P0 = np.diag([1, 1, 0.1])\n", + "\n", + "# Combine the sensors\n", + "C = np.vstack([C_lon, C_lat])\n", + "Rw = sp.linalg.block_diag(Rw_lon, Rw_lat)\n", + "\n", + "estim = ct.create_estimator_iosystem(discsys, Rv, Rw, C=C, P0=P0)\n", + "print(estim)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3d02ec33", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY4AAAEKCAYAAAAFJbKyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAA0kElEQVR4nO3deXxU9bn48c+TBBICCMomIBiUfZHFCCKIVty1rrd1aa21XrH31r601bZerYJ67b14/bnVVkVRtCCuoAhBcWNTgYQgJBAW2cMOIUDClsk8vz/OGScJCWSSOTkzyfN+vc4rc5Y55wkkeea7i6pijDHGVFeC3wEYY4yJL5Y4jDHGRMQShzHGmIhY4jDGGBMRSxzGGGMiYonDGGNMRJL8DqAutG7dWtPS0vwOwxhj4sbixYt3q2qbys41iMSRlpZGVlaW32EYY0zcEJGNVZ2zqipjjDERaRAlDmOMaTCOHoVvvoEZM2D7dpg4MeqPsMRhjDHxbts2mDnTSRaffw4HDoTPTZoEF1wAs2dH7XGWOIwxJt6UlkJWlpMoZsyA7Ozy5/v0gQULoFkzTx4fs4lDRHoA75Y5dAbwKNASuAvY5R5/SFUz6jY6Y4ypY3v3wqxZTqKYORN27y5//uyzITMTRDwPJWYTh6quAgYAiEgisAWYCtwBPKuqT/sXnTHGeEwVcnMhI8NJFvPmlT/fq5dzrFWrOg8tZhNHBSOBtaq6UeogmxpjjC+Ki+Grr5xEMWECHDkSPte5M0yeDEOGQGKibyFC/CSOm4HJZfbvEZFfAVnA/aq615+wjDGmltauDZcqZs92kkWzZnDllXDVVXDFFdChg99RliOxvpCTiDQGtgJ9VHWHiLQDdgMKPAG0V9XfVPK+UcAogM6dO5+9cWOVY1mMMabuHD3qVDGFGrZXr3aON2kChw6Fr4tyT6hIichiVU2v7Fw8lDiuALJVdQdA6CuAiLwKTK/sTao6DhgHkJ6eHtvZ0RhTv23dWr67bFGR04hd9oP74MG+JopIxEPiuIUy1VQi0l5Vt7m71wO5vkRljDFVKS2FRYvCVVBLljjHk5PD7RaqvpcqaiqmE4eIpAKXAHeXOfyUiAzAqaraUOGcMcb4o6AAPvvMSRaffnpsd1lwGrbnzKn72KIsphOHqh4EWlU4dptP4RhjTJgq5OSE2yq++w6CwfLXnHeeM/1HPRPTicMYY2JKUVG4u2xGBuTnlz8/cKAzCM/n7rJei3riEJFTqnFZUFULo/1sY4yJujVrnCSRkeE0bJdt0O7e3WmjaN/et/D84EWJY6u7HW+kXiLQ2YNnG2NM7Rw5AnPnhhu216wpf/6ss5xSRePG/sQXA7xIHHmqOvB4F4jIEg+ea4wxNbNlS7hUMW1a+baKrl2dRu8zzvAvvhjjReIYGqVrjDHGG6WlsHBhuGF76VLneKdOMGqUM2L7oosgNdXfOGNU1BOHqh4GEJF04GHgdPc54pzWs0LXGGNMndmzxyk5zJjhdJctKHAasctOPb55M+TlwUsv+RdnHPCyV9Uk4E9ADhA8wbXGGBNdqk5JItRWsWCBUwXVqBGUlDjXlJbCgAFxOQjPT14mjl2qOs3D+xtjTHlFRfDFF+H2ii1bjr1m6NB6MQjPT14mjtEi8hrwJfDj3MCqOsXDZxpjGprVq8OlirlznUkEyxo6FL791p/Y6ikvE8cdQE+gEeGqKgUscRhjau7wYSdBhAbh/fDDsdecf75zjfGEl4mjv6r28/D+xpiGIj8/XKr44gs4eLD8+cGDnV5Spk54mTgWiEhvVV3h4TOMMfVRIOA0ZoeSxbJl5c/37eskCusu6wsvE8dw4HYRWY/TxvFjd1wPn2mMiVe7dzvdZDMy4P33neQRcsYZzsC83r2ddSyMr7xMHJd7eG9jTLxThe+/d0oUTz0FBw6Ez7VrB3//O1xyCbRs6VeEpgqeJQ5VtbVajTHlHTjgtFGEGra3uWuynXOOs8b2lVdCejokJPgbpzkuL2bHzVbVQbW9xhhTD6jCqlXhcRVz5zqD71q0KD9JYGam014xZoxvoZrq86LE0UtElh3nvAAtPHiuMSYWHD7sjMQONWyvW+ccT00Nj9jety9ul0013iSOntW4ptSD5xpj/LJpU7hU8eWXTnfZhITys8yec44linrCi0kOrW3DmPouEHCWSg3NLpube+w1550H8+bVfWzGczG9dKyIbAAO4JRQAqqa7q4w+C6QBmwAfq6qe/2K0ZgGY9cup7vsjBnOLLOFheXPp6fDokXWXbYBiOnE4fqJqu4us/8g8KWq/q+IPOju/8Wf0Iypx4JBWLIk3FaxaFH5ZVMBhg2D+fP9ic/4Jh4SR0XXAhe6r98EZmOJw5jo2L/fWVc71F6xfXv584MGOT2grLtsg+ZZ4hCRZOBGnCqlH5+jqo9HcBsFZomIAq+o6jignapuc++1TUTaVvH8UcAogM6dbXlzYyoV6i4bGlfx9dflSxU9ejhdaNtW+mtmGigvSxwfA/uAxZSZVj1Cw1R1q5scPheRldV9o5tkxgGkp6frCS43puE4dCjcXfa115zusyGdOsGkSc5U5EnxWCFh6oKXPxmnqWqtph1R1a3u150iMhUYDOwQkfZuaaM9sDMKsRpTv23cGG6r+OorJ3k0aQIXX+ysr33llWAlc1NNXiaOb0Wkn6rm1OTNItIUSFDVA+7rS4HHgWnA7cD/ul8/jlbAxtQbJSXO4kWhZLF8uXM8JSVcwjh0yJkC5Le/9S9OE5e8nh3317WYHbcdMFWcrn1JwNuq+qmIZALvicidwCbgZ9EP3Zg4tHMnzJzpJIvPPnNGZyclQbNm4WsOH7YR26bWvEwcV9Tmzaq6DuhfyfE9wMja3NuYeiEYhOzscMN2ZqbTsN24cXj51EAA+ve3RGGiytPZcUWkP3C+e2ieqi716nnGNAj79jndZWfMcEoXO3Yce82558KcOXUfm2kwvOyOey9wF+E1xieKyDhV/btXzzSm3lGFvLxwW8X8+eUXOAJnao9vvvEnPtMgeVlVdScwRFWLAURkLPAdYInDmOM5eNAZTxEahLdhQ/nzAwY41VLWXdb4xMufPKH8LLil7jFjTEUbNoTbKr76qvzYCoAhQ5w1uI2JAV4mjjeAhe74C4DrgPEePs+YuFFSArn/nMvJ8z8hbUUGrFhR/oJ+/Zy5oVJS/AnQmOPwsnH8GRGZAwzDKWncoapLvHqeMbFu1apCnntuFTNnJrB7Yy8+5y90JAsIsCXlTDp+PwO6d7fZZU3M87SSVFUX40w5YkyDEwzCp5/u5plnVrFw4SkUFfUChiCyjeuuP0LRRRM4fEMHGnVoTke/gzUmAl6sOT5fVYeLyAGcSQp/PIUzAPCkaD/TmFixZUsxL7ywkhUrurBw4Sns2tUaOJmEhBzgc6Atqn0pKEjkknta+RytMTXjxQqAw92vzaN9b2NijSrMnJnPyy9vYv785uzd2ws4G5Eid5LZILCT888fYGPwTL3h5TiOsar6lxMdMybeFBQc5Z13dvH99x3JyFC2bDkNOA2R5cDXwMmo9nNn9kgA2vsbsDFR5mUbxyUcu8DSFZUcMybmffPNTl58cR1ffZXMzp29gY4kJiqlpQKsAJowYkQfZs/u43OkxnjPizaO/wD+EzhTRJYRHrvRHLDhrSYuFBWVMnduAp9+KkyaVEBBQVugLbAamAs0ZejQIcyblwj09jVWY+qaFyWOScBM4G8464ELTiP5AVXd68HzjImKJUsKef75H/j880Zs3dodaFLm7Gf07duZZct6ItLdrxCNiQleJI4Mt1fVNcDVZY6LiFivKhMzjh51pnh6++1CJk3ay6FDXYB0YB0wl27derBsWRopKacAl/kbrDExxMteVc1OdK0xdW316mJeeGENM2YE2batL0eONAZa4FRB5dGrVwdycvqRmHiGz5EaE7tsljRTrwUC8N138Le/LWH+/BYUFZ0BDAA2A0vo02cI330nNG8+2N9AjYkjCV7dWER+JiLN3dePiMgUERnk1fOMCdm8+Sh//vNy+vdfSZMmMGIEfPppP4qKdtGixXRefXUhhw+3Q3UIubnQ3EYcGRMRL0scj6jq+yIyHGe98KeBl4AhHj7TNEClpfDZZwW89NIG5s8/icLCrkAfYBu/+MURrr8+mWHDSjj1VPvRMyYavEwcoSnVrwJeUtWPRWSMh88zDciePaX8858/kJt7Ol99lcLu3acALRDJBj4GTgbOJj8/mRtvhPI9pIwxteFl4tgiIq8AFwNjRSSZCKrGRKQT8BZwKs68DeNU9Xk3+dwF7HIvfUhVM6IauYk5qvDdd4W8+OJ6vvwymZ07uwE9SEg4TDAIUARs4vzz05kzx2aXNcZLXiaOnwOXA0+raqGItAf+FMH7A8D9qprttpUsFpHP3XPPqurTUY7XxJhDh5Tp04uZM6cZn3xSyqZNLYGBQC7OUKFUgsF0Lrgghdmzm2ED8YypG16ux3FQRNYCl4nIZcA8VZ0Vwfu3Advc1wdEJA9s9un6Li+vmOefX8PMmcLmzd1RbUZCAgSDicBsoAUjRvRnzpy+PkdqTMPlZa+qe3FGkYfmapgoIr+v4b3ScD5qLnQP3SMiy0TkdRE5uYr3jBKRLBHJ2rVrV2WXmBgQCMC8efDgg9C69VZ6927KK68MYNOmk1D9GviaYcOcqirVC1EdyJw5nv3YGmOqQVT1xFfV5MbOPFVDVbXY3W8KfKeqZ0V4n2bAHOBJVZ0iIu2A3TjTmDwBtFfV3xzvHunp6ZqVlVWTb8N4YMuWI/z976uZOvUoa9d2pbS0RegMkE337m1ZtmwgycmN/QzTmAZNRBaranpl57xs4xDCPatwX0fUaikijYAPgUmqOgVAVXeUOf8qML32oRovqcL338Orr27lnXf2s3dvd6AfsANYRNeuA8jKakOLFh2x2khjYp+XieMNYKGITMVJGNcC46v7ZhER9/o8VX2mzPH2bvsHwPU4LaUmxhQUBHj55TW8+24xK1b0IxBIxlmXYgfNm2fw2GMtueuuQTRrdonfoRpjIuRl4/gzIjIbGO4eukNVl0Rwi2HAbUCOiHzvHnsIuEVEBuBUVW0A7o5GvKZ2VGHp0gBjx+a4a1Z0A3oBhcB6evToyZw50LbtAEQG+husMaZWvFwBMAW4EDgfZxxGoojkqerh6rxfVedTedWWjdmIEQcPKuPHr+OzzxL54os0jhxJwunDsJzU1M/4059S+d3vBtGmTU/3HTa+wpj6wMuqqreAA8AL7v4twL+An3n4TOOx1auLeO65NcyYAZs390D1TESKueYa5aqrhP79t5Ce3ouEBFsJz5j6ysvE0UNV+5fZ/1pElnr4POOBQED58MPN5OR0IiNDWLKkGU6pYiNOZ7ckVM+isLApd90F1rhtTP3nZeJYIiLnquoCABEZgi0dGxe2bz/MCy+sYurUI6xe3ZVgsDNOkxI4PaE2Mnz4QObNO93HKI0xfvEycQwBfiUim9z9zkCeiOQAGul4DuMdVVi+PMjMmQm8884BsrObAP1xhst8DwQ455xBLFrUGmjnbsaYhsrLxHG5h/c2tVRUFODll1cyefIBcnI6U1ISqmJqCnwBNGfYsAHMn3+Rj1EaY2KRl91xN3p1b1Mz27ZBRgY88UQ2Gzd2B/oCxTilii0MGTKYBQsScJZPMcaYytnSsfVYIBBk8uTVvP76DnJyOrNnTxf3zOnAIjp1akxmZj/atRvmZ5jGmDhjiaOeKSyEf/xjFRMm7OaHH7oDPYHuQC5paSV8/HEj+vVrhYhVQRljasbLAYDPAX9Qr2ZRNAAEg8qMGet55ZXNzJ17LgcOJAM9gLYkJy/n9ttXct99PenVy/oiGGOiw8sSRxEwTURuVtViEbkUGK2qVi9SS4WFR3jhhRw++OAgeXlnEAicAZxB5857ueeeZEaOPMKwYc1JSRl+wnsZY0ykvGwc/6uI3ArMFpEjOK2wD3r1vPruu++2uAPwOvDll404fDgdKKZRo1xgLdCVTZs68u238Le/JfscrTGmPvOyqmokztrgxTjTot6pqqu8el59c/BgCa+9toJJk/aydGkHjhzpDkBKChw+nADkAN0477whzJ7tZ6TGmIbGy6qqh4FHVHW+iPQD3hWRP6rqVx4+M66tW1fEvHnNmD4dpk49Qmlpf6AEJ0nMATozeHAX5swBZz0LY4ype15WVV1U5nWOiFyBsyjTeV49M94EAkHefnslb7yxg8zMNhQX9y5zdg+Qy5AhvVmwYJBfIRpjzDHqrDuuqm5zq68atD174PPPYdy4zcyZk0Iw2Buny+wKYA79+6ezZElzRE7HGW9hjDGxpU7Hcajqobp8XiwIBpWpU9czbtxWFiw4maKi3gSDgtPsswBYRXp6TzIz+/ocqTHGVI8NAPTAwYMwY8ZhxozJYtWqrpSWngGkAcuBlQwc2IvMzCQSE627rDEm/ljiiJL58/P5xz/WsmzZ6axcmUYwmAIMAJbSrl0eGRndGDTIGrSNMfEvLhOHiFwOPA8kAq+p6v/WdQwHDsCLL67m3Xf3sGxZR1Q7A6cBW+jYESZMgOHDU0lJsfGOxpj6Je4Sh4gkAv8ALgHygUwRmaaqK7x+9vLlO3n22bVMnXouBQWCMwfUAZKScvjpT9dy111pXHZZGgkJoXckVH0zY4yJU3GXOIDBwA+qug5ARN4BrsXplhRVgUCQSZPymDBhJ5mZrSgu7gu0pX37I/zhD8mcf/4eLrigMaecYj2MjTENRzwmjo7A5jL7+TirDUbVww/D3/52FOgD9EIkD5gPtGXbth5kZ8Mzz7SK9mONMSbmxWPikEqOHTMDr4iMAkYBdO7cOeKHPPkklJYm8sUX3zBxYk969uwT8T2MMaY+isdK+HygU5n904CtFS9S1XGqmq6q6W3atIn4IWPGwNixjVi8eBi9erVizJiahmuMMfWLxNtyGSKSBKwGRgJbgEzgVlVdXtV70tPTNSsrq44iNMaY+Ccii1U1vbJzcVdVpaoBEbkH+AynO+7rx0saxhhjoivuEgeAqmYAGX7HYYwxDVHcVVXVhIjsAjbW8O2tgd1RDCdaLK7IWFyRsbgiUx/jOl1VK20gbhCJozZEJKuqej4/WVyRsbgiY3FFpqHFFY+9qowxxvjIEocxxpiIWOI4sXF+B1AFiysyFldkLK7INKi4rI3DGGNMRKzEYYwxJiKWOIwxxkTEEkcVRORyEVklIj+IyIN+xxMiIq+LyE4RyfU7lhAR6SQiX4tInogsF5F7/Y4JQERSRGSRiCx143rM75jKEpFEEVkiItP9jqUsEdkgIjki8r2IxMxcPSLSUkQ+EJGV7s/a0BiIqYf77xTa9ovIfX7HBSAif3B/7nNFZLKIpETt3tbGcSx3sajVlFksCrilLhaLOhERGQEUAW+pal+/4wEQkfZAe1XNFpHmwGLgOr//vUREgKaqWiQijXDmxb9XVRf4GVeIiPwRSAdOUtWr/Y4nREQ2AOmqGlMD2kTkTWCeqr4mIo2BVFUt9DmsH7l/N7YAQ1S1pgOOoxVLR5yf996qekhE3gMyVHVCNO5vJY7K/bhYlKoeBUKLRflOVecCBX7HUZaqblPVbPf1ASAPZ90UX6mjyN1t5G4x8UlJRE4DrgJe8zuWeCAiJwEjgPEAqno0lpKGaySw1u+kUUYS0MSdGDaVSmYRrylLHJWrbLEo3/8QxgMRSQMGAgt9DgX4sTroe2An8LmqxkRcwHPAn4Ggz3FURoFZIrLYXdcmFpwB7ALecKv3XhORpn4HVcHNwGS/gwBQ1S3A08AmYBuwT1VnRev+ljgqV63Fokx5ItIM+BC4T1X3+x0PgKqWquoAnHVbBouI79V7InI1sFNVF/sdSxWGqeog4Argd271qN+SgEHAS6o6ECgGYqntsTFwDfC+37EAiMjJOLUkXYAOQFMR+WW07m+Jo3LVWizKhLltCB8Ck1R1it/xVORWa8wGLvc3EgCGAde4bQnvABeJyER/QwpT1a3u153AVJyqW7/lA/llSowf4CSSWHEFkK2qO/wOxHUxsF5Vd6lqCTAFOC9aN7fEUblMoJuIdHE/SdwMTPM5ppjlNkKPB/JU9Rm/4wkRkTYi0tJ93QTnl2mlr0EBqvpfqnqaqqbh/Gx9papR+zRYGyLS1O3ggFsVdCngew8+Vd0ObBaRHu6hkYDvnVXKuIUYqaZybQLOFZFU9/dzJE7bY1TE5XocXovlxaJEZDJwIdBaRPKB0ao63t+oGAbcBuS47QkAD7nrpvipPfCm29slAXhPVWOq62sMagdMdf7WkAS8raqf+hvSj34PTHI/zK0D7vA5HgBEJBWnB+bdfscSoqoLReQDIBsIAEuI4vQj1h3XGGNMRKyqyhhjTEQscRhjjImIJQ5jjDERaRCN461bt9a0tDS/wzDGmLixePHi3VWtOd4gEkdaWhpZWTEzV5sxxsQ8Ealy6hSrqjLGGBMRSxzGGGMiYonDGGNMRCxxGGNMfTNmDIiEtzFjonp7SxzGGFPfjBkDqjB6tPPVEocxxphyPC5hVGSJwxhj4p3HJYyKLHEYY0w8quNSRlmWOIwxJh7VcSmjLEscxhgTD3wsYVRkicMYY+KBjyWMiixxGGOMiYglDmOMiUUxVDVVkSUOY4yJBRUTBcRM1VRFljiMMSYWxFAbxomcMHGIyCnV2FrWQazGGFN/xHBV1IlUZyGnre4mx7kmEegclYiMMaYhGDOm/BZHqlNVlaeqZ6hql6o2YI/XgRpjTNyL41JGWdVJHEOjdI0xxjRscdSOcTwnTByqehhARNJFZKqIZIvIMhHJEZFlZa8xxhhTRj0pYVQUSa+qScAbwI3AT4Gr3a/GGGMgrrrU1kYkiWOXqk5T1fWqujG0eRaZMcbEm3pSFXUikSSO0SLymojcIiI3hLZoBSIinUTkaxHJE5HlInJvJddcKCL7ROR7d3s0Ws83xpiI1dOqqBOJJHHcAQwALsepogpVV0VLALhfVXsB5wK/E5HelVw3T1UHuNvjUXy+McYcXwOpijqR6ozjCOmvqv28CkRVtwHb3NcHRCQP6Ais8OqZxhhzQmPGwGOPhfdHjw4fb6AiKXEsqKIEEHUikgYMBBZWcnqoiCwVkZki0qcu4jHGNCBWqjihSBLHcOB7EVlVsTtuNIlIM+BD4D5V3V/hdDZwuqr2B/4OfHSc+4wSkSwRydq1a1e0wzTG1BeWKCIWSeK4HOgGXIpH3XFFpBFO0pikqlMqnlfV/apa5L7OABqJSOvK7qWq41Q1XVXT27RpE80wjTHxzBJFrVU7cZTtgutFd1wREWA8zhQnz1RxzanudYjIYJz4bboTY0zVLFFEXXVmx82OxjXVMAy4DbioTHfbK0XktyLyW/eafwNyRWQp8AJws6pqFJ5tjKkvLFF4rjq9qnqdoC1DgBa1DURV53P8GXhR1ReBF2v7LGPqgqoSCAQoLS0lJSUFgKKiIo4ePUro806rVq38DLH+sJ5Pdao6iaNnNa4prW0gxsQKVQgGoaTE2Ro1AvfvPsuXL2fp0qWsXHmA7OxmrFnTjqNHS7nppss4+WSYOfM9Fi1aSUlJCwKBlkAyKSn5vP76A/TuDY8+eg/Tpr0HlJCcJBwuOerjdxpHKiaGCy6AOXPC+2VLE6FkYUnDMydMHDatiKnvVCEvDz75BKZNUxYuhNLS8oXfli2VU08ViouT2bz5HJx+IgAHgBLGjlWcAvPP3eNHgYOAcvhwC269NXSnCe4GswIjOJzUlJT2p8App0CrVnDyyc7rE22pqeFqmPqgJokh9D5LEHUukgGAxtQreXkwYUIJEyeWsHVrqnt0J/AmTkIIACcDHSksvJ6iolQCgU44SeEQAwemkJnZnMRE52/aoUNw4ACcdBI0adIYaAzAkSMwfDhkZYWf/e8ynkJtyQ+lafTNX0EgfztJBGr2jXTuDAMGhJNKxeTTqlX49UkneZdwTvTH/0T7lVUvWWKISZY4TINRXAxZWcr7729lypQktm1rh9M/ZD7QBhjIOefs5+67W9G//0X07t2b1NTUCndJdrfyRJxCwDGXA8nJkJlZ8eid7uY45hdRFQ4ehL174ckn4eWXw+e6dIH168P7+/fDtGnH/d6rdMopUFAQ3j/zTFi7Nrx/zjnlg6/4x76s6pYKjrdvSSIuWOIw9VYgAPPnw5QpQWbPDrJiRZJbBdURZ1KCCbRvf5jx4wczfHhXmjcHpwqq2/FuWzdEoGlTZ3vpJWerSsVP+uefD/Pmhff79IHly8P76elO8ahjRydptGgBW7c6RaOySQPKJ40WLWDTJjj7bCehnXOOk3hyc+G665zX06Y516xYES79mHqn2olDRJJx1uJIK/s+m2jQxJrMTPjnP5WPPgpQWNgIp2ppLdCHPn2Ue+/9mGuuGUq7dn/xOdIoiaQ6p2ySWb7cKUGUrUP705/g3/8dnnoKbrzRSSx79sCUKdC/f3h/xw5YsiS8XzZRAbzxRvh1o0bw+uvhKrQdO2DLlnAVWna2c/+TT4bt22HzZudckya1+EcxXoqkxPExsA9YDBzxJhxjam7fPujdG7ZuVaAImIbIDK67Lom77rqFK67og9OAfZ2vcfrqRElmzBjo0cN5PX68U/00ZoyTIKqqXho9Gv74R+eaggJ47jm4+upwUpkxA3r2dEope/bA7t3OsT174Kjbq+yTT8L3fuUV52tyspN0PvggXHrZtMlpSAq12eTmwuefh/cPH3aqy+pTx4EYFEniOE1VL/csEmNqqLQ0yOjRyxk/vi87dwpnnz2fJk3+h9/85mfccMNLtGhR62FGDUdl7Q+hP8KPPVa+jeOxx5ykIeJUY7Vo4bS/nHkm3HRT+B6BQOVJR9VpeHrkEbjjDifR/POfcOmlTlIpKHCSQpcuzuuNG522nTVrnPeFfPhh+e/h6aedJKMKn30WTjpr1jixhpLMmjWwcGH4fDAYrX/Fei+SxPGtiPRT1RzPojEmAkVFB3nggTm8+WZ7Dh8eQPfu+5g+vQUDBw4jISHD7/Dqh+OVUMpWe4WSSCSN2yLQrJmTcM46yzk2e7ZTVRbSpEnlSefIESeZ/Pd/wy23hEs377zjtL0UFMDcudC8uVM1lpfntOMsWlQ+hrffLr//wgvhxLJ3L6xcGd7PzIQJE5zXZdtxTjml+t9zPRFJ4hgO/FpE1uNUVQmgqnqWJ5EZU4WdO4u4664MZszoR2npFcAWYCGrVw/g/vth9uxI5u40NVaT0km0ek0lJ0P79tCmjdPXOWTjxvI9tCrG99e/QmGhk1jGjg234xQUOO0sZ50VTkLbtzvtLwUFThIJBmHWrPD9KmvHCSWS7dudRBXaD7XjhPb373d6zcVpO04kieMKz6Iw5gQKC2Hq1BKmT2/EtGlNCQR+DmwCVvHXv3bniSc6+hyhqVb7SdnEEs1EUl1JSdC6tbN16gRXXhk+d7x2nGAQHnoI7r772HacggKYPt1pxwklnd27nXabgoLK23EAnn023I7z/vvhpFK2HadVK6cTQ4y141Q7cajqRhHpD5zvHpqnqku9CcsYR34+PPLIPv71ryaUljamQwdlwAAhK+sw0BmAxER/YzTVdKISSmgAYCxKSHDmnenSpfJ2nJKSqttxDh50Sjq//nU4sbz5plNSqtiOs2GD046zerXzvpAPPigfT6gdp1Urp70nM9NJKqtXl2/Hqdi9Okoi6Y57L3AXEFonY6KIjFPVv3sSmWnQNmyAhx8u4p13UggGU4G3gd1s3Xon3bq1RDXF5whNrVUc+Feb9pJYFRqP06KF0505JDcX/uJ2B6+qHefw4XA7zq23HtuOs2ePsy1eXHU7TrNmnnxbkVQG3wkMUdVHVfVR4FycRGJM1JSUwMiR0KVLkLffbkQw+BpnnfU4W7Zcgur9qLZk9my/ozRRF/p0HtrA+aP72GPO1/qQRCKVkgIdOkDbtk7p5JprnN5nQ4c6yeSll+C99+C225yxOOvWwYMPOr9Eu3bBqlXwi194ElokiUMoPwtuKSeYBt2YSMyefZCePYv46itwfrTGAVdy/fVP0KFDB3+DM3XLEknNhdpxuneHU0/15BGRJI43gIUiMkZExgALcFbsM6bGVGH+/ABDh67iJz9JYf36fUyaVISqoPp7VDvb3whjiSTGVDtxuMu5/gYoAPYCd6jqcx7FZeq5gwedsV5nnnmA889PYsGCDsAHqG5l3Dhv6mVNPWKJxFcRTXKoqotxphwxpsZuvx3eeiu0t5rU1Cm8+uq53HLLzxCbKsLURGVdgS15eOaEJQ4Rme9+PSAi+8tsB0Rkv/chmvpi3z64/fYjZZIGwE7++MfR3HrrTy1pmOgIdfO10odnTpg4VHW4+7W5qp5UZmuuqid5H6KJd5s2wRNPBDn99GLeeisJkf/H6tVb3FqGK3jiicZ+h2jqE6vG8ly12zhEZGx1jhkDUFoKEyfCRRfB6afDo48msG9fJvCfDBo0km7dbKS3qSOWSKIukl5Vl1RyzKYhMcf49lto2dLpXv7116XAEzRufB7vvLODYPBlsrIG+ByhadAqJhJLHBGrThvHf4hIDtBTRJaJSI67bQCiOlOuiFwuIqtE5AcRebCS8yIiL7jnl4nIoGg+39TO9u3OrArDhkHjxgeZPFkJBhP5+uvz2bNnFjfddJO1Y5jYY20iEatOr6pJwEzgb8CDuLPiAgdUdW+0AhGRROAfOCWbfCBTRKap6ooyl11BeG3PIcBL7lfjsy+/dKbu2bcvSOvW49m9+w+ceup0RC7kwgsv9Ds8Y6p2oskZzTGqU1WVoaobgGuAXJxSRi6wKcq9qgYDP6jqOlU9CrwDXFvhmmuBt9SxAGgpIu2jGIOJkKozUejFFyt79uQTCPRm9+4n6dNnIhdccIHf4RkTOSuBnFAkvaqaedyrqiOwucx+vnss0msAEJFRIpIlIlm7du2KYpgm5NAh+NWv4A9/gGbNvqBx4wE88sjPKS5eQW7udVYtZeKTNaafUEQDAD1W2V8ZrcE1zkHVcTiTHZGenl7pNabmVqyAa64pZt26VJ54QjjvvEZ07ryArl27+h2aMdFlgwuPEUl33J+JSHP39SMiMiXKjdP5QKcy+6cBW2twjfGQKjz55F769DnK2rXFqE7nkUfg8ccvtKRh6j+rxgIi6477iKoeEJHhwKXAmziN09GSCXQTkS4i0hi4GZhW4ZppwK/c3lXnAvtUdVsUYzDHkZ9/hN69V/PXv55MQsLXPPDAvzh48GJUsanOTcNgXXmByBJHaEr1q4CXVPVjIGpDflU1ANwDfAbkAe+p6nIR+a2I/Na9LANYB/wAvAr8Z7Seb46vXz/o1OkQK1eeDownPb0H//d/99MkTtdMNiYqGmgJJJI2ji0i8gpwMTBWRJKJLPGckKpm4CSHssdeLvNagd9F85nm+LKy8vjHP7qQm5tC166NeeCBxdx9951+h2VMbGigXXkj+cP/c5zSwOWqWgicAvzJi6CM/3bu3MOVV77OOeckMGFCYx54AHJzU7n77vP8Ds2Y2NVASiCRrMdxEFgLXCYi9wBtVXWWZ5EZXxQWHuHf/u0L2rXbx8yZvwFOBvaTmQnJyX5HZ0yMayBtIJH0qroXZxR5W3ebKCK/9yowU/emTIGOHQ/z4YcX06JFCc8+u4lAoK2t821MTdXTEkgkVVV3AkNU9VFVfRQ4F7jLm7BMXZo6dR5XXbWfG2+EgwdTgEXs29eDjz7qTGKi39EZE8fqaQkkksQhhHtW4b62ocFxbMGCRfTv/yQ33NCdTz9N5fHH4ejRZFQHWxdbY7xQT0ogkSSON4CFIjJGRB4DFgDjvQnLeCk7+3vS0/+XoUOTWbbsYQCCwVK+/BIaNfI5OGPqs3pSAomkcfwZ4A6gANgD3KGqz3kUl+9CHwxCW5z+/5aj6rRjXHbZqSxe/CCtW5/GK68coqSkHarJVsIwpi7FcekjksbxFOBC4CfABcCF7rF6KfTBYPTouP5gAMDcuXM599zf06zZHm68EXbvbgMU07t3K0aNakJSLM1YZkxDEcelj0iqqt4C+gAvAC8CvYB/eRGUqb1AIMC7777HWWfdxgUXbGThwr+TlJTKK69ASUkiqk2ZM8fvKI0x8SiSxNFDVe9U1a/dbRTQ3avATM2owrx50KXLVG6++Rxycv4F3AKU0K9fE0aNwkoYxsSiOKq6iiRxLHEnFgRARIYA30Q/JBMpVeWbb76hb9+HSEgoZcQIyM+/gfbtm/DKK0F27EhCtRHz5/sdqTGmSnFUdRVJ4hgCfCsiG9z1xr8DLnDXH1/mSXTmuDZv3sxTTz1F3779GT78XZYvf6jM2URGjTqVUaMSaNvWtxCNMTUVwyWQSCotLvcsClNtwWCQhIQEVq5cSe/evVE9j2bNJgH9uPTSAK++mkjnzn5HaYyptRieQLHaiUNVN3oZiKmcqrJmzRqmT5/ORx99RM+ePRk3bhzFxT3o2XMteXldKCpyrp01K4lf/coG7hljvBXVadEbsrfegm7doFcvGDgQhg6F++6DwsKa33PMmDGkpaXRo0cP7r//fvbt209y8sXccAOkpws7dnRh7FgoLg5Xi1rSMKaeiqGqK+tfEwVDhsCiReH9pCQIBGDBAnj+eSeZLF/u/F8XF8OMGfDJJ7B5M2zdWsqOHUGKihJJSdkLbOLaawdw+ulCdvYAOnQYxS9/2Y2TTvoJkye34cUXw88pKICMDPjzn+v8WzbG1LUYqrqqduIQkS+A+1V1qYfxxKwxY5xEHzJ6tHMsGITUVDjpJFixAjp2DF+TnQ13361kZQnDhh0gIWE/ixa1paQkNK/HZpyZW3YABzh4sCsivZk8OQgkAtcB17FggXN1v37w6qtw663OM40xxg+RVFX9GXhWRN4QkfZeBRQrAgF49tl9vPVWNv36reOll/bSpMkRzjzzW1asWMWYMbB27VquvvojZs+GQYMm8fDDv+b6669nyZIlAGzYMIUlS1KA3/Pdd8o33yRRUjKOrl1zCARg0aLtjB27noyMM8nP/xWqPyMY7INqIoEAbNniVHuF5OTAxImWNIwx+Fp1Jc5qrBG8QeRG4FFgCvCUqh7yIrBoSk9P16ysrIjeowrNm5dSXBwA1gCrgGTgagYM2MpXX3Xgiy++4+c/70NCwhJatryRpk1TadmyJS+++CIjRoxgxYoVTJw4kVNPPZXWrduTltaZ7t3PpFWrVojYxMLGmCirWJ1Vi+otEVmsqumVnlTVam8406j3BX4L7AbygdsiuYcf29lnn601sX79Ub3//l26a9cu/dOfdivsVihWCGrLlqojRgS1SZOg/vBDjW5vjDHRMXp02aGDzn7oeA0BWVrF39RIJjmcD2wBngU6Ar/GmfRwsIiMq1FKi3FpaY1o1qw1rVu35qmnWqHaCtVU7rxTKCyEuXOFQ4eEf9mMXcYYP9XxqPNIelX9FljuZqKyfi8iebUJQkT+D/gpcBRnXfM7VLWwkus2AAdwFpEKaFXFKI+ddpofTzXGmNgQyXocuZUkjZCrahnH50BfVT0LWA3813Gu/YmqDvAraUBcTSljjGmIPG44j8oAQFVdV8v3z1LVgLu7ALDP9MYYU1Mef7qNxZHjvwFmVnFOgVkislhERh3vJiIySkSyRCRr165dUQ/SGGMaqjobOe4OIDy1klMPq+rH7jUPAwFgUhW3GaaqW0WkLfC5iKxU1bmVXaiq44Bx4HTHrfU3YIwxBqjDxKGqFx/vvIjcDlwNjKyqLUVVt7pfd4rIVGAwUGniMMYY442YqKoSkcuBvwDXqOrBKq5pKiLNQ6+BS4HcuovSGGMMxEjiwFnDvDlO9dP3IvIygIh0EJEM95p2wHwRWQosAmao6qf+hGuMMQ1XTMyOq6pdqzi+FbjSfb0O6F+XcRljjDlWrJQ4Yk4MTX1vjDExJeJJDuNRTSY5NMaYhux4kxxaicMYY0xELHEYY4yJiCUOY4wxEWkQbRwisgvYWMO3t8ZZeyTWWFyRsbgiY3FFpj7GdbqqtqnsRINIHLUhIll+zsRbFYsrMhZXZCyuyDS0uKyqyhhjTEQscRhjjImIJY4Ti9VlcS2uyFhckbG4ItOg4rI2DmOMMRGxEocxxpiIWOIwxhgTEUscVRCRy0VklYj8ICIP+h1PiIi8LiI7RSRm1iIRkU4i8rWI5InIchG51++YAEQkRUQWichSN67H/I6pLBFJFJElIjLd71jKEpENIpLjLnEQM5O8iUhLEflARFa6P2tDYyCmHu6/U2jbLyL3+R0XgIj8wf25zxWRySKSErV7WxvHsUQkEVgNXALkA5nALaq6wtfAABEZARQBb6lqX7/jARCR9kB7Vc12F9taDFzn97+XiAjQVFWLRKQRMB+4V1UX+BlXiIj8EUgHTlLVq/2OJ0RENgDpqhpTA9pE5E1gnqq+JiKNgVRVLfQ5rB+5fze2AENUtaYDjqMVS0ecn/feqnpIRN4DMlR1QjTubyWOyg0GflDVdap6FHgHuNbnmABw11gv8DuOslR1m6pmu68PAHlAR3+jAnUUubuN3C0mPimJyGnAVcBrfscSD0TkJGAEMB5AVY/GUtJwjQTW+p00ykgCmohIEpAKbI3WjS1xVK4jsLnMfj4x8IcwHohIGjAQWOhzKMCP1UHfAzuBz1U1JuICngP+DAR9jqMyCswSkcUiMsrvYFxnALuAN9zqvdfcJaRjyc3AZL+DAFDVLcDTwCZgG7BPVWdF6/6WOConlRyLiU+qsUxEmgEfAvep6n6/4wFQ1VJVHQCcBgwWEd+r90TkamCnqi72O5YqDFPVQcAVwO/c6lG/JQGDgJdUdSBQDMRS22Nj4Brgfb9jARCRk3FqSboAHYCmIvLLaN3fEkfl8oFOZfZPI4rFvPrIbUP4EJikqlP8jqcit1pjNnC5v5EAMAy4xm1LeAe4SEQm+htSmLtkM6q6E5iKU3Xrt3wgv0yJ8QOcRBIrrgCyVXWH34G4LgbWq+ouVS0BpgDnRevmljgqlwl0E5Eu7ieJm4FpPscUs9xG6PFAnqo+43c8ISLSRkRauq+b4PwyrfQ1KEBV/0tVT1PVNJyfra9UNWqfBmtDRJq6HRxwq4IuBXzvwaeq24HNItLDPTQS8L2zShm3ECPVVK5NwLkikur+fo7EaXuMiqRo3ag+UdWAiNwDfAYkAq+r6nKfwwJARCYDFwKtRSQfGK2q4/2NimHAbUCO254A8JCqZvgXEgDtgTfd3i4JwHuqGlNdX2NQO2Cq87eGJOBtVf3U35B+9Htgkvthbh1wh8/xACAiqTg9MO/2O5YQVV0oIh8A2UAAWEIUpx+x7rjGGGMiYlVVxhhjImKJwxhjTEQscRhjjImIJQ5jjDERscRhjDEmIpY4jDHGRMQShzFVEJFWZabL3i4iW8rsNxaRbz167mkiclMlx9NE5FCZsTKVvbeJG99REWntRXzG2ABAY6qgqnuAAQAiMgYoUtWny1wStSkcKhgJ9AbereTcWnfurUqp6iFggDudiTGesBKHMTUkIkVuKWClO1trrohMEpGLReQbEVkjIoPLXP9Ld2Gp70XkFXdEe8V7DgeeAf7Nva7LcZ7fVERmuAtV5VZWSjHGC5Y4jKm9rsDzwFlAT+BWYDjwAPAQgIj0Am7CmXl2AFAK/KLijVR1Ps5cadeq6gBVXX+c514ObFXV/u6iXrEyNYip56yqypjaW6+qOQAishz4UlVVRHKANPeakcDZQKY7D1QTnDVCKtMDWFWN5+YAT4vIWGC6qs6r+bdgTPVZ4jCm9o6UeR0ssx8k/DsmwJuq+l/Hu5GItMJZdKfkRA9V1dUicjZwJfA/IjJLVR+POHpjImRVVcbUjS9x2i3aAojIKSJyeiXXdaGaa7+ISAfgoKpOxFntLZbWpzD1mJU4jKkDqrpCRP6KsyRrAlAC/A6ouD71Spwp83OBUap6vC6//YD/E5Gge7//8CB0Y45h06obEyfc9dynuw3hJ7p2A5Cuqru9jss0PFZVZUz8KAVaVGcAINAIp43FmKizEocxxpiIWInDGGNMRCxxGGOMiYglDmOMMRGxxGGMMSYiljiMMcZExBKHMcaYiFjiMMYYExFLHMYYYyLy/wFBySYNl3uSNQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Compute the inputs to the estimator\n", + "Y = np.vstack([xd[0:2] + W_lon, xd[0:2] + W_lat])\n", + "U = np.vstack([Y, ud]) # add input to the Kalman filter\n", + "# U = np.vstack([Y, ud * 0]) # with no input information\n", + "X0 = np.hstack([xd[:, 0], P0.reshape(-1)])\n", + "\n", + "# Run the estimator on the trajectory\n", + "estim_resp = ct.input_output_response(estim, T, U, X0)\n", + "\n", + "# Run a prediction to see what happens next\n", + "T_predict = np.arange(T[-1], T[-1] + 4 + Ts, Ts)\n", + "U_predict = np.outer(U[:, -1], np.ones_like(T_predict))\n", + "predict_resp = ct.input_output_response(\n", + " estim, T_predict, U_predict, estim_resp.states[:, -1],\n", + " params={'correct': False})\n", + "\n", + "# Plot the estimated trajectory versus the actual trajectory\n", + "plt.subplot(2, 1, 1)\n", + "plt.errorbar(\n", + " estim_resp.time, estim_resp.outputs[0], \n", + " estim_resp.states[estim.find_state('P[0,0]')], fmt='b-', **ebarstyle)\n", + "plt.errorbar(\n", + " predict_resp.time, predict_resp.outputs[0], \n", + " predict_resp.states[estim.find_state('P[0,0]')], fmt='r-', **ebarstyle)\n", + "plt.plot(T, xd[0], 'k--')\n", + "plt.ylabel(\"$x$ position [m]\")\n", + "\n", + "plt.subplot(2, 1, 2)\n", + "plt.errorbar(\n", + " estim_resp.time, estim_resp.outputs[1], \n", + " estim_resp.states[estim.find_state('P[1,1]')], fmt='b-', **ebarstyle)\n", + "plt.errorbar(\n", + " predict_resp.time, predict_resp.outputs[1], \n", + " predict_resp.states[estim.find_state('P[1,1]')], fmt='r-', **ebarstyle)\n", + "# lims = plt.axis(); plt.axis([lims[0], lims[1], -5, 5])\n", + "plt.plot(T, xd[1], 'k--');\n", + "plt.ylabel(\"$y$ position [m]\")\n", + "plt.xlabel(\"Time $t$ [s]\");" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "44f69f79", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the estimated errors\n", + "plt.subplot(2, 1, 1)\n", + "plt.errorbar(\n", + " estim_resp.time, estim_resp.outputs[0] - xd[0], \n", + " estim_resp.states[estim.find_state('P[0,0]')], fmt='b-', **ebarstyle)\n", + "plt.errorbar(\n", + " predict_resp.time, predict_resp.outputs[0] - (xd[0] + xd[0, -1]), \n", + " predict_resp.states[estim.find_state('P[0,0]')], fmt='r-', **ebarstyle)\n", + "lims = plt.axis(); plt.axis([lims[0], lims[1], -0.2, 0.2])\n", + "# lims = plt.axis(); plt.axis([lims[0], lims[1], -2, 0.2])\n", + "\n", + "plt.subplot(2, 1, 2)\n", + "plt.errorbar(\n", + " estim_resp.time, estim_resp.outputs[1] - xd[1], \n", + " estim_resp.states[estim.find_state('P[1,1]')], fmt='b-', **ebarstyle)\n", + "plt.errorbar(\n", + " predict_resp.time, predict_resp.outputs[1] - xd[1, -1], \n", + " predict_resp.states[estim.find_state('P[1,1]')], fmt='r-', **ebarstyle)\n", + "lims = plt.axis(); plt.axis([lims[0], lims[1], -0.2, 0.2]);" + ] + }, + { + "cell_type": "markdown", + "id": "6f6c1b6f", + "metadata": {}, + "source": [ + "## Things to try\n", + "* Remove the input (and update P0)\n", + "* Change the sampling rate" + ] + }, + { + "cell_type": "markdown", + "id": "8f680b92", + "metadata": {}, + "source": [ + "## Predictor-corrector form" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fa488d51", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "#\n", + "# Predictor-corrector calculations\n", + "#\n", + "# Instead of using create_lqe_iosystem, we can also compute out the estimate\n", + "# in a more manual fashion, done here using the predictor-corrector form.\n", + "\n", + "# System matrices\n", + "A, B, F = discsys.A, discsys.B, discsys.B\n", + "\n", + "# Create an array to store the results\n", + "xhat = np.zeros((discsys.nstates, T.size))\n", + "P = np.zeros((discsys.nstates, discsys.nstates, T.size))\n", + "\n", + "# Update the estimates at each time\n", + "for i, t in enumerate(T):\n", + " # Prediction step\n", + " if i == 0:\n", + " # Use the initial condition\n", + " xkkm1 = xd[:, 0]\n", + " Pkkm1 = P0\n", + " else:\n", + " xkkm1 = A @ xkk + B @ ud[:, i-1]\n", + " Pkkm1 = A @ Pkk @ A.T + F @ Rv @ F.T\n", + " \n", + " # Correction step\n", + " L = Pkkm1 @ C.T @ np.linalg.inv(Rw + C @ Pkkm1 @ C.T)\n", + " xkk = xkkm1 - L @ (C @ xkkm1 - Y[:, i])\n", + " Pkk = Pkkm1 - L @ C @ Pkkm1\n", + "\n", + " # Save the state estimate and covariance for later plotting\n", + " # xhat[:, i], P[:, :, i] = xkk, Pkk\n", + " xhat[:, i], P[:, :, i] = xkkm1, Pkkm1 # For comparison to Kalman form\n", + " \n", + "plt.subplot(2, 1, 1)\n", + "plt.errorbar(T, xhat[0], P[0, 0], fmt='b-', **ebarstyle)\n", + "plt.plot(T, xd[0], 'k--')\n", + "plt.ylabel(\"$x$ position [m]\")\n", + "\n", + "plt.subplot(2, 1, 2)\n", + "plt.errorbar(T, xhat[1], P[1, 1], fmt='b-', **ebarstyle)\n", + "plt.plot(T, xd[1], 'k--')\n", + "plt.ylabel(\"$x$ position [m]\");" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4eda4729", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the estimated errors (and compare to Kalman form)\n", + "plt.subplot(2, 1, 1)\n", + "plt.errorbar(T, xhat[0] - xd[0], P[0, 0], fmt='b-', **ebarstyle)\n", + "plt.plot(estim_resp.time, estim_resp.outputs[0] - xd[0], 'r--')\n", + "lims = plt.axis(); plt.axis([lims[0], lims[1], -0.2, 0.2])\n", + "\n", + "plt.subplot(2, 1, 2)\n", + "plt.errorbar(T, xhat[1] - xd[1], P[1, 1], fmt='b-', **ebarstyle)\n", + "plt.plot(estim_resp.time, estim_resp.outputs[1] - xd[1], 'r--')\n", + "lims = plt.axis(); plt.axis([lims[0], lims[1], -0.2, 0.2]);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bfe8aec", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/pvtol-outputfbk.ipynb b/examples/pvtol-outputfbk.ipynb new file mode 100644 index 000000000..e025e4f5d --- /dev/null +++ b/examples/pvtol-outputfbk.ipynb @@ -0,0 +1,478 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c017196f", + "metadata": {}, + "source": [ + "# PVTOL LQR + EQF example\n", + "RMM, 14 Feb 2022\n", + "\n", + "This notebook illustrates the implementation of an extended Kalman filter and the use of the estimated state for LQR feedback." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "544525ab", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.patches as patches\n", + "import control as ct" + ] + }, + { + "cell_type": "markdown", + "id": "859834cf", + "metadata": {}, + "source": [ + "## System definition\n", + "The dynamics of the system\n", + "with disturbances on the $x$ and $y$ variables is given by\n", + "$$\n", + " \\begin{aligned}\n", + " m \\ddot x &= F_1 \\cos\\theta - F_2 \\sin\\theta - c \\dot x + d_x, \\\\\n", + " m \\ddot y &= F_1 \\sin\\theta + F_2 \\cos\\theta - c \\dot y - m g + d_y, \\\\\n", + " J \\ddot \\theta &= r F_1.\n", + " \\end{aligned}\n", + "$$\n", + "The measured values of the system are the position and orientation,\n", + "with added noise $n_x$, $n_y$, and $n_\\theta$:\n", + "$$\n", + " \\vec y = \\begin{bmatrix} x \\\\ y \\\\ \\theta \\end{bmatrix} + \n", + " \\begin{bmatrix} n_x \\\\ n_y \\\\ n_z \\end{bmatrix}.\n", + "$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ffafed74", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object: pvtol\n", + "Inputs (2): F1, F2, \n", + "Outputs (6): x0, x1, x2, x3, x4, x5, \n", + "States (6): x0, x1, x2, x3, x4, x5, \n", + "\n", + "Object: noisy_pvtol\n", + "Inputs (7): F1, F2, Dx, Dy, Nx, Ny, Nth, \n", + "Outputs (6): x0, x1, x2, x3, x4, x5, \n", + "States (6): x0, x1, x2, x3, x4, x5, \n" + ] + } + ], + "source": [ + "# pvtol = nominal system (no disturbances or noise)\n", + "# noisy_pvtol = pvtol w/ process disturbances and sensor noise\n", + "from pvtol import pvtol, noisy_pvtol, plot_results\n", + "\n", + "# Find the equiblirum point corresponding to the origin\n", + "xe, ue = ct.find_eqpt(\n", + " pvtol, np.zeros(pvtol.nstates),\n", + " np.zeros(pvtol.ninputs), [0, 0, 0, 0, 0, 0],\n", + " iu=range(2, pvtol.ninputs), iy=[0, 1])\n", + "\n", + "x0, u0 = ct.find_eqpt(\n", + " pvtol, np.zeros(pvtol.nstates),\n", + " np.zeros(pvtol.ninputs), np.array([2, 1, 0, 0, 0, 0]),\n", + " iu=range(2, pvtol.ninputs), iy=[0, 1])\n", + "\n", + "# Extract the linearization for use in LQR design\n", + "pvtol_lin = pvtol.linearize(xe, ue)\n", + "A, B = pvtol_lin.A, pvtol_lin.B\n", + "\n", + "print(pvtol, \"\\n\")\n", + "print(noisy_pvtol)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1e1ee7c9", + "metadata": {}, + "outputs": [], + "source": [ + "# Disturbance and noise intensities\n", + "Qv = np.diag([1e-2, 1e-2])\n", + "Qw = np.array([[2e-4, 0, 1e-5], [0, 2e-4, 1e-5], [1e-5, 1e-5, 1e-4]])\n", + "Qwinv = np.linalg.inv(Qw)\n", + "\n", + "# Initial state covariance\n", + "P0 = np.eye(pvtol.nstates)" + ] + }, + { + "cell_type": "markdown", + "id": "e4c52c73", + "metadata": {}, + "source": [ + "## Control system design" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3647bf15", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object: sys[1]\n", + "Inputs (5): x0, x1, x2, F1, F2, \n", + "Outputs (6): xh0, xh1, xh2, xh3, xh4, xh5, \n", + "States (42): x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12], x[13], x[14], x[15], x[16], x[17], x[18], x[19], x[20], x[21], x[22], x[23], x[24], x[25], x[26], x[27], x[28], x[29], x[30], x[31], x[32], x[33], x[34], x[35], x[36], x[37], x[38], x[39], x[40], x[41], \n" + ] + } + ], + "source": [ + "# Define the disturbance input and measured output matrices\n", + "F = np.array([[0, 0], [0, 0], [0, 0], [1, 0], [0, 1], [0, 0]])\n", + "C = np.eye(3, 6)\n", + "\n", + "# Estimator update law\n", + "def estimator_update(t, x, u, params):\n", + " # Extract the states of the estimator\n", + " xhat = x[0:pvtol.nstates]\n", + " P = x[pvtol.nstates:].reshape(pvtol.nstates, pvtol.nstates)\n", + "\n", + " # Extract the inputs to the estimator\n", + " y = u[0:3] # just grab the first three outputs\n", + " u = u[3:5] # get the inputs that were applied as well\n", + "\n", + " # Compute the linearization at the current state\n", + " A = pvtol.A(xhat, u) # A matrix depends on current state\n", + " # A = pvtol.A(xe, ue) # Fixed A matrix (for testing/comparison)\n", + " \n", + " # Compute the optimal again\n", + " L = P @ C.T @ Qwinv\n", + "\n", + " # Update the state estimate\n", + " xhatdot = pvtol.updfcn(t, xhat, u, params) - L @ (C @ xhat - y)\n", + "\n", + " # Update the covariance\n", + " Pdot = A @ P + P @ A.T - P @ C.T @ Qwinv @ C @ P + F @ Qv @ F.T\n", + "\n", + " # Return the derivative\n", + " return np.hstack([xhatdot, Pdot.reshape(-1)])\n", + "\n", + "estimator = ct.NonlinearIOSystem(\n", + " estimator_update, lambda t, x, u, params: x[0:pvtol.nstates],\n", + " states=pvtol.nstates + pvtol.nstates**2,\n", + " inputs= noisy_pvtol.state_labels[0:3] \\\n", + " + noisy_pvtol.input_labels[0:pvtol.ninputs],\n", + " outputs=[f'xh{i}' for i in range(pvtol.nstates)],\n", + ")\n", + "print(estimator)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9787db61", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object: control\n", + "Inputs (14): xd[0], xd[1], xd[2], xd[3], xd[4], xd[5], ud[0], ud[1], xh0, xh1, xh2, xh3, xh4, xh5, \n", + "Outputs (2): F1, F2, \n", + "States (0): \n", + "\n", + "A = []\n", + "\n", + "B = []\n", + "\n", + "C = []\n", + "\n", + "D = [[-3.16227766e+00 -1.31948924e-07 8.67680175e+00 -2.35855555e+00\n", + " -6.98881806e-08 1.91220852e+00 1.00000000e+00 0.00000000e+00\n", + " 3.16227766e+00 1.31948924e-07 -8.67680175e+00 2.35855555e+00\n", + " 6.98881806e-08 -1.91220852e+00]\n", + " [-1.31948923e-06 3.16227766e+00 -2.32324805e-07 -2.36396241e-06\n", + " 4.97998224e+00 7.90913288e-08 0.00000000e+00 1.00000000e+00\n", + " 1.31948923e-06 -3.16227766e+00 2.32324805e-07 2.36396241e-06\n", + " -4.97998224e+00 -7.90913288e-08]]\n", + " \n", + "\n", + "Object: xh5\n", + "Inputs (13): xd[0], xd[1], xd[2], xd[3], xd[4], xd[5], ud[0], ud[1], Dx, Dy, Nx, Ny, Nth, \n", + "Outputs (14): x0, x1, x2, x3, x4, x5, F1, F2, xh0, xh1, xh2, xh3, xh4, xh5, \n", + "States (48): noisy_pvtol_x0, noisy_pvtol_x1, noisy_pvtol_x2, noisy_pvtol_x3, noisy_pvtol_x4, noisy_pvtol_x5, sys[1]_x[0], sys[1]_x[1], sys[1]_x[2], sys[1]_x[3], sys[1]_x[4], sys[1]_x[5], sys[1]_x[6], sys[1]_x[7], sys[1]_x[8], sys[1]_x[9], sys[1]_x[10], sys[1]_x[11], sys[1]_x[12], sys[1]_x[13], sys[1]_x[14], sys[1]_x[15], sys[1]_x[16], sys[1]_x[17], sys[1]_x[18], sys[1]_x[19], sys[1]_x[20], sys[1]_x[21], sys[1]_x[22], sys[1]_x[23], sys[1]_x[24], sys[1]_x[25], sys[1]_x[26], sys[1]_x[27], sys[1]_x[28], sys[1]_x[29], sys[1]_x[30], sys[1]_x[31], sys[1]_x[32], sys[1]_x[33], sys[1]_x[34], sys[1]_x[35], sys[1]_x[36], sys[1]_x[37], sys[1]_x[38], sys[1]_x[39], sys[1]_x[40], sys[1]_x[41], \n" + ] + } + ], + "source": [ + "#\n", + "# LQR design w/ physically motivated weighting\n", + "#\n", + "# Shoot for 1 cm error in x, 10 cm error in y. Try to keep the angle\n", + "# less than 5 degrees in making the adjustments. Penalize side forces\n", + "# due to loss in efficiency.\n", + "#\n", + "\n", + "Qx = np.diag([100, 10, (180/np.pi) / 5, 0, 0, 0])\n", + "Qu = np.diag([10, 1])\n", + "K, _, _ = ct.lqr(A, B, Qx, Qu)\n", + "\n", + "#\n", + "# Control system construction: combine LQR w/ EKF\n", + "#\n", + "# Use the linearization around the origin to design the optimal gains\n", + "# to see how they compare to the final value of P for the EKF\n", + "#\n", + "\n", + "# Construct the state feedback controller with estimated state as input\n", + "statefbk, _ = ct.create_statefbk_iosystem(pvtol, K, estimator=estimator)\n", + "print(statefbk, \"\\n\")\n", + "\n", + "# Reconstruct the control system with the noisy version of the process\n", + "# Create a closed loop system around the controller\n", + "clsys = ct.interconnect(\n", + " [noisy_pvtol, statefbk, estimator],\n", + " inplist = statefbk.input_labels[0:pvtol.ninputs + pvtol.nstates] + \\\n", + " noisy_pvtol.input_labels[pvtol.ninputs:],\n", + " inputs = statefbk.input_labels[0:pvtol.ninputs + pvtol.nstates] + \\\n", + " noisy_pvtol.input_labels[pvtol.ninputs:],\n", + " outlist = pvtol.output_labels + statefbk.output_labels + estimator.output_labels,\n", + " outputs = pvtol.output_labels + statefbk.output_labels + estimator.output_labels\n", + ")\n", + "print(clsys)" + ] + }, + { + "cell_type": "markdown", + "id": "7bf558a0", + "metadata": {}, + "source": [ + "## Simulations" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c2583a0e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Create the time vector for the simulation\n", + "Tf = 10\n", + "T = np.linspace(0, Tf, 1000)\n", + "\n", + "# Create representative process disturbance and sensor noise vectors\n", + "np.random.seed(117) # avoid figures changing from run to run\n", + "V = ct.white_noise(T, Qv) # smaller disturbances and noise then design\n", + "W = ct.white_noise(T, Qw)\n", + "plt.plot(T, V[0], label=\"V[0]\")\n", + "plt.plot(T, W[0], label=\"W[0]\")\n", + "plt.legend();" + ] + }, + { + "cell_type": "markdown", + "id": "4d944709", + "metadata": {}, + "source": [ + "### LQR with EKF" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ad7a9750", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Put together the input for the system\n", + "U = np.vstack([\n", + " np.outer(xe, np.ones_like(T)), # xd\n", + " np.outer(ue, np.ones_like(T)), # ud\n", + " V, W # disturbances and noise\n", + "])\n", + "X0 = np.hstack([x0, np.zeros(pvtol.nstates), P0.reshape(-1)])\n", + "\n", + "# Initial condition response\n", + "resp = ct.input_output_response(clsys, T, U, X0)\n", + "\n", + "# Plot the response\n", + "plot_results(T, resp.states, resp.outputs[pvtol.nstates:])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c5f24119", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Response of the first two states, including internal estimates\n", + "plt.figure()\n", + "h1, = plt.plot(resp.time, resp.outputs[0], 'b-', linewidth=0.75)\n", + "h2, = plt.plot(resp.time, resp.outputs[1], 'r-', linewidth=0.75)\n", + "\n", + "# Add on the internal estimator states\n", + "xh0 = clsys.find_output('xh0')\n", + "xh1 = clsys.find_output('xh1')\n", + "h3, = plt.plot(resp.time, resp.outputs[xh0], 'k--')\n", + "h4, = plt.plot(resp.time, resp.outputs[xh1], 'k--')\n", + "\n", + "plt.plot([0, 10], [0, 0], 'k--', linewidth=0.5)\n", + "plt.ylabel(\"Position $x$, $y$ [m]\")\n", + "plt.xlabel(\"Time $t$ [s]\")\n", + "plt.legend(\n", + " [h1, h2, h3, h4], ['$x$', '$y$', '$\\hat{x}$', '$\\hat{y}$'], \n", + " loc='upper right', frameon=False, ncol=2)" + ] + }, + { + "cell_type": "markdown", + "id": "0c0d5c99", + "metadata": {}, + "source": [ + "### Full state feedback" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3b6a1f1c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Compute the full state feedback solution\n", + "lqr_ctrl, _ = ct.create_statefbk_iosystem(pvtol, K)\n", + "\n", + "lqr_clsys = ct.interconnect(\n", + " [noisy_pvtol, lqr_ctrl],\n", + " inplist = lqr_ctrl.input_labels[0:pvtol.ninputs + pvtol.nstates] + \\\n", + " noisy_pvtol.input_labels[pvtol.ninputs:],\n", + " inputs = lqr_ctrl.input_labels[0:pvtol.ninputs + pvtol.nstates] + \\\n", + " noisy_pvtol.input_labels[pvtol.ninputs:],\n", + " outlist = pvtol.output_labels + lqr_ctrl.output_labels,\n", + " outputs = pvtol.output_labels + lqr_ctrl.output_labels\n", + ")\n", + "\n", + "# Put together the input for the system\n", + "U = np.vstack([\n", + " np.outer(xe, np.ones_like(T)), # xd\n", + " np.outer(ue, np.ones_like(T)), # ud\n", + " V, W * 0 # disturbances and noise\n", + "])\n", + "\n", + "# Run a simulation with full state feedback\n", + "lqr_resp = ct.input_output_response(lqr_clsys, T, U, x0)\n", + "\n", + "# Compare the results\n", + "plt.plot(resp.states[0], resp.states[1], 'b-', label=\"Extended KF\")\n", + "plt.plot(lqr_resp.states[0], lqr_resp.states[1], 'r-', label=\"Full state\")\n", + "\n", + "plt.xlabel('$x$ [m]')\n", + "plt.ylabel('$y$ [m]')\n", + "plt.axis('equal')\n", + "plt.legend(frameon=False);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc86067c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/pvtol.py b/examples/pvtol.py new file mode 100644 index 000000000..277d0faa1 --- /dev/null +++ b/examples/pvtol.py @@ -0,0 +1,315 @@ +# pvtol.py - (planar) vertical takeoff and landing system model +# RMM, 19 Jan 2022 +# +# This file contains a model and utility function for a (planar) +# vertical takeoff and landing system, as described in FBS2e and OBC. +# This system is approximately differentially flat and the flat system +# mappings are included. +# + +import numpy as np +import matplotlib.pyplot as plt +import control as ct +import control.flatsys as fs +from math import sin, cos +from warnings import warn + +# PVTOL dynamics +def pvtol_update(t, x, u, params): + # Get the parameter values + m = params.get('m', 4.) # mass of aircraft + J = params.get('J', 0.0475) # inertia around pitch axis + r = params.get('r', 0.25) # distance to center of force + g = params.get('g', 9.8) # gravitational constant + c = params.get('c', 0.05) # damping factor (estimated) + + # Get the inputs and states + x, y, theta, xdot, ydot, thetadot = x + F1, F2 = u + + # Constrain the inputs + F2 = np.clip(F2, 0, 1.5 * m * g) + F1 = np.clip(F1, -0.1 * F2, 0.1 * F2) + + # Dynamics + xddot = (F1 * cos(theta) - F2 * sin(theta) - c * xdot) / m + yddot = (F1 * sin(theta) + F2 * cos(theta) - m * g - c * ydot) / m + thddot = (r * F1) / J + + return np.array([xdot, ydot, thetadot, xddot, yddot, thddot]) + +def pvtol_output(t, x, u, params): + return x + +# PVTOL flat system mappings +def pvtol_flat_forward(states, inputs, params={}): + # Get the parameter values + m = params.get('m', 4.) # mass of aircraft + J = params.get('J', 0.0475) # inertia around pitch axis + r = params.get('r', 0.25) # distance to center of force + g = params.get('g', 9.8) # gravitational constant + c = params.get('c', 0.05) # damping factor (estimated) + + # Make sure that c is zero + if c != 0: + warn("System is only approximately flat (c != 0)") + + # Create a list of arrays to store the flat output and its derivatives + zflag = [np.zeros(5), np.zeros(5)] + + # Store states and inputs in variables to make things easier to read + x, y, theta, xdot, ydot, thdot = states + F1, F2 = inputs + + # Use equations of motion for higher derivates + x1ddot = (F1 * cos(theta) - F2 * sin(theta)) / m + x2ddot = (F1 * sin(theta) + F2 * cos(theta) - m * g) / m + thddot = (r * F1) / J + + # Flat output is a point above the vertical axis + zflag[0][0] = x - (J / (m * r)) * sin(theta) + zflag[1][0] = y + (J / (m * r)) * cos(theta) + + zflag[0][1] = xdot - (J / (m * r)) * cos(theta) * thdot + zflag[1][1] = ydot - (J / (m * r)) * sin(theta) * thdot + + zflag[0][2] = (F1 * cos(theta) - F2 * sin(theta)) / m \ + + (J / (m * r)) * sin(theta) * thdot**2 \ + - (J / (m * r)) * cos(theta) * thddot + zflag[1][2] = (F1 * sin(theta) + F2 * cos(theta) - m * g) / m \ + - (J / (m * r)) * cos(theta) * thdot**2 \ + - (J / (m * r)) * sin(theta) * thddot + + # For the third derivative, assume F1, F2 are constant (also thddot) + zflag[0][3] = (-F1 * sin(theta) - F2 * cos(theta)) * (thdot / m) \ + + (J / (m * r)) * cos(theta) * thdot**3 \ + + 3 * (J / (m * r)) * sin(theta) * thdot * thddot + zflag[1][3] = (F1 * cos(theta) - F2 * sin(theta)) * (thdot / m) \ + + (J / (m * r)) * sin(theta) * thdot**3 \ + - 3 * (J / (m * r)) * cos(theta) * thdot * thddot + + # For the fourth derivative, assume F1, F2 are constant (also thddot) + zflag[0][4] = (-F1 * sin(theta) - F2 * cos(theta)) * (thddot / m) \ + + (-F1 * cos(theta) + F2 * sin(theta)) * (thdot**2 / m) \ + + 6 * (J / (m * r)) * cos(theta) * thdot**2 * thddot \ + + 3 * (J / (m * r)) * sin(theta) * thddot**2 \ + - (J / (m * r)) * sin(theta) * thdot**4 + zflag[1][4] = (F1 * cos(theta) - F2 * sin(theta)) * (thddot / m) \ + + (-F1 * sin(theta) - F2 * cos(theta)) * (thdot**2 / m) \ + - 6 * (J / (m * r)) * sin(theta) * thdot**2 * thddot \ + - 3 * (J / (m * r)) * cos(theta) * thddot**2 \ + + (J / (m * r)) * cos(theta) * thdot**4 + + return zflag + +def pvtol_flat_reverse(zflag, params={}): + # Get the parameter values + m = params.get('m', 4.) # mass of aircraft + J = params.get('J', 0.0475) # inertia around pitch axis + r = params.get('r', 0.25) # distance to center of force + g = params.get('g', 9.8) # gravitational constant + c = params.get('c', 0.05) # damping factor (estimated) + + # Create a vector to store the state and inputs + x = np.zeros(6) + u = np.zeros(2) + + # Given the flat variables, solve for the state + theta = np.arctan2(-zflag[0][2], zflag[1][2] + g) + x = zflag[0][0] + (J / (m * r)) * sin(theta) + y = zflag[1][0] - (J / (m * r)) * cos(theta) + + # Solve for thdot using next derivative + thdot = (zflag[0][3] * cos(theta) + zflag[1][3] * sin(theta)) \ + / (zflag[0][2] * sin(theta) - (zflag[1][2] + g) * cos(theta)) + + # xdot and ydot can be found from first derivative of flat outputs + xdot = zflag[0][1] + (J / (m * r)) * cos(theta) * thdot + ydot = zflag[1][1] + (J / (m * r)) * sin(theta) * thdot + + # Solve for the input forces + F2 = m * ((zflag[1][2] + g) * cos(theta) - zflag[0][2] * sin(theta) + + (J / (m * r)) * thdot**2) + F1 = (J / r) * \ + (zflag[0][4] * cos(theta) + zflag[1][4] * sin(theta) +# - 2 * (zflag[0][3] * sin(theta) - zflag[1][3] * cos(theta)) * thdot \ + - 2 * zflag[0][3] * sin(theta) * thdot \ + + 2 * zflag[1][3] * cos(theta) * thdot \ +# - (zflag[0][2] * cos(theta) +# + (zflag[1][2] + g) * sin(theta)) * thdot**2) \ + - zflag[0][2] * cos(theta) * thdot**2 + - (zflag[1][2] + g) * sin(theta) * thdot**2) \ + / (zflag[0][2] * sin(theta) - (zflag[1][2] + g) * cos(theta)) + + return np.array([x, y, theta, xdot, ydot, thdot]), np.array([F1, F2]) + +pvtol = fs.FlatSystem( + pvtol_flat_forward, pvtol_flat_reverse, name='pvtol', + updfcn=pvtol_update, outfcn=pvtol_output, + states = [f'x{i}' for i in range(6)], + inputs = ['F1', 'F2'], + outputs = [f'x{i}' for i in range(6)], + params = { + 'm': 4., # mass of aircraft + 'J': 0.0475, # inertia around pitch axis + 'r': 0.25, # distance to center of force + 'g': 9.8, # gravitational constant + 'c': 0.05, # damping factor (estimated) + } +) + +# +# PVTOL dynamics with wind +# + +def windy_update(t, x, u, params): + # Get the input vector + F1, F2, d = u + + # Get the system response from the original dynamics + xdot, ydot, thetadot, xddot, yddot, thddot = \ + pvtol_update(t, x, u[0:2], params) + + # Now add the wind term + m = params.get('m', 4.) # mass of aircraft + xddot += d / m + + return np.array([xdot, ydot, thetadot, xddot, yddot, thddot]) + +windy_pvtol = ct.NonlinearIOSystem( + windy_update, pvtol_output, name="windy_pvtol", + states = [f'x{i}' for i in range(6)], + inputs = ['F1', 'F2', 'd'], + outputs = [f'x{i}' for i in range(6)] +) + +# +# PVTOL dynamics with noise and disturbances +# + +def noisy_update(t, x, u, params): + # Get the inputs + F1, F2, Dx, Dy, Nx, Ny, Nth = u + + # Get the system response from the original dynamics + xdot, ydot, thetadot, xddot, yddot, thddot = \ + pvtol_update(t, x, u[0:2], params) + + # Get the parameter values we need + m = params.get('m', 4.) # mass of aircraft + J = params.get('J', 0.0475) # inertia around pitch axis + + # Now add the disturbances + xddot += Dx / m + yddot += Dy / m + + return np.array([xdot, ydot, thetadot, xddot, yddot, thddot]) + +def noisy_output(t, x, u, params): + F1, F2, dx, Dy, Nx, Ny, Nth = u + return x + np.array([Nx, Ny, Nth, 0, 0, 0]) + +noisy_pvtol = ct.NonlinearIOSystem( + noisy_update, noisy_output, name="noisy_pvtol", + states = [f'x{i}' for i in range(6)], + inputs = ['F1', 'F2'] + ['Dx', 'Dy'] + ['Nx', 'Ny', 'Nth'], + outputs = pvtol.state_labels +) + +# Add the linearitizations to the dynamics as additional methods +def noisy_pvtol_A(x, u, params={}): + # Get the parameter values we need + m = params.get('m', 4.) # mass of aircraft + J = params.get('J', 0.0475) # inertia around pitch axis + c = params.get('c', 0.05) # damping factor (estimated) + + # Get the angle and compute sine and cosine + theta = x[[2]] + cth, sth = cos(theta), sin(theta) + + # Return the linearized dynamics matrix + return np.array([ + [0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, (-u[0] * sth - u[1] * cth)/m, -c/m, 0, 0], + [0, 0, ( u[0] * cth - u[1] * sth)/m, 0, -c/m, 0], + [0, 0, 0, 0, 0, 0] + ]) +pvtol.A = noisy_pvtol_A + +# Plot the trajectory in xy coordinates +def plot_results(t, x, u): + # Set the size of the figure + plt.figure(figsize=(10, 6)) + + # Top plot: xy trajectory + plt.subplot(2, 1, 1) + plt.plot(x[0], x[1]) + plt.xlabel('x [m]') + plt.ylabel('y [m]') + plt.axis('equal') + + # Time traces of the state and input + plt.subplot(2, 4, 5) + plt.plot(t, x[1]) + plt.xlabel('Time t [sec]') + plt.ylabel('y [m]') + + plt.subplot(2, 4, 6) + plt.plot(t, x[2]) + plt.xlabel('Time t [sec]') + plt.ylabel('theta [rad]') + + plt.subplot(2, 4, 7) + plt.plot(t, u[0]) + plt.xlabel('Time t [sec]') + plt.ylabel('$F_1$ [N]') + + plt.subplot(2, 4, 8) + plt.plot(t, u[1]) + plt.xlabel('Time t [sec]') + plt.ylabel('$F_2$ [N]') + plt.tight_layout() + +# +# Additional functions for testing +# + +# Check flatness calculations +def _pvtol_check_flat(test_points=None, verbose=False): + if test_points is None: + # If no test points, use internal set + mg = 9.8 * 4 + test_points = [ + ([0, 0, 0, 0, 0, 0], [0, mg]), + ([1, 0, 0, 0, 0, 0], [0, mg]), + ([0, 1, 0, 0, 0, 0], [0, mg]), + ([1, 1, 0.1, 0, 0, 0], [0, mg]), + ([0, 0, 0.1, 0, 0, 0], [0, mg]), + ([0, 0, 0, 1, 0, 0], [0, mg]), + ([0, 0, 0, 0, 1, 0], [0, mg]), + ([0, 0, 0, 0, 0, 0.1], [0, mg]), + ([0, 0, 0, 1, 1, 0.1], [0, mg]), + ([0, 0, 0, 0, 0, 0], [1, mg]), + ([0, 0, 0, 0, 0, 0], [0, mg + 1]), + ([0, 0, 0, 0, 0, 0.1], [1, mg]), + ([0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, mg + 1]), + ] + elif isinstance(test_points, tuple): + # If we only got one test point, convert to a list + test_points = [test_points] + + for x, u in test_points: + x, u = np.array(x), np.array(u) + flag = pvtol_flat_forward(x, u) + xc, uc = pvtol_flat_reverse(flag) + print(f'({x}, {u}): ', end='') + if verbose: + print(f'\n flag: {flag}') + print(f' check: ({xc}, {uc}): ', end='') + if np.allclose(x, xc) and np.allclose(u, uc): + print("OK") + else: + print("ERR") + diff --git a/examples/vehicle.py b/examples/vehicle.py new file mode 100644 index 000000000..b316ceced --- /dev/null +++ b/examples/vehicle.py @@ -0,0 +1,111 @@ +# vehicle.py - planar vehicle model (with flatness) +# RMM, 16 Jan 2022 + +import numpy as np +import matplotlib.pyplot as plt +import control as ct +import control.flatsys as fs + +# +# Vehicle dynamics +# + +# Function to take states, inputs and return the flat flag +def _vehicle_flat_forward(x, u, params={}): + # Get the parameter values + b = params.get('wheelbase', 3.) + + # Create a list of arrays to store the flat output and its derivatives + zflag = [np.zeros(3), np.zeros(3)] + + # Flat output is the x, y position of the rear wheels + zflag[0][0] = x[0] + zflag[1][0] = x[1] + + # First derivatives of the flat output + zflag[0][1] = u[0] * np.cos(x[2]) # dx/dt + zflag[1][1] = u[0] * np.sin(x[2]) # dy/dt + + # First derivative of the angle + thdot = (u[0]/b) * np.tan(u[1]) + + # Second derivatives of the flat output (setting vdot = 0) + zflag[0][2] = -u[0] * thdot * np.sin(x[2]) + zflag[1][2] = u[0] * thdot * np.cos(x[2]) + + return zflag + +# Function to take the flat flag and return states, inputs +def _vehicle_flat_reverse(zflag, params={}): + # Get the parameter values + b = params.get('wheelbase', 3.) + dir = params.get('dir', 'f') + + # Create a vector to store the state and inputs + x = np.zeros(3) + u = np.zeros(2) + + # Given the flat variables, solve for the state + x[0] = zflag[0][0] # x position + x[1] = zflag[1][0] # y position + if dir == 'f': + x[2] = np.arctan2(zflag[1][1], zflag[0][1]) # tan(theta) = ydot/xdot + elif dir == 'r': + # Angle is flipped by 180 degrees (since v < 0) + x[2] = np.arctan2(-zflag[1][1], -zflag[0][1]) + else: + raise ValueError("unknown direction:", dir) + + # And next solve for the inputs + u[0] = zflag[0][1] * np.cos(x[2]) + zflag[1][1] * np.sin(x[2]) + thdot_v = zflag[1][2] * np.cos(x[2]) - zflag[0][2] * np.sin(x[2]) + u[1] = np.arctan2(thdot_v, u[0]**2 / b) + + return x, u + +# Function to compute the RHS of the system dynamics +def _vehicle_update(t, x, u, params): + b = params.get('wheelbase', 3.) # get parameter values + dx = np.array([ + np.cos(x[2]) * u[0], + np.sin(x[2]) * u[0], + (u[0]/b) * np.tan(u[1]) + ]) + return dx + +def _vehicle_output(t, x, u, params): + return x # return x, y, theta (full state) + +# Create differentially flat input/output system +vehicle = fs.FlatSystem( + _vehicle_flat_forward, _vehicle_flat_reverse, name="vehicle", + updfcn=_vehicle_update, outfcn=_vehicle_output, + inputs=('v', 'delta'), outputs=('x', 'y', 'theta'), + states=('x', 'y', 'theta')) + +# +# Utility function to plot lane change manuever +# + +def plot_lanechange(t, y, u, figure=None, yf=None): + # Plot the xy trajectory + plt.subplot(3, 1, 1, label='xy') + plt.plot(y[0], y[1]) + plt.xlabel("x [m]") + plt.ylabel("y [m]") + if yf: + plt.plot(yf[0], yf[1], 'ro') + + # Plot the inputs as a function of time + plt.subplot(3, 1, 2, label='v') + plt.plot(t, u[0]) + plt.xlabel("t [sec]") + plt.ylabel("velocity [m/s]") + + plt.subplot(3, 1, 3, label='delta') + plt.plot(t, u[1]) + plt.xlabel("t [sec]") + plt.ylabel("steering [rad/s]") + + plt.suptitle("Lane change manuever") + plt.tight_layout() From a211dad6857b8eef2bb521914a96cb3980679a78 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 16 Mar 2022 22:58:34 -0700 Subject: [PATCH 41/87] allow legacy matrix representation --- control/stochsys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/stochsys.py b/control/stochsys.py index e99e4e87e..961d429c3 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -455,8 +455,8 @@ def _estim_output(t, x, u, params): # Define the estimator system return NonlinearIOSystem( _estim_update, _estim_output, states=state_labels + covariance_labels, - inputs=sensor_labels + sys.input_labels, - outputs=output_labels, dt=sys.dt) + inputs=sensor_labels + sys.input_labels, outputs=output_labels, + dt=sys.dt) def white_noise(T, Q, dt=0): From d3e738710a197f81adfd5b85aa1332773ff2adb0 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 17 Mar 2022 21:59:14 -0700 Subject: [PATCH 42/87] additional stochsys documentation, unit tests --- control/sisotool.py | 21 +++---- control/stochsys.py | 77 ++++++++++++++++++------- control/tests/stochsys_test.py | 101 ++++++++++++++++++++++++++++----- doc/control.rst | 14 ++++- 4 files changed, 168 insertions(+), 45 deletions(-) diff --git a/control/sisotool.py b/control/sisotool.py index b47eb7e40..41f21ecbe 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -215,14 +215,14 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', derivative terms are given instead by Kp, Ki*dt/2*(z+1)/(z-1), and Kd/dt*(z-1)/z, respectively. - ------> C_ff ------ d - | | | - r | e V V u y - ------->O---> C_f --->O--->O---> plant ---> - ^- ^- | - | | | - | ----- C_b <-------| - --------------------------------- + ------> C_ff ------ d + | | | + r | e V V u y + ------->O---> C_f --->O--->O---> plant ---> + ^- ^- | + | | | + | ----- C_b <-------| + --------------------------------- It is also possible to move the derivative term into the feedback path `C_b` using `derivative_in_feedback_path=True`. This may be desired to @@ -234,8 +234,8 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', Remark: It may be helpful to zoom in using the magnifying glass on the plot. Just ake sure to deactivate magnification mode when you are done by - clicking the magnifying glass. Otherwise you will not be able to be able to choose - a gain on the root locus plot. + clicking the magnifying glass. Otherwise you will not be able to be able + to choose a gain on the root locus plot. Parameters ---------- @@ -269,6 +269,7 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', ---------- closedloop : class:`StateSpace` system The closed-loop system using initial gains. + """ plant = _convert_to_statespace(plant) diff --git a/control/stochsys.py b/control/stochsys.py index 961d429c3..9d590983d 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -31,7 +31,7 @@ # contributed by Sawyer B. Fuller -def lqe(*args, **keywords): +def lqe(*args, **kwargs): """lqe(A, G, C, QN, RN, [, NN]) Linear quadratic estimator design (Kalman filter) for continuous-time @@ -126,18 +126,20 @@ def lqe(*args, **keywords): # Process the arguments and figure out what inputs we received # + # If we were passed a discrete time system as the first arg, use dlqe() + if isinstance(args[0], LTI) and isdtime(args[0], strict=True): + # Call dlqe + return dlqe(*args, **kwargs) + # Get the method to use (if specified as a keyword) - method = keywords.get('method', None) + method = kwargs.pop('method', None) + if kwargs: + raise TypeError("unrecognized kwargs: ", str(kwargs)) # Get the system description if (len(args) < 3): raise ControlArgument("not enough input arguments") - # If we were passed a discrete time system as the first arg, use dlqe() - if isinstance(args[0], LTI) and isdtime(args[0], strict=True): - # Call dlqe - return dlqe(*args, **keywords) - # If we were passed a state space system, use that to get system matrices if isinstance(args[0], StateSpace): A = np.array(args[0].A, ndmin=2, dtype=float) @@ -179,7 +181,7 @@ def lqe(*args, **keywords): # contributed by Sawyer B. Fuller -def dlqe(*args, **keywords): +def dlqe(*args, **kwargs): """dlqe(A, G, C, QN, RN, [, N]) Linear quadratic estimator design (Kalman filter) for discrete-time @@ -251,7 +253,9 @@ def dlqe(*args, **keywords): # # Get the method to use (if specified as a keyword) - method = keywords.get('method', None) + method = kwargs.pop('method', None) + if kwargs: + raise TypeError("unrecognized kwargs: ", str(kwargs)) # Get the system description if (len(args) < 3): @@ -340,14 +344,14 @@ def create_estimator_iosystem( If the system has all full states output, define the measured values to be used by the estimator. Otherwise, use the system output as the measured values. - {state, covariance, output}_labels : str or list of str, optional + {state, covariance, sensor, output}_labels : str or list of str, optional Set the name of the signals to use for the internal state, covariance, - and output (state estimate). If a single string is specified, it - should be a format string using the variable ``i`` as an index (or - ``i`` and ``j`` for covariance). Otherwise, a list of strings - matching the size of the respective signal should be used. Default is - ``'xhat[{i}]'`` for state and output labels and ``'P[{i},{j}]'`` for - covariance labels. + sensors, and outputs (state estimate). If a single string is + specified, it should be a format string using the variable ``i`` as an + index (or ``i`` and ``j`` for covariance). Otherwise, a list of + strings matching the size of the respective signal should be used. + Default is ``'xhat[{i}]'`` for state and output labels, ``'y[{i}]'`` + for output labels and ``'P[{i},{j}]'`` for covariance labels. Returns ------- @@ -478,6 +482,10 @@ def white_noise(T, Q, dt=0): sample time). """ + # Convert input arguments to arrays + T = np.atleast_1d(T) + Q = np.atleast_2d(Q) + # Check the shape of the input arguments if len(T.shape) != 1: raise ValueError("Time vector T must be 1D") @@ -502,7 +510,37 @@ def white_noise(T, Q, dt=0): # Return a linear combination of the noise sources return sp.linalg.sqrtm(Q) @ W -def correlation(T, X, Y=None, dt=0, squeeze=True): +def correlation(T, X, Y=None, squeeze=True): + """Compute the correlation of time signals. + + For a time series X(t) (and optionally Y(t)), the correlation() function + computes the correlation matrix E(X'(t+tau) X(t)) or the cross-correlation + matrix E(X'(t+tau) Y(t)]: + + tau, Rtau = correlation(T, X[, Y]) + + The signal X (and Y, if present) represent a continuous time signal + sampled at times T. The return value provides the correlation Rtau + between X(t+tau) and X(t) at a set of time offets tau. + + Parameters + ---------- + T : 1D array_like + Sample times for the signal(s). + X : 1D or 2D array_like + Values of the signal at each time in T. The signal can either be + scalar or vector values. + Y : 1D or 2D array_like, optional + If present, the signal with which to compute the correlation. + Defaults to X. + squeeze : bool, optional + If True, squeeze Rtau to remove extra dimensions (useful if the + signals are scalars). + + Returns + ------- + + """ T = np.atleast_1d(T) X = np.atleast_2d(X) Y = np.atleast_2d(Y) if Y is not None else X @@ -516,10 +554,7 @@ def correlation(T, X, Y=None, dt=0, squeeze=True): raise ValueError("Signals X and Y must have same length as T") # Figure out the time increment - if dt != 0: - raise NotImplementedError("Discrete time systems not yet supported") - else: - dt = T[1] - T[0] + dt = T[1] - T[0] # Make sure data points are equally spaced if not np.allclose(np.diff(T), T[1] - T[0]): diff --git a/control/tests/stochsys_test.py b/control/tests/stochsys_test.py index a8319fd2d..11084d9db 100644 --- a/control/tests/stochsys_test.py +++ b/control/tests/stochsys_test.py @@ -13,18 +13,18 @@ def check_LQE(L, P, poles, G, QN, RN): P_expected = asmatarrayout(np.sqrt(G @ QN @ G @ RN)) L_expected = asmatarrayout(P_expected / RN) poles_expected = -np.squeeze(np.asarray(L_expected)) - np.testing.assert_array_almost_equal(P, P_expected) - np.testing.assert_array_almost_equal(L, L_expected) - np.testing.assert_array_almost_equal(poles, poles_expected) + np.testing.assert_almost_equal(P, P_expected) + np.testing.assert_almost_equal(L, L_expected) + np.testing.assert_almost_equal(poles, poles_expected) # Utility function to check discrete LQE solutions def check_DLQE(L, P, poles, G, QN, RN): P_expected = asmatarrayout(G.dot(QN).dot(G)) L_expected = asmatarrayout(0) poles_expected = -np.squeeze(np.asarray(L_expected)) - np.testing.assert_array_almost_equal(P, P_expected) - np.testing.assert_array_almost_equal(L, L_expected) - np.testing.assert_array_almost_equal(poles, poles_expected) + np.testing.assert_almost_equal(P, P_expected) + np.testing.assert_almost_equal(L, L_expected) + np.testing.assert_almost_equal(poles, poles_expected) @pytest.mark.parametrize("method", [None, 'slycot', 'scipy']) def test_LQE(matarrayin, method): @@ -51,9 +51,9 @@ def test_lqe_call_format(cdlqe): # Call with system instead of matricees L, P, E = cdlqe(sys, Q, R) - np.testing.assert_array_almost_equal(Lref, L) - np.testing.assert_array_almost_equal(Pref, P) - np.testing.assert_array_almost_equal(Eref, E) + np.testing.assert_almost_equal(Lref, L) + np.testing.assert_almost_equal(Pref, P) + np.testing.assert_almost_equal(Eref, E) # Make sure we get an error if we specify N with pytest.raises(ct.ControlNotImplemented): @@ -156,10 +156,10 @@ def test_estimator_iosys(): # Check to make sure everything matches cls = clsys.linearize(0, 0) nstates = sys.nstates - np.testing.assert_array_almost_equal(cls.A[:2*nstates, :2*nstates], A_clchk) - np.testing.assert_array_almost_equal(cls.B[:2*nstates, :], B_clchk) - np.testing.assert_array_almost_equal(cls.C[:, :2*nstates], C_clchk) - np.testing.assert_array_almost_equal(cls.D, D_clchk) + np.testing.assert_almost_equal(cls.A[:2*nstates, :2*nstates], A_clchk) + np.testing.assert_almost_equal(cls.B[:2*nstates, :], B_clchk) + np.testing.assert_almost_equal(cls.C[:, :2*nstates], C_clchk) + np.testing.assert_almost_equal(cls.D, D_clchk) def test_estimator_errors(): @@ -185,3 +185,78 @@ def test_estimator_errors(): sys_fs.C = np.eye(4) C = np.eye(1, 4) estim = ct.create_estimator_iosystem(sys_fs, QN, RN, C=C) + + +def test_white_noise(): + # Scalar white noise signal + T = np.linspace(0, 1000, 1000) + R = 0.5 + V = ct.white_noise(T, R) + assert abs(np.mean(V)) < 0.1 # can occassionally fail + assert abs(np.cov(V) - 0.5) < 0.1 # can occassionally fail + + # Vector white noise signal + R = [[0.5, 0], [0, 0.1]] + V = ct.white_noise(T, R) + assert abs(np.mean(V)) < 0.1 # can occassionally fail + assert np.all(abs(np.cov(V) - R) < 0.1) # can occassionally fail + + # Make sure time scaling works properly + T = T / 10 + V = ct.white_noise(T, R) + assert abs(np.mean(V)) < np.sqrt(10) # can occassionally fail + assert np.all(abs(np.cov(V) - R) < 10) # can occassionally fail + + # Make sure discrete time works properly + V = ct.white_noise(T, R, dt=T[1] - T[0]) + assert abs(np.mean(V)) < 0.1 # can occassionally fail + assert np.all(abs(np.cov(V) - R) < 0.1) # can occassionally fail + + # Test error conditions + with pytest.raises(ValueError, match="T must be 1D"): + V = ct.white_noise(R, R) + + with pytest.raises(ValueError, match="Q must be square"): + R = np.outer(np.eye(2, 3), np.ones_like(T)) + V = ct.white_noise(T, R) + + with pytest.raises(ValueError, match="Time values must be equally"): + T = np.logspace(0, 2, 100) + R = [[0.5, 0], [0, 0.1]] + V = ct.white_noise(T, R) + + +def test_correlation(): + # Create an uncorrelated random sigmal + T = np.linspace(0, 1000, 1000) + R = 0.5 + V = ct.white_noise(T, R) + + # Compute the correlation + tau, Rtau = ct.correlation(T, V) + + # Make sure the correlation makes sense + zero_index = np.where(tau == 0) + np.testing.assert_almost_equal(Rtau[zero_index], np.cov(V), decimal=2) + for i, t in enumerate(tau): + if i == zero_index: + continue + assert abs(Rtau[i]) < 0.01 + + # Try passing a second argument + tau, Rneg = ct.correlation(T, V, -V) + np.testing.assert_equal(Rtau, -Rneg) + + # Test error conditions + with pytest.raises(ValueError, match="Time vector T must be 1D"): + tau, Rtau = ct.correlation(V, V) + + with pytest.raises(ValueError, match="X and Y must be 2D"): + tau, Rtau = ct.correlation(T, np.zeros((3, T.size, 2))) + + with pytest.raises(ValueError, match="X and Y must have same length as T"): + tau, Rtau = ct.correlation(T, V[:, 0:-1]) + + with pytest.raises(ValueError, match="Time values must be equally"): + T = np.logspace(0, 2, T.size) + tau, Rtau = ct.correlation(T, V) diff --git a/doc/control.rst b/doc/control.rst index 8bd6f7a32..20f363a1e 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -108,10 +108,11 @@ Control system synthesis :toctree: generated/ acker + create_statefbk_iosystem + dlqr h2syn hinfsyn lqr - lqe mixsyn place rootlocus_pid_designer @@ -143,6 +144,17 @@ Nonlinear system support tf2io flatsys.point_to_point +Stochastic system support +========================= +.. autosummary:: + :toctree: generated/ + + correlation + create_estimator_iosystem + dlqe + lqe + white_noise + .. _utility-and-conversions: Utility functions and conversions From bd1e8e07bdea4b0c93f2ced987ae6c9e3f309b92 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 17 Mar 2022 22:19:00 -0700 Subject: [PATCH 43/87] replace correlation_lags with calculation + new example --- control/stochsys.py | 4 +- examples/stochresp.ipynb | 268 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 examples/stochresp.ipynb diff --git a/control/stochsys.py b/control/stochsys.py index 9d590983d..02f19ebbf 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -565,6 +565,8 @@ def correlation(T, X, Y=None, squeeze=True): [[sp.signal.correlate(X[i], Y[j]) for i in range(X.shape[0])] for j in range(Y.shape[0])] ) * dt / (T[-1] - T[0]) - tau = sp.signal.correlation_lags(len(X[0]), len(Y[0])) * dt + # From scipy.signal.correlation_lags (for use with older versions) + # tau = sp.signal.correlation_lags(len(X[0]), len(Y[0])) * dt + tau = np.arange(-len(Y[0]) + 1, len(X[0])) * dt return tau, R.squeeze() if squeeze else R diff --git a/examples/stochresp.ipynb b/examples/stochresp.ipynb new file mode 100644 index 000000000..c16a6a5e7 --- /dev/null +++ b/examples/stochresp.ipynb @@ -0,0 +1,268 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "03aa22e7", + "metadata": {}, + "source": [ + "# Stochastic Response\n", + "Richard M. Murray, 6 Feb 2022\n", + "\n", + "This notebook illustrates the implementation of random processes and stochastic response. We focus on a system of the form\n", + "$$\n", + " \\dot X = A X + F V \\qquad X \\in {\\mathbb R}^n\n", + "$$\n", + "\n", + "where $V$ is a white noise process and the system is a first order linear system." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "902af902", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import scipy as sp\n", + "import matplotlib.pyplot as plt\n", + "import control as ct\n", + "from math import sqrt, exp" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "60192a8c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYkAAAEGCAYAAACQO2mwAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAABKy0lEQVR4nO2deZwUxdnHf8/swX1fci8glyggrHigaFQUwSuJd0xMfBPUSEzUHCSaaGJM1HglamLUEI9o4hWPiIqoqBHwWJBbUIQlrNz3uXe9f8z0TE93VXf1NT2783w/H11mprvq6e7qeup56qmnSAgBhmEYhpGRiFsAhmEYJn9hJcEwDMMoYSXBMAzDKGElwTAMwyhhJcEwDMMoKY5bgDDp2rWrKCsri1sMhmGYJsWCBQu2CSG6yX5rVkqirKwMFRUVcYvBMAzTpCCidarf2N3EMAzDKGElwTAMwyhhJcEwDMMoYSXBMAzDKGElwTAMwyhhJcEwDMMoYSXBMAzDKGElwTBMXnOgth7/XlgF3tYgHprVYjqGYZofv355BZ6uWI8+nVpj3IDOcYtTcLAlwTBMXrNpTzUAYH9tfcySFCasJBiGyWvYyRQvrCQYhmEYJawkGIZhGCWsJBiGaRJQ3AIUKKwkGIZhGCV5oSSIaAYRbSGiZabvbiaiL4loUeq/yXHKyDAMU4jkhZIA8CiASZLv7xFCjE7992qOZWIYhil48kJJCCHeA7AjbjkYhsk/eKV1vOSFknBgGhEtSbmjOskOIKKpRFRBRBVbt27NtXwMwzDNmnxWEn8BMAjAaAAbAdwlO0gI8ZAQolwIUd6tm3Qfb4ZhGMYneaskhBCbhRANQohGAA8DGBe3TAzDxAcRB8HGQd4qCSLqafr4VQDLVMcyDMMw0ZAXWWCJ6J8ATgLQlYiqANwE4CQiGo1k6pZKAFfEJR/DMEyhkhdKQghxseTrv+VcEIZhGCaLvHU3MQzDMPHDSoIpCIbc+BqmPl4RtxhMAHjaOh5YSTAFQW19I95YsTluMRimycFKgmEYhlHCSoJhmLymkLNybN1bgwMxb9vKSoJhGCZPOerWN3H2/XNjlYGVBMMweU2hL7RevWVfrPWzkmAYJq8pZHdTPsBKgmGYJkGhWxRxwUqCYRiGUcJKgmEYhlHCSoJhmLxGgCcl4oSVBMMwTQLixByxwEqCYZgmAVsU8cBKgmEYhlHCSoJp0uw+UIffvrICtfWNcYvCRIwXd9NrSzdi0fpd0QlTQLCSCJGa+gbs2F8btxgFxe2zVuKR99fi5cUb4haFiQg/i+muenIhzn0g3nQWzQVWEiFy5RMLMOaW2XGLUVDUNyQtiIZGtiQYJgpYSYTInFVbY6t72lMLcf0zi2OrPy4SqWW4jTynyTCRwEqimfDKko14fmFV3GLkHCNVQyMn+Gn2cFqOeGAlwTRxkj0H64jmDz/jeGAlwTRpEqnRZdT9x4V/nY+Jd78bWnll02fix88WnnuQaXqwkmCaNIYLQkQ8zPxw7Q58HnJe/+cWFJ57MAjsbooHVhJMk4Y8uptmLd8UoTRMFBSqm2nznuq4RQDASoJp4iQ8Tlxf8cSCCKUpHHbur0Ujh5RFytf/Mi9uEQDkiZIgohlEtIWIlpm+60xEs4no89TfTnHKyOQnxCGwOWfH/locects3DV7VU7qK1Q3U9XOg3GLACBPlASARwFMsnw3HcBbQojBAN5KfW4SBPWP/3thFT6u3BGSNP65c9Yq3P76yrjFcMRYJxH1nASTYdu+GgDAG8s3R1L+/C+24x8frEt/5kcbL8VxCwAAQoj3iKjM8vU5AE5K/fsxAO8A+FnupPKPEMFGP9elFsVV3jYlJIn8cf+c1QCAn00aFqscTmQmruOVo1C4e/ZneCDVLhIRDfEvfvgDAMClx/TP+r5ADYrYyRdLQkYPIcRGAEj97S47iIimElEFEVVs3Rrfimcz3F/lDqPj4DTSueFPb32OhpRvr1DdQEGp3LYfI2+ehfU7DsQtihb5rCS0EEI8JIQoF0KUd+vWLW5xALDrI5ckEryYLgo+37wX+2vqHY8h1hK+eHbBeuyprsdLi76MWxQt8llJbCaingCQ+rslZnm04f4qdxjdFE9ch4cQAhPveQ/ffazC8TgjsmzH/lq88El0az7YSoyXfFYSLwO4LPXvywC8FKMsnuA8QjkkveKa73lYGM13/prt2Ftdh+q6BulxxpzE1U8uxLVPL47efRKx4bJh10GUTZ+J15fxWhozeaEkiOifAOYDGEpEVUT0fwBuAzCRiD4HMDH1uUnAOiJ3ZKKbYhakGWG+lUfc/AbOuu996XGGJbEpteirriHidO0RP+NlX+4GwCvhreRLdNPFip9OyakgDABgX009dh+si1sMLdKL6djfFBrWOTVlOpI8DT/2K4/5rMfmVeKkod3Qv0ubcIRqwuSFJdHcyLN3xjPn/WUext/2tu37B+asRtn0memNfvKBdFqOEMt8dO5azP9ie6AyPq7cgc837w1Jotyiey+tyRXzZSI76Hihpr4BN728HOc/OD8cgZo4rCQioKn7x1dukndu97+djI+vjmE/6e8/uQDnP2hPU5CIYJ3Ezf9ZkY7V98v5D87HxHveC0mi3KJ7L62uvvxQEcHnBI3z91Y7R3cVCqwkfPDE/Eosqdql/L0pWBJCiLQP1iuH3zQrZGnceXXpJnxcudP+QzotR+amb9pdjbLpM/FMxfpcides0L1vCYtWiNqQ0H2twgocyRPDKHZYSfjgly8tx9n3qzdZbwI6As8uqMKZ972PN1fop1bIx5dGtp/EF1uTPvQXP9GPQ9++rwb/2940FjdFzY0vLnM/CBn3UhDLeWnVbtS6WKZe+3y/OqIpDO7igJVEBOTbRJ6MTzfuAQBUbt8fsyTByKQKz9xzP7f/uNvexoQ/zPElQ219o2+rrCljdfWRR4fTuu37cdb97+OWV1ZoHa/7XDkEPVxYSURA1E10/hfb8WxAV4oRDVRs8hmcds+7GPGr1wOVm2tkuZuMka0Xy6fG4zzLlj1Jl9Zbn27GZTM+wpn3vd9k0iyEhW1OwqOluWN/LQA4um7N6FosQSeuWcdkw0oiAkTE87oXP/wBfvLcEulvB2sb8L/tB1KLgjYqy2hIvQlFJiXx2eZ92F8rXziVrzjtJ+F1ZOuFpSnL4dF5lZi/JhkJFUXY8JKqXXh07trQyw2DoAn+0komDGFMhDVxnYfe1TQrNuzBn976PCd1sZKIgDijmyb98b202+TKfyxUjtKMKNZ8CVv0i2w/iVyMBI06GkwVR3Erz75/Lm7+j547RoUQAq8t3Rj6Yreg1+s1g6/2cb4vU3iqJ07Ovv993D37MyxavyvyulhJaLL7QB0enbtWa74hzka2zjL5qppgFxJLoimS7mgkijlK/WeMNs3POkrLJQhvr9yCq55ciPtCHnla9/LwPWHsMqjyWmxQS8Lt7PqGRtQ3NOIbj3wQ+j3VpT41ODn3AXUATVjkxYrrpsANLy7FK0s24rBeHVyPbQIDkfQIuKiJWxIJkoQ35QCR/pv/T3t7yvf/5a5w90y23nqv98Lr/uS5CoE1lJ7Kyj7xD+9g54FaHKhtwNzV2/GDUwYHqi/fYUtCE2NhjVv6ZKBpRDcZcxKJAJbErTNXoGz6zLBE8kUmC6wpuikH9RrVNYVsIFENA6wT116bfVQbRkU9cf3lroM40MTm7oLASkKTkqLkrarV8Os2gX4jHd0UxNv08H/1J1Qrt+13nEj3Sxw7023cfTCdGbUp5YwK2+pJWFx9Ud8J3cFX0EFaxbrkos2wlevO/bV4fH5lk5jzMMPuJk1aFCeVhM7kX1NoBA0pGXM1JzHxnndR1yBC35LVLUKmctt+HFSkuvbLsb/P5LUyWzBRe+7219SjdWmR52CDqIITrOV67Zy9egr13U2exMiUH/F7++NnF+OtlVswYUh+bI6mC1sSmpQUJVu0lpJoArZExpLIjZKoa4j2nmS5m0z/PunOd3DGH/8bYb3ez/Ez0t24+yBG3DQLM+ZWeq8wIqyL6RoaBd5euVn7+mQLIcMgysV0QSzHnQeSc0M1IQ9aooaVhCZpd5PGoqsmYUkYE9chWBJxzsHI9pMIKyupznX5sST83C4jZcisIBviaNT75a6D2sVZM/A+/N81uPzRCsxariej58eTByuu6z0oiYsf+iBrzo401oVMuGMOfvrcYr/iRQIrCU1Kio05idyGwO6rqcfqLeGnnDZeJC86QnWo9Xq37K3Gpt3hRtKoyIxmZYvp7HjpmHR2KGvwMbL00zzS1URs+HnZlS2R6j2MW1+ZUmRb99Vqna89n5Sj3E06eFFAxiJLg/SjcyjifzsO4JmK/Nr0iOckNClNWRJ1OpZEiO6my2Z8hAXrdjr68l9b6n1COKMkQrAkLJ/H3foWAChlfmJ+JTq0LsXZo3rh7tmfoThBOH3EIdi0pxonevTXyhbTOd1+L1e7eY9d0VmVkble3XUSQSyvQE9L42RPgwbFnIROEX+YtRKPzVuXPE/zfdFPy+Hv/trOslzI4vW7MLCb/02InNb05DNsSWhSGuHE9bIvd+Mv73wh/W1BKtLCiaueXOitQsTrbvrlS8txzT8/AQD86a3Pcffsz3D6ve/hshkfea7bGmFjRqb/vLigZMdaDQe3azfyE2Wdoy2B+ZwQOhaNIrwMGqzHGm1Kp4gH5nyBfalwct3ms7Rqj23DKyEEfvXSsqwEi2EFnJkvY9mXu3HOA3Nx9+zPApQXzVa7877Yhg/WBNskywlWEpoYE9dacxIeyz7zvvdx++srfUjlH8NrFmSdhEHYUaANjQI19erJvfqGRlz00HzMW73NtJ+EXtlBr9Y6SnVLyzHmltm27/x0En6T6AHertlLc0hY/CfpNuVRSN3bcc+bn+EuSye960AdHp+/Dt945MP0d1HMSRju0+Vf7vFfSETrPi95+ENc9FCwTbKcYCWhiad1EhE00rDLFAHdTVmpuU3NPoz8QN997GMMvVGdjXbbvlp8sGYHrn1mkbQDdBp1Wy/34ffW4G/v66/3sCkJ88S1dineaQghGk2nBXmxtOyL6fTdTVlyeWjb1pTsxanB2+6DdTjtnnc9l6eLUWJDgLKt82cvLtrQJDbG4jkJTYz0FTqhnFFMnAkhH0X6fSH8TLha5ZH9uz6EUNc5q7YGksdAOnFt+fbWVz9Vlim/3+71uuHHdWR0Tjp9+AufVIFAOPfI3trnGDgpob3V2Vluresc/Coy1d0QQtiUlm0exPTvzzYnN5uKYn2j8Z7puH9VWNve6i378NPnlmDVpr340amD0a5lSSAZo4ItCU3innRSv0j+yvOjJMwvqFKeHN8fz7UFHO5bLQmvro2XFn3paCWp8DIYuPbpxfjR04s81wE4u5u+ZZkzUs1JeDclFF+7XPLC/+3EyJvfsH0fdABkYG7vukVu2VON+9/+XB5tp3A3/e39tbhndjyJAnVgJaHglSUb0ttgAqYYZ43GEoUloeqM/FaVyWLqMxJEsROc9WWadO97gTdIchHE+IdUHite+i/Zsdbr86okbnvN39xTY8qLN3f1dlRU7vBVhtOzPljbgD3VdY5WwCf/25X12TjSKNctYu65BVVYu82+E6J6wGH8lR/xxnL71rtCiNCUhFwaZ374r0W4843PsEwyd+EU8lvfGPEmNAFgJaFg2lOf4JS73rV973WBlResZetsyem3LuNF8vs6mc9TrXYGgJWb9mZtkPTZ5vDXfCTrtf9b5l8PGvFrsyRM77ZO2V4WY6nqfX6htzh6HblOvftdjLz5DU/3R+1ukh//42cXY7Jk9bvqnXJb+yL7vVFEM3Gt+9gO1CYjtmRzl9bFh00FVhIe0Wksvjtey4n3vb3aVK/qRfJXV/o6fJ5vrnf9zgP4PNX5u92f0+55z1+FChmcqtOZk/Bcp+Xd99ohWUM4dTHXoztSVgURLKnahQffzQ65NlZae5lPsIZ06kRgyfJouVkSWXWS8+8NjXJL4sM12z2n1Miqy+HUf5uVduqkJz9Ypy5PUlg+Z2nIeyVBRJVEtJSIFhFRRXxyJP9+rGHq+3bhWD7rjBj9zgEYWUx1orXc6p10738x0ej8s0b00bR841ls2VuDB+astv3uVKtbiKdMZrOLxHlOwr2DtU7sb9x9EGXTZ+Kh976QrqnI1CP/txPVlg7ZOO3s++cq3V5+LC3jnqXTz3uduFZcT9VOe4qQnftrMe2phSibPhNPzLd3xI1C2J7R2ys348KHPsCj8yqlsj9TsR619Y02OXYdqEtvDeA0GLjumUwaDePKN+xWpzdRlXTeX+Ypz4mTvFcSKb4ihBgthCiPW5AlVbtdjzE3gvqGRpx13/t49zN5xM6nGzO+S2sHZX7VwrYkKrcnO74rnljg63wd95cf2fZUu+8TbS53854aZV1+FtNl9/nJY79y5zvpr+xKwrE4G1Z3U0VlMlrmd6+uxJhbZivX4ZhHx7ojYsMV5sV68mRJWA71OxegGuic/+B823eLq3bjlSXJDAMyq6RRCFjHPcZmS+Y5RoOZSzfip88twX1vyyeOp/97aUpGPYx7smarfe4lkx1AXlpFgMipKGkqSiJnrNm6D+u2Sx6whxfN3Aa276/F0i934yfP2pN27amuy8pQam065pGU6v3zqySq66KZKFPNVegii1ax1+HfQnF7ila7wDoat01c6+YDSWGdoLSueH975RbpeVnuJs37aj1O5zSVjti+r0Z5jlGsU2ZhJ6vS/NMrSzak/71tn3oAoKJR2JWVcYtl79CuA3WpuuRW3PINu1FT3+Ahs22SLXvt98vB2+Tapi9+6ANc/ujHWjKETVNYJyEAvEFEAsBfhRAPRVnZyZLJagB4Y4WX7JvJB751bw1WbEhaCrIGWm3Z3craeMyjTuXkXg6nwbInDeXHmOV0m6S9901/KQ7cXjLHF9pFS2wzdYbrtu/HsF9mh6vaggtc5LJiXWdjd3+5W4y6I/agYc5m/vnR/6Tfm7OcpiNgLUXsra6z7b0uo6JyB6Y99YmeoApk7ibrXtxAcr5m8A2voXfHVo7lrdm6Hxc8OB/fGT9Aq34nS9Upusmt7ViTBeaSpqAkxgshNhBRdwCziWilECI9+0lEUwFMBYB+/fpFJoQ1/M+JU+9+D78993A8MGc1NqazoUomq2yf1S0lbEsiKCpZzXIa+fNV3Pumv9jwh/+7xi6PVBxJdJNL2WYXx6rNdvdE2NGV1lG3qnxzx6drobnlUpItVlPN2ehEim1KJUS0XtN3/v6xoyvFuBwdV6MbjZKJ64wlkfnesBDNqdFVbXpx1W7te+7UvtKWhFZJ3tmxvxad25SGXm7eu5uEEBtSf7cAeAHAOMvvDwkhyoUQ5d26BdvxSbWa0s+I7Df/WWFSEPKX39rwDtQ4bEaiUhKeJQsHpSVhkuiv79o78zD4e4CNd9zmJP63w3nEa31mWdaVD3nsSkJeirkNalsSDlZPsi53ebJ/06rW1lG6+dq/3HXQ0Z3lBXMIrHEpskzBXvca0R2M6RQrs3TDeI9vfHFpCKXYyWslQURtiKid8W8ApwFYFlV9X1dEF4SRj8hoGLsP1KUXl1lfUlX9yWNVI/dw1ITXSCTV0eZiohjVqNCdGfC0DkDyndP9dnPByZIWWheXqfr/v76XUbjm5vjy4g3K/UbcJrhl1+KkCOwdq/xg62E6yuWO11cFDk8GkgrUuqgvIZkwDmlRuA2na4hqG1mDg7XR7HiX7+6mHgBeSN3cYgBPCSG85zQISBhKwnhff/LcYryxYjNG9OqA9q2yb/8ayWpUA51OOQheF3mpF0CFIU0wnOL1CcDVTy1Erw4tccOUwxzL0cndlPWb4ik1NAoUFxFuf22V7Tdr7ijVfV29ZZ/0GCPlumzvDsPi+MMse72AXEk4+tSVvziXQUQ5axjCFN1kKCfrNquA95G79iDKScmmy/JYuSaRLDRHnisJIcQaAKPilsPv/sxtSouwP6XdjUZmTIweqK1Hu5b2269qjMoRbEgN44DHUYiqWpnf1y8frd2Bw3q1R9sW/pqpdDEdEWamQijdlIQM63PQ2Y/DeHmrdrpP3upYhrrRTRt3V2NfTX06Ss6+ot9+jrO7iaDT4KwlJAhwawlhDbLN0U3kYEmEZTkD2W1AdhnG3I9T/rcwFEdUajivlUQ+8OSH67T2kJDRssSsJJLfFaf2fKxvtEdhmI/T/t5H0/horX1BoFaHrrEC1fy1LI7dCxf8dT5OHd4dj1x2lOux2fKo74nTgjUdrKM1c6fqtnbESGvtWL5GU9Odk7j4Yec9Bjy7Ki3iqzp2q6IhDeVCZC8fAN5csdlTC28QZndTtpzm22YtU7aYzoyTUjGPE2T3pFEAyUefvUK9qZDXcxL5wA0vLMOv/7NC+fvvX/tUuX2ozE9udBQNjUIRyilHbWEoRVNywV/ti5Q8+zNVHaJJoDDWYny6Mdvf7iUUOOjoVFaVtWPNDr2Vl2OcU5Rwf910HmdYCexkxTh1hvrupuzPehPe8oO++7i3JAvm6KYiJ0vC0jTdshs4RlWT2ZKwX4d1Ij0qHRFVhgO2JAJiRPDYfMIkN28N07S+UWD3QXvIn7oTTLL7QPY5YTSML7buQ43HDl3HglHF1nvB2uFE5XfVxXq/zdNVbmHBxVquKfcLdDrGabLa+ovUklWcS2R/FioxrMfpTEiH527KWBK1DY14fkEVWpQklbPx7NZt348PJda0c7nq39wea1pJpD7L31m9hr1m6z5cL1mYmyxXqwjPsCURIeYX1tpRzF6xCec8MNd2jpuv/8z7s7No6rSL9S5hnZfN+Miza8irW8wvdiXh7ucKSwaV60BLHskxOvMXWlmGHfS5lwAE62jarX6rG0l17OPz12UNEHQsCYW3yTPmOYm6BoHrn12M+V8kF6IZ9+3cB+bip6bMxDo4zQOZ74tTsIOTJaHbZu+a/ZlyzVZUC2tZSURIdlK2bJfD7BX2XPiAe+e7fsdB6fdOmNMzvypxjdU3CD13k4NP1yDsNM3KjW00CCOk0opTgr/Ne6ptu7cBmcGCniXhLoNTh+Xl/sielZOSsUqvEuOdVVvx839nYvZ18kGFaUlY74HRto3r3XlAvmjP6c45BR0UuSgJ2459EY342ZJogpgbq/GvktSchKqhqlAn+HNvGXtTmSwB4PtPLpSW7d2ScHaLhYX1nQt78yWvWEfxZmvx8kcrcLokFbpxiI4lMXf1NncZHJ65l81rZOW8qRi8APbQVt2Rq44CCEuhN0oCQhKJ7MV0HVt73ybUaWGocX1fbN2Huavt6TN05iRCiW5iJdH0yGqsqX8aDVYVMaV68aKMgBXQjEQyRzcpDgnbkiAirdXGWRs0aZbtpmB1FtNZxdmwu9pWbjq6SUNJGBlOAXXEmdO8g5cNzpZvsO+e9uKiDZIjk/idH0poXHdyziO4oki6myz1p9dJCOyprksn9QsLQ+4H3/lC+ntmV1f7BLrBDpcUNum6HH6LYrMlgJVEIMwd1jzJCND8zHQ7Cs8hsCGNQA7W1rsfqFGvKtLLLwTgjtczex/ouFOsIzcVfrJqWq9bJo/1GKNT14luMvPDf30i/X5x1W7s3F8rVXKOHYXlJ+ue1W7oWnVWdOYynB6Vl+CMhkZhc8eZo5veXSVP2R8GqhDntPypn617igBq97MVJ0UalTXNSiKFar8HJ8wrsS955EPb77KkbMUeOwqDa/71iWKiW69pPPOx0z7TznMSv3zRnglFVe+db/jL7KqEgFeXZRSP0pIw/9syUahijo8Ow3nTIbssyWOSf4s8Pvp5EteFwStLNmiF6IYFgezuJs2qrI9Btjg1rJQVjULYLC2zuymKvaStkYt2mZJ/jV/NSQXDFSSaYllJpLjM46gKcN/VLSsENvXXryWxaP0uLF6/S1KHY3Fpfvr8EuVCMiGAAw7upickWzHmahKAgKzwXJ3VxsYRry71kt5dDy0lYQ2TFQIbdh3EnoPerDWn4XUiQdK6dVdjh4F2ZlSLApAt5gwLIdT7Scz/YjuufVoePpo8N9i9Uw0AM5ZtcEXoaHFF9FLyOokA1DmsxCZYVngao0mXVbdeH7RXU1xep31vC9d6PR3tHyLKUqw6A8GoFhUBdqUsdTdZz2kUOOGOOaHKUZwg6TN4fsGXynPWbtsvHWjoQGQPZXW7zXur69CuZYntvEv/Zre6iZJrAIJiXnFtxe9WvW4IADPeX4t9NfJBQKMQeOS/a/BxhMoR4InrvMRrUjzAvyUR1vEyduyvxZ5qb6PcWcvDH6XLIGQrVpW7QEgUchTYJ6WdZQGA5xa471XulQTJLYnbX5fvXQ0AKzbukbosdbGOhN0sCSNTgVYILMgxs4Eusk2HvJzrh73V9fjNKyuUz3nb3lr8duan6f02dNi0W/9YA564zkP85HQq8eqYDhEnK0W2SbwV86v+q5eWBxdIA6JsM15lSQgA+2vq8Zv/rPCcrFBdtyzFgvt51vv8x7f8ba7k1LUWJShLGfnNLxZEHrdbsSeVUUArBDakdRLmLLCZ7/TOjcjQwOQ//df9IAtXPOEtHQkQ48Q1EQ2LqO4mjx8XgltGU68P2svoIWp39YPvykMAg5AgypoQdPK5//W9NZgxdy0enbc2dDkMdO530Pus4y6zKokhN75mS9mig+7iu8fmVWK7ZU7LNYQ49dj0LIlwaGgEGiwjCd3H4bb/Ri6RpewBnJVpnO6mV4loBhFFtzdoAVHiNifh8Ul7OTysxHAqbntN7eoIgtlF57ROwnjJwxpVy+qKyqQ3o+PGlLmbVB2LE7p7pWyUuD/c5oeMdQFBLQkvd7xRCJsl2aCZ6j+Xk/5+eclhHUucIbDDAHwC4F0iupeIgu0RWsAIIVxDRL0+aC/HH3fb2x5Lj5+a+kZs2ZvZ2tKpkzZ0SVgvu5dU7l6PcaImpeScomGKJNFNpcXeXZl+5tUMQl1xHVYIbKNdSTxd4RT+ncGcSqQpElXAhmurEkLUCiHuAzAcQBWAD4noN8a2oowcWZvXeSG9T1zn/+gnCGu37c8K3XVcJ2EsmgrJt+x3HULQUEQjas4t2V4YT74+gCPerTkb74COZeekIlQJ7VQyBd3sKh/w82xjj24SQlQLIe4EcASAagALiejH0YjVPJGttLTh4UELIWJPnZ1rlC4zYbIkQttvQeZucj8v6MuqYwkVJ8iWxdWPK8zvrouA+3UaSmKfTuRciAn+wgpcaGrEvk6CiMqQdD0NBdAPwF4AvwNwZySSNUPqNIa4Xh508iXNjZYomz4zJ/W44aQAjGycQVwoZmSdrs6iy6DzFsbcild3k59ag+3frlfjfo1OO6wEf09XrMfqzcHXW8SNnyYUmyVBREuIaAeAFwF8G0BHAG8DuAxA22jEap44Lb4z8NK/HahrQOU2932T4+D7Jw2KpFyVAhAQpm0qw3lb/BYTtHYdSyIhWUznJzpHy7pV4OpuSnX8xw3q4lpWWCGwM5dsxKrNe90PzHP8WAVReRV0LImvAlgjmrvzO2RkW3fqjHDH3DJbu45pTy3EOxEmLAvCMQO74M+KrJhBcLIkjJF3lO4mHQK7mzTkL5JEN2nnUqLMsXWNjdI9MHRwvT+pjr9r2xbuMvmSgDET2/alQojw3/QCJZhpb8fYcSsf0YmN90NNvdp1kQhdSfg77+2Vehk9VegkKBSwryzWzqWEjLVT19CIG16wJ3DUQff+6Mj1cWW0KSuaGn76+5vPHhG+IOAV1zklyCShDD+bp+SKiHQEvvk3+ZzAtn212JMaEYelJBas2+nrvNVbnH3iUycMdPxdLx06bH4tPwn3FqzbiZcXq2PvHWXQvM86R31c6e9eMxl6dmgZSbmc4C+HBAk3lNGxVSk276lxP7AA+GjtjnR20bgXRblNwropUK1Mt5LINj9X/XmASV43ZWZcJnuqvePnlkVlvbOSQPhuIHU94b4sqk1O8oE4+4WoV5a7cf+c1Y6/u73Mm3dX45S73nU8pqFR4JaZ2QnxdDtjnZ3+dHCzXAyLhXVEbojKemd3E+TbOEZB2Bue5PMaiahitnWIW0m44fYuX/HEAtcy5qzagplLsncBrPDhsglidem6t8JOZaKzV3ghEpUlkfdKgogmEdEqIlpNRNOjqMMtn1JYhG2x5FNCMis8elTj9jLvVexLYEb26Kf7SCuhm9dIVwYZYbeF1iVF4RaYh/hx0RWkkiCiIgAPADgDwGEALiaiw8KupzRH6bvDdjflItmcX/JXsvgJ413evi+cuSjdvEYydOck/IxlendspfytVWnzVxJ+KFR30zgAq4UQa4QQtQD+BeCcsCvxs8fDmSN7ejq+b+dWgRYuychrJZHHssVNGO/yrOXBwmzDwH1OwviX97bg5FJqXQBKIp/ennxXEr0BmIc6Vanv0hDRVCKqIKKKrVv9LSwr8ZE9s11Lb+Gn63cclG7bGIR87ofNorELOZuwMp7GzZKq3VrH+bEknJSEn2y3TQ0/73ZUrSrf77bsurNunxDiISFEuRCivFs3f1nM/cxJ5Oo9L+vSWvlbPlsS5qcke+GvcFkr0JxpJjrClSAhsE73KCrfOyMn35VEFYC+ps99APhb+eOAnzmJXI2OrzxRnQMp7vUATgiI9D3ilzqbQrkfhsXky5JwuEeFcv/yhXxXEh8DGExEA4ioFMBFAF4OuxI/cxK5aqhOiiDkiNpQEQIoTt3XYolGzV/1lk0U4ZaF1MUdqK3HPkm0Vte2pY7nOd33QtARvkLII7oveb2YTghRT0TTAMwCUARghhBiedj1+FESuWqneRzl6ogQSQuttr4RCY8dbXGCQkv3HZSOrUpsezsHxev9aKoQgLG3vImDkk2AenZohW371PfVad5GJ2EgEx75bklACPGqEGKIEGKQEOLWKOrwNyeRmxf9/LF9lL/l85yEQOa+mkeF5f074fHLxzmee+KQ/Nkht0ME+bEKYSRsIFMQgPvks5MevefC0QEk8k+HVrnLlbZzv/fMvB1bOVtnfsl7JZEL/HT4YbzoV500yDVJX0uHhUN5rSSESLubzP7lowd2xoQh3RwnM/OpE+0YQccQ1gY7eY/DZQZZm9S5TTSdoRtTNMPeP77h1MB11XpceDuwW5vIor5YSfgkjDkJma/eC34XcA/q1iZQvToIACUJuyWhR/L4ti1y4w09ZVh35W8dW4ffIRWIt8lRGbp1aPk0UDBwmkw3061d83KHsZLwSRgTmkWJYGPKbT5X3ebC3S9EZv2J7F45GUHG4W1a5GbR1KHd2+Kp7x0t/S0KF0M+doB+8Xstbi7eHOXc9ESh5oxiJeGTMJpLEVEsC6tykwBPpAMCzFaXjlo0jm9dmqO4ClJbhlGs7m1O7qZ2DtaeU9N263Ab8jB0z3o9Pdrnj8UQZYtiJZHin987xtPxYXTuRUXxdBcqJXHysO6Yfe2EUOoQIhM15nUElki1ylY5SuRGIKWSaFEcgZJoPjrCMfOA02W6RRTqRrcdf2hXrePCwOpuuvfCI/GfacfnrH4nohxsspJIMbJPB0/H9+2sTkCmS9A5Cb+oJo0TBAzu0S6UOk4c2i3tUvB7nblK5EaknicIO/fjmH4dm1UIZ7uWakvCqZt3m7jWzXCcS4VrraukiCKvf3jP9tFWoAEriRReN/C5ZFy/wHUWJRKxjCrVC/TCEeYP541E69LitHKQrQtw6gIM8XKVyI2gXrsQ5gitvH8n/OO7R+Oc0b1CKzNuHJWEw0N2UwG6a0lysai1e2oi2no9RQm1BRoW+ZDMkJVEiuKE/q04blCXcNxNBMSx/lY1SAvLsDHuTWZOwtv5xsvoFP4bJuQwJxFmH9CnUyu0Li0GEaFlSfN49ZzmjZ5fWKX8zW1eTNf69NK2Lj3G38DOaAPWwVX7ViWu7eO7xw/wVadXeE4iB3jxm4fVcRQVJRBHggqVKR/WqMi4lUaYo7lDGNO/IwC9LJe5nZNQ/6aLlzZkvv4bpwzXPi/f8Bub75Z3TLctlpd11q7zknH9tY+VyWK8NxOGdMPfLivHoG5tXeX8wSmDfdWZT7CSMPHeT76C2752RM7qS1A86b5VL2hYys8oxxgNGkriyH4dcfKwHgCcc9MYv+VMSYRkSXhSEqm/D146Bt89IdyMuD8+bUio5TnhV0m4ZYbVvZdXnTgIp4/ooXWs3xBW4yxjrNOuZTFOGZ6s063IoO9UPsQ4sJIw0a9L65zueiVEPInuorckUhPWKXeToZS6e1xkVFKcm1eEoH6ZvfQruoutgMwz6N8l/IWN/SIoU0ULv5aEi7tJt0NPJAiHdm+rdaxvJZF6rkY7zgrpzodeHNHKwUoiRvbV1PvKtf+No4NNmqveT1VDK+/fCVN97P9gRLAYIe+6rhvjluQsJTSpJyC9uJvMfvRrJG4G8203QjyjiHDzoqyC4ldJjOzT0fF3T+5fzWfkRdZfTB6WKT9VvEgrCVPdLvda9youVgTC5IMSYiURI3sO1vla/XxYr2BhcaqcT6qO8uiBnVGnWAI7dcJAW0gnpS2J5N9Ne6oBAB+u3a4lnyFdrpQEOdRl7asuLO+b1YGYKTJFyPXs0FKr7igywoaxMvjC8r7uB8FfDqbXfngCrnLYJwXwpjx1m4kX19jUCRn5jLZhWD/mtuLWRnUDXM5S5IXKh/RsrCRiZE91nb9du0zjkwvK1VliVahMfaM9f/DzU2z1qcQ8e1QvDO+ZvbbCeL+NiDGjvp0HMpkt86HxGwgh1OshLC95IqHuGMwjeN3RfBSWhFuREzSy7J6m6efX7XiH9Mi4hIb3bO+qHL0MEHSP9JtU0BDVGCeZRXOdk9CsQ6VMzO7vn5w+1KEeXkyXM3KZJmPcgC5ZLoijyjq5nkOU3UjLunr3P6s6aOPFPEQyCv6hIkpD9jIbDbbUYU7hPIcU6Ll2N9U0NCqfu/VbckilktV5aHb+UeQDclut/K1j+uOPF422fW/+Tvfe665I9xJiDgCj+nbUP1hTVr/tKR3dlGqY5gGAtXO+/5Ij/YgmPa5ti2Lcdf6o9OfJR2RbG7lai8tKQoHTCCkMrb3k5tNw9qheWY7qEb3cV30TsjsuPyNRa4M0ijB/X3nbFFw/MRMlo1o0JXv3jfLOGplcNNZGEgxweG+NayXgMdPeE1FFO9XWNzq4myyWBDm9nCZLQvPN8tp5mjsNFSrXoJlzRvd2/i5kF46X/vmu80dh+iS5Sy9QGm6fr216nYTE3WS9rjNHZi+U1O0rZO3vuolD0L19ZsBmPSJXE+isJBQMDSk9hYr2qZw3eyVbOzphbUyqTsZphPr8VcfheycMSH82Bp6vLt2YdZx5PKoqT2pJpL467tCuqLxtCgZpRp9Ya05QcuFi1NTUN6rXSVi+d8rzZHytWokrO8uLJbHmd5NxwmB7rqJbv3p41uf6BmdLQqdD0XbhRLCHwQmDuyKRIOngQpqGW9N36bcjTc9JGBPXpksOa05JVoz1O/uAJTemBCsJBX7u/9fG2EdnYWN1N6nSifTv0lpZxuG9O+D60+z+zeo6+Qg0WafexG7qjKxPtfXeMno2mtxNuchvVd+gtiSsm0I5WRIE4I7zRmLWj07Q7vxVx8mUYyJB0t7bKrubJaGlJDRfgCCbB6krT/7RnbbSDf7w25KMW9EosyQ0z9Wtw4xVAdkGLOxuihcnLa366e4LRkcjjLluUNZLobIkJg7vgcN7R58cjMg+qW3t97wqiUzh2Z2Vr83hNahvFMpnesm4frjm5EPTnxMJ5zmJC8r74tDu7bQnrlVKQnW6zH1hLaIuhFTwuv1PCw/pRf7706/g1WtOUNdJwA2Th6N7u6SLRTe4QXeHRr/zjcY99xPdpJZF5xiL18AyIMzV/hasJBTkQ3yyDKLs6CSVJVGUIBw3SJ1GWef6dN69BJGt87Y27pqUktBNxy4ki5aipKFRKOsqLkrgW8eVpT87uptMXauuG8JqKR1V1gkXlvdV3ntZ1db73csl/NbJT/7ytPG4fuIQ/bBSTUuCCOjbubVj+Hbvjq3wPR/rcWQ6sVOIe5OnLQnJOgm/K67t981d+VsHhOxuigmy/PWKzI+qS59O7unHibLdCSp3THFRwnkfaY0rNDp/pyN1fKmGkhjocdtUa9Hmy5H55v1S3ygcR2XZI0cHd5Ppe7+WxLNXHofbzxuZvtZ7LsyeqJaVau0sjJQRfhjZpyN+cMpg7QnXMOck/IZFy9r5Xy4di49usIZy+yMT3ZT6bHpm7ovpCPOmn2z73nrfzM1g4mFGyo/ssq2KxfxzlPvds5JQ4FVLP/F/ySicd37yFbx5nfeNe0qLE7h8/ADX4wiUrSSKEtLV0KH68h3uhdPEtcHUCcnrsvr3VVgX0z1wyRjbZki3nhtejq2GBrW7CcjuXBIO6aHN3wadk8iU6T5Z6XmLVS1Xh15RYW7KZO3sdd2L1qN6dWiJsf072WTzO/AekbJ+jPariip65FvltnOJgF4d7YM/e4efKchYGW6zJIrUbSHKdUesJBRYG9R1E4dgxrftjcBg2CHJhtStXQsc2t17ZNT4QV20XBQJAupM0SvFCZLuT+Da+YSkQ2QyW0dXUycMQuVtUzx3KEYxU0b2tG2GRAS8dPV4T+WoqHdwN1nPtwYOZB/n3d2ksjjSVpzGZOXQHu3w9+8chTu+PlJrpzQdyfSVRHhdiN+pFGsuspvOHoGSooStk/Ubuv6zM4bh+auOxbBDkm0w292U+dDVQ24yqyUxsFsbnDmyJ174/nFpBSKbk3h5WqbN8zqJmDCei6xBOTWyoJ1urebO70SUFeJYnCCpXG6WhOzXowfop102kGWyDdp2jfKsL4m5GiJoJ3Zzo6FRHd1klSNBehvNyDp/WR/odeWxdOI6AXxlaHdccFRfHOFxh0XdelXoKgmnd+emsw4DYLcc9Ceusz8frG1IlWeRgYCubUsx0OMC1JKiBMb275we5Jj30DBflewKVVdtVRLFCcL9l4zBkf06pb+zPoOSRCIr51WWJeF4BcFgJaHCyxOHN/fUFEmeFh1XkyGC2d2k2ivY3ZKw//7LMw/L+qw7ca1TtheMap3nQvS2jmzXothVaSUtCfXv5noSJF9AaDsu4JulXHUunQPydr+law0s6JYYxpyEMaCxdvbGx79/+yi0a1mM9ooFnYY/3ojmM7IQCMm4q+LGiXj7xyd5ks+4vReP64frJg7BVSdl8jqZ0+7rBBU8e+WxGNW3oy0E3axEVe3fOqAwf/aT3kcX9bZSMUNENwP4HoCtqa9+IYR4NfJ6U49G1mk4vTi6L9UJg7vigUvG2L4/USOfDpCauG7MtP6ihLyzLE6o8y2pcPO1//Gi0di6twa/nflpljz2crzVa0UnuilBcgvKzEtXj0eP9i0x/va3HTVeQ6PwtDJWZ++JoJlYM/My6jrSdXm44X//9lF6K/s15VcNUrxgdHaqR3TUgM5YcONE5aDAaC9fH9MHMy7rmV6lTBbR/D4S47TS4oQtu6850lDufUhyaPe2WL1lH44q64yXrh6PTzfucZXNTV52NyW5RwgxOvVf5AoCyJi81o5ACOcXR3c0p3ID6Z5PRKir1wiBdXl5ZWe5jX7PGd0bp484JPscmSURUrIxW9GmTiThMDdgMKpvRxzSoaW7JdEgtLU8Qa9TrnNZ9ayLtc1JDVwPt1s3J5JumWHE6mcUqiLxJJIdtEohmRdfmtNYtG9ZgvsuPtJUjj9Znd77Tq1LTcfJzk3+fXnaeFTcmEkp4vS+61oF7G6KGc+jDu2XSn7LdetLTlybQ2ATSkvCCemIVEMI2SFui+n84lhMiKOo+sZG7egmECnvk/mlHd2vI84b2wcf/eIU3HvhaO9CpTs+iyySur1YLbpH6h5nHqR0blOqPM5JxCKFuwkql5uFg3XJOQjZvuFnjcoEdQS1JGSUFicc52WM59W6tDgrpX4Y70jWfSng6KZpRLSEiGYQUSfZAUQ0lYgqiKhi69atskM8YYw2rKMOt3A83Yeu6rx1zXsiQr3V3WSS1Yh0so7wdDobp4niTDnZxxQn7IvpdHsYc/poGU6TurpzEjq47ZJmvi9f7jyolMv8bdsWxbjz/FFZI1s/yBIMuh3jpTwV2pZE6sCOrUsw92f29QBaZaTdTfIQWDdZdh2oTcmgVlJA8HUSYWKzEH1U4aSUwyRWJUFEbxLRMsl/5wD4C4BBAEYD2AjgLlkZQoiHhBDlQojybt30/Pp+MRqx8YDN23HqPvQihXtIFwJQa3I3mTuNwd3bpl8EqzLSUUJWxSJzApiPeev6E6Uvpu5L9eo1J+D2r9vXO6isbbMy0pmTMJCJ07Vti/T6kvpGoZ1h9sSh3XKy81va7Wn1q0ujmzzIo31o8kAj7FN5VOpetCktztr74FeWIAgn0kpCWYfz+btS+5R0clMSioKGHdIOxw7M5MqyWga6j9tLswgjPPcek4XabN1NQohThRCHS/57SQixWQjRIIRoBPAwgHFu5UUrqz3q4KMbzD5GvXKCLnKzWhLmEbVAZmFVu5YlWQ1Hp1p7GoCUG8A8OWc6ZlC3pCVg7dR1JzOLixLStROqeSEzBA8vr+QFHNGrPb56ZDIhY0OjQElRApW3TVHWBSTdGWeP6pXu1I4ZmB0y7CWq69krj7VlbzWjCgMOOnGte6hRj1vZqp8vP36A9oZYRZJ2liWLSwdq7J/hlopDVYr5Hv/hvJG2TbfccAqbV2ELbXaYz1DRtW1GKRbkimsiMseJfhXAstzUm/xri9l2O0+zgQSd6CPK3lQm6W7K8LMzhuE354zAqcO7W85zr9fm2kjJaq5Px0rwssDKaY7D6VYlLQk9VGtQiiXXJ8NQescfmkwFYtwXqwTK6BtJ6zmqrDO+cXR/x3oBvfvtyZDQdTdplmfIJ5tsNd8fp/KK0mVkf2+sVHYT+U8XHYkfnjLYdd2MuRwiYPyhXdKynTAk+WzHDeiMThY3ThSWhFNks253n6sN0vI2BBbAHUQ0Gsl7VgngilxW7tUPKXvosocd2JJA0pSfuWSjtN7WpcX41rFl0vPcsLotMhOKJqWkcV+8hEXK7rNqjYC5E6FEsJdEAOjTKZlOXbXrnkFpcQJvXX8ieqc6LeM2WUdvbtJ4kdZLCKy3OQm940jRcavqNg47f2wfbN5bk/pOM0pH4W7619RjsGDdTtf21K9La1xr2iBLhbm9rP39FCyt2o2z7n8fRMCVEwbh62P6oIdkDsltOJKex/Tk9XN3B7vVa36WUablyFslIYT4Zpz1tyn1dmvcGkiL4gRq6htRHDCuPEGEHu1bYmiPdli1eW+2u8k68WfuVDUasFUBGAqtwaMl4WWBlZNl5RZxZP75kPYtsWlPtXa9QHL/YJWLyYrhWgNMI19PtXlj0ohDsGDdTvTtlL0viDxVuH83h/q45F9XCzrd9pJ//6DaOU8WlZUgNDQK02K67Np6dmiFM0fKk17+bNIwtG0RTt4ooqSikikI43e38/3UmfXZ9G9rh3/b147Axt32tm1+d6JKow/ksbspLswPa5A5a6mLqnZ7+X4xeTiAMOYkkn8bsxacuXdaOs4Zq2hFEiVhXaAkq7fEw+S87L5dNK4vWpUUYdLh2WsyzCNKa3STLPzRiSArVBOKHjRM8/+7JwzAkptPQ8+O2R1X0DkJM9aFYVn16C4uTFsB9vvpboVYyvDwSK46aRC+KbGYvaDbsere3SDrg5zmJC4a109qKXGCv5jxqpmtz9jaYRh+78BzEqm/6U3ZzU/QltZAHgWlQuVusk6U27DU62W3Mplch3Zvi09vmZR2Bxn8+/vHZcmRvSFR7pC54QD3zsSLjESE9i1LJCGwMktCv1zz+ddNHOIQkq1bnn7d9jqSJ2css2ie4p3nj3LcjtjVnRRy2LDsWD9t2fwsx/nIu6YLKwkLpwzvgQvK++A352RHnoj0/+S4WRLGRPJ5Y/UiPlQYjcmIzOnatkVWdJPbeU5YryFjSZi+C9nddFSZfuMe3jOzYY1VDKfojqenHpOlYIKinJOIYB7RNviQHeOhYq+TsG4Wl9HB+hnJGvdRlbspLM4b2wezrrWn73eT2ZBLd67Jy+MPw+o0K+jffy281Pm2eiIruYlSWpzAHeeNQo/2LbO1u4t/3+2Z9+/SBpW3TdHKm+OEUc/VXzkUK2+ZhI6tS7UaZ7A5iYyW0CnHy8R1pzalGGyJStEx221KwiGJ7tEDu2CMKbsmoO4ghh3SDpMsqUfsdcs7NZXcQdwQOqGSMq49VT6Rq1ue1zUo7tF/allU7ruoSYe0Ky71xavH45pTBmuvQ/E2ce0gl6bGNXslwtzXwworiZCwjgx+e+7hvjJkjurTARMckv2lY7KJ0NKyAMypcelMWFrnG4xz3EJgrW4Cr0nf/MR42yOfwulhXv/RBDz4zbEAgNNHyHd4U4V9RmJJOLglfJXnsV6322qU58+SSLmbHOY1oiS9OFbx++G9O+A6jaipDMHm4rySqxBYVhKaeG3AF4/rhyU3nea5npemHY/HL1evG5Q1LiMq44oTB9l+M5C1J+tWqzZLIjUB3ZilJJRVpPEycQ34S4ZnvQ9huyoW/nIi7rvYnq03WXc0dcrw2xEoLQTN4vp3aY3iBLmHlqbLk0xcu9RhyKLM3RQx6eqCKl4f5+uckisl4AYrCQf8bqdokAsfNQC0aVGMytum4OJx/bK+N4svUy5jLfMB9jmJZPOwLt5zldHjhbvlTpLWYfmsY428ePV4fPOY5AI2t2fZuU2p0hLM7HlstSRy+1Kb52isqCSxZ5WVH9m6tBirfzfZFmFmJWNV2X8zf9dFkmcobUko7mfUpFe1BywnPScRlrspiDARkLfrJPKNHLdfJb5HlpLvrJEt1jxBxsubFQIbQUdYp7krnxmrGIaEM685HlP+9L70nNF9O2JfdT2e+GCd5/pkddvnJPxz/cQhWL5hj/uBSCqwa04+FF8f2wdbUgvXVDJGjVGNUwc/tEc73ClZP2E0vyIfIbDhkHI3hXSvPBWTOri0OIFnrjg2eHkRwkrCB9p+3Qges98GLevcf/+1I/CXd77Ao/MqAThFNzm/vcbL/eClY9Gnk3zxkxOGpVJalPC0jasZwyWmmxkzSIcUxZzED1xWfZtZ+MuJ6X+3a+mcryhq0iuzHY751nH9bakugEz7CmNPCj+EZUkYBXiKMkud1K5FMUZb9/jIkwGpAbubPOC1Y4liNHepRr4fgyP7dXSUpUf7lrj57BHpzzorrmUYv3ZpW4rDe3uP3jIsCa9zGWYy60acywjjmcjcTT3at8DPzxgevPAc01axJaguOhPX6p384lUSvVMDmtNcotl08XIVxjW3aaG+/3kyJcGWhC7mdyDsJGm66KaQMDhndG90a9sClzzyodaEs/WyMovpwl2ZaiWtJIoTQGoTe68YIuYijbds4vrDX5wqPzgmrJFvKp678licfNe7vusxwkPbSjq7TDZfxblk/I2nN+zZoRUW33Sacu9sr3i5jM5tSnHD5OG2nR6B3Ed5ucFKQhM/7omw/Pev/OB4fLF1n69zjVXLWiGwlmOMjkbWAZi56sRB+O7jFRjc3XnvARX1qegmI3TWz20TupZE+njvdaTLMFkS15x8KOav2e6/sIj45rH9sfNALR6btw77auqVxw3s5pw51Y0OrUrwqzMPw6nD5eHCgLrtpddJxDhiNlLrByE9ce1xmPS91H4m6nLzw5RgJREhYT3iw3t38OXGCSrLMQM74xeTh+GC8r5Z3399TJ+sNQSnHtbDs5VjxjwnAfjrwNOZY916HCP+P8BoLb32SwDXnTbUdzlR0qK4CD85fRie/rjKUUmEweXHD3D8XTVYMpREvoR6+iVs+fMlSMaA5yQ0yTcTUJfu7VugXYti3DBFvVNYO4WlQESYOmGQbfe5uy4YFZof14yfxYcG2QkPo0UVAqsizrYTxe3QHhC4JfhrZr1P2Pc6X3QnWxIREvQhf21M78AytCwpwtJfnw4AuPop+TGv/vAELPtyd+C6ghIkQ64xP6BbRBjRTbpKwrqy2A/jPOS4MqNT46/PHpGVCj1slGs28sSdEhTZe/7j04bg6Yr1uRcmAlhJ6JLjOYkvfjc5Z77avp1bo2/n1u4HRozRifq5bccM7Iw5q7ai2GV4GkbHlF4nobm8Y/IRPbF4/W784ORDfdX30Q2noL3PUFcdy+qy48p8le2G2yuTcds1TSvdivlWTzt5MKadrB/WbCbfbkczM/jCxZp+N5cPryhBTd5Xq4vu2gYn/vyNsZh97QSUFie0tk8N8ii7tWuB0qIEfjpJbz6ipCiBX511mHStgA7d27XUjlayElYTOmVYd/eDPMqQiCkdR1SE9b62SO2NEldosBW2JBz49dmHo7Ze4PmFVdkhsLFJ1Dz5zw+Ox2eb9+L211YC8KeMW5UWYXBqz4DFDjmzOrZOjsgHdGmjPMaNliVF+OzWM3yfn0vCaKtLbj4NrUqKMPiG10IoLYN5UeKpw3vgvLHB3atxQJa/Qbn5rBHo2aGVUjF3al2CnQfqQqrNHVYSDpQWJzCkR9JXK4R8+nHGt8vx4doduRWsmdG7Yyv07tgqrSSC4jTqHt6zPR79zlE4ZmCXUOrKd8IY3cpcXfOmnxx4pGteb/LIZeWByoqTsC3+Tm1KMf2MYcrfP/jFKTn1arCScMFt74iTh/XAycPUMeKMd5zeuVk/moCVm/RyHKk4aah/10lTw7iX3zthgHZuKB16dfSefsXKXeePxt2zV2FgN/9WXT6QtiRy5GKIcu8IGawkmCbF0EPaYegh/hbtFSJGx3XpMf3RP4CLzQ/p/RoUnecRfTrg799Rp8VvajSXaC0rPHGtSb5FHDAZJh8R/pqN5kKQ7UWDkt75rZl2nlaaa5wJWxIuNLcGPiiPTXs/Hdna308OXxCG8UB6t8gY6p5+xjCs33Eg0jpYSWjSHAyJd358Ejq3DR5umk8USpiwX3T3oGZCIIameKXDbpRhwUrCBfNev0190U9Z1/y1IoDma67HiWrvCyZMmnfDjXVOgojOJ6LlRNRIROWW335ORKuJaBURnR6XjHL0G8Wlx/TDE//XfCbnooT7sfAxWmocC9YK5XmmBzfN9HrjtiSWAfgagL+avySiwwBcBGAEgF4A3iSiIUIIf5sNhICAQJeUq8ZLyN5vzz0iKpGaLWxRhEj6XjavJINM7ohVSQghPgWkfuVzAPxLCFEDYC0RrQYwDsD83EqYLdvY/p3x+OXjCmYhFtP0CWP/DEaP5nqL47YkVPQG8IHpc1XqOxtENBXAVADo169fZAIZL9mEId0iq4NhwiY9JxFCWY9dPs7TJj3NtdO00twNpciVBBG9CUAWyH6DEOIl1WmS76RtTgjxEICHAKC8vDz0dtncGwDTvElnrA3BlDiRB0gFSeRKQgjhZ/PfKgDm7dD6ANgQjkRMvvLbrx6OW15ZgQF5HoXVlIh1MV2B+LjMEZDNkXxdcf0ygIuIqAURDQAwGMBHcQjCk26546iyznh52vE5z03TnDH2CfGbajwMmvtalrQibqYOtljnJIjoqwDuA9ANwEwiWiSEOF0IsZyIngGwAkA9gKvjimzKTPxF0wD+/I0x6T2eGSZs7r5wFOZ+vo2tM8Y3cUc3vQDgBcVvtwK4NbcS5Z7JR/SMWwSmGdO+ZQnO4DaWE9jdVOA00+fPMExARvbpACBel16U5GsIbN7Q3P2pDBMV5f074ZUlG1HWJf7906Pk3otGY+WmvaFsw5uPsJLQpLmakgwTFZcdV4aTh/VAv2auJFqXFmNMv05xixEZ7G5ygQ0JhvEHETV7BVEIsJLQpLmGtzEMwzjBSsIFzn3DMEwhw0rCDfY3MQxTwLCSYBiGYZSwktCEvU0MwxQirCRcYGcTwzCFDCsJF0qKkmqitIhvFcMwhQcvpnPhq0f2wZpt+zHtK4fGLQrDMEzOYSXhQmlxAj8/Y3jcYjAMw8QC+1AYhmEYJawkGIZhGCWsJBiGYRglrCQYhmEYJawkGIZhGCWsJBiGYRglrCQYhmEYJawkGIZhGCUkmtFGCUS0FcC6AEV0BbAtJHGaAoV2vQBfc6HA1+yN/kKIbrIfmpWSCAoRVQghyuOWI1cU2vUCfM2FAl9zeLC7iWEYhlHCSoJhGIZRwkoim4fiFiDHFNr1AnzNhQJfc0jwnATDMAyjhC0JhmEYRgkrCYZhGEYJKwkARDSJiFYR0Woimh63PFFDRDOIaAsRLYtbllxBRH2JaA4RfUpEy4noh3HLFDVE1JKIPiKixalr/nXcMuUCIioiok+I6JW4ZckVRFRJREuJaBERVYRadqHPSRBREYDPAEwEUAXgYwAXCyFWxCpYhBDRBAD7ADwuhDg8bnlyARH1BNBTCLGQiNoBWADg3Gb+nAlAGyHEPiIqAfA+gB8KIT6IWbRIIaLrAJQDaC+EODNueXIBEVUCKBdChL6AkC0JYByA1UKINUKIWgD/AnBOzDJFihDiPQA74pYjlwghNgohFqb+vRfApwB6xytVtIgk+1IfS1L/NetRIRH1ATAFwCNxy9JcYCWR7CjWmz5XoZl3HoUOEZUBOBLAhzGLEjkp18siAFsAzBZCNPdrvhfATwE0xixHrhEA3iCiBUQ0NcyCWUkAJPmuWY+2ChkiagvgeQA/EkLsiVueqBFCNAghRgPoA2AcETVb9yIRnQlgixBiQdyyxMB4IcQYAGcAuDrlUg4FVhJJy6Gv6XMfABtikoWJkJRf/nkATwoh/h23PLlECLELwDsAJsUrSaSMB3B2yj//LwAnE9E/4hUpNwghNqT+bgHwApJu9FBgJZGcqB5MRAOIqBTARQBejlkmJmRSk7h/A/CpEOLuuOXJBUTUjYg6pv7dCsCpAFbGKlSECCF+LoToI4QoQ/I9flsIcWnMYkUOEbVJBWOAiNoAOA1AaJGLBa8khBD1AKYBmIXkZOYzQojl8UoVLUT0TwDzAQwloioi+r+4ZcoB4wF8E8nR5aLUf5PjFipiegKYQ0RLkBwMzRZCFExYaAHRA8D7RLQYwEcAZgohXg+r8IIPgWUYhmHUFLwlwTAMw6hhJcEwDMMoYSXBMAzDKGElwTAMwyhhJcEwDMMoYSXBMAzDKGElwTASiKiLaT3FJiL6MvXvfUT05wjqe5SI1hLRlQ7HnEBEKwopxTsTP7xOgmFcIKKbAewTQtwZYR2PAnhFCPGcy3FlqeOabQ4mJr9gS4JhPEBEJxmb2RDRzUT0GBG9kdr05WtEdEdq85fXU7miQERjiejdVIbOWam9LdzqOZ+IlqU2DHov6utiGBWsJBgmGIOQ3L/gHAD/ADBHCHEEgIMApqQUxX0AzhNCjAUwA8CtGuX+CsDpQohRAM6ORHKG0aA4bgEYponzmhCijoiWAigCYOTMWQqgDMBQAIcDmJ3MMYgiABs1yp0L4FEiegZAQWWsZfILVhIME4waABBCNBJRnchM8jUi+X4RgOVCiGO9FCqEuJKIjkbSSllERKOFENvDFJxhdGB3E8NEyyoA3YjoWCC5pwURjXA7iYgGCSE+FEL8CsA2ZO95wjA5gy0JhokQIUQtEZ0H4E9E1AHJd+5eAG7p6P9ARIORtETeArA4UkEZRgGHwDJMHsAhsEy+wu4mhskPdgO4xW0xHYD/IOl+YpicwJYEwzAMo4QtCYZhGEYJKwmGYRhGCSsJhmEYRgkrCYZhGEbJ/wO366csEP4IyAAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# First order system\n", + "a = 1\n", + "c = 1\n", + "sys = ct.tf(c, [1, a])\n", + "\n", + "# Create the time vector that we want to use\n", + "Tf = 5\n", + "T = np.linspace(0, Tf, 1000)\n", + "dt = T[1] - T[0]\n", + "\n", + "# Create the basis for a white noise signal\n", + "# Note: use sqrt(Q/dt) for desired covariance\n", + "Q = np.array([[0.1]])\n", + "# V = np.random.normal(0, sqrt(Q[0,0]/dt), T.shape)\n", + "V = ct.white_noise(T, Q)\n", + "\n", + "plt.plot(T, V[0])\n", + "plt.xlabel('Time [s]')\n", + "plt.ylabel('$V$');" + ] + }, + { + "cell_type": "markdown", + "id": "b4629e2c", + "metadata": {}, + "source": [ + "Note that the magnitude of the signal seems to be much larger than $Q$. This is because we have a Guassian process $\\implies$ the signal consists of a sequence of \"impulse-like\" functions that have magnitude that increases with the time step $dt$ as $1/\\sqrt{dt}$ (this gives covariance $\\mathbb{E}(V(t_1) V^T(t_2)) = Q \\delta(t_2 - t_1)$." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "23319dc6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mean(V) [0.0] = 0.14775487875720242\n", + "cov(V) * dt [0.1] = 0.09761761761761763\n" + ] + } + ], + "source": [ + "# Calculate the sample properties and make sure they match\n", + "print(\"mean(V) [0.0] = \", np.mean(V))\n", + "print(\"cov(V) * dt [%0.3g] = \" % Q, np.round(np.cov(V), decimals=3) * dt)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "2bdaaccf", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Response of the first order system\n", + "# Scale white noise by sqrt(dt) to account for impulse\n", + "T, Y = ct.forced_response(sys, T, V)\n", + "plt.plot(T, Y)\n", + "plt.xlabel('Time [s]')\n", + "plt.ylabel('$Y$');" + ] + }, + { + "cell_type": "markdown", + "id": "ead0232e", + "metadata": {}, + "source": [ + "This is a first order system, and so we can use the calculation from the course\n", + "notes to compute the analytical correlation function and compare this to the \n", + "sampled data:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "d31ce324", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* mean(Y) [0] = 0.0985\n", + "* cov(Y) [0.05] = 0.0207\n" + ] + } + ], + "source": [ + "# Compare static properties to what we expect analytically\n", + "def r(tau):\n", + " return c**2 * Q / (2 * a) * exp(-a * abs(tau))\n", + " \n", + "print(\"* mean(Y) [%0.3g] = %0.3g\" % (0, np.mean(Y)))\n", + "print(\"* cov(Y) [%0.3g] = %0.3g\" % (r(0), np.cov(Y)))" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "1cf5a4b1", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Correlation function for the input\n", + "# Scale by dt to take time step into account\n", + "# r_V = sp.signal.correlate(V, V) * dt / Tf\n", + "# tau = sp.signal.correlation_lags(len(V), len(V)) * dt\n", + "tau, r_V = ct.correlation(T, V)\n", + "\n", + "plt.plot(tau, r_V, 'r-')\n", + "plt.xlabel(r'$\\tau$')\n", + "plt.ylabel(r'$r_V(\\tau)$');" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "62af90a4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYwAAAEGCAYAAAB2EqL0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAABDWUlEQVR4nO3deVyVZfr48c/NrgiiiIqAoIkLiivuWmZapmWmlTraPtliezNlNlPfVus3TYtW02ilOZltVpq55NZiboCKioiiIiggyCb7ev/+gGNIqIezPAfwer9e5zWec57nvi8mDte5d6W1RgghhLgUJ0cHIIQQonGQhCGEEMIskjCEEEKYRRKGEEIIs0jCEEIIYRYXRwdgL23atNEhISGODkMIIRqV6OjoM1prv7rea7IJIyQkhKioKEeHIYQQjYpS6sSF3pMuKSGEEGaRhCGEEMIskjCEEEKYRRKGEEIIs0jCEEIIYRbDEoZSapxSKl4plaCUmlPH+0opNb/6/X1Kqf413ktUSu1XSu1VSsnUJyGEcABDptUqpZyB94GxwEkgUim1Smt9sMZl1wOh1Y/BwH+q/9fkaq31GSPiFUII8WdGtTAGAQla62Na61LgC+CmWtfcBCzVVXYAPkopf4PiE0IIcQlGJYwAILnG85PVr5l7jQZ+UkpFK6VmXagSpdQspVSUUioqIyPDBmELYVt5eXlkZ2c7OgwhLGJUwlB1vFb75KaLXTNca92fqm6r2UqpK+uqRGu9UGsdobWO8POrc2W7EA711Vdf0bp1a7Zv3+7oUISoN6MSxkkgqMbzQCDF3Gu01qb/TQe+o6qLS4hGJy4uDoAXX3zRwZEIUX9GJYxIIFQp1Ukp5QZMA1bVumYVcEf1bKkhQK7WOlUp5amU8gJQSnkC1wIHDIpbCJsyJYz4+HgHRyJE/RkyS0prXa6UehhYDzgDn2itY5VSD1S//yGwBhgPJACFwN3Vt7cDvlNKmeL9XGu9zoi4hbC1gwerJgYmJiZSUFCAp6engyMSwnxK69pDCU1DRESElt1qRUOSn5+Pl5cX/fr1Y8+ePURFRTFgwABHhyXEeZRS0VrriLrek5XeQhho4cKFzJlTtW41NjbWwdEIUT9N9jwMIRqaFi1acN9991FWVsa///1vBg8efOmbhGhAJGEIYZC9e/fi5uZGWFgYTz75pKPDEaLepEtKCIPMmTOHGTNmAJCWlsbmzZsdHJEQ9SMJQwiDxMbG0rNnT6BqLGPMmDEUFhY6OCohzCcJQwgD5ObmcvLkyXMJIywsDK21rMcQjYokDCEMYFp/UTNh1HxdiMZAEoYQBjBNoTUljNDQUFxcXGRqrWhUZJaUEAa46aab6NChA506dQLA1dWVrl27SgtDNCqSMIQwgJ+fH+PHjz/vtUWLFuHr6+ugiISoP+mSEsIAH3zwAfv27TvvtWHDhtGtWzcHRSRE/UnCEMLOsrOzmT17NuvWnb9nZmZmJgsXLuTYsWMOikyI+pGEIYSdmQa2e/Xqdd7rOTk53H///bKATzQakjCEsLMDB6qObzHNkDLp1KkTnp6ef+qqEqKhkoQhhJ3t27ePli1b0rFjx/Ned3JyIjw8nP379zsoMiHqRxKGEHa2f/9+evfuTfUhYOfp3bs3+/bto6meSyOaFkkYQtjZxo0bWb58eZ3v9e7dm6ysLFJTUw2OSoj6k4QhhJ25u7sTEBBQ53szZ84kOzubDh06GByVEPUnCUMIO9q6dStPPvkkWVlZdb7fsmVLfHx8jA1KCAtJwhDCjjZs2MC7775Ls2bNLnjNggULePvttw2MSgjLSMIQwo5iYmLo2rXrRRPGTz/9xMcff2xgVEJYRhKGEHYUExNDnz59LnpN7969OXToECUlJQZFJYRlJGEIYSc5OTkkJiaalTAqKiqIi4szKDIhLCMJQwg7SU5Opl27dmYlDEBWfIsGT7Y3F8JOwsPDSUtLu+SivNDQUNq0aUN2drZBkQlhGUkYQthZXSu8a3JxceH06dM4OUmDXzRs8hsqhJ1MmzaNV1991axrJVmIxkB+S4Wwg/LyclatWkVmZqZZ1//+++8MHjxYzsYQDZokDCHs4NChQxQVFdG/f3+zrm/WrBm7du0iOjrazpEJYTlJGELYQVRUFAARERFmXd+zZ09cXV0lYYgGzbCEoZQap5SKV0olKKXm1PG+UkrNr35/n1Kqf633nZVSe5RSq42KWQhLRUdH06JFC7p27WrW9e7u7vTq1Yvdu3fbOTIhLGdIwlBKOQPvA9cDYcB0pVRYrcuuB0KrH7OA/9R6/zFAVjaJRiEgIICpU6fWazC7f//+7N69W87GEA2WUdNqBwEJWutjAEqpL4CbgIM1rrkJWKqrPi07lFI+Sil/rXWqUioQmAC8CjxpUMxCWGzOnD81oi/pmmuuIScnh8LCQjw9Pe0QlRDWMapLKgBIrvH8ZPVr5l7zDvA0UHmxSpRSs5RSUUqpqIyMDKsCFsJSZWVlFrUSpk+fzjfffCPJQjRYRiWMulYu1f5E1XmNUuoGIF1rfcnRQK31Qq11hNY6ws/Pz5I4hbDasmXLaNWqFUlJSRbdX1paauOIhLANoxLGSSCoxvNAIMXMa4YDE5VSicAXwGil1Gf2C1UI60RHR1NeXn7BU/Yu5tprr2Xy5Ml2iEoI6xmVMCKBUKVUJ6WUGzANWFXrmlXAHdWzpYYAuVrrVK31s1rrQK11SPV9m7XWMw2KW4h6i46Opl+/fjg7O9f7Xn9/f5laKxosQxKG1roceBhYT9VMp6+01rFKqQeUUg9UX7YGOAYkAIuAh4yITQhbKi8vZ+/evQwYMMCi+/v3709aWhopKbUb4EI4nmGbD2qt11CVFGq+9mGNf2tg9iXK+Bn42Q7hCWETphXeliaMQYMGAbBr1y4mTZpkw8iEsJ6s9BbChry8vHj22WcZMWKERff369cPFxcXdu7caePIhLCebG8uhA0FBwfz2muvWXy/h4cHL774osUtFCHsSRKGEDa0e/duunXrZtVairlz59owIiFsR7qkhLCR4uJihgwZwssvv2xVORUVFRw4cMDsrdGFMIokDCFsZM+ePZSVlTFkyBCryjly5Ajh4eGsXLnSRpEJYRuSMISwkR07dgAwePBgq8rp2rUrLVu2lIFv0eBIwhDCRnbs2EFwcDD+/v5WlePk5MTAgQPZtWuXjSITwjYkYQhhIzt37rS6dWEyePBg9u/fT2FhoU3KE8IWZJaUEDagtWb58uW4u7vbpLzBgwdTUVFBdHQ0I0eOtEmZQlhLEoYQNqCUYujQoTYrb+TIkfz444/06dPHZmUKYS1JGELYwNq1aykrK2PixIk2Kc/Hx4fx48fbpCwhbEXGMISwgTfeeINXXnnFpmXGxsby9ttv27RMIawhCUMIK5WXlxMVFWX1+ovatmzZwpNPPmnxQUxC2JokDCGstH//fgoKCmw2Q8pk+PDhAGzdutWm5QphKUkYQljJ9Afd0h1qLyQ8PBwvLy9JGKLBkIQhhJX27NlDUFAQwcHBNi3XxcWFoUOH8vvvv9u0XCEsJQlDCCt9/PHHREZG2qXsESNGkJCQQEFBgV3KF6I+JGEIYSWlFO3atbNL2Y899hhZWVlWbZcuhK1IwhDCCitXruTuu+/m7Nmzdinf29vbZqvHhbCWJAwhrPDDDz+wcuVKWrRoYbc63nvvPR566CG7lS+EuSRhCGGF3377jeHDh+PkZL+P0rFjx1i8eDGlpaV2q0MIc0jCEMJCp0+f5vDhw3bfHHDEiBEUFxeze/duu9YjxKVIwhDCQqbprvZOGKYFfL/99ptd6xHiUiRhCGGh0tJSwsPD6d+/v13radeuHV27duWXX36xaz1CXIokDCEsNG3aNPbt22fILKbJkyfj6+tr93qEuBjZ3lwIC5SXl+Pk5GTXwe6a5s2bZ0g9QlyMtDCEsMAPP/xA27ZtOXTokKH1FhcXG1qfEDVJwhDCAps3b6aoqIjOnTsbVuekSZNsdkCTEJaQhCGEBTZv3szIkSNxc3MzrM7g4GC2bt1KSUmJYXUKUZNhCUMpNU4pFa+USlBKzanjfaWUml/9/j6lVP/q1z2UUruUUjFKqVil1ItGxSxEXdLS0jh48CCjR4+26H6ttUX3XX311RQVFbFr1y6L7hfCWoYkDKWUM/A+cD0QBkxXSoXVuux6ILT6MQv4T/XrJcBorXUfoC8wTill26PNhKiHLVu2AFiUMBLPFNDp2TVsPnS63vdeddVVKKXO1S+E0YxqYQwCErTWx7TWpcAXwE21rrkJWKqr7AB8lFL+1c/zq69xrX5Y9hVNCBsICwvj6aefpl+/fvW+NzIxC4DPdybX+95WrVrRr18/Nm/eXO97hbAFo6bVBgA1PyEngdrnWdZ1TQCQWt1CiQa6AO9rrXfWVYlSahZVrRM6duxom8iFqKVPnz706dPHonuTsgoBKKuotOj+p59+moqKCovuFcJaRiUMVcdrtVsJF7xGa10B9FVK+QDfKaV6aa0P/OlirRcCCwEiIiKkFSJs7syZM8THxzNo0CBcXV3rff+htDwATmRadiDS1KlTLbpPCFswqkvqJBBU43kgkFLfa7TWOcDPwDibRyiEGVauXMmIESOIj4+36P54U8LIKqS4zLKWQkJCghzbKhzCqIQRCYQqpToppdyAacCqWtesAu6oni01BMjVWqcqpfyqWxYopZoBYwBjV0sJUW3dunUEBATQs2fPet+bX1JOUlYh3dt7oTUkpOdf+qY6zJo1iwcffNCie4WwhiEJQ2tdDjwMrAfigK+01rFKqQeUUg9UX7YGOAYkAIsA04kx/sAWpdQ+qhLPBq31aiPiFqKm8vJyNm7cyHXXXYdSdfWgXtzh01Wtixv7dDjveX2NGzeO/fv3k5JSu5EuhH0Ztg5Da71Ga91Va32F1vrV6tc+1Fp/WP1vrbWeXf1+uNY6qvr1fVrrflrr3lrrXlrrl4yKWYiadu3aRU5ODuPGWdYjGpdadYzr+HB/XJ0V8VYkDID169dbdL8QlpKV3kKY6aeffsLJyYkxY8ZYdH9c6lm8PFwI8W1O5zYtOHLasi6p8PBw/P39WbdunUX3C2EpSRhCmGnu3Lns3LmTVq1aWXT/wZSz9PD3RilF1/ZeFndJKaW47rrr2Lx5M5WVlk3PFcISkjCEMJObmxsREREW3VtRqTmUlkeYvzcAXdu24GR2EQUl5RaV9+KLL3Lo0CHDtlcXAiRhCGGWdevW8fTTT5Ofb1k30onMAgpLKwjrUJUwQtt5AZYPfHfs2FEOVBKGk4QhhBk+//xzPvnkE5o1a2bR/XGpVYnB1MIw/e/B6oFwS3z55Zc89thjFt8vRH1JwhDiEioqKli/fj3XXnstzs7OFpVxMDUXFydFl7YtAAhq3Qyf5q7sP5lrcVxxcXEsWLCAM2fOWFyGEPUhCUOIS9i1axfp6enceOONFpdxMOUsXdq2wMO1KuEopQgPaEmMFQnjxhtvRGvNmjVrLC5DiPqod8JQSnlWbwYoxGVh1apVuLi4WLz+Aqq6nkzdUCZ9An04fDqPolLLtgjp378/HTp0YOXKlRbHJUR9XDJhKKWclFJ/UUr9qJRKp2pbjtTqw4z+pZQKtX+YQjiO1pobbrjB4um0mfklnD5bQo9aCSM8sCUVlZqDqZa1MpRSTJw4kfXr18tZ38IQ5uxWuwXYCDwLHNBaVwIopVoDVwOvK6W+01p/Zr8whXCc119/3ar7zw14d/hzCwNg38lcBgS3tqjsm2++mdjYWNLS0ggJCbEmTCEuyZyEMUZrXVb7Ra11FrACWKGUqv8+z0I0AgUFBXh6elpVhqkFUbuF0b6lB2293NlnxTjGtddey7XXXmtVfEKY65JdUqZkoZTadqlrhGhqJkyYwJQpU6wq42DKWdp7e9Da0+1P7/UKaMnBFMun1prk5ubKqm9hd/UZ9Pao/YJSaqQNYxGiQcnMzGTr1q2EhdU+fr5+4lLz/tQdZdK9vRdHM/IpKbf8FL2ffvqJNm3aEB0dbXEZQpijPgmjm1LqO6XUK0qpaUqpq4EldopLCIdbu3YtFRUVTJw40eIyissqSMjI/9MMKZMe/t6UV2qLz8YAGDBgAFprmS0l7K4+CeM48BpwFBgA/BV40R5BCdEQrFixgg4dOjBgwACLyzhyOp+KSn3BFoZpXMM0MG4JX19fRowYwffff29xGUKYoz4Jo1RrHam1Xqy1/rvWeobWeqndIhPCgfLy8li7di233HKLVRv8XWjA26RTG088XJ3OnZVhqSlTphAbG8vBgwetKkeIi6nPJ+Equ0UhRAPj4uLCRx99xH333WdVOXGpeTR3cya4dfM633d2UnRr52WThKGU4uuvv7aqHCEu5pLTapVSqvo0vAu2mU3X2DY0IRynWbNmzJw50+pyTGdgODld+EjX7u29+elgGlpri45+BejQoQOLFy9m5EiZhyLsx5wWxhal1CNKqY41X1RKuSmlRiulPgXutE94QhgvLy+Pt956i9OnT1tVTmWlrnNLkNp6+HuRXVjG6bMlVtV355130rlzZ6vKEOJizEkY44AKYLlSKkUpdVApdRw4AkwH3tZaL7FjjEIYavXq1Tz11FMcOXLEqnJOZheRX1J+wfELk3MD32nWr8dYsWKFdEsJu7lkl5TWuhj4APhAKeUGTANWaa1z7BybEA7x9ddf06FDB4YNG2ZVOaazLi40Q8qk+7mZUme5ultbq+p87733OH36NLfccovF3VtCXEi9pn9orUuBDsCt9glHCMfKy8tjzZo1TJkyxerjTw+mnsVJQbfq0/UupGUzVwJ8mlk1tdbktttuIy4ujtjYWKvLEqK2en8itNavA2VKqbeVUiOVUi3sEJcQDrFixQpKSkqYNm2a1WUdTDlLZ78WNHO79GkAPfytnykFMHnyZJycnPjiiy+sLkuI2iw5D+NBYBJwABgE/MfGMQnhMPHx8XTt2pWhQ4daXVZc6tlLjl+Y9PD35lhGPsVllm8RAtCuXTvGjBnDsmXLZG8pYXNmJYzqMzHmVj9N01pP0lp/rLX+t9b6djvGJ4Sh5s2bR0xMjNX9/7mFZZzKKbrkDCmTHv7eVGo4fNr6bqnbb78dFxcXUlJSrC5LiJrMShjVZ2CMqf73d3aNSAgHKS0tBcDD40/7bNabacC7h//Fxy9MTC2RQzYYx5g+fTqHDx8mMDDQ6rKEqKk+XVJ7lFIvKJl6IZogrTWDBg3iqaeeskl5pvEIc1sYwa2b08zV+VyisYazszNKKUpKSs4lQSFsoT4JI4iqKbWpSqmVSqmXlVIyW0o0CTExMcTExHDFFVfYpLy41LP4errh5+Vu1vVOTopu7W0z8A1w+PBh2rdvz7fffmuT8oSAeiQMrfVtWuseQDBVu9QmUDXoLUSj97///Q9XV1emTp1qk/Li0qoGvOvTIO/h701c6llssctOly5d8PLyYulS2R9U2I4l02pLtNa7tdafaq3/bu59SqlxSql4pVSCUmpOHe8rpdT86vf3KaX6V78epJTaopSKU0rFKqUeq2/MQlxMaWkp//vf/7jhhhvw9fW1urzyikoOn843e/zCJMzfi7PF5aTkFlsdg5OTEzNnzmT9+vWkpaVZXZ4QYEHCsIRSyhl4H7geCAOmK6VqH2N2PRBa/ZjFH9N1y4Gnqls3Q4DZddwrhMVWrlxJRkaG1TvTmhw/U0BpeaXZU2pN/hj4tk231J133kllZSWffvqpTcoTwpCEQVXXVYLW+lj1avEvgJtqXXMTsLR6Z9wdgI9Syl9rnaq13g1QvWNuHBBgUNziMnDllVfyzjvvcO2119qkPNPAdff29UsYNbcIsYVu3bpx5ZVXsnDhQlmTIWzikntJ2UgAkFzj+UlgsBnXBACppheUUiFAP2CnXaIUl6V27drx2GO26+mMS83D1VnRpW39NkFo4e5Cx9bNbTJTyuT1119HKSX7SgmbMCph1PXbWntk76LXVG9BsgJ4XGtd5ydKKTWLqu4sOnbsWNclQpxnyZIlNGvWzGaD3VDVQrjCrwVuLvVvwFdtEWL9WgwTW6xYF8LEqC6pk1RNyzUJBGovQ73gNUopV6qSxTKt9QXnCWqtF2qtI7TWEX5+fjYJXDRd5eXlzJ07l//97382LTfOjDMwLqSHvzeJmQUUlpbbLJ7jx49z//33y+C3sJpRCSMSCFVKdaq5RXqta1YBd1TPlhoC5GqtU6sXCn4MxGmt3zIoXnEZWL16NampqcyaNctmZWbml5CeV1LvAW+TMH9vtIZDabZrZZSWlrJw4UIWL15sszLF5cmQhKG1LgceBtZTNWj9ldY6Vin1gFLqgerL1gDHqFrfsQh4qPr14cDtwGil1N7qx3gj4hZN24IFCwgMDGT8eNv9Opm6kyxNGKb7DqbYbhyjW7dujBo1ikWLFlFRYd3mhuLyZtQYBlrrNVQlhZqvfVjj3xqYXcd9W6l7fEMIi+3fv5/Nmzczb948XFxs9zGIq+ceUrUFtmqGl4eLzWZKmTz44INMnTqVNWvWcOONN9q0bHH5MKpLSogGJTMzk379+tls7YVJXNpZ2nq549vCvC1BalNKnVvxbUuTJ08mKCiId955x6blisuLJAxxWRo1ahS7d++2ycrumuJS8yzujjIJ8/fmUFoelZXWbxFi4uLiwjPPPEPPnj0pL7fdgLq4vBjWJSVEQ7Fnzx5CQ0Np0cK2h0WWlleSkJ7HVV2tm6EX5u9NYWkFJ7IK6dTG00bRwezZf+rxFaJepIUhLivl5eVMnDiRGTNm2Lzsoxn5lFVoi8cvTHrYeMV3TVprfvnlF9LT021etmj6JGGIy8qXX37JyZMnuffee21e9h8D3tZ1SYW2a4Gzk7LpTCmT48ePM2rUKD744AObly2aPkkY4rJRWVnJ66+/TlhYGDfccIPNy49LPYubixOdrexG8nB15go/T7u0MDp37swNN9zAe++9R0FBgc3LF02bJAxx2fjxxx85cOAAc+bMwcnJ9r/6cal5dG3XAhdn68vu4e9t0z2lanr22WfJzMxk4cKFdilfNF2SMMRlY8OGDYSEhDBt2jSbl621Ji71LD3quUPthYT5e5OaW0xOoe2PWB02bBhXX301b775JiUlJTYvXzRdkjDEZWP+/Pns2rULV1dXm5edkVdCZkGp1eMXJudWfNuplfHcc89RVlZGXFycXcoXTZMkDHFZOHPmDAD22pQyLs26LUFqs8cWITWNHj2aEydO0LdvX7uUL5omSRiiydu6dSuBgYFs2rTJbnVEJ2bhpCCsg20Shp+XO35e7jbd6rwmpRTNmjWjvLyco0eP2qUO0fRIwhBNmtaaf/zjH/j4+Nj1bIhfjpyhb5APLZvZrrsrzN+b2JRcm5VXl+nTpzN27FhKS20/ViKaHkkYoknbtGkTv/zyC8899xzNmze3Sx3ZBaXsO5nDyFDbdndFBLci/nQe2QX2+2N+9913c/z4cT766CO71SGaDkkYosnSWvPcc88RFBRk0zMvavv96Bm0hiut3BKktmFd2qA1bD+WadNya7r++usZMWIEL7/8MoWFhXarRzQNkjBEkxUTE0NUVBTPP/887u6W7R5rjl8PZ+Dt4UKfwJY2LbdPYEu83F347cgZm5Zbk1KKefPmkZaWxoIFC+xWj2gaJGGIJqtv374cPHiQO++80251aK357cgZhndpY5MFezW5ODsx9Apffj2cQdVxMfYxYsQIxo8fz48//mjXekTjJwlDNEmmabTdunWzy7oLk4T0fFJzi23eHWVyZVc/TuUUceyMfbfxWLp0KVu2bKHqRGQh6iYJQzQ5OTk59OjRg9dee83udZnGF0Z0aWOX8k1bpf96OMMu5Zv4+vri7OxMdnY2J0+etGtdovGShCGanFdeeYXMzEybntV9IXuTcvDzciewVTO7lB/Uujkhvs3tnjCgauv3gQMH8sADD9i9LtE4ScIQTUpCQgLz58/n7rvvNmQV897kHPoE+ti1K+fKrn7sOJZFSXmF3eqAqlP5HnjgAX788UfWrl1r17pE4yQJQzQZWmsefvhhPDw8eOWVV+xeX25hGcfOFNCvo49d6xkZ6kdRWQXRJ7LtWg/Ao48+SmhoKE888YQs5hN/IglDNBnHjx9nx44dvPLKK/j7+9u9vr0ncwDoG+Rj13oGhrQCYE9Sjl3rAXBzc+Ptt98mPj6ef//733avTzQucqa3aDI6d+5MfHw8bdrYZwC6tj1J2SgFvW28/qI2n+ZudGrjaUjCAJgwYQJTpkwhJiYGrbXMnBLnSMIQTcK2bdsYMmQI7dq1M6zOvck5dPFrgZeH/abtmvQN8uG3I2cM+wP+2Wef4e7uLslCnEe6pESj9/vvvzNixAjeffddw+rUWhOTnGP37iiTfh19OJNfwqmcIkPq8/DwQCnFkSNHWL9+vSF1ioZPEoZo1AoKCrjzzjsJCQnhr3/9q2H1Hs0oILuwjP7BrQypz5SY9ibnGFKfyezZs5k+fTqpqamG1isaJkkYolF75plnOHbsGIsXL8bLy8uwencer1qwN7hTa0Pq697eGzcXJ/YaNI5hsmDBAoqLi7nnnntk2xAhCUM0Xhs3buT999/n8ccf56qrrjK07p3HsvDzcqdTG09D6nNzcSI8oKXhLYxu3brx5ptvsm7dOj744AND6xYNjyQM0Wi5ublx3XXX8eqrrxpar9aaHccyGdyptaGDwv2CfNh3KpfiMvsu4KvtwQcfZNy4cfztb38jPj7e0LpFwyIJQzRaV155JevWraNZM/tsy3EhiZmFpOeVMKSzr6H1Du7sS2l5JTEGtzKUUnzyySc8/PDDBAUFGVq3aFgMSxhKqXFKqXilVIJSak4d7yul1Pzq9/cppfrXeO8TpVS6UuqAUfGKhuvtt99mzpw5VFQY+03bZGf1hoNDOhszfmEyKKQ1Stn3QKUL8ff351//+hfNmzenuLjY8PpFw2BIwlBKOQPvA9cDYcB0pVRYrcuuB0KrH7OA/9R4bwkwzv6RioZux44dPP300xw+fBgnJ8c0kCMTs2nt6cYVfi0Mrbdlc1d6dvBmhwMShklycjLh4eEsW7bMYTEIxzHqEzcISNBaH9NalwJfADfVuuYmYKmusgPwUUr5A2itfwWyDIpVNFDp6elMnTqVwMBAPvnkE4ctKtubnE3fIPtuOHghQzv7sjspx/BxDBN/f3/8/f2ZNWsWsbGxDolBOI5RCSMASK7x/GT1a/W95qKUUrOUUlFKqaiMDPtvB90YVVRq5q2N494lkUSfyOJkdiH5JeWODuuSSktLmTJlChkZGXzzzTf4+Pg4JI7cojKOZhQYtmCvtiHV4xi7k+y/EWFdXFxc+PLLL/Hy8mLy5MlkZzsmjvqoqNQknikgOauQh5ZF89RXMWQXyMaKljBqa5C6vorVntRtzjUXpbVeCCwEiIiIkEnjdXhrQzz//eUYnm7ObDqUDkCbFm5899Bwglo3d3B0FxYZGUlkZCSffvopAwYMcFgc+wzacPBCIoKrxk32JOUw7Apj9syqzd/fn2+++YZrrrmGW265hXXr1tn1VENrFJSUc/fiSHYlVnVQuLk4UVmpSc4uZPl9Q3B2kq1P6sOoFsZJoOb0ikAgxYJrhBV+ik3j/S1HmTYwiG3PXsNTY7vy9+u6UVJeycOf77b7eQvWGD58OEePHmXq1KkOjcM0Q6mPgxJGy+audG7jafh6jNpGjBjBokWLKCkpIT8/36GxXIjWmmdW7CM6KZv7r+rM42NCWf/4lbwxpTe7jmex8Ndjjg6x0TGqhREJhCqlOgGngGnAX2pdswp4WCn1BTAYyNVay34ENpKaW8TTK/bRK8CbF2/qibuLM49cEwrAFX4teOCzaB75fA//75be+DR3c3C0f/j6668pKirijjvuICCgXj2UdrE3OYfOfp60bOa4b9R9g3z4LcG4jQgv5I477mDGjBk4OztTWVnpsEkIdSkpr+Bf6+JZvS+Vv1/XjdlXdzn3XohvczYdOs1bG+IZGdqGXgH23W24KTHkv7DWuhx4GFgPxAFfaa1jlVIPKKVM50GuAY4BCcAi4CHT/Uqp5cB2oJtS6qRS6l4j4m4qKis1T30VQ2l5JfOn9cPdxfm898f1as9z43uwJT6d2z/e5bAB1do2b97MzJkzWbRokcOm0NaktWavgRsOXkifIB8y8kpIyXX89FZnZ2fy8/MZO3YsS5cudXQ4QNV/p8eW7+WjrceZMbgjD1x1xXnvK6V4dVI4Ps3deOqrmAbdsm5oDPtKoLVeo7XuqrW+Qmv9avVrH2qtP6z+t9Zaz65+P1xrHVXj3ulaa3+ttavWOlBr/bFRcTcFn+9KYtvRTJ6/IYzOF5gKet+VnfnPjAHsP5XLG+sOGRzhn+3Zs4dJkyYRGhrKqlWrcHZ2vvRNdnYyu4gz+aX0c3DCOLcRocH7Sl2Ii4sLSinuuecevv/+e0eHw7e7T7EuNo2547vz6s3hdY5TtPJ0440p4cSfzuPdjUccEGXj1HDakMIuissqmL/pCIM6tWbqwIuv0h0T1o47hwaz+PdEth09Y1CEf3bgwAGuu+46WrVqxfr162nVypgdYS/FNG7QN8ix8fTwr96IMLlhzFDy8PDg+++/JyIigqlTp7Jp0yaHxZJTWMpra+Lo19GHv47ofNFrR3dvxy0DAln46zHSGkBrrTGQhNHE/RCTQnpeCY9fE2pWf/ec63sQ4tucud/ud1jX1IYNG3B1dWXDhg0NYtzCJCoxi2auznT3N25X3Lq4uTjRs4M3Mcm5Do2jphYtWrBmzRq6du3KxIkT2bJli0Pi+H/r48kpKuPVSeE4mTED6tHRoVRozWc7ThgQXeMnCaOJ+zIymc5+ngy9wrx9j5q5OfPazeEkZhby/pYEO0d3PtM4xRNPPMH+/fvp2rWrofVfSmRiNv2DfXB1dvzHpm+QD/tP5VJeUenoUM5p3bo1GzduZNiwYYacqV7b7qRsPt+ZxF3DQgjr4G3WPR19m3NN93Z8viupwYzdNWSO/80XdpOQnkfUiWymDQyq12yaYV3aMLlfAB/+cpSE9Dw7RviHHTt20KNHD/bs2QNU/fFpSM4WlxGXdvbcOghH6xvkQ1FZBfGnjfnvY6527dqxYcMGunfvXjVJYO9eQ+rNLynnmW/20d7bgyfG1u+Lxj3DQ8gqKGXVXpnFfymSMJqwT7edwNVZcXO/wHrf+9yEHni6u/Dst/sps/O32NWrVzN69GgqKyvx9jbvm6HRok9kozUMMujApEsxDXzvaSAD33X58MMPiYiIYNGiRXatp7JS88SXezl2poA3b+1DC/f6rRYYeoUv3dt78cnvx+WQqEuQhNFEnT5bzJdRyUzpH4ifl3u97/dt4c4LN4YRmZjNMyv22e2D9MknnzBp0iTCwsLYtm0bV1xxxaVvcoDI41k4Oyn6dfRxdCgAdGzdHF9PN3afaBgD33WZOXMmY8eOZdasWcydO5fKSvt88Xhn42E2HDzNPyb0YERo/Ve/K6W4Z3gnDqXlOWQn4MZEEkYT9d9fjlFRqXloVJdLX3wBN/cL5PExoXy7+xSf7UyyYXRVvvvuO+69915Gjx7Nli1baNu2rc3rsJWoxGx6dfCmuZtRa10vTilFREgrohpwwvDy8uKHH35g1qxZzJs3jxkzZth8a/QV0SeZvzmB2yICuWtYiMXlTOzbgdaebnz823HbBdcEScJogpIyC1m28wST+gbQ0de6/aEeHR3KqG5+vPzDQQ7buL98woQJvPnmm6xevdrQ87jrq6S8gr0ncxgY0jC6o0wigluTlFVIel7DnRLq4uLChx9+yOuvv84333xj0zGN3KIyXlgVy5DOrXllUrhVq949XJ25d0QnNh1K57cjsnHphUjCaGK01sz9bj+uzk787TrrZxk5OSneuq0vTk7wyVbrv33t27ePcePGkZmZiZubG0899RRubg1nK5K67DuZS2l5JQMbyPiFyYCQqvUg0YkNt5UBVa2hZ555hiNHjjBkyBAAjh49anW5X0clk19Szj8mhOHmYv2fsntHdCLEtznPr4yltLzhzD5rSCRhNDEb49LZmnCGZ8Z1w7+lbY4ube3pxqS+AXy/9xS5RWUWlaG15v3332fQoEHExMSQlGT7Li57iaze6TQiuGEsIDTp1aEl7i5ORDbwhGESEhICVK2z6datGy+88AJlZZb9PlVWapbtTGJAcCub7QXl4erMCxN7cvxMAUu3J9qkzKZGEkYT899fjhLg04zpgzratNzbhwZTXFbJ2xsO13sAPDMzk0mTJvHwww9zzTXXsG/fPvr162fT+OwpKjGbzn6e+Lao/+QBe3JzcaJPkA/RJxrX2WLDhg1j5syZvPTSS4wYMYLDhw/Xu4zPdp7g+JkCq8Yt6nJ1t7Zc1dWPdzcdIUvOzPgTSRhNSPSJLKJOZPPXkZ1wsfHisp4dWjJzSEeWbEtk6sIdpJ81v9/8iSeeYO3atbz99tusXr0aPz8/m8ZmT5WVmt1J2QxsIOsvaosIbkVsylmKShvPojNPT0+WLFnCV199xZEjR+jbty+LFy82696i0gruWxrF8ytjGdGlDRPCbb9A8B8TelBQUs5/f7W+26ypkYTRhPz3l2O0bObKbREX3zPKUi/f1IuXJ/Vi38kc7lwcScFFTupLTU0lObnqAMXXXnuNyMhIHn/8cYdux22JY2fyySksY0AD644yiQhpRXmldvj5GJa49dZb2b9/PyNHjqSkpOSS11dUah5ZvptNcaf5+3Xd+OSugWZt/1Ffoe28GNerPct3Jl30d/xyJAmjiTiakc+GuNPcMTQYz3ouXDKXUorbhwTz4cwBxKedZd7auD9dU1ZWxoIFCwgLC+PBBx8EIDAwkD59+tglJnuLrp622r+BJowBHataPlGJjatbyiQgIIB169Yxa9YsAJYsWcLcuXMpLCz807Wf70piY1w6L9zYk9lXd7HJQPeF3DuiE2eLy1mx+6Td6miMJGE0clprth/N5PEv9tLc1Zk7hobYvc5R3doyfVBHvoxMJjnrjw/2Tz/9RN++fXn00UeJiIjgrbfesnss9hZ9Ihuf6lPuGqKWzV3p2q5Fg16PcSlKqXOHL8XExDBv3jzCwsJYtmzZucV+RaUVLNh0hIEhrbhjaLDdY+rfsRV9g3xYsDmBHbKY7xxJGI3EqZwi7lsaRa8X1jP+3d946YeDJJ4pYO53+5m+aAencop489Y+Fq3qtsTDo7uglGL+pqqzBJYuXcp1111HSUkJK1eu5KeffmpwmwdaIvpENv07trJL14etDAhuze6kbCoqG/+2Fm+//TY///wzvr6+zJw5k4iICLZv387ibcdJzyvh6XHdDenWVErx/I1hOCvFtIU7+OunkWQVlLJ8VxIT5v9Gv5d+YsZHO4hLPWv3WBoSSRiNQHJWITMW7WBbwhnGh7enjZc7S7cnMurNn1m+K5n7r+zMb09fzfV2GAC8EP+WzbjOv4TPftjIwZSzTJw4kTfffJPY2FgmTpzY6MYq6pJdUMrRjIIGO35hMjCkFXnF5TZfWOkoV111FZGRkXz22WdkZWWRfDqTD7YcZXRoa0OnNvfv2Iqf/z6KOdd3Z0t8Bv1f3sCz3+7H2UlxXc/2HErNY+p/t7PvZI5hMTmc1rpJPgYMGKCbguMZ+XrAyz/p8BfW6egTWedeT84q0B9sSdC/H8kwPKbff/9dT548WSultGfHnvrGBb/p0vIKw+Owt01xaTr4mdV6+9Ezjg7lok6cKdDBz6zWS7cnOjoUmysuLtaPLt+tu8z9Ud83+zE9fPhwvWbNGl1ZWWloHAdO5eh3NhzWa/ennKs7OatAD5u3SXf7xxr96+F0Q+OxJyBKX+DvqrQwGrCMvBLu+GQXFZWabx8aTv+Of3y7CmzVnAdHXcGwLvXfbM1SGzZsYOjQoQwfPpwtW7Ywd+5cPl72FftO5vL8ygPn1mdorTmakc/ps8WNevfP6BPZODsp+gT6ODqUiwpq3Qw/L3eiG+nAN1TNgEpIzyM2JZfKGl1rX+5OZeXeFGZf3YXB/cJJSkpi/Pjx9OnTh4ULF1JQUGBIfD07tOSxMaGM6+V/rvUc2Ko5388eToivJ/f/L5o9SY13HMlcDWMntctAZaXm811JzN905FyTdmRoG9xcnHBWioBWzQj2/WNgNb+knHuWRJKeV8zy+4bQpW3dZ3HbW2JiIq1bt8bb25ukpCTS09NZsGABd999N56eVfGeyHfig5+PojVM7NOBj7YeZ/OhdADatHBjSGdfnrq2G50a6MDxhUSfyCbM35tmbo4/T/xilFIMDGnVaFZ813Qmv4Sl20/wxa4k0vOqptb2CvDm8Wu6En86j3//FM+YHm15ZHQozk5duf3221m2bBnvvvsu999/P1FRUSxcuNBh8ft5ubP03kHc8p/t3L0kkm8eGEqXtn/si5ZXXMbOY1lUak1+STmZ+aWs3p/K8Yx8pgwI5Klru9V7O3ZHUo35G+DFRERE6KioKEeHgdaa6BPZvLXhMNuOZjIguBVtvdzZdCj9vP1qnBRc06MdJzILOJVdhIerMzlFZSy8fQDX9GhnaMyFhYWsXr2ajz/+mA0bNvDuu+/yyCOPUFZWhpOTE87O5/8B1Vrz+tpD/PfXYwB4uDrxyOhQmrs5E5tylnUH0iguq2DmkGAeGd2lwa2Yrkt5RSW9X/yJWwcE8uJNvRwdziV9vPU4L68+yPZnR9tsSxh7Mp01/9HW45RVVDK6W1uu69WekvJK3tt8hNNnq5LHVV39+GBG/z9NFddas23bNnx9fenevTvR0dE8/PDD3HHHHUydOtXwA7hOZBYw5T/bKauoxMO16kvg4M6+bDx4mrxaazlC27YgtF0L1h5Io52XBy/e1JNrw9o1mHE/pVS01jqizvckYdjP8TMF/OP7/fyekIlPc1eeurYbMwd3RClFblEZCen5VGpNeYVmfWwaX0clE9rOiw4+HpSWa+4ZHmJol1NFRQV33HEHK1eupKCggI4dO3L33Xdzzz330LHjpbcaSTxTwKmcIrq0bUE7b49zr6fnFfPOxiN8GZmMl4cLL07syU19G85Z3XWJTcllwvytvDO1L5P6NexYAfafzOXG97Yyf3o/Jvbp4OhwLurn+HRe/OEgx88UMLlfALNHd+EKvz9a0Pkl5ew6nknnNi0I9m1u1h/SzZs389hjj3HgwAHc3NyYMGECU6ZM4ZZbbsHd3ZgvKEdO5/HOpiPkF5fjpGDn8SxGdfPjL4OC8WnuSgt3F7ybudKquStKKfYkZfPst/s5lJZHzw7e/POGMIZ0Nu8oZXuShGGAotIKVu9L4auoZNLzSvB0c+FIeh4ers48PqYr0wcFNZizFEzS09NZt24dKSkpzJkzB4Cbb76Ztm3bcttttzFq1Kg/tSascfh0HnNW7GN3Ug5/HdGJf9wQZrOybe3DX47y+tpDbJszmg4+Df8be3lFJX1e/ImJfQOYNznc0eFc0LKdJ3juuwN09vPkpYm9LDrw6EK01sTExLB06VKWL19OQUEBGRkZuLu7ExkZSUBAAB06NKxkWlZRyddRJ/nPLwkkZxXRJ8iHykpNeaXmyq5tmDEo2OojCupLEoad/RCTwj++P0BuURlX+HnSs0NLzhaXEdSqOY+M7kLbGt+2He3AgQN89dVXrF27FtP/P0FBQRw5csSQb2LlFZW8tPogS7ef4I0p4UwdaNtNEm3ltg+3k1dSztrHRjo6FLPNXrabncez2Dn3Gpwb4LqRoxn5TJj/G4M6+bLojgG4u9hvbKiyspKjR48SGhoKQK9evYiNjaVnz56MHTuWsWPHctVVV50bh3O0wtJy/vPzUSITs2ju5kJJeQU7jmWhgDuHhfC3a7sZNpZ2sYTh8Omv9noYMa22pKxC/9+qAzr4mdX65ve36u1Hzxg+3e9i8vPz9YYNG/Q///lPnZmZqbXW+vXXX9dOTk562LBh+uWXX9ZRUVG6osLYKbHlFZV65kc7dOhza/SepGxD6zZHXGquDn5mtV6w6bCjQ6mXH2JO6eBnVuu1+1McHcqfZOaX6Kv+32bd98X1Oi23yPD69+/fr9944w09ZswY7e7urgE9depUrbXWlZWVes2aNTojw/gp6heTlluk56yI0cHPrNYT5v+q088WG1IvF5lWKy0MC+UVl3HPkkgiE7O5Z3gnnh3fHVcb7xBricOHD/PGG28QHR3NgQMHqKiowMnJiY0bN3L11VeTmVm1zYGvr2P7SrMKSrnp/a3kFJZx97AQ2rX0YGpEkM132a2v4rIKJr3/O6fPFrPpqVG09mzYhzvVVF5RyfXv/kZhaQXrHh+Jl4ero0Ni7f5UtiacISoxm+OZBSy/bzADHLzzb1FREVu3bsXb25vBgwdz4sSJc2d1dO3alaFDhzJgwAAmTJhA586dHRorwKa40zz8+R78vNxZ9tfBBLW2bxeVdEnZWHFZBXct3kVUYjZvTe1r6CBjRUUFR44cIS4ujoMHDxIXF8eBAwd44oknuPPOO4mNjWXUqFEMGDCAiIgIRowYwbBhw/D29jYsRnOl5BRxz5JIDqVVrVAe3b0tH84cYNdN5S7lpR8O8snvx1l810Cu7t5wzxi/kOgT2dz64Tam9A/kX7c6bsNHrTUf/nKMN9Ydws3FCZ9mrrx6czhjw4yd8WeO0tJSdu3axbZt2849MjIyWL58OdOmTSMmJoaXX36ZPn360KNHD7p3706XLl3w8DCuq3lPUjZ3LY6khbsLX94/hMBW9ksakjBs6PTZYh77Yg87j2fxztS+dpntU1RURGJiIsePHz/36NevHzNmzCArK+u81kHHjh3P7Qw7ceLEcwvlGsoUvUvRumqA74tdSfxzZSyDOrXmgas6E+bfknbe7ob+HOl5xQx5bRPTBnXktZsb7sDxpfxr/SHe33KUL2YNMXzWzZn8EnYey+K7PSfZGJfOjX068O9b+zj0S0B9aa1JSUnB29sbLy8vNmzYwEMPPURCQsK5a5RS7Ny5k4EDB7J79262b99OcHAwISEhBAcH2+WM+gOncvnLoh34ebnz0Z0D7bau6WIJo2FN22ng1h1I45kV+ygpr+Ct2/pYnCzi4+NJTk4mJSWF1NRUUlJS6Ny5M4899hgA/v7+5Obmnrvew8ODBx98kBkzZtC6dWuWL19Oly5d6N69Oy1anL+gr7EkChOlFK7OituHhtDczYUXf4jlniVVid7TzZl2LT0Y3Kk1I0P9OJR6ltsGBtnt29WP+1Kp1HC3jU9xM9ojo0P5dNsJvtt9ym4Jo6CknCXbEvH1dCM5u5C1+9PIyC8hr7hqzUELdxeevb47943s3KA3bqyLUoqAgD8+22PHjuXIkSPk5+dz+PBh4uPjiY+Pp0uXLgD8+OOPPP/88+eV0apVKw4dOkTbtm1Zt24de/fuxd/fn/bt2597tG3btl6f114BLVl0RwQPfBbNDfN/47XJ4YZPTzeshaGUGge8CzgDH2mtX6/1vqp+fzxQCNyltd5tzr11sWULQ2vNx1uP8+qaOHoHtOTft/amXXNFXl4eeXl55OTkUFZWxogRIwBYuHAhMTExZGZmnnt06tSJFStWABAeHs6BAwfOle/l5cXEiRP57LPPAPjvf/+Lp6cnnTp1olOnTrRv3/7c9s9NXVFpBftO5hB/Oo9jGVXrOn6OT6esour3tE0Ld/537yB6+Nu+i+3mD36nqLSCdY9fafOyjfbkl3vZGHeayH+MsflsJNMuBLuO/7EVyVVd/ejYujkdfJoxpHNregW0bBBjekaorKwkLS2NEydOnHskJSUxf/58nJ2defTRR1mwYMF597i4uFBSUoKTkxOvvPIKv/76K61btz738Pf3Z/bs2UDVuGR5eTmtWrXCy8uL3DInHvtiL1EnsrlrWAjPTehh0/+vHd4lpZRyBg4DY4GTQCQwXWt9sMY144FHqEoYg4F3tdaDzbm3LtYkjCVLlrB582Zyz57lWMoZUjKyKCjV3PX6Z7w9tS8zpt3Kd999d949wcHBJCYmAjB+/Hh27tyJr6/vuUd4eDjz5s0D4Oeff8bJyQl/f3/8/f3/1EoQ5zuRWcDh0/l08PHg3iVRFJSWc9ewEDzdXfD1dOOWAYFWt6ySswoZ+f+28PS4bjw0qouNInecLfHp3L04koW3D+Danu2tLm/jwdPsSc6mZTNXPtmaSHpeMe9O60enNp64OCu6t294Y2QNSX5+PqdPnyYtLY20tDRyc3O55557AHjppZdYu3YtWVlZ5x6BgYGcOHECgOuvv55169adK0spRb/+/Zn4wqcs/j2Ros3/wac8kys6tKG1T0t69+7N008/bXGsDSFhDAX+T2t9XfXzZwG01vNqXPNf4Get9fLq5/HAKCDkUvfWxdKEkX62mEef+Bub1q6kCDfKnTxo5ulJWKcAtq79FqUU3377LUePHj3Xx9myZUvatm3LwIEDqY6t0XUNNRbJWYW8sCqWLfHpmH51/zGhB38dad1slve3JPCv9fH89vTVdp+FYoSyikoGv7aJYVf48t5f+ltV1g8xKTyyfM+55z07ePPSTb0a/LbvjVVlZSWFhYXnvkju3LmTxMREsrKyyMvLIz8/Hx8fH5588km2HErn/odmk3TkIKqsmOaqjJ69e/Ppsi8tHuNoCGMYAUByjecnqWpFXOqaADPvBUApNQuYBZi1lUVdfj96hh1+1+N5x/X0b+fF0+O6Mbr7+X2NkydPvmgZkizsJ6h1cz65ayAZeSVoNM9/H8sb6w5xTY92Fn9ASssr+SoqmUEhrZtEsgBwdXZiQrg/X0cnk11QSisLpwefLS7jxR9i6Rvkw5K7B5JXXE5gq2byO25HTk5O5/U6DB48mMGD6/yTx9Xd23J489ccOJXLf34+ypoDqSRp+G73SZ68tpvNYzMqYdT121W7aXOha8y5t+pFrRcCC6GqhVGfAE2uDPXju4eG4evpTlBr+WA0VKaTBV+a1JPf/pXBqz8e5KM7B9a7nLKKSp79dj8nMgv554SGu1WJJWYOCeaLyCSe+Govi+6IsKif+73NCWQWlLL4rkH4NHfDp3njWZdyOekV0JL3Z/Tn9NliUnKKztvLzZaMGpU6CQTVeB4IpJh5jTn32oxvC3f6dWxFRzM3PROO1dbLg0euCWVjXDq/HM6o171aa55feYAVu0/y6DWhjGmAawSs0a29F/83sSc/x2fw+Bd7Ka+ovPRNNRw/U8Di349zS/9AwgNb2ilKYUvtvD3o17GV3fY/M6qFEQmEKqU6AaeAacBfal2zCnhYKfUFVV1OuVrrVKVUhhn3isvY3cND+GJXEi/9EMuPj47Ew/XPs4K01vy4P5W1B9KICK76QP1yOIPlu5J5aNQVPDm28Z8/XpcZg4MpLKng1TVxlFdWckPvDrg6O7E+No0+gS2ZPrhjnbOoissqeGFVLG7OTvx9nO27NkTjZOS02vHAO1RNjf1Ea/2qUuoBAK31h9XTat8DxlE1rfZurXXUhe69VH0NYXtzYZzNh05zz5Io/Ft6cFtEELdGBNLO24PfE87wQ0wqh9LOEptyFi93l/POJ5g2MIhXbw5vkJv12dKHvxzlX+vjqag+zc7dxYmS8koCfJoxNqwdw7u0YegVvpRXVPJFZDL/236CUzlF/N+NYdw1vJODoxdGcvgsKUeQhHH5+eVwBh/9doytCWfQumrhX0FpBZ5uzoQHtmRcz/bMHBLMrsQsPFydae7mTLd2XpdN1+PJ7EIy80vJKixlSCdfok5ksWBTAvtP5VJUVgGAs5OiolIzpHNrHhzVhau6+jk4amE0SRjispKcVcj3e05xKqeIMT3aMayLb4M7i6QhKS6rICoxm5/j03F2VtzUJ4CwDrKu4nIlCUMIIYRZLpYwLo+1+0IIIawmCUMIIYRZJGEIIYQwiyQMIYQQZpGEIYQQwiySMIQQQphFEoYQQgizSMIQQghhlia7cK9608ITjo7DAm2AM44OwmDyMzd9l9vPC433Zw7WWte5J0yTTRiNlVIq6kKrLJsq+Zmbvsvt54Wm+TNLl5QQQgizSMIQQghhFkkYDc9CRwfgAPIzN32X288LTfBnljEMIYQQZpEWhhBCCLNIwhBCCGEWSRgNmFLqb0oprZRq4+hY7Ekp9S+l1CGl1D6l1HdKKR9Hx2QvSqlxSql4pVSCUmqOo+OxN6VUkFJqi1IqTikVq5R6zNExGUUp5ayU2qOUWu3oWGxFEkYDpZQKAsYCSY6OxQAbgF5a697AYeBZB8djF0opZ+B94HogDJiulApzbFR2Vw48pbXuAQwBZl8GP7PJY0Cco4OwJUkYDdfbwNNAk5+VoLX+SWtdXv10BxDoyHjsaBCQoLU+prUuBb4AbnJwTHaltU7VWu+u/nceVX9AAxwblf0ppQKBCcBHjo7FliRhNEBKqYnAKa11jKNjcYB7gLWODsJOAoDkGs9Pchn88TRRSoUA/YCdDg7FCO9Q9YWv0sFx2JSLowO4XCmlNgLt63jrOWAucK2xEdnXxX5erfXK6mueo6oLY5mRsRlI1fFak29BAiilWgArgMe11mcdHY89KaVuANK11tFKqVEODsemJGE4iNZ6TF2vK6XCgU5AjFIKqrpndiulBmmt0wwM0aYu9POaKKXuBG4ArtFNd3HQSSCoxvNAIMVBsRhGKeVKVbJYprX+1tHxGGA4MFEpNR7wALyVUp9prWc6OC6rycK9Bk4plQhEaK0b466XZlFKjQPeAq7SWmc4Oh57UUq5UDWofw1wCogE/qK1jnVoYHakqr71fApkaa0fd3A4hqtuYfxNa32Dg0OxCRnDEA3Be4AXsEEptVcp9aGjA7KH6oH9h4H1VA3+ftWUk0W14cDtwOjq/7Z7q795i0ZIWhhCCCHMIi0MIYQQZpGEIYQQwiySMIQQQphFEoYQQgizSMIQQghhFkkYQgghzCIJQwghhFlkaxAhDKKU8gZ+Adyo2v7lMFAMDNNaN6lN6kTTJAv3hDCYUmoQVZsuNumtzUXTI11SQhivF9DUtwQRTZAkDCGMFwYccHQQQtSXJAwhjNcBaLRb1YvLlyQMIYy3HvhYKXWVowMRoj5k0FsIIYRZpIUhhBDCLJIwhBBCmEUShhBCCLNIwhBCCGEWSRhCCCHMIglDCCGEWSRhCCGEMMv/B2O74aYJVdGvAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Correlation function for the output\n", + "# r_Y = sp.signal.correlate(Y, Y) * dt / Tf\n", + "# tau = sp.signal.correlation_lags(len(Y), len(Y)) * dt\n", + "tau, r_Y = ct.correlation(T, Y)\n", + "plt.plot(tau, r_Y)\n", + "plt.xlabel(r'$\\tau$')\n", + "plt.ylabel(r'$r_Y(\\tau)$')\n", + "\n", + "# Compare to the analytical answer\n", + "plt.plot(tau, [r(t)[0, 0] for t in tau], 'k--');" + ] + }, + { + "cell_type": "markdown", + "id": "2a2785e9", + "metadata": {}, + "source": [ + "The analytical curve may or may not line up that well with the correlation function based on the sample. Try running the code again from the top to see how things change based on the specific random sequence chosen at the start.\n", + "\n", + "Note: the _right_ way to compute the correlation function would be to run a lot of different samples of white noise filtered through the system dynamics and compute $R(t_1, t_2)$ across those samples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd5dfc75", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 7821e2b62ad45661a699e1aaf63abcc37a0e12d8 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 18 Mar 2022 08:15:15 -0700 Subject: [PATCH 44/87] add jupyter notebooks to documentation + updated notebooks --- doc/examples.rst | 3 + doc/kincar-fusion.ipynb | 1 + doc/pvtol-outputfbk.ipynb | 1 + doc/stochresp.ipynb | 1 + examples/kincar-fusion.ipynb | 130 +++++++++++++++++++++++--------- examples/pvtol-lqr-nested.ipynb | 2 +- examples/pvtol-outputfbk.ipynb | 81 ++++++++++++++------ examples/stochresp.ipynb | 86 +++++++++++++-------- examples/vehicle-steering.png | Bin 0 -> 13510 bytes 9 files changed, 212 insertions(+), 93 deletions(-) create mode 120000 doc/kincar-fusion.ipynb create mode 120000 doc/pvtol-outputfbk.ipynb create mode 120000 doc/stochresp.ipynb create mode 100644 examples/vehicle-steering.png diff --git a/doc/examples.rst b/doc/examples.rst index 89a2b16a1..0f23576bd 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -44,6 +44,9 @@ using running examples in FBS2e. cruise describing_functions + kincar-fusion mpc_aircraft steering pvtol-lqr-nested + pvtol-outputfbk + stochresp diff --git a/doc/kincar-fusion.ipynb b/doc/kincar-fusion.ipynb new file mode 120000 index 000000000..def600898 --- /dev/null +++ b/doc/kincar-fusion.ipynb @@ -0,0 +1 @@ +../examples/kincar-fusion.ipynb \ No newline at end of file diff --git a/doc/pvtol-outputfbk.ipynb b/doc/pvtol-outputfbk.ipynb new file mode 120000 index 000000000..ffcfd5401 --- /dev/null +++ b/doc/pvtol-outputfbk.ipynb @@ -0,0 +1 @@ +../examples/pvtol-outputfbk.ipynb \ No newline at end of file diff --git a/doc/stochresp.ipynb b/doc/stochresp.ipynb new file mode 120000 index 000000000..36190a54c --- /dev/null +++ b/doc/stochresp.ipynb @@ -0,0 +1 @@ +../examples/stochresp.ipynb \ No newline at end of file diff --git a/examples/kincar-fusion.ipynb b/examples/kincar-fusion.ipynb index 04a1a968d..d8e680b81 100644 --- a/examples/kincar-fusion.ipynb +++ b/examples/kincar-fusion.ipynb @@ -5,20 +5,17 @@ "id": "eec23018", "metadata": {}, "source": [ - "# Kinematic car sensor fusion example\n", + "# Discrete Time Sensor Fusion\n", "RMM, 24 Feb 2022\n", "\n", - "In this example we work through estimation of the state of a car changing\n", - "lanes with two different sensors available: one with good longitudinal accuracy\n", - "and the other with good lateral accuracy.\n", + "In this example we work through estimation of the state of a car changing lanes with two different sensors available: one with good longitudinal accuracy and the other with good lateral accuracy.\n", "\n", - "All calculations are done in discrete time, using both the form of the Kalman\n", - "filter in Theorem 7.2 and the predictor corrector form." + "All calculations are done in discrete time, using both a Kalman filter formulation and predictor-corrector form." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 17, "id": "107a6613", "metadata": {}, "outputs": [], @@ -29,6 +26,7 @@ "import control as ct\n", "import control.optimal as opt\n", "import control.flatsys as fs\n", + "from IPython.display import Image\n", "\n", "# Define line styles\n", "ebarstyle = {'elinewidth': 0.5, 'capsize': 2}\n", @@ -43,12 +41,29 @@ "source": [ "## System definition\n", "\n", - "### Continuous time model" + "We consider a bicycle model for an automobile:\n", + "\n", + "\n", + "\n", + "### Continuous time model\n", + "The dynamics are given by\n", + "\n", + "$$\n", + " \\begin{aligned}\n", + " \\dot x &= \\cos\\theta \\, v, \\qquad\n", + " \\dot y &= \\sin\\theta \\, v, \\qquad\n", + " \\dot \\theta &= \\frac{v}{l} \\tan\\phi,\n", + " \\end{aligned}\n", + "$$\n", + "\n", + "where $(x, y, \\theta)$ are the position and orientation of the vehicle, $v$ is the forward velocity, $\\phi$ is the steering wheel angle, and $l$ is the wheelbase.\n", + "\n", + "These dynamics are included in the file `vehicle.py`:" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 18, "id": "a04106f8", "metadata": {}, "outputs": [ @@ -75,9 +90,17 @@ "print(vehicle)" ] }, + { + "cell_type": "markdown", + "id": "e8ae0344", + "metadata": {}, + "source": [ + "This system is differentially flat and so we can define a trajectory for the system using the `flatsys` module. We generate a motion that corresponds to changing lanes on a road:" + ] + }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 19, "id": "69c048ed", "metadata": {}, "outputs": [ @@ -125,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 20, "id": "2469c60e", "metadata": {}, "outputs": [ @@ -133,7 +156,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Object: sys[2]\n", + "Object: sys[6]\n", "Inputs (2): u[0], u[1], \n", "Outputs (3): y[0], y[1], y[2], \n", "States (3): x[0], x[1], x[2], \n", @@ -186,13 +209,13 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 21, "id": "0a19d109", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -238,12 +261,14 @@ "id": "c3fa1a3d", "metadata": {}, "source": [ - "## Linear Quadratic Estimator" + "## Linear Quadratic Estimator\n", + "\n", + "To estimate the position of the vehicle, we construct an optimal estimator (Kalman filter)." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 22, "id": "993601a2", "metadata": {}, "outputs": [ @@ -251,7 +276,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Object: sys[3]\n", + "Object: sys[7]\n", "Inputs (6): y[0], y[1], y[2], y[3], u[0], u[1], \n", "Outputs (3): xhat[0], xhat[1], xhat[2], \n", "States (12): xhat[0], xhat[1], xhat[2], P[0,0], P[0,1], P[0,2], P[1,0], P[1,1], P[1,2], P[2,0], P[2,1], P[2,2], \n" @@ -266,7 +291,6 @@ "# Disturbance and initial condition model\n", "Rv = np.diag([0.1, 0.01]) * Ts\n", "# Rv = np.diag([10, 0.1]) * Ts # No input data\n", - "# \n", "P0 = np.diag([1, 1, 0.1])\n", "\n", "# Combine the sensors\n", @@ -277,15 +301,25 @@ "print(estim)" ] }, + { + "cell_type": "markdown", + "id": "d9e2e618", + "metadata": {}, + "source": [ + "Finally, we estimate the position of the vehicle based on sensor measurements. We assume that the input to the vehicle (velocity and steering angle) is available, though we can also explore what happens if that information is not available (see commented out code).\n", + "\n", + "We also carry out a prediction of the position of the vehicle by turning off the correction term in the Kalman filter." + ] + }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 23, "id": "3d02ec33", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -337,15 +371,23 @@ "plt.xlabel(\"Time $t$ [s]\");" ] }, + { + "cell_type": "markdown", + "id": "9f9e3d59", + "metadata": {}, + "source": [ + "More insight can be obtained by focusing on the errors in prediction:" + ] + }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 24, "id": "44f69f79", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXwAAAD8CAYAAAB0IB+mAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAxbUlEQVR4nO3deZQU5dX48e9lAAEFUUFlMSLKqmyCoBKRCAioETQa4edCjBGNGy7R6GuUJh6jJr4ak1cliAQ8QYkxGonBLRrFXRYHFFBBVBiQRWQJLqz398ftsnvanqWnq6d7uu7nnD7V1VVT9TBM3376PpuoKs4554pfvXwXwDnnXO3wgO+ccxHhAd855yLCA75zzkWEB3znnIsID/jOORcRoQR8ERkmIh+IyDIRuT7N8bNFZGH88bqI9Ajjvs4556pPsu2HLyIlwIfAEKAMmAOMVtXFSeccCyxR1Y0iMhyIqWq/rG7snHMuI2HU8PsCy1R1uapuB2YAI5JPUNXXVXVjfPdNoG0I93XOOZeB+iFcow2wMmm/DKis9n4B8HRFB0VkLDAWYM899+zduXPnGhVq9Wpo3bpGP+qcc3XWvHnzPlfVlumOhRHwJc1rafNEIvIDLOB/v6KLqeokYBJAnz59dO7cuTUqVCxmD+ecixIR+bSiY2EE/DLgoKT9tsDqNIXoDkwGhqvqhhDu65xzLgNh5PDnAB1E5BARaQiMAmYmnyAi3wMeB85V1Q9DuKdzzrkMZV3DV9WdInIZ8CxQAkxR1UUicnH8+ETgZmA/4D4RAdipqn2yvbdzzrnqCyOlg6rOAmalvDYx6fnPgJ+FcS/nnHM14yNtnXMuIjzgO+dcRHjAd865iPCA75xzEeEB3znnIsIDvnPORYQHfOeciwgP+M45FxEe8J1zLiI84DvnXER4wHfOuYjwgO9qbO1aOP106NYN7rkHtmzJd4mcc5XxgO9qZNYsOOggeOIJeO89uPJK2HdfGDcOVqyo+XV37oRVq2DOHHjySXj//dCK7FzkecB3Gdm61YL6ySdD584W7FXh7beha1f4wx/g4INBBMaPr/51N2+GUaNgjz2gbVvo2xdGjoQuXexaP/O5Vp3Lmgd8VyVVq3H37g1Nm1pQBzj1VDj8cHt+1FGW3kk2YwasWVP19efPhwMPhL/+FXbvttdapqzI+eCDMGgQlJZm9U9xLtI84LtKlZZaoO/bFxYuLH+sfspqCrGYfTjs2gV33w2ffgpHHAGPPpr+2qrwpz/BscdCixbw2mv2miqsW5d4vmULtG8PL74IvXpZjf/443Pxr3WuuIUS8EVkmIh8ICLLROT6NMc7i8gbIrJNRH4Rxj1d7nXvbgH2nXdsv1+/RBBWrXiR+Hr1LKffrRts2ABnnWVB+thj7bgqjBlj5118MWzbBqNHJ46natoUPvrIPgSGDrXXWrf2RmLnMpV1wBeREuBeYDjQFRgtIl1TTvsCuAK4M9v7udrx5z/D4sUW9MvKLEi/+mpm15gzB3bsgNtug4YN4cMP4c47YcAAeOih8uc2aVL19Vq2tMbiW2+1bw29e8PkybBxY2blci6qwqjh9wWWqepyVd0OzABGJJ+gqutUdQ6wI4T7uRxShQkT4Kc/hRNOgFdegTZtan69+vXh+uvtW0KjRnDttYkPjhtvrPrbQqp69eB//se+PSxbBhdeaL2DWrSAv//d0knOufTCCPhtgJVJ+2Xx12pERMaKyFwRmbt+/fqsC+eqTxX6908E3+efh7vuCufaXbvah0iy1DaATJSWWgPvnDlw9NGWOjrjDLvmj37kgd+5dMII+JLmNa3pxVR1kqr2UdU+LVO7aricUYWrroI33sjdPX796+q1AVSXCPTpk8jrBx5/3BqL77kHZs60D4VVq/xDwLkwAn4ZcFDSfltgdQjXdbVEFa64wgLkuHFWcw4rKNeGoHdQ0EPozDNtwNaVV8KIEdbDqG1bq/2LWBfSBx6wlJBzURJGwJ8DdBCRQ0SkITAKmBnCdWtdLGYBIXjUhWCXrd274dJL4f/+D665xrpTSrrvbHVEvXqWPkoWjBUIvPMOjB0LHTrYv7V//0SXUOeKWdYBX1V3ApcBzwJLgEdVdZGIXCwiFwOIyIEiUgZcDfxKRMpEpFm29w7bmWdawAAL9sUe8HfvtgbZ+++3/f/9X/jBD/JbpjAk1/hVE6OBg8eNN5Y//4034Pvft//7qHzQu2gKpR++qs5S1Y6qeqiq3hp/baKqTow/X6OqbVW1mao2jz8vuF7UZ5yRGOkZi3135GhdcMUV5b+lpAa3wK5d1oi6Zg386leJNM5LL9VqcfNiwoTyHwDXXVf++NKl5fd37vT8vysOPtI27s03Le97yy02EOj44+Ff/7Kv+nXBokVw9tmWmkl2//3w1FPlX9u5E847D6ZNs+B3yy11O42Trdtvt8C/cKF193z4Yevpc+qp1ve/QYNE/t9r/64u84CPvdlvuAH2398a+ho2tJ4e7dpZo9/TT8MXX+S7lBW74grrlfLww/ZvueYa277wgs1R88MfWkNlv37QqZO99vDD8JvfwM0357v0haNbN2je3J4//jj885/w1Vflz/GavqvLPOBj/c1feslSG3vtZa/tu69199uwAU46Cfbbz4LByJGFN8An9VtI8G844QQbkAQwd67NaPnxx/ZvAhvANHBgrRWzTpg9u3y658svbTtjhh0v5A9+56qSxdCX4rB7t9Xu27WznhvJ9t23/P4ee9gc7U8+afunnAJ/+5uNIM2XVavg3Xfh5z+H++777vHZs2u/TMXorLNg3jz43e+s7//55+e7RM5lLtIBPxazHHbgttvK52hTe+qknv/UU9C4sT0fPz4/+d277rIPrWuvrf17R81vfmNTOW+98Eq2/F8pzZrmu0Su2j75xKZvDey9ty3CUNP9ZLm8VsiBJfIB/6OP4B//gE2boKSk6vOD3/348TZyNPD11zkpYqU2bICJE22myUMOqf37R039+nD5179lw64tLJu/hV68w7Y99qbRtgIIDH6tqvcPPti27dolXvvkk5rvZ/OzVV3rm2+sJ0nYVLVgH71799aaGj++euckZ2yr8zOpXn9dtWFD1UGDVHfsyPzns3HzzVbu996r3ftG2X+OH6/vcriu5kBV0I8PPr78H9HxKfuVHct0369Ve9cOgkFqUEjer+xYpvtVnZsBYK5WEFNrLXjX5JHrgP/116olJao33ljj26iq6pQp9pu84orsrpOJLVtUmzdXHTmy9u7pXGSk1gbz9SFXg8BfWcCPdEpn0SLrbdOzZ3bXOf98azi9+24b1n/RRaEUrxxVW/Djm2/sMWWKpaFuuCH8ezkXeUU61D7S3TKD9VGzDfiQ6Ap58cU2QGf06MSo3Wzs3g0//rEN+2/e3PrQt2uXaD/o168o/y6dczkQ+YC/1162Xmq26qX8JmfMgC5drNtmTajCM8/YgKnUa3TsWLNrOueiLfIBv0eP7wbrmkiesGvHDhua/+GHVjuvaEi+asXXqlcPhg+3boAjRth0CMH1P/igfOLPa/jOueqIbMDfvRsWLAgnnZOqfn2b6iDZiy8mAvyWLTZXTzA7Y+oHwvz55X+2Z8+qu4w651xVIhvwP/4Y/vvf3AR8SNT4d+6ECy6wtWEvv9wadtu3/+4I2ODDYPZsS+UMHWrfFLwG75wLS2QDfpgNtpUpKbHVlY45Bu69F66+2gZMXXih9RAaN87OW7HCpuU9/XT7QJgxI7s1X51zLlXRB/yKVrEqLbVgnLoaUi6IwJAh5V9r3dpSOnffbdM1TJ1qszXu3m2zNAazNjrnXFhCCfgiMkxEPhCRZSJyfZrjIiJ/iB9fKCJHhnHf6rjkEpg+3WaGTE6PlJZC586JuXByLXXRjaAcIonum9u2wcaNVl7nnAtb1kkDESkB7gWGYAuazxGRmaq6OOm04UCH+KMfcH98mxM7d1pPltNOswnOdu6E444rf05pqTWcOudcVISRJe4LLFPV5QAiMgMYASQH/BHAQ/Fhv2+KSHMRaaWqn4Vw/3K2brVFPlavtgVNxo2z3jivvmq9Y5o1g88/h7Ky3Ofvq6tIB/U55wpMGCmdNsDKpP2y+GuZngOAiIwVkbkiMnf9+vUZF2avvaxXzFlnWVC/80649VabjmDSJDtnwQLbFkrAd8652hBGwE+3GmrqkKLqnGMvqk5S1T6q2qdly5Y1KtCvf235+QYNrObcL548uvZaW9Uq6KHTo0eNLu+cc3VSGAG/DDgoab8tsLoG5+RE0B/++edt/3vfs4Dfpo0tUO2cc1ERRsCfA3QQkUNEpCEwCpiZcs5M4Lx4b52jgc25yN9XZtAgW5rut7+1peo8neOci5qsG21VdaeIXAY8C5QAU1R1kYhcHD8+EZgFnAQsA74Can1FUBGbSvhHP7L900+v7RI451x+hTKWU1VnYUE9+bWJSc8VuDSMe2UjaKwFa8itX997xzjnoiNSg/cnTLApi885x+bSOeigqn/GOeeKRaQCPsCoUbBwoQd751z0FP1cOsmCeXVuu63iOeqdc65YRaqG7yNanXNRFqkavnPORZkHfOeciwgP+M45FxEe8J1zLiI84DvnXER4wHfOuYjwgO+ccxHhAd855yLCA75zzkWEB3znnIsID/jOORcRWQV8EdlXRJ4XkaXx7T4VnDdFRNaJyHvZ3M8551zNZVvDvx54QVU7AC/E99OZCgzL8l7OOeeykG3AHwFMiz+fBoxMd5Kqzga+yPJezjnnspBtwD8gWIw8vt0/2wKJyFgRmSsic9evX5/t5ZxzzsVVOR++iPwbODDNoRvDLw6o6iRgEkCfPn00F/dwzrkoqjLgq+rgio6JyFoRaaWqn4lIK2BdqKVzzjkXmmxTOjOBMfHnY4Ans7yec865HMk24N8ODBGRpcCQ+D4i0lpEZgUnicgjwBtAJxEpE5ELsryvc865DGW1pq2qbgAGpXl9NXBS0v7obO7jnHMuez7S1jnnIsIDvnPORYQHfOeciwgP+M45FxEe8J1zLiI84DvnXER4wHfOuYjwgO+ccxHhAd855yLCA75zzkWEB3znnIsID/jOORcRHvCdcy4iPOA751xEeMB3zrmI8IDvnHMRkVXAF5F9ReR5EVka3+6T5pyDROQ/IrJERBaJyLhs7umcc65msq3hXw+8oKodgBfi+6l2AteoahfgaOBSEema5X2dc85lKNuAPwKYFn8+DRiZeoKqfqaq8+PP/wssAdpkeV/nnHMZElWt+Q+LbFLV5kn7G1X1O2mdpOPtgNnAEaq6pYJzxgJj47udgA9qWLwWwOc1/Nlc8nJlxsuVGS9XZoqxXAerast0B6pcxFxE/g0cmObQjZmUQET2Av4OXFlRsAdQ1UnApEyuXcH95qpqn2yvEzYvV2a8XJnxcmUmauWqMuCr6uCKjonIWhFppaqfiUgrYF0F5zXAgv10VX28xqV1zjlXY9nm8GcCY+LPxwBPpp4gIgI8CCxR1buyvJ9zzrkayjbg3w4MEZGlwJD4PiLSWkRmxc/pD5wLnCAipfHHSVnetzqyTgvliJcrM16uzHi5MhOpcmXVaOucc67u8JG2zjkXER7wnXMuIoou4IvIMBH5QESWiUi6kb95ISJTRGSdiLyX77IECnXaCxFpJCJvi8iCeLkm5LtMyUSkRETeEZGn8l2WZCLyiYi8G28nm5vv8gREpLmIPCYi78f/1o4pgDJ1SmpTLBWRLSJyZb7LBSAiV8X/7t8TkUdEpFFo1y6mHL6IlAAfYg3IZcAcYLSqLs5rwQARGQBsBR5S1SPyXR6AeFfaVqo6X0SaAvOAkfn+fcV7du2pqlvjXXpfBcap6pv5LFdARK4G+gDNVPWUfJcnICKfAH1UtaAGEonINOAVVZ0sIg2BJqq6Kc/F+lY8bqwC+qnqp3kuSxvs772rqn4tIo8Cs1R1ahjXL7Yafl9gmaouV9XtwAxs+oe8U9XZwBf5LkeyQp32Qs3W+G6D+KMgaiYi0hY4GZic77LUBSLSDBiAdc1GVbcXUrCPGwR8lO9gn6Q+0FhE6gNNgNVhXbjYAn4bYGXSfhkFEMDqgvi0F72At/JcFODbtEkpNpjveVUtiHIBvweuA3bnuRzpKPCciMyLT1FSCNoD64E/x9Ngk0Vkz3wXKsUo4JF8FwJAVVcBdwIrgM+Azar6XFjXL7aAL2leK4iaYSGr7rQXtUlVd6lqT6At0FdE8p4GE5FTgHWqOi/fZalAf1U9EhiOzUo7IN8FwmqrRwL3q2ov4EvSz6qbF/EU06nA3/JdFoD4FPMjgEOA1sCeInJOWNcvtoBfBhyUtN+WEL8OFaNCn/Yi/vX/JWBYfksC2CDCU+O58hnYYMK/5LdICaq6Or5dBzyBpTjzrQwoS/qG9hj2AVAohgPzVXVtvgsSNxj4WFXXq+oO4HHg2LAuXmwBfw7QQUQOiX9yj8Kmf3BpFOq0FyLSUkSax583xt4E7+e1UICq3qCqbVW1Hfa39aKqhlb7yoaI7BlveCeeMjkRyHuPMFVdA6wUkU7xlwYBee9EkWQ0BZLOiVsBHC0iTeLvz0FY21ooqpw8rS5R1Z0ichnwLFACTFHVRXkuFgAi8ggwEGghImXAeFV9ML+l+nbai3fj+XKA/1HVWRX/SK1oBUyL956oBzyqqgXVBbIAHQA8YTGC+sDDqvpMfov0rcuB6fFK2HLg/DyXBwARaYL16Lso32UJqOpbIvIYMB9bPOodQpxmoai6ZTrnnKtYKCmdqgY7icjZIrIw/nhdRHqEcV/nnHPVl3UNvzqDnUTkWCxPvFFEhgMxVe2X1Y2dc85lJIwafpWDnVT1dVXdGN99E+s945xzrhaF0WibbrBTZbX3C4CnKzooSWva7rnnnr07d+5co0KtXg2tW9foR51zmUp9w2WzH+a1cnnt2rxWBubNm/d5RWvaoqpZPYAzgclJ++cCf6zg3B9gXYz2q861e/furTU1fnyNf9Q5l6nUN1w2+2FeK5fXrs1rZQCYqxXE1DBq+NUa7CQi3bH5R4ar6oYQ7uuccy4DYeTwqxzsJCLfw0aMnauqH4ZwT1dDsRiIJB6xWPnjN91U+XHnXN2VdQ1fKxjsJCIXx49PBG4G9gPuiw8M2amqfbK9t8tcLAYrVsBDD8GoUTBwIKjCli3wwAMwdaqdd+KJ8OyzeSyocy50oYy0VRuZOSvltYlJz38G/CyMe7nsqMLTT0PTpvDUUzB9evnj7dpB8+awbl0+Suecy6Vim0vHVeHKK2HNGti0CTZvhpEjYcgQ6NYN5s2DMWPsWGmpp3ScKzaRCvhV5a+j4KijbPvOO1bbf+IJeO45OP10OPJI+51ceCG0aGHHo/g7clXwN1KdFbmArwrjx0c3mL32mqVzunWz/eC9O2FC4r3bsSN8/jl8UVDrc7mC4W+kOitSAd/B66/D0UdDSYntB+/d4BGLQaf4RLYffJCvUjrncsEDfoRs3gzvvgv9+1d+ngd854qTB/wIefNNq8VXFfAPOQTq14cPfcSEc0XFA36EvPYa1KsH/aqYp7RBAzj0UK/hO1dsPOBHyGuvQffu1mhblY4dPeA7V2w84EfEzp3w1ltVp3MCnTrBsmWwa1duy+Wcqz0e8CNi4UL48svMAv62bfDpp7ktl3Ou9kQu4G/enGi8LEYVjYl57TXbZhLwwdM6Dh9oVUQiF/D/8Q+bFGzRonyXJDdiMZgyxZ7/8pc2NgYs4LdpAwcdVOGPlhMEfO+p43ygVfGIXMDfsKH8tths2mSBfo894I47YNw42L3bBlz1728VtOpo2RL23ttr+M4Vk1Bmy6xLNsZX1i3WaQOGD4f16xP7f/wjrFoFK1fCL35R/euIWC3fA75zxSNyNfwg4BdjDX/JEpg71yY/U7Wa/YAB8PjjdnzcuMy+jXvAd664RC7gBzX7Yqvhq8JVV8Gee8Ktt9prIvDyy3DnnTaQaseOzAP+qlWwdWtOiuycq2WhBHwRGSYiH4jIMhG5Ps3xziLyhohsE5EMEgvhK9Ya/lNPWWN0LGb592TXXAPnnGPTJWQiaLhdujSUIjrn8izrgC8iJcC9wHCgKzBaRLqmnPYFcAVwZ7b3y1Yx5vBvvhlOPdWeX3VV+Vp8uumPq6tjR9t6Wse54hBGDb8vsExVl6vqdmAGMCL5BFVdp6pzgB0h3C8rxRjwzzvPtj/84Xd7zaWb/ri6OnSwDwkP+M4VhzACfhtgZdJ+Wfy1GhGRsSIyV0Tmrk/ubhKSINAXU0rnllts+89/hjsupnFj+N73POBHjg+0KlphBPx0PbtrPI5VVSepah9V7dMyNRmdJdXirOGfcIJtly4Nf1yM99SJIB9oVbTCCPhlQPL4zbbA6hCuG7qtWxOTgRVTwF+xwrZt24Z/7U6dbLRtsU5F4VyUhBHw5wAdROQQEWkIjAJmhnDd0N10U+L5qlXFU3FZuRL23x8aNQr3urGYDdzautXm0S+W35dzUZV1wFfVncBlwLPAEuBRVV0kIheLyMUAInKgiJQBVwO/EpEyEWmW7b0z9ZOf2LZvX9v+8pe1XYLcWLmy+nPkZCIWgxdftOePPeYB37m6LpR++Ko6S1U7quqhqnpr/LWJqjox/nyNqrZV1Waq2jz+fEsY985EkL8/9FDbFkvD7YoV1riaC8cdB82awcSJubm+c672RGqkbZC3P+yw8vt1maoF/FzV8Bs0gC1b4N//hssuC/8ezrnaE6mAH9Twiyngb95sOfZc1PCDzhpr1ljgLykJ/x6uAHg3zMiIZMAvppTOyvgIiFzU8AMHHABnnAFTp5afV8fjRJHwbpiREamA/8UXVks9+ODEfl0XdMnMZcAHuPRSS+1Mn5547ZJLbDRuixYeJ5yrCyIV8DduhH33hf32s/1iquHnqtE2cOyx0KMH3HuvBffNm2HoUBvs9fnntl+M/FuMKyaRC/j77GNTBjRqVBw1/JUrbRbMAw/M7X1ErJb/7rvw/PM2b8/ChYnjzZsXZzCMxeC226B9e/8W4+q+SAX8L76wgA9W0y+GgL9iha1VWxsNqh9/bNuhQ+GVV+C006CszF774x+LMxh+/LGltpcvh7Vr810a57ITqYAfpHTA0jrFktLJdf4+8JvfwNVX2/OJE20wVuvWlsMvLa2dMtS2U06B7dvt+YEHFueHmouOyAX8Yqzh11bABwv6Y8fCRRfZvojl9hcsqL0y1JZXX4XFi+Haa21qiZtuKpKA7w0TkRXpgF/Xa/i7d1tKJdcNtoFYzNo+Jk0qHyd69rTc/s6dtVOO2rB7ty0m07q1pXQOPxzefjvfpQqJd8OMrMgE/N27YdOmRMDfb7+6X8Nft87Wqa2tGn5Fi6n06AHbthXXNMoPP2wLwt92m60T3LcvzJnjs4a6ui0yAX/zZnuzBjn8IKVTl9/AQR/82qrhV6RnT9sWQ1onyHace67tf/SRbfv2tb+X5cvzVjTnshaZgB+Msk1O6WzbBl99lb8yZas2RtlWR+fO0LBhcTTcXn55YkGZl1+2tYAhMcNq0aR1XCRFJuAH6ZvklE7y63VRodTwGzSwHHddr+EvXAhHHQWvvQYjRsCAAYljhx9u4zfqbMD3hlpHhAJ+UMNPTulA3Q74K1dCkyaJD7F86tmz7tbwg1jYo4f1u9+2DZ58snxcbNAAjjyyjgd8b6iNvFACvogME5EPRGSZiFyf5riIyB/ixxeKyJFh3Lc6gjfziSfa/iOP2LYYplcIumRKulWFa1mPHtaIvGZNvkvyXVVVbkeMsG3btrB6dfqGabDa//z51lDuXF2UdcAXkRLgXmA40BUYLSJdU04bDnSIP8YC92d73+oKKjYnn2z7N95o22Kp4ec7nRMIGm4LsZYfi9kMqfvsY2sapwb8Bx+07qZnnw2tWlV8nb594ZtvYNGiXJbWudwJo4bfF1imqstVdTswAxiRcs4I4CE1bwLNRaSSt1bN7dwJjz5qa9Ym+/pr26amdOpyDb82R9lWpXt32xZiHn/LFutts3Ej/Oc/5Y9t2wZTplggv+OOytPb3nDr6rowAn4bYGXSfln8tUzPAUBExorIXBGZu379+owLU1ICF1xQfmIvsDf0HntYwxvU/Rr+9u2WPimUGv4++9i004VYw7/yysTzwYPLB/SnnrLKwDPPpE/jJGvf3v5u6kTA90Zal0YYAT9dBjm1d3t1zrEXVSepah9V7dOyZcvMCyPWTfDzz8u//vXX5Rs3Gze2RyYBv5DeQ6tWWXAqlBo+WB6/EAN+7962PeMMa3y99NLEsWnTbDTt4MFVX0fEavl1JuB7I61LEUbALwOSw05bYHUNzglNuoD/zTeJWn0g0+kVzjwTzjvP5pLJ93uothY+yUTPnvDhh4U3tmHBAmuknzDBGlynTbPX166FWbNskFV1Zxvt29dy+F9+mbvyOpcrYQT8OUAHETlERBoCo4CZKefMBM6L99Y5Gtisqp+FcO+0unSxvO1//5t4LbWGD1VPr5Bao+/eHR56yIbdf/ppTopebbW18Ekmeva0KSzeey/fJSmvtNS+fXTtCv3721xAqvb/uGsXjBlT/Wv17Wv/xvnzc1Zc53Im64CvqjuBy4BngSXAo6q6SEQuFpGL46fNApYDy4AHgEuyvW9lOne2bfLcLukCflU1/FjMgkWwuMgZZ1ij344d1usnn6s8Fcoo22Q9eti2kBpud+2yD6CgbGPH2ipdL79sa/QedZRVEKpr+62/Yw++4e0BV4MILw2M5aLYmSukfKMrWKH0w1fVWaraUVUPVdVb469NVNWJ8eeqqpfGj3dT1blh3LciwRv4/fcTr1WU0qmshr9rFwwZYt8U/v53u+4PfmA9OxYtslWe8vX+WrHCvqE0aVK7961MkCoZO9Z+J5065T8GLV1qH/ZBt9Ezz7T/t6uvtob9n/wks+ud9toveLLxaHo3fA82bWLgS7FwC1xTnrN31VCUI20PPdQCzJIliddqktJZsADWr7cVnk4/vfxskVOn2jlBGqO231+F1CUzMGEC3H47HHCA9Yj68MPEsfr14Z57av8DIGhEDmr4jRtDx47wzju2f+mlGZZDlZLjjuW47S9Ar15w4YX5+VTzGr2rgaIM+A0bWu09qOHv2GHdGCtK6VQ0Y+bs2bZNlycfMwauv94Cyrvvhlb0aonF4F//snsX0ns9FrPfydq19i3oiiusUfTkk20RkZNPTvwuf/lLuOGG3JdpwQLrmZOctpk82bZdu2ZeGX7phF8z+LnrKGE3fPwxux+cAnffDTffXLs1a6/RuxooyoAPtuxeUMPftMm26QL+jh0V97j4059s+/vfpw+swRS6tR3wgwFAAwcW1ns9db78ffaBk06yD6fbbrPFWoLeRXfcYaNb01VQVW0sRRgV2AULLNg3bJh4rVs3G2w1aFDm1xv4UtI/csMG6v3wFFsp5a9/ta+LXvN2BayoA/6yZRbQUydOC1Q2n46qpXN+8pP0A3JiMZtBEeCcc2rvfb1uHZx/vgWt/v1r5541lfoB8NJLieczZ8L++9t5nTvbAKggRtarZwG5fn07/oc/1Pz3W1qayN8nl+unP7WF17OKyfvuC//4h9UIli61FM/QobmpefsHiQtBUQf8HTtswYrUufADlY22XbLEPgiSp8hNFgSzTp0S+f1cU7VAtWGDfau49da6+97/4Q/t3zBokI1gXbo0cax7d1s8PFgy8Yor7HecqfXr4bPPEvn7QOoHUVa/PxEYN87+Y0pK7A/md79LXDg5SA8cmFnQTv75CRPsQ8RTOC4LRRvwg0G677//3bnwA5XNif/yy7atKOAHunQp3zhckfHjs6+g3X+/pUfuuivEgJUnsZg17r7wguX5x42DefPgZz+zWvk//2n/tq+/tp5RM2faa5kIuoemBvxQBUF58mSbW7lDB7juOpuW9bLLytf2g684wT5U/oEAnqd3oSragB8E8yVLKk7pVDaB2uzZNuS+ffvK79Oli9VOq5oyt0OHxPmZvneDmBJMCVCXJ3wLpNay69WzKRAmT7bnwe+nUSObm/6AA+DUUzP7wEztoZMTqf+QRYvg3nvtq2XPnvDKK1X/bEUfCB7gXcjq57sAudKokQXs99+Hpk3tteqmdFQt4A8YUPVc8126WOrho48SA75SrViRCNY1mS8+FrM+4888Y3EkGAhWTGKxiuNb06ZWW+/Xzz68V660hcWrsmABtGlj6b1aIwKXXGI3f/FFq7Uff7z13a1XtPUrV0cU9V9g585Ww68opVNRwF++3BbCqCqdA4nufhWldXbvtobfb76x/Y0ba5bSmTMHDjusOIN9dbRoYQ25GzfCTTdV72cWLMhx7b4yrVrZ/AtnnWXDs4cNs/6qzuVRUQf8Ll2shr9xo/XFTu6aB/YtoEmT76ZIgv73xx9f9T2CWv3ixemP33OPvd/vuy+Rg3711cwC/nXXWZfGd9+tu4202YrFrLIM1u39ggsqP3/bNvsQTu2hk3NB/m3CBGjWzHJ5p5xiqZ2ePa3W71yeFHXA79zZJlFbvDgxD36yWMxmdrzrrvKBdPZsq1FWZ46VvfayEa+pNfzgfX/11ba/YkUi+GQ618ywYbZ97rnopnaDdPeWLTZ46/XXE9+a0lm82FJttV7DT83pT5hgjRNvv21zOgwebLn6XbtquWDOFXnADwL2G29YbT5VLGYBoWPH8oF09mw47rjqrxWbrqdOLAbDh9s3iDVr7H3fpo2lkTKdM75WGh/riKZN4YEH7Jtb48bpG3FjMVtwHCyjUhAfkN26WV7u3HOtC9jgwZY3dK4WFXXAD9ItW7akr+GDBeBg+cOgVr58OTzxRPUDRZA62r078dpXX9m39+7drYcJ2LV79Mi8hr9ggaWEg4FKUXfiiZbSKSmBn/88/aC4yy6zgVs7dxZIwAf7Ojhtmq2a/vbb9pXvuefyXSoXIUUd8Fu3TvTQqW7Af/hhez5vXmYB/6uvElMWg82ptW0bvPlm+Rpoz56Wi8/kG3260aJRd8cdFvCDSdCS7dplH9jt21d/YZOcSM7np/4RzJ1rNYGhQ20wQjDKzLkcKuqAHyx3COlTOmD99TdvtulQrr7aRsnvsUdm6ZN0PXVatLAPma+/Ll8D7dHDXkseWVqZ7dvz1PhY4P74R/vdpH6ggg16XbXKZuvMayN3ZUN6u3SxWv6FF1or/sCB5WsMzuVA0fbDD3TpYqnTigL+979vq1g9+GDifXnEEZnVDJMDftDA+swz9h5OvW/yIiEV9dtPtnixDery/H15sZilxc84w7IiQ4YkjjVsaNmTtWsLa72A72jc2Jbf2rDB/hE9e9ofo3M5klUNX0T2FZHnRWRpfLtPBedNEZF1IlLri98FQbWilM6559pUvcGSiFu3Wi+6TLRsad8Ughr+xx9b7XLo0O+e27WrdRGtbh4/aLD1Gv53nXwy7L03TJ+eeG3bNnjsMTjttAIP9sm6dbMc4ve+Z398zz1nX1+cC1m2KZ3rgRdUtQPwQnw/nanAsCzvVSNB7buigB/mfYKA/+yzth2W5l/csKGdW92eOgsWWNkPOyyUYhaVRo3gRz+y1ciChdNnzbLpsP/f/8tr0TLXsaN1J7vkEtsOGACffJLvUrkik23AHwHEF7ZjGjAy3UmqOhuoZG2p3OnVy2rUqfPohK1LF0u/qFo6p107ew+nk0lPndJS6+mT18bHAnbOOfatLBjUNn269WYaPDi/5UqrokbcQKNGNg/PmWda7aFXL2t9di4k2Qb8A1T1M4D4NuuOgyIyVkTmisjc9evXZ3s5Dj7YujtXNQkaVP1+rEyXLjZFw+rV1uli2LCK+/H37GnnVfXPU/UeOlU5/ngb3zB9ujW+P/WU9b2vX4itU9Wdl7lrV+t+dNhhNi/0009brsq5LFUZ8EXk3yLyXprHiFwUSFUnqWofVe3TMpjjOEstWlRvEFU286R37WrbyZOtxpkufx9IbritzMqVlp7wBtuK1asHo0dbTHzgAYuLZ5+d71KFoH17eO01uPJK683Tv7/N0OdcFqoM+Ko6WFWPSPN4ElgrIq0A4tt1uS5wmLKp0acK2gruu89qlyecUPG51Q343mBbPeecY93Yf/UrW8A+WAKyzmvY0CYOGjXKRgP26mXTLztXQ9mmdGYCY+LPxwBPZnm9WhXmykcHHWRT9q5bZ5WxZs0qPrdFC0tDBAE9dWGkoBwLFth+t241L1cUPP64bbdts0rwhAn5LU/oOnWyFM/hh1sXpJ//PDFa0LkMZBvwbweGiMhSYEh8HxFpLSKzgpNE5BHgDaCTiJSJSBVzHdY9EyYkFkN/+eWqPzySG27Hj7cJ3Jo3t3l8gp8tLbU07l575abMxWLCBPjtb+3D8f33C2gqhapk8hXz4IPtj+PYY2HiRDj6aPjgg9oqqSsSWTVtqeoGYFCa11cDJyXtj87mPnVBLGa1y7/8xSpjVaVhevSw7tZr18LFF9ta2A0a2KpOr7xig79KS22iRVe1q66y8Q+dOuW7JBmobNWXdBo0sBFmN94I551nfxwnnpir0rkiVNRTK9S200+3rpjdu1d97tKllnc+8EAL9mAjajdtskrce+8lVslzlYvFLBbef39E1gs46SSrDRx5pHXbvOCCxEAE5yrhAT9Ep51mPUaqs5LdbbfZtlkzG2cTtCMsWGA/Hyy+4j10qhZmW0yd0batTcd63HHw5z/DUUd5g66rkgf8kGTa4+eww6y//kUXWTo20L07jByZWHbxlFMiEsBc5oLuYM8+C59/bkH/nXfsU8+5NDzghyTTWmYsBoMGwe9+990PiKlTbQBRnz42x74HfFepIUPsq+Exx8DMmZbf37o136VyBcgDfp5U9gERi1nNfu5cS+94wI+IbAaGHHig9QIYONAWdejTBxYuzFFBXV3lAb8ARTIn7bL/jy8pscafF16w6V/79rVag6d4XJwHfOeKzcCB1ovn+OPhX/+yngRbtuS7VK4AeMB3rhjtv79NMDRokI3OPfJImD8/36VyeeYB37liVa+eLen20ks278Qxx9hEbJ7iiSwP+M4Vu+9/37prDhlitf4zzrARfi5yPOA7V6jCnM61RQvrsjlkiG179bKV3l2keMB3rlCF3V2rXj2bt+PVV+16U6bYrH2e4okMD/jORU2/fpbi6dgRrrnGZuzbsCHfpXK1oBAXgnPO5do++8CPf2ypnl/8wlI8BbkQsAuT1/CdiyoRuPxyeP11m2506lS4/Xabz8MVJQ/4zkVd797WR79LF7jhBpt+eV2dWq3UVVNWAV9E9hWR50VkaXy7T5pzDhKR/4jIEhFZJCLjsrmncy4H9t7bumvef7/12+/ZEz75JM+FcmHLtoZ/PfCCqnYAXojvp9oJXKOqXYCjgUtFpGuW93XOhU3Ell976y1o2hQeeghuuQV27cp3yVxIsg34I4Bp8efTgJGpJ6jqZ6o6P/78v8ASoE2W93UuWsLsk1+VHj1s0rUjjoCbb4ahQ2HNmtzdz9Ua0Sz64IrIJlVtnrS/UVW/k9ZJOt4OmA0coappZ3MSkbHA2PhuJ6CmKzW3AD6v4c/mkpcrM16uzHi5MlOM5TpYVVumO1Blt0wR+TdwYJpDN2ZSAhHZC/g7cGVFwR5AVScBkzK5dgX3m6uqfbK9Tti8XJnxcmXGy5WZqJWryoCvqhV2zhWRtSLSSlU/E5FWQNqmfRFpgAX76ar6eI1L65xzrsayzeHPBMbEn48Bnkw9QUQEeBBYoqp3ZXk/55xzNZRtwL8dGCIiS4Eh8X1EpLWIzIqf0x84FzhBRErjj5OyvG91ZJ0WyhEvV2a8XJnxcmUmUuXKqtHWOedc3eEjbZ1zLiI84DvnXEQUXcAXkWEi8oGILBORdCN/80JEpojIOhF5L99lCRTqtBci0khE3haRBfFyTch3mZKJSImIvCMiT+W7LMlE5BMReTfeTjY33+UJiEhzEXlMRN6P/60dUwBl6pTUplgqIltE5Mp8lwtARK6K/92/JyKPiEij0K5dTDl8ESkBPsQakMuAOcBoVV2c14IBIjIA2Ao8pKpH5Ls8APGutK1Udb6INAXmASPz/fuK9+zaU1W3xrv0vgqMU9U381mugIhcDfQBmqnqKfkuT0BEPgH6qGpBDSQSkWnAK6o6WUQaAk1UdVOei/WteNxYBfRT1U/zXJY22N97V1X9WkQeBWap6tQwrl9sNfy+wDJVXa6q24EZ2PQPeaeqs4Ev8l2OZIU67YWarfHdBvFHQdRMRKQtcDIwOd9lqQtEpBkwAOuajapuL6RgHzcI+CjfwT5JfaCxiNQHmgCrw7pwsQX8NsDKpP0yCiCA1QXxaS96AW/luSjAt2mTUmww3/OqWhDlAn4PXAcU4qTxCjwnIvPiU5QUgvbAeuDP8TTYZBHZM9+FSjEKeCTfhQBQ1VXAncAK4DNgs6o+F9b1iy3gS5rXCqJmWMiqO+1FbVLVXaraE2gL9BWRvKfBROQUYJ2qzst3WSrQX1WPBIZjs9IOyHeBsNrqkcD9qtoL+JL0s+rmRTzFdCrwt3yXBSA+xfwI4BCgNbCniJwT1vWLLeCXAQcl7bclxK9DxajQp72If/1/CRiW35IANojw1HiufAY2mPAv+S1Sgqqujm/XAU9gKc58KwPKkr6hPYZ9ABSK4cB8VV2b74LEDQY+VtX1qroDeBw4NqyLF1vAnwN0EJFD4p/co7DpH1wahTrthYi0FJHm8eeNsTfB+3ktFKCqN6hqW1Vth/1tvaiqodW+siEie8Yb3omnTE4E8t4jTFXXACtFpFP8pUFA3jtRJBlNgaRz4lYAR4tIk/j7cxDWthaKolrEXFV3ishlwLNACTBFVRfluVgAiMgjwECghYiUAeNV9cH8lurbaS/ejefLAf5HVWdV/CO1ohUwLd57oh7wqKoWVBfIAnQA8ITFCOoDD6vqM/kt0rcuB6bHK2HLgfPzXB4ARKQJ1qPvonyXJaCqb4nIY8B8bPGodwhxmoWi6pbpnHOuYsWW0nHOOVcBD/jOORcRHvCdcy4iPOA751xEeMB3zrmI8IDvnHMR4QHfOeci4v8Dq6AtM5aM0CMAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -383,7 +425,9 @@ "id": "6f6c1b6f", "metadata": {}, "source": [ - "## Things to try\n", + "### Things to try\n", + "\n", + "To gain a bit more insight into sensor fusion, you can try the following:\n", "* Remove the input (and update P0)\n", "* Change the sampling rate" ] @@ -393,18 +437,20 @@ "id": "8f680b92", "metadata": {}, "source": [ - "## Predictor-corrector form" + "### Predictor-corrector form\n", + "\n", + "Instead of using `create_estimator_iosystem`, we can also compute out the estimate in a more manual fashion, done here using the predictor-corrector form:" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 25, "id": "fa488d51", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYQAAAD4CAYAAADsKpHdAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAA15ElEQVR4nO3deXxU9bnH8c+THUIA2bfEsKOyG3ZUFFRQq1j3pQpVqdxrq7VXtNVal2rV21KuVkVERAEVWkFQWWQXRFDCLmEJq4FIApKVkG2e+8cZJCKYmTCTM0me9+s1L2bmnDPny48hT875nfP7iapijDHGhLkdwBhjTGiwgmCMMQawgmCMMcbLCoIxxhjACoIxxhivCLcDnI1GjRppYmKi2zGMMaZKSU5OPqyqjU99v0oXhMTERNauXet2DGOMqVJEZN/p3nftlJGIhIvIehH5xPu6gYgsFJGd3j/PcSubMcbURG72ITwIpJR5/RiwWFXbA4u9r40xxpyitBSCcU+xKwVBRFoBVwMTy7x9HfCO9/k7wPBKjmWMMSErK8vD+PGHGTECmjeHYJwtd6sPYRwwBogr815TVU0HUNV0EWlyug1FZBQwCiAhISHIMY0xxj0bNuTx8su7WbAggoMH2wONiIhQSkqE3r2ddS65BJYtC8z+Kr0giMg1QIaqJovIIH+3V9UJwASApKQkG4jJGFNtFBXBihUwdy5Mnfo9GRkNgK6EhW2lY8dPufHGGB5/fDC1akUGZf9uHCEMAK4VkauAGKCuiEwFDolIc+/RQXMgw4VsxhhTqfbsKeZf/9rFnDnF7N7dBo8ntszSWXTq1JxNmy4kMvL8oGep9D4EVf2jqrZS1UTgVmCJqt4JzAHu9q52NzC7srMZY0ywlZTAypXw29/mUr/+Xtq0iWTs2E6kptalRYsl/O//biMvD1QboHo9KSl9iYwMzhHBqULpPoQXgBkicg+wH7jJ5TzGGBMQR44oEybs44MP8tiypT0eTzRQB9hCXNxX/PWvDRgxog916/7C1ZyuFgRVXQYs8z4/Agx2M48xxgSCKnzzDYwdu51588L47rs2QCKQQdu2X/PCCwO5/HKhbt2+iPRzOe1JoXSEYIwxVdaxY/D++9/x/vs5pKZ2YN8+gI6EhW2gU6dZ3HhjDP/1X71o3nxgma3EpbSnZwXBGGMqaOfOEl59dS8ff1zCnj2JqDYD6nDOOcdxrpk5hMdzPk2bdufZZ10O6wMrCMYY46PiYpg7N5vFi2NYtCialJQIoB2wk/j4T7nqKnjgga507tzeu0VTF9P6zwqCMcb8jIwM5c03v2X69Dy2bo2ntLQeUOpdmgusYsCAfqxceYOLKQPDCoIxxpShCuvXK59+Knz8cSlffy1AApBOw4ZLGDz4GGPGXMiFF3bAGWzhSncDB5AVBGNMjZeXB++/n8k772Sydm0TCgsbeZeEA/NJSDjOqlV9aNnyOjdjBp3PBUFEGviwmkdVsyoexxhjKseuXfDpp/DGG9+SktIUZ76YKGJjv2Dw4DzefvtmmjQBGOpy0srjzxHCQe/j566TCsc5tjLGmJBSXAzz5uUwYcIBVqyoR05OC++S2sBHNG8OCxZ0pXPnYYiE1uWglcWfgpCiqj1+bgURWX+WeYwxJmAyM+H997OYOPE7tm5tSWlpXSCayMhVPPKIh1GjWtGuXUPgZrejhgR/CoIvt9OFzi13xpgaRxVWry7k9df3M3NmC/LzY4H6QAEREYu44YZjjB7dnkGDLiY8PNzltKHH54KgqscBRCQJeBw417u9OIu164l1jDGmsuTmwvTpR5g8+USHcAOgLU2a7GPMmNYMG6Y0bVpKQsL1bkcNeRW5ymga8AiwGfAENo4xxpRv27ZSpk3LYvXqhixfrhQXNwQiqF17JYMHf8+997Zk+PD+xMSA8ztrK3cDVxEVKQiZqjon4EmMMeYMiopg7tw83nzzICtW1CU3txnQkNq1leJiAb4CYklKuopFi2pmh3AgVKQg/EVEJgKLgcITb6rqzIClMsbUeAcPKnPnwty5wty5RRQW1gESiIxcSVLSAu688xz++7+vIiIiAujtdtxqoSIFYSTQCYjk5CkjBawgGGMqrLQUvviiiDfe+JaFC6PIzIwvs/QIsJAuXdqzfv2l1iEcJBUpCN1UtUvAkxhjapysLFiwAP7973w+/riEoqJ6QCIiq+nYcQlPPnkht93WGWdW3btcTlv9VaQgrBaR81V1a8DTGGOqNVXYvNnDhAkH+PjjUvbvT8CZybc28DH162fy+uvxXHvtQGrXHuBy2pqnIgVhIHC3iOzB6UP44bLTgCYzxlQLBQWwZAm8+upeVqyoQ15eIyAeWE/37t/w2mtX07u3EB5+rdtRa7yKFISaM7CHMaZC0tJg0qRDzJpVxKZN8Xg8AM2AxcTFHeS55xpy++2X0LDhzw5+YCqZ3wVBVfcFI4gxpuoqLYVVq4oZPz6Nzz6L4vDhljiTw+zhnnvyuemmWM47L4cWLa70XhVkQpE/o52uU9WeZ7uOMaZ6yMmBf/87i8WLa7NoURSZmZFAPCJf0qHDfG68MYZ77ulPmzax3i2auBnX+MCfUn2eiGz6meUC1DvLPMaYEJaa6uGNN9KYObOYPXsSUK1PWNhx7ymhHOALBgy4mBUrLnI5qakIfwpCJx/WKS1/FWNMVVFSAqtWObOHzZ7tYfv2MJwR7lNo3nw6V13l4Q9/6Md557UH6gLD3A1szoo/g9tZ34ExNcDRo/Duu4eYMuUoGze2oKSkLiKgGgbMAUrp128gq1bd6XZUE2DWu2NMDacK27admD3sAKmpTXE6hMOoW3cxl19eyKRJt1K3LoBdGlqdWUEwpgYqKIDZs7OZODGd1asbkJ9/osM3Evg3LVuGs2TJhXToYENG1yRWEIypIfbvh2nTsnjnnQx27kzA46kHRBAdvZInnriAUaNaER/fBLjN7ajGJX4XBBGJBm4AEstur6rPBC6WMeZslZTA4sUFTJiQxty5rTh+vBbO7GFHiIyczR13lHD//efRt+/lhIWFuZzWhIKKHCHMBrKBZMoMf22McV9mJkyZksm0aVls2tSMkpI4IJGWLXfx+9934qqrlPr1Y2ne/Ba3o5oQVJGC0EpVbfgKY0KAKqxdW8J772WzenVD1qxRVBsDJdStO5+LL87nvvsSGTq0P1FR4Nwu1Mzd0CZkVaQgrBKRLqq6OeBpjDHlysuDWbNymDQpgzVrGlBQ0AA4h7g4RVWANcA59OhxEx9/7HJYU6VUdLTTETbaqTGVZ/du5ZNP4NNPhcWLSygtrQso0dHLGDgwkxEjmnL33cO84wT1cTuuqaIqUhDsVkRjgqy0FJYtO8748WksXlybo0dblFn6HbCQ7t27kJz8C+sQNgFTodFORaQbcGKwkhWqujGwsYypeU7MHjZ9ej6ffFJCcXE94FzCwr6gc+d5/PWvfbnuuguAVjgz2RoTWBW57PRB4D5OzqE8VUQmqOorAU1mTDWnCikppbz22n4+/thDWlprPJ6Ts4fBEc4//1zWrRtAdPQgd8OaGqEip4zuAfqoaj6AiLwIfAlYQTCmHEVFsGIFjBu3k+XL48jNbQa0BjbTo8dn/OtfQ+nTx2YPM+6oSEEQfjyqaan3Pd82FokH3sW59s0DTFDV/xORBsB0nBve9gI3q+rRCuQzJqRkZipvvnmAWbOKWL++DaWlAOcCy4mLW8ALLzTk9tsHUr9+F5eTmpquIgXhbWCNiMzyvh4OvOXH9iXAH1R1nYjEAckishAYASxW1RdE5DHgMeDRCuQzxlWqsG5dIf/6117mz4/gu+9a45z3P8iddx7jpptq061bDq1aXUZ4eLjbcY35QUU6lceKyHJgAM6RwUhVXe/H9ulAuvd5roikAC2B64BB3tXeAZZhBcFUEYWF8OGHh1m2LI6FC6PZuzca6EhY2Do6dPiAG2+M4Te/6U1CQm3vFo3cjGvMaVVocDtVTcYZuuKsiEgi0APnTpqm3mKBqqaLyGnn2xORUcAogISEhLONYEyFpaeX8uqre/nww+Ps2JGIx9MIkWJUAXKBtQwY0I/PP7dZZU3V4M+cyitVdaCI5AJadhHOjWl1/dmxiNQBPgQeUtUcEd+6IVR1AjABICkpSctZ3ZiAUYUNGzzMnRvGRx+VsnatAG2BNJo1W8jQoSU88siFnH9+ayAOuNTdwMb4yZ8Z0wZ6/4w7252KSCROMZimqicuXz0kIs29RwfNgYyz3Y8xZ6ugQJkyJY3Jkw+zbl1zCgtPjAMUDiwCSujXrzerVg13L6QxAeL3LY7ey0zLfe9nthecTugUVR1bZtEc4G7v87txRlU1ptKlp8Nbb0HXrqnExh7jN7+J58svOxAd/Q3XXjuH9HTnaEF1CKpDWbWqgduRjQmIivQhXM5PO3uHnea9MxkA/ArYLCIbvO/9CXgBmCEi9wD7gZsqkM0Yv3k8MH/+YcaP38/KlfU4erStd0kTYCktWghLl3ahQ4fBbsY0Juj86UMYDfwX0FZENnHy3oM44AtfP0dVV3Lm+xbsf5ypFDk58P77ThHYsiWBkpJGwDlERa3jd7+L5d57m9G5c11ErnE7qjGVxp8jhGnAPOB5nHsEBKdzOdduIDOhThWSk/N45ZXdbN3amuTkOFQb4fQFJNO48TE+/LAdAwcm4esFDsZUN/4UhLneq4yuBcr+2iQi4vdVRsYE2/HjMG3aASZPzmDt2mYcP94c6ErDhgcYMyaOYcOUjh1LadZsiNtRjQkJFbnKqE7w4hhzdvbsKWLKlCMkJzdn0SLl2LGWQH3i4r7ioou+ZNSoVgwf3pOICHAOcu0GMWNOqNCNacaECo8HFiw4wuuvf8uKFXXJymoDNCc6WiksFGAjUJ+ePS/ls89cDmtMiKvI8Nc3AfO9w078GedO47+q6rqApzPmNHJzPSxeLHz8sTBjRh55eQ2B+kRFraVXr/XcdVcD7r//YiIiwoFubsc1psqoyBHCn1X13yIyELgC+DvwOjZvnwmiHTvy+Oc/d/Lpp2F8+20HoJZ3SQGwiG7d2rJ+fW9E7GtoTEVVZO69E0NfXw28rqqzgajARTLGuSpo/Xr4wx+yqFdvGx071mH8+B6kpdWnffslvPFGKkVFoNoY1eFs2NDFrg4y5ixV5AjhgIi8AQwBXhSRaCpWWIz5kaNHi3n11W3MmJHP/v2dyc6uA9THuU9xLh06NGbz5u5ERZ3rblBjqqmKFISbgaHA31U1yzvu0COBjWVqij174Jln1jF/fhjffXce0AXIpl27nYwb14Nhw6Bp065AV5eTGlP9VWQ+hGMisgu4UkSuBFaoql2/YXxSXKy8++5OZszIZ+XKHhw7BtATSKV27eU8+mhtHnigOw0a9HA5qTE1T0WuMnoQuA84MUrpVBGZoKo2p7I5rW+/zWfcuG3Mnl3C7t0dUe0AFHHxxUX88pdRDByYTY8ebQgLa+d2VGNqtIqcMroH6KOq+fDDSKdfAlYQDOB0CC9ZcoAvvmjA4sW1WLmyFh7PhYhkkpCwjquvhoceuoD27Zt6t6jnal5jjKMiBUE4eaUR3ud2eUcNV1BQwhtvbGXatGw2bUqgqKhsx+9x4BsGDOjGihWXuRXRGFOOihSEt4E1IjILpxBchzO/galh0tNLWbgwnNmzi5k16xiqXYHjNGy4kUGDUnn44Y70798KqA30cjmtMaY8FelUHisiy4CB3rdGqur6gKYyIam0VJk+PZVJk75jzZqG5OV18i6JBHYBhfTtewFffmk3hxlTFVWkUzkGGARcBHiAcBFJUdXjAc5mQkB2Nnz2Gbz44hbWr2+Kx9MeaEts7GaGDFnBiy9eQvfuEBZmE8kbU9VV5JTRu0Au8LL39W3AFGyGs2pj+fKDvPLKHpYvj+PIkS6onphM/iuaNVMWLuxE5842RpAx1U1FCkJHVS3702CpiGwMVCBT+YqK4D//+Y5x43aycWM8RUWJQAsiI7fz618fZuTIxvTtW4vw8EvcjmqMCaKKFIT1ItJXVVcDiDOamM9TaJrQkJKSxbhx29m5swOff34OpaXNgHOADTRsmMr06a257LIONj6QMTVIRQpCH+AuEdnvfZ0ApIjIZkDVudTEhJiSEmXGjL289VY6a9Y0Ij+/A9CHOnWOcu+9MHSokpRUQKtW1iFsTE1VkYIwNOApTFAcPHicKVMy2bw5nvnz4ciR1kA8sbGbuPTSBfz618249dbOZWYPq+9qXmOMuypy2em+YAQxZ08VFi06xGuv7WP58jocPdoRiCcyUikuFiAFqEtSUk+WLHE5rDEm5NgUmlVcdnYpS5eGMW+e8MEH2eTkNAWaEhW1maSkz7j99no88EAfIiPDgfPcjmuMCWFWEKoYVVi7Nod//Ws3CxdGkJ7eHoj2Li0B5tG1axs2bOiMSBcXkxpjqhq/J7YRkXFil55UquPHYd48uPvuHGrXPkDv3nV5993uZGREcN55i5g4cTeFhaDaENVhbNzY0a4OMsb4rSJHCHnAHBG5VVXzReQK4C+qOiDA2Wq01NRCXn55Fx9/7OHAgY4UF0cCccAmYC3nndeSzZt7EB7eqZxPMsYY31SkU/kJEbkdWCYihUA+8FjAk9UwJSWwejW89NI3LF8eS05OInA+Invo1m01f/vbRVxyiVCr1sDyPsoYYyqkImMZDcaZICcfaA7co6rbAx2sJsjM9DB+/B5mzy5l48YOlJQAdATWEBeXzEsvNeJXv+pFbGxrl5MaY2qCipwyehz4s6quFKfXcrqIPKyqdiFjOVThyy/zePXVvSxaFEVGRlucMYIyuOWW49xwQwx9+uQTH9/f+gCMMZWuIqeMLivzfLOIDAM+BPoHMlh1kZcHU6YcZM2ahixeHE1aWh2gM+HhG7jggo+4+eZYRo/uRePGMd4tbPYwY4w7zvqyU1VN955GMjhHAVu2FPHaa3uZO1f59tvWqLYgJqaIOnXAGSj2G0pLk2jUqDtPPulyYGOM8QrIfQiqWhCIz6mqiovh889L+fTTcD76yMOePVFAB0S2cu65c7j22ggefDCJNm1a4Vwp1NflxMYY81N2Y1oFHT3qYfz4vbz/fj5bt55LaWldREA1DJgHRDBgQH9WrDjf7ajGGOMTKwh+SEuDjz6CceP2smtXS6ANkEHjxp8zbFgJr7463HtaaJirOY0xpiKsIJRj0aL9vPzyt3z+eSOyszt6340DPiUhIZo1a5Jo1uwaNyMaY0xAWEE4hccDn3ySxt/+lsK6dYkUFbUHEoiJ2cRDD2Xwm980oVOnhsBwl5MaY0xghVRBEJGhwP8B4cBEVX2hMva7c+dhxo3byr5957N2bSMOHWoFNOecczZzzTXLeeihRC66yOb9McZUbyFTEEQkHHgVuBxIA74WkTmqujXQ+yotVWbM2MGkSemsWXMOubmdgYuJjs6nbl0ABQo4erQ7R47ARRcFOoExxoSekCkIQG8gVVV3A4jIB8B1QMALwn33wdtvdwQ6Urt2Chdf/AUjRjTljjvaExUFzuxhdQK9W2OMCWmhVBBaAt+WeZ2GM3/zj4jIKGAUQEJCgt87GTQIli8XYCdQj169zmPZMps4xhhjQqkgnG7wHv3JG6oTgAkASUlJP1lenmXLTjxr7++mxhhTrfk9QU4QpQHxZV63Ag66lMUYY2qcUCoIXwPtRaS1iEQBtwJzXM5kjDE1hqj6fdYlaETkKmAczmWnk1T1uXLWzwT2VXB3jYDDFdw2mCyXfyyXfyyXf0I1F5xdtnNVtfGpb4ZUQahMIrJWVZPcznEqy+Ufy+Ufy+WfUM0FwckWSqeMjDHGuMgKgjHGGKBmF4QJbgc4A8vlH8vlH8vln1DNBUHIVmP7EIwxxvxYTT5CMMYYU4YVBGOMMUANKAgiMlREtotIqog8dprlIiIve5dvEpGeIZJrkIhki8gG7+PJSsg0SUQyRGTLGZa71Vbl5ar0tvLuN15ElopIioh8IyIPnmadSm8zH3O58f2KEZGvRGSjN9fTp1nHjfbyJZcr3zHvvsNFZL2IfHKaZYFtL1Wttg+cG9x24cx1GQVsBM4/ZZ2rcCZBFqAvsCZEcg0CPqnk9roY6AlsOcPySm8rH3NVelt599sc6Ol9HgfsCJHvly+53Ph+CVDH+zwSWAP0DYH28iWXK98x774fBt473f4D3V7V/QjhhyG1VbUIODGkdlnXAe+qYzVQX0Sah0CuSqeqnwPf/8wqbrSVL7lcoarpqrrO+zwXSMEZtbesSm8zH3NVOm8b5HlfRnofp17V4kZ7+ZLLFSLSCrgamHiGVQLaXtW9IJxuSO1T/2P4so4buQD6eQ9j54nIBUHO5As32spXrraViCQCPXB+uyzL1Tb7mVzgQpt5T39sADKAhaoaEu3lQy5w5zs2DhgDeM6wPKDtVd0Lgi9Davs07HaA+bLPdTjjjXQDXgE+CnImX7jRVr5wta1EpA7wIfCQquacuvg0m1RKm5WTy5U2U9VSVe2OM5pxbxHpfMoqrrSXD7kqvb1E5BogQ1WTf26107xX4faq7gXBlyG13Rh2u9x9qmrOicNYVZ0LRIpIoyDnKk9IDlHuZluJSCTOD91pqjrzNKu40mbl5XL7+6WqWcAyYOgpi1z9jp0pl0vtNQC4VkT24pxWvkxEpp6yTkDbq7oXBF+G1J4D3OXtre8LZKtqutu5RKSZiIj3eW+cf6sjQc5VHjfaqlxutZV3n28BKao69gyrVXqb+ZLLjTYTkcYiUt/7vBYwBNh2ymputFe5udxoL1X9o6q2UtVEnJ8RS1T1zlNWC2h7hdKMaQGnqiUi8gCwgJNDan8jIvd7l48H5uL01KcCx4CRIZLrRmC0iJQABcCt6r2sIFhE5H2cqykaiUga8BecDjbX2srHXJXeVl4DgF8Bm73nnwH+BCSUyeZGm/mSy402aw68IyLhOD9QZ6jqJ27/f/Qxl1vfsZ8IZnvZ0BXGGGOA6n/KyBhjjI+sIBhjjAGsIBhjjPGq0p3KjRo10sTERLdjGGNMlZKcnHxYTzOncpUuCImJiaxdu9btGMYYU6WIyL7TvR8yp4zEhxEajTHGBE8oHSGUAH9Q1XUiEgcki8hCVd3qdjBjjAmUggI4eBDy8qB1a6hb1+1EJ4VMQfDeXZfufZ4rIidGaLSCYIwJaarOD/jvvoNDhyAtrYR9+wpJSyslJ6cO6elh7NtXTHq6kJv74x+7sbFKz55C/fqZxMTsp3HjLBo3PkqdOseIivLwm9+MICoKli9fxrZtzg3U3bt3p2/fvgH/e4RMQSjr50ZoFJFRwCiAhISEyg1mjKlRCgsLCQ8PJyIigt27dzN//lo2bhR27KjF/v0NOHq0AXFx7Th8OIJjx8puGeF9FANKXBy0aLGD3NylOEMNHcC54bkN7dr9FdUIFi2KoaDgwp9keNB78jw8fAClpT2A48TFRTN7Nlx6aWD/viF3p7J3hMblwHNnGCzsB0lJSWqdysaYisrOzqG4OBLVWmzcuJNp02aRlnaEAweySE/PJisLRowYS0ZGC778Mp+jR2PLbJ0O7AfOp1WrOK68cju7d6+iUaMSmjRRmjUTGjeGO++8ndjYWHbs2MGePXuIiYkhOjqaqKgoIiIi6Ny5M2FhYWRmZnLwYB533BHDN99E4YxqIyQm1uOuuyAr6zjHjpXy8cfCoUMnpm2ASy6BZcv8+3uLSLKqJv3k/VAqCN4RGj8BFvzMYGE/sIJgjPFVZmYm06dPJyVlG2vWCCkpPTl27GqgSTlbKk2aCIMGFdK6dQ79+9eiX79YGjc+3cjTVcOZCkLInDLyceRIY4w5o9LSUrZt20ZycvIPjxEjRnDvvffy1VfH+O1vcxD5H5wBRItwZrIVoDEdOnh4+mkhNlb4979hypQTnyqMHg1PPRUN/OTS/WolZI4QRGQgsALYzMnZgf7kHXv8tOwIwZiaS1XZvXs3eXl5dOvWjWPHjtGkSRPy8/MBqFWrNp06XUunTo+xZUs3Nm+G8HDl8svh9tuF4cMhLs7dv4NbQv4IQVVXcvrZf4wxBoAtW7awfPlyVqxYweeff056ejpXX301n3zyCbVr1+bhh/9IQUFv0tN78uGHDVi/Xli/3tm2XTtYtco5r29OL2QKgjHGlKWqbN26lS1btnDLLbcAMHr0aFauXEnLli0ZNGgQAwZcRKdOVzBhAsydC4sWPU5+PsTEwGWXwdVXwzXXgF2Q6BsrCMaYkJGVlcWiRYtYsGAB8+fPJy0tjYiIWrRv/wvS02vTv/97tGtXl8OH67JxozBrFhw/7mybkAB33eUUgUsvhdq13f27VEVWEIwxrlFVtmzZQps2bYiNjeXNN99kzJh/UKvWYOLjX6Fdu758+20TLrzwxCg78cTGQtu20KED5ORAWpqzZP9+2LoVXnvNtb9OlWcFwRhTqTweD19//TUzZ87kww9nsmtXBL/+9TQKCnqycuXvgUcoKIC9eyE6GgoLT27brx988QWI9TYGhRUEY0ylycjIoHv3QaSnn4fIMERWAk2ZNMlZ3qhRBH//O/TvDz16OH0BpvJYQTDGBIWq8tVXXzFlylRyc9vQvv3vmT+/Md99txkIp04dZcgQ4cor4YorIDHRfvN3W7kFQUQa+PA5HlXNOvs4xpiqLjU1lalTpzJ58mfs2zcQuB+4wLtUGDgwnOefh759hchIF4Oan/DlCOGg9/FztTscsAu7jKmhDh8+TIMGDTh+PIwHHviSBQv6AX8GwmnRooSDB0+uO3gwXHSRW0nNz/GlIKSoao+fW0FE1gcojzGmivB4PCxdupQJE95k5sxMLr98KitWNCcv71e0alXCiBHh3HUXtG9vZ6arCl/+pfoFaB1jTDWQn5/Pyy+/zPjxn7J//yDgeaAN8+Y5I8506wbr1kUQFjLzMRpflVsQVPU4gIgkAY8D53q3E2exdj2xjjGmevJ4POzdu5fmzdswc2Y0Tz3Vj6KiR4EwLrmklHvugV/+MozY2HI/yoQwf47lpgGP8OPB54wx1VhWVhaTJk1i7NhlZGffREREa7KyIoiPv5iRI8MYMQJatw53O6YJEH8KQqaqzglaEmNMyEhNTeVvf3uVKVNKKC6+G3gYZ9pzBYTWrcN4+ml3M5rA86cg/EVEJgKLgR/uHSxvVjNjTNVQWlpKQcFxtmyJ5ckno1m48K9ALO3bF/C738Edd0RwzjlupzTB5E9BGAl0wpm37cQpIwWsIBhTheXn5zN+/BT+9reDhIWNJjMzltjYVtx5ZyG//S306lXLbhirIfwpCN1UtUvQkhhjKlVGRgbPPfcOEyZEcfz4r4AGQBYA3bsLU6bYuBE1jT8FYbWInK+qW4OWxhhTKZKT4YYbdrBv30NAGIMGHeX556Ffv/ouJzNu8udK4YHABhHZLiKbRGSziGwKVjBjTGCtWrWGPn3+Rq9e+SQlweHD/bj77lx27w5n6dJG9LO7iWo8f44QhgYthTEmKFSVhQs/54471nL48DXAH4FjXHEFzJgRTr16vgxVZmoKnwuCqu4LZhBjTGBlZSm9er1FaurVwCXAIaAAqE2/flCvnrv5TOgp95SRiKwLxDrGmODzeDx88MFyHn1UOfdcITX1Xjp1KuHTT4vweJqiWgtVeOopt5OaUOTLEcJ55fQVCGC/axjjory8Up555gsmTDhCdvZVhIXBjTfCmDFw4YXxbsczVYQvBaGTD+uUnm0QY4x/MjNhzhwP48cfJDm5AaoXA3nAt3g8rTl0KJwLL3Q7palKfBnczvoOjAkRBw/CtGkwezasWgWqYYSFwTnnfMTo0a144omBxMS0czumqaJsoHJjqoDcXLj6alixwhlLCHZx8cWtGTcujNjYAtq1u5UwG2/anCX7BhkTwoqL4bXXoG1bZcUKgHlAe+AX9Oixnx49oEOH9lYMTEDYt8iYEKQKH30EXbrAf/83FBSsB3rTocPvee+9Zygp2cy4cYkupzTVjc+njEQkGrgBSCy7nao+E/hYxtRca9bA//wPrFwJnTrBrFkeJk78Czfe+F/ceeedRETYmV4THP58s2YD2UAyZYa/NsacPY8HPvkExo2DpUshKup7atd+lpUrn6Jhw3oMH/6x2xFNDeBPQWilqjZ8hTEBlJcHkyfD//0fpKZCWFgm8L8UFc2gqOh3DB8e5e07MCb4/OlDWCUiNvy1MQGwfz888gi0agW//S3UqVMA3EJsbCeefbYuublbUH2YFStquR3V1CD+HCEMBEaIyB6cU0YCqKp2DUoyY6qZjAzntNBHH8HcuQBK795pjB0bT9++tZg48XKuv/41GjZs6HJSU1P5UxCGBS2FMdXUtm0wZ45zI9mXXzpXDzkTDq4E7mPNmjQ6dkwDzuHee+91NasxPp8y8t6xXB/4hfdR3+5iNuan9u51Tgd17AjnnQePPgrHj8Of/lTMgw9Opn79RsAl3HbbhWzfvpFzbKJiEyL8uez0QeA+Ts6hPFVEJqjqK0FJZkwVk53t3Dfw7bcn32vXDpYsgfh42LVrP5063ccVV1zB888/T7du3dwLa8xp+NOpfA/QR1WfVNUngb44BSJgRGSod0a2VBF5LJCfbUywFBfDq686P/zT0uDuu52i4PEoL7zwIX//+4MAtG3blm3btvHpp59aMTAhyZ+CIPx4VNNS73sBISLhwKs4fRXnA7eJyPmB+nxjAk3V6STu0gUeeAA6d4a1a53LSLdvX0zv3r258cYbWbx4MTk5OYBTFIwJVf4UhLeBNSLylIg8BawG3gpglt5AqqruVtUi4APgugB+vjEBs2EDDBkCv/iFUxhmz3ZODTVosJfLL7+cIUOGkJGRweTJk9m4cSN169Z1O7Ix5fJnCs2xIrIcGIBzZDBSVdcHMEtLoMzZV9KAPqeuJCKjgFEACQkJAdy9MeVLTYXrr4ctW06+d/PNMGxYMSKR1K9fn3379jF27FhGjx5NTEyMe2GN8ZNfg6KoajLO0BXBcLrTT3qaDBOACQBJSUk/WW5MMBw8CM8+CxMnnrrkAHPnPsO8ecl89dVX1K9fn23bttnoo6ZK8mVO5ZXeP3NFJKfMI1dEcgKYJQ0oO9dfK+BgAD/fGL99/71z2Wi7dk4xGDXKucv4+++P8uijjxET047Nm9+mf//+HD9+HMCKgamyfJkxbaD3z7ggZ/kaaC8irYEDwK3A7UHepzGnlZ/vjC/00kuQkwN33AFPPw1t2sCGDRu49NJLyc7O5vbbb+eZZ56hTZs2bkc25qz5/KuMiLzoy3sVpaolwAPAAiAFmKGq3wTq843x1ciRUKcOPP64c2/Bb34Db71VxLFjTsfBBRdcwM0338z69euZOnWqFQNTbYiqb6fhRWSdqvY85b1Nbo5llJSUpGvXrnVr96aaOX4cnngC/vGPsu+Wcv3177Nhw5McO3aM3bt3U7t2bbciGhMQIpKsqkmnvu9LH8JoEdkMdBKRTSKy2fvYC2wOQlZjKt3atdCzp1MM7r8fcnKU2bPn0Llzd2bN+hX169dn8uTJ1Kplo4+a6suXq4ym4Uzk+jzwGN5RToFcVT0axGzGBF1xMTz3HPz1r9CsGcyfD1deCV98sYrrrruO9u3b88EHH3DTTTdZZ7Gp9nwpCHNVdaCIXAtcU+Z9ERFVVbvjxlRJW7fCXXdBcjLceSfcfvtq9u/fDNxH//79mTlzJtdccw2RkZFuRzWmUpT7K0+Zq4zqqGrdMo84KwamKtq3z5m4vkcP5/lLL+3i8OFhXHVVP55//nmKi4sREa6//norBqZGsWNgU2Ps2AG//rVzT8Hrr0NRUTaHD49kzJh2LFr0FS+++CJbtmyxImBqLH8uO71JROK8z/8sIjNFpGd52xnjtk2b4NZbnbkJ3n8f7r/fw759sGVLGvXrf8RTTz3F4cO7GTNmDLGxsW7HNcY1/gxd8WdV/beIDASuAP4OvM5pxhsyJhSsXesMNzFnjnNfwciRhzly5AkyM7OIj/8AuIADBw7YZaTGePlzyujE0NdXA6+r6mwgKvCRjDk7+/Y5Q1L36uUUA8gkPv4+Jk1qwsKFzo1kJ+6/sWJgzEn+FIQDIvIGcDMwV0Si/dzemKDKzobHHnOmrty27cS7M4CmpKa+z5gxY9izZw/PP/88IgGbysOYasOfH+g34wwrMVRVs4AGwCPBCGWMP8rOWPbiizBkyPfMmrURVTh0aBCPPfYoBw7s4YUXXqBx48ZuxzUmZPk8dAWAiHQDLvK+XKGqG4OSykc2dEXNdmLGskcege3boWfPbGrXfpKVK1/miiuuYMGCBW5HNCYkVXjoijIf8CDOXctNvI+pIvLbwEU0xne5uXDbbXDttXDsWAFdujzBunX1SUmZxrPPPsv06dPdjmhMlePPVUb3AH1UNR9+GOn0S+CVYAQz5ky++QZ69VIKCgCEb7+dyKFDb/GPf/yDUaNGUadOHbcjGlMl+dOHIJy80gjvc+uZM5Vq8uQievYspqjoCH/4w1xUoaDgPrKz9/Dwww9bMTDmLPhzhPA2sEZEZuEUguuAt4KSyphTHDx4hOuv381XX/UCVtCly3MMGfIQgM1bbEyA+FwQVHWsiCwDBnrfGqmq64OSypgynPsKDpGb24vWrWcyYUJDBg+eZ5eOGhNg/nQqxwCDgEuBS4BB3veMCShVZfHixdx0003MmJFLz55QWtqBf/5zP7t3/5IhQy6xYmBMEPhzyuhdIBd42fv6NmAKcFOgQ5maKT8/nylTpvDyy6+QktKEsLCn+M9/TkzlHUFWVoKb8Yyp9vwpCB1VtVuZ10tFxNX7EEz18f3339OmTVuyswcQG/s+0JVatZT8fLeTGVNz+HOV0XoR6XvihYj0Ab4IfCRTExQXFzNr1iyee+45Skth4cIG1Kq1DfiERo268NprcPiwoMoPj6eecju1MdWbP0cIfYC7RGS/93UCkOKdb1lVtWvA05lqJzU1lbfeeou3336bQ4cyaNTod0ye7CE1NYxOnZry4otw222CTUlgTOXzpyAMDVoKUyNMnjyZkSNHEh4eTv/+D5GT8ziHD5/D4cPO8iZNnCktjTHu8Oey033BDGKqF1Xlyy+/ZNq0aQwbNoxrrrmGwYMH8+ij49iz5z5mzKhN06bw2mtOEbD5641xnz9HCMaUa/v27UybNo1p06axe/duYmJiaNOmDZdffg3vvRfPq68+SFERPPoo/OlPUNdm5TYmZFhBMGctPz+f2NhYVJVrr72W1NRULrvsMp588kmuu+56li+vywUXwK5dzmB0//iHM1S1MSa0+FwQRGQc8Hv1Z7xsU23t27ePWbNmMXPmTL755hsOHDhATEwM77zzDk2bJrB9ewueeAJGjDi5zZ13wpQprkU2xpTDnyOEPGCOiNyqqvkicgXwF1UdEKRsJgQtXryYRx99lOTkZAC6du3K7373OzIyCvniixhmz+7L3LnO8NRRp0yw2ratC4GNMT7zp1P5CRG5HVgmIoVAPvBY0JIZ1xUWFrJy5Urmz5/PDTfcQN++fYmNjSUyMpKXXnqJyy67gU2b2jBjBjz3nDNzWZMmcOutMHw4XHYZ2LhzxlQd/pwyGgzch1MImgP3qOr2YAUz7igsLGTixInMnz+fJUuWcOzYMSIjI4mPj6dv376cd15fRo/+khkz4PHHnSJQVqdOMGGCO9mNMWfH5yk0RWQJ8KSqrhSRLjjjGD2sqkuCGfDn2BSaZ+/gwYOsWLECgFtuuQWPx0PTpk2pV68eV1wxjAEDfkHHjgPYsSOW6dNh/nwoKoKEBLj5ZrjlFrjwQrCx5oypOs40haZfcyqf8oHNgQ9Vtf/ZhqsoKwgVM2vWLObMmcPnn3/O7t0HgEto1Og2Bg8eQWYmpKeXcORIBIcPg8fz42379oV//hP69LEiYExVdaaCUOHLTlU13XsayYSojIwMkpOTSU5OJiUlhalTpyIiTJ36BQsWNKZOnelERnajuDiSw4eVE9MQN2oUwQ03OP0Ba9fCvHknP/PKK52iYIypfs7qPgRVLQhUEFNxHo+H/fv307x5c6Kjo3n33Xd5/PHHSUtL864RTsuWt/LAAwUsXVqblJS/A9C0qXPaZ9gwGDRIqFXLvb+DMcZ9dmNaFbR7926mTp1KSkoK27ZtY/v27RQUFLBq1Sr69etHs2bN6N9/MLGxw0lP78uiRU04cCCM115ztm/bFubOhfbt7bSPMeakkCgIIvK/wC+AImAXzvScWa6GqmSlpaWUlJQQHR3NkSNHmDr1PXbsyGDXrlz27SsgPb2UG264n969k0hPL+Tpp1fRvHlt2rXrxV13XUeXLgk0bNiG996DWbOuYN68K8jPh3r1nI7f6693TvfYHPTGmDOpcKdyQEM4N7ktUdUSEXkRQFUfLW+7YHQqezywfj00bAiJif5s56G0tJTIyEhUlZSUFHJycn545Obm0qlTJ/r168ehQ9mMHPlnDh4MJzMzhqysuhw71pCOHQcTE9OW9PQSMjIUqNgY0M2aOfcBXH89DBr00xvEjDE1W8A7lQNJVT8r83I1cGOw93n33Xezd+9eVBWPR8nP70B4+J3s3HkpOTkn1jpArVpf07jxV9SqtZqrrurO2LFjATj//PPJzMyksLCQ48ePU1xczD333MPEiRNRVTp37oxqONAR6AZ0IyGhH0VF8N139Tg5E6kjLCyP7T/c1RFGy5bH+J//iaBpU6FJE6eDt3Fjp2BlZUF2tvNnVhb85S+wc+fJz+rQAV5/PWhNZ4yppkKiIJzi18D0My0UkVHAKICEhIrPsSsiFBQkkpExmIyMSykoiCcsrIR27fAWhHeA9hQWDmb//uGAM81jQYFzyWWrVn+kaVMhLCyGsLBoRKIpLIzn6adh//4wEhOPkJZWl+LicACiopQjRzw/mhKya1f4z3+gVSuoVavsuZww4Mzndlq0+PHr226rcDMYY8wPKu2UkYgsApqdZtHjqjrbu87jQBLwS18G0avoKaPJk51r6Tdtcsbhv/RSZ7iFX/4SGjT48bqlpbBxIyxdCm+88ePfxMvTqhW88AJ06wYdO2KzgBljQoLrp4xUdcjPLReRu4FrgMHBHFF10CBYvvzk6z59YNGiM68fHg49ezqP3Fx4+umTyx56yBnXPyrK+WF/4k+b7MUYUxWFSqfyUGAscImqZvq6XUWPEFTtcktjTM11piOEUPld9l9AHLBQRDaIyPhg7syKgTHG/FRIdCqrqs2fZYwxLguVIwRjjDEuC4k+hIoSkUxgXwU3bwQcDmCcQLFc/rFc/rFc/gnVXHB22c5V1canvlmlC8LZEJG1p+tUcZvl8o/l8o/l8k+o5oLgZLNTRsYYYwArCMYYY7xqckEI1Zl/LZd/LJd/LJd/QjUXBCFbje1DMMYY82M1+QjBGGNMGVYQjDHGADWgIIjIUBHZLiKpIvLYaZaLiLzsXb5JRHqGSK5BIpLtHcpjg4g8WQmZJolIhohsOcNyt9qqvFyV3lbe/caLyFIRSRGRb0TkwdOsU+lt5mMuN75fMSLylYhs9OZ6+jTruNFevuRy5Tvm3Xe4iKwXkU9Osyyw7aWq1fYBhONMydkGiAI2Auefss5VwDxAgL7AmhDJNQj4pJLb62KgJ7DlDMsrva18zFXpbeXdb3Ogp/d5HLAjRL5fvuRy4/slQB3v80hgDdA3BNrLl1yufMe8+34YeO90+w90e1X3I4TeQKqq7lbVIuAD4LpT1rkOeFcdq4H6ItI8BHJVOlX9HPj+Z1Zxo618yeUKVU1X1XXe57lACtDylNUqvc18zFXpvG2Q530Z6X2celWLG+3lSy5XiEgr4Gpg4hlWCWh7VfeC0BL4tszrNH76H8OXddzIBdDPexg7T0QuCHImX7jRVr5yta1EJBHogfPbZVmuttnP5AIX2sx7+mMDkAEsVNWQaC8fcoE737FxwBjAc4blAW2v6l4QTjfQ9amV35d1As2Xfa7DGW+kG/AK8FGQM/nCjbbyhattJSJ1gA+Bh1Q159TFp9mkUtqsnFyutJmqlqpqd6AV0FtEOp+yiivt5UOuSm8vEbkGyFDV5J9b7TTvVbi9qntBSAPiy7xuBRyswDqVnktVc04cxqrqXCBSRBoFOVd53GircrnZViISifNDd5qqzjzNKq60WXm53P5+qWoWsAwYesoiV79jZ8rlUnsNAK4Vkb04p5UvE5Gpp6wT0Paq7gXha6C9iLQWkSjgVmDOKevMAe7y9tb3BbJVNd3tXCLSTMSZykdEeuP8Wx0Jcq7yuNFW5XKrrbz7fAtIUdWxZ1it0tvMl1xutJmINBaR+t7ntYAhwLZTVnOjvcrN5UZ7qeofVbWVqibi/IxYoqp3nrJaQNsrJCbICRZVLRGRB4AFOFf2TFLVb0Tkfu/y8cBcnJ76VOAYMDJEct0IjBaREqAAuFW9lxUEi4i8j3M1RSMRSQP+gtPB5lpb+Zir0tvKawDwK2Cz9/wzwJ+AhDLZ3GgzX3K50WbNgXdEJBznB+oMVf3E7f+PPuZy6zv2E8FsLxu6whhjDFD9TxkZY4zxkRUEY4wxgBUEY4wxXlYQjDHGAFYQjDHGeFlBMMYYA1hBMMYY4/X/9Uh0y5lWQ6MAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -416,12 +462,6 @@ } ], "source": [ - "#\n", - "# Predictor-corrector calculations\n", - "#\n", - "# Instead of using create_lqe_iosystem, we can also compute out the estimate\n", - "# in a more manual fashion, done here using the predictor-corrector form.\n", - "\n", "# System matrices\n", "A, B, F = discsys.A, discsys.B, discsys.B\n", "\n", @@ -446,8 +486,8 @@ " Pkk = Pkkm1 - L @ C @ Pkkm1\n", "\n", " # Save the state estimate and covariance for later plotting\n", - " # xhat[:, i], P[:, :, i] = xkk, Pkk\n", - " xhat[:, i], P[:, :, i] = xkkm1, Pkkm1 # For comparison to Kalman form\n", + " xhat[:, i], P[:, :, i] = xkk, Pkk\n", + " # xhat[:, i], P[:, :, i] = xkkm1, Pkkm1 # For comparison to Kalman form\n", " \n", "plt.subplot(2, 1, 1)\n", "plt.errorbar(T, xhat[0], P[0, 0], fmt='b-', **ebarstyle)\n", @@ -460,15 +500,23 @@ "plt.ylabel(\"$x$ position [m]\");" ] }, + { + "cell_type": "markdown", + "id": "a9d5cb32", + "metadata": {}, + "source": [ + "We can compare the results of the predictor-corrector form to the Kalman filter form used at the top of the notebook:" + ] + }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 26, "id": "4eda4729", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -492,10 +540,18 @@ "lims = plt.axis(); plt.axis([lims[0], lims[1], -0.2, 0.2]);" ] }, + { + "cell_type": "markdown", + "id": "3f7e3e4d", + "metadata": {}, + "source": [ + "Note that the estimates are not the same! It turns out that to get the correspondence of the two formulations, we need to define $\\hat{x}_\\text{KF}(k) = \\hat{x}_\\text{PC}(k|k-1)$ (see commented out code above)." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "9bfe8aec", + "id": "0796fc56", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/pvtol-lqr-nested.ipynb b/examples/pvtol-lqr-nested.ipynb index 59e97472a..63fde31f3 100644 --- a/examples/pvtol-lqr-nested.ipynb +++ b/examples/pvtol-lqr-nested.ipynb @@ -532,7 +532,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/examples/pvtol-outputfbk.ipynb b/examples/pvtol-outputfbk.ipynb index e025e4f5d..8656ed241 100644 --- a/examples/pvtol-outputfbk.ipynb +++ b/examples/pvtol-outputfbk.ipynb @@ -5,15 +5,15 @@ "id": "c017196f", "metadata": {}, "source": [ - "# PVTOL LQR + EQF example\n", + "# Output feedback control using LQR and extended Kalman filtering\n", "RMM, 14 Feb 2022\n", "\n", - "This notebook illustrates the implementation of an extended Kalman filter and the use of the estimated state for LQR feedback." + "This notebook illustrates the implementation of an extended Kalman filter and the use of the estimated state for LQR feedback of a vectored thrust aircraft model." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 11, "id": "544525ab", "metadata": {}, "outputs": [], @@ -30,8 +30,12 @@ "metadata": {}, "source": [ "## System definition\n", - "The dynamics of the system\n", - "with disturbances on the $x$ and $y$ variables is given by\n", + "We consider a (planar) vertical takeoff and landing aircraf model:\n", + "\n", + "![PVTOL diagram](https://murray.cds.caltech.edu/images/murray.cds/7/7d/Pvtol-diagram.png)\n", + "\n", + "The dynamics of the system with disturbances on the $x$ and $y$ variables are given by\n", + "\n", "$$\n", " \\begin{aligned}\n", " m \\ddot x &= F_1 \\cos\\theta - F_2 \\sin\\theta - c \\dot x + d_x, \\\\\n", @@ -39,17 +43,27 @@ " J \\ddot \\theta &= r F_1.\n", " \\end{aligned}\n", "$$\n", + "\n", "The measured values of the system are the position and orientation,\n", "with added noise $n_x$, $n_y$, and $n_\\theta$:\n", + "\n", "$$\n", " \\vec y = \\begin{bmatrix} x \\\\ y \\\\ \\theta \\end{bmatrix} + \n", " \\begin{bmatrix} n_x \\\\ n_y \\\\ n_z \\end{bmatrix}.\n", - "$$\n" + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "198a068d", + "metadata": {}, + "source": [ + "The dynamics are defined in the `pvtol` module:" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 12, "id": "ffafed74", "metadata": {}, "outputs": [ @@ -93,9 +107,17 @@ "print(noisy_pvtol)" ] }, + { + "cell_type": "markdown", + "id": "be6ec05c", + "metadata": {}, + "source": [ + "We also define the properties of the disturbances, noise, and initial conditions:" + ] + }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 13, "id": "1e1ee7c9", "metadata": {}, "outputs": [], @@ -114,12 +136,14 @@ "id": "e4c52c73", "metadata": {}, "source": [ - "## Control system design" + "## Control system design\n", + "\n", + "We start be defining an extended Kalman filter to estimate the state of the system from the measured outputs." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 14, "id": "3647bf15", "metadata": {}, "outputs": [ @@ -127,7 +151,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Object: sys[1]\n", + "Object: sys[3]\n", "Inputs (5): x0, x1, x2, F1, F2, \n", "Outputs (6): xh0, xh1, xh2, xh3, xh4, xh5, \n", "States (42): x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12], x[13], x[14], x[15], x[16], x[17], x[18], x[19], x[20], x[21], x[22], x[23], x[24], x[25], x[26], x[27], x[28], x[29], x[30], x[31], x[32], x[33], x[34], x[35], x[36], x[37], x[38], x[39], x[40], x[41], \n" @@ -175,9 +199,17 @@ "print(estimator)" ] }, + { + "cell_type": "markdown", + "id": "8c97626d", + "metadata": {}, + "source": [ + "We now define an LQR controller, using a physically motivated weighting:" + ] + }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 15, "id": "9787db61", "metadata": {}, "outputs": [ @@ -209,14 +241,11 @@ "Object: xh5\n", "Inputs (13): xd[0], xd[1], xd[2], xd[3], xd[4], xd[5], ud[0], ud[1], Dx, Dy, Nx, Ny, Nth, \n", "Outputs (14): x0, x1, x2, x3, x4, x5, F1, F2, xh0, xh1, xh2, xh3, xh4, xh5, \n", - "States (48): noisy_pvtol_x0, noisy_pvtol_x1, noisy_pvtol_x2, noisy_pvtol_x3, noisy_pvtol_x4, noisy_pvtol_x5, sys[1]_x[0], sys[1]_x[1], sys[1]_x[2], sys[1]_x[3], sys[1]_x[4], sys[1]_x[5], sys[1]_x[6], sys[1]_x[7], sys[1]_x[8], sys[1]_x[9], sys[1]_x[10], sys[1]_x[11], sys[1]_x[12], sys[1]_x[13], sys[1]_x[14], sys[1]_x[15], sys[1]_x[16], sys[1]_x[17], sys[1]_x[18], sys[1]_x[19], sys[1]_x[20], sys[1]_x[21], sys[1]_x[22], sys[1]_x[23], sys[1]_x[24], sys[1]_x[25], sys[1]_x[26], sys[1]_x[27], sys[1]_x[28], sys[1]_x[29], sys[1]_x[30], sys[1]_x[31], sys[1]_x[32], sys[1]_x[33], sys[1]_x[34], sys[1]_x[35], sys[1]_x[36], sys[1]_x[37], sys[1]_x[38], sys[1]_x[39], sys[1]_x[40], sys[1]_x[41], \n" + "States (48): noisy_pvtol_x0, noisy_pvtol_x1, noisy_pvtol_x2, noisy_pvtol_x3, noisy_pvtol_x4, noisy_pvtol_x5, sys[3]_x[0], sys[3]_x[1], sys[3]_x[2], sys[3]_x[3], sys[3]_x[4], sys[3]_x[5], sys[3]_x[6], sys[3]_x[7], sys[3]_x[8], sys[3]_x[9], sys[3]_x[10], sys[3]_x[11], sys[3]_x[12], sys[3]_x[13], sys[3]_x[14], sys[3]_x[15], sys[3]_x[16], sys[3]_x[17], sys[3]_x[18], sys[3]_x[19], sys[3]_x[20], sys[3]_x[21], sys[3]_x[22], sys[3]_x[23], sys[3]_x[24], sys[3]_x[25], sys[3]_x[26], sys[3]_x[27], sys[3]_x[28], sys[3]_x[29], sys[3]_x[30], sys[3]_x[31], sys[3]_x[32], sys[3]_x[33], sys[3]_x[34], sys[3]_x[35], sys[3]_x[36], sys[3]_x[37], sys[3]_x[38], sys[3]_x[39], sys[3]_x[40], sys[3]_x[41], \n" ] } ], "source": [ - "#\n", - "# LQR design w/ physically motivated weighting\n", - "#\n", "# Shoot for 1 cm error in x, 10 cm error in y. Try to keep the angle\n", "# less than 5 degrees in making the adjustments. Penalize side forces\n", "# due to loss in efficiency.\n", @@ -256,12 +285,14 @@ "id": "7bf558a0", "metadata": {}, "source": [ - "## Simulations" + "## Simulations\n", + "\n", + "We now simulate the response of the system, starting with an instantiation of the noise:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 16, "id": "c2583a0e", "metadata": {}, "outputs": [ @@ -302,7 +333,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 17, "id": "ad7a9750", "metadata": {}, "outputs": [ @@ -337,17 +368,17 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 18, "id": "c5f24119", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 8, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" }, @@ -389,12 +420,14 @@ "id": "0c0d5c99", "metadata": {}, "source": [ - "### Full state feedback" + "### Full state feedback\n", + "\n", + "As a comparison, we can investigate the response of the system if the exact state was available:" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 19, "id": "3b6a1f1c", "metadata": {}, "outputs": [ diff --git a/examples/stochresp.ipynb b/examples/stochresp.ipynb index c16a6a5e7..224d7f208 100644 --- a/examples/stochresp.ipynb +++ b/examples/stochresp.ipynb @@ -9,6 +9,7 @@ "Richard M. Murray, 6 Feb 2022\n", "\n", "This notebook illustrates the implementation of random processes and stochastic response. We focus on a system of the form\n", + "\n", "$$\n", " \\dot X = A X + F V \\qquad X \\in {\\mathbb R}^n\n", "$$\n", @@ -18,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 83, "id": "902af902", "metadata": {}, "outputs": [], @@ -27,18 +28,35 @@ "import scipy as sp\n", "import matplotlib.pyplot as plt\n", "import control as ct\n", - "from math import sqrt, exp" + "from math import sqrt, exp\n", + "\n", + "# Fix random number seed to avoid spurious figure regeneration\n", + "np.random.seed(1)" + ] + }, + { + "cell_type": "markdown", + "id": "d020a2ec", + "metadata": {}, + "source": [ + "We begin by defining a simple first order system\n", + "\n", + "$$\n", + "\\frac{dX}{dt} = - a X + V, \\qquad Y = c X\n", + "$$\n", + "\n", + "and a (scalar) white noise signal $V$ with intensity $Q$." ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 84, "id": "60192a8c", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -53,7 +71,7 @@ "# First order system\n", "a = 1\n", "c = 1\n", - "sys = ct.tf(c, [1, a])\n", + "sys = ct.ss([[-a]], [[1]], [[c]], 0)\n", "\n", "# Create the time vector that we want to use\n", "Tf = 5\n", @@ -61,9 +79,7 @@ "dt = T[1] - T[0]\n", "\n", "# Create the basis for a white noise signal\n", - "# Note: use sqrt(Q/dt) for desired covariance\n", "Q = np.array([[0.1]])\n", - "# V = np.random.normal(0, sqrt(Q[0,0]/dt), T.shape)\n", "V = ct.white_noise(T, Q)\n", "\n", "plt.plot(T, V[0])\n", @@ -81,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 85, "id": "23319dc6", "metadata": {}, "outputs": [ @@ -89,8 +105,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "mean(V) [0.0] = 0.14775487875720242\n", - "cov(V) * dt [0.1] = 0.09761761761761763\n" + "mean(V) [0.0] = 0.17348786109316244\n", + "cov(V) * dt [0.1] = 0.09633133133133133\n" ] } ], @@ -100,15 +116,23 @@ "print(\"cov(V) * dt [%0.3g] = \" % Q, np.round(np.cov(V), decimals=3) * dt)" ] }, + { + "cell_type": "markdown", + "id": "3196c60d", + "metadata": {}, + "source": [ + "The response of the system to white noise can be computed using the `input_output_response` function:" + ] + }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 86, "id": "2bdaaccf", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -121,8 +145,7 @@ ], "source": [ "# Response of the first order system\n", - "# Scale white noise by sqrt(dt) to account for impulse\n", - "T, Y = ct.forced_response(sys, T, V)\n", + "T, Y = ct.input_output_response(sys, T, V)\n", "plt.plot(T, Y)\n", "plt.xlabel('Time [s]')\n", "plt.ylabel('$Y$');" @@ -133,14 +156,12 @@ "id": "ead0232e", "metadata": {}, "source": [ - "This is a first order system, and so we can use the calculation from the course\n", - "notes to compute the analytical correlation function and compare this to the \n", - "sampled data:" + "This is a first order system, and so we can compute the analytical correlation function and compare this to the sampled data:" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 87, "id": "d31ce324", "metadata": {}, "outputs": [ @@ -148,8 +169,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "* mean(Y) [0] = 0.0985\n", - "* cov(Y) [0.05] = 0.0207\n" + "* mean(Y) [0] = 0.165\n", + "* cov(Y) [0.05] = 0.0151\n" ] } ], @@ -162,15 +183,23 @@ "print(\"* cov(Y) [%0.3g] = %0.3g\" % (r(0), np.cov(Y)))" ] }, + { + "cell_type": "markdown", + "id": "28321bee", + "metadata": {}, + "source": [ + "Finally, we look at the correlation function for the input and the output:" + ] + }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 88, "id": "1cf5a4b1", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -183,9 +212,6 @@ ], "source": [ "# Correlation function for the input\n", - "# Scale by dt to take time step into account\n", - "# r_V = sp.signal.correlate(V, V) * dt / Tf\n", - "# tau = sp.signal.correlation_lags(len(V), len(V)) * dt\n", "tau, r_V = ct.correlation(T, V)\n", "\n", "plt.plot(tau, r_V, 'r-')\n", @@ -195,13 +221,13 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 89, "id": "62af90a4", "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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zry5Npzqeov/EgkMGxV9JosVB5N4sJc3zVxrWrucCsvKyuLChQsAvPLKK2zZsoW33nrL0DmGRNWTkZFBSEgId999NxMmTOD06dPWLqnCajs78M8RbTicmMG60KpXf0VIYNigS1N81HZy4IlBraxdToVorfn1119p3749L7zwAgDdunVj4MCB1i1M2CQ3Nzf27NnDW2+9xerVq2nbti0LFiwwfEEmc7u7iydtG7vw3sZI8gurVu0VIYFhgzYePc3WqFRmDvXDrXbVGW4aHh7OyJEjGTNmDLVr12bixInWLklUAU5OTrz66quEhITQpUsX3njjjbKr06rC3k7x4si2nEjL5ps9x61djmEkMGxMdn4hb64Np21jFybf5mPtcky2dOlSOnbsSFBQEB999BFHjhzhtttus3ZZogpp3bo1f/zxB3v27KFu3brk5+czc+ZM4uLirF2aSQa28WBQGw8++P0YieeyrV2OISQwbMziHfGcOp/LW3e2t/mHgc6dO0dCQgIA/fv3Z8aMGURHR/PMM8/IMxaiUpRSeHt7A3D48GEWL16Mv78/jz/+eNm/NVullOLfd3VAo/lo8zFrl2MI2/6NdIvJzi/kq13xDPVvaOg04jcrNTWV119/HR8fn7IlOX19ffn0009l/QphNj169CA6OpopU6awePFiWrVqxfTp07l48aK1S7umZq41ebC3D78cOklMStW6rWYKCQwb8tP+RNKzC3h0QEtrl1KuyMhIpk+fjpeXF2+99RbDhw/n7bfftnZZohpr2rQpX3zxBTExMTzyyCMcOXKEWrVKlgZOTEy0cnXlm9HfFycHOxZuj73xwVWMBIaNyC8sZuH2OLp5u9HNhq4uCgsLKSwsBOCHH35g2bJlPPTQQ4SHh/Pf//6X9u3bW7lCcSto3rw5n376Kbt370YpRWZmJu3ataNnz55899135OXlWbvEMvXrOPO3bl6sOnSS0+dvvBpgVSKBYSNWHkzi1PlcnhxsG8NoY2JiePnll2nevDlr1qwB4JlnniEhIYEFCxbg7+9v5QrFrcjevmTqfkdHR959910yMjKYOHEiTZo04cknnyQ+Pt7KFZZ4pJ8vxRqW7KwaHfamksCwARnZ+bz/exSBXq4MaG29PoDCwkLmzZtH37598fPzY/bs2XTt2rVsjQo3NzfpoxA2oWbNmjzxxBNERETw+++/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\n", "text/plain": [ "
" ] @@ -214,8 +240,6 @@ ], "source": [ "# Correlation function for the output\n", - "# r_Y = sp.signal.correlate(Y, Y) * dt / Tf\n", - "# tau = sp.signal.correlation_lags(len(Y), len(Y)) * dt\n", "tau, r_Y = ct.correlation(T, Y)\n", "plt.plot(tau, r_Y)\n", "plt.xlabel(r'$\\tau$')\n", @@ -230,9 +254,9 @@ "id": "2a2785e9", "metadata": {}, "source": [ - "The analytical curve may or may not line up that well with the correlation function based on the sample. Try running the code again from the top to see how things change based on the specific random sequence chosen at the start.\n", + "The analytical curve may or may not line up that well with the correlation function based on the sample. Try running the code again with a different random seed to see how things change based on the specific random sequence chosen at the start.\n", "\n", - "Note: the _right_ way to compute the correlation function would be to run a lot of different samples of white noise filtered through the system dynamics and compute $R(t_1, t_2)$ across those samples." + "Note: the _right_ way to compute the correlation function would be to run a lot of different samples of white noise filtered through the system dynamics and compute $R(t_1, t_2)$ across those samples. The `correlation` function computes the covariance between $Y(t + \\tau)$ and $Y(t)$ by varying $t$ over the time range." ] }, { diff --git a/examples/vehicle-steering.png b/examples/vehicle-steering.png new file mode 100644 index 0000000000000000000000000000000000000000..f10aab853f4fe6a445a2e71a91dfbbd82ccddccc GIT binary patch literal 13510 zcmcJ$WmJ?=`!76*Gzx-*q=0~cgh&o0EiLHK-QC>^h_oOn0t(XI4bmaf-9t+bJ-`6( z=6TNf@Sb(nI-mZs)O*d`vG2XF>-xpD345z7`v8{$7XpDikb5Jg27#c7f#WW0Ozj}V6nlTlZfG}`l^)ur`?2Z7JK z3zcz80tRn2M&Nj+!jLacgce6;O}?LF&;v_X<$hs62uZyEuwW2jq@$OYJjTR!s=sH! z<`i|$g7pxNmip^HUuP29+brk$QCkSlzPL!%m!tE{G0>a$th!cQnS2(~15 z+cz;~C2aAX@&q`Ueu0o6T zTCl|RcWL_+x|cn-Lf5U#^W{9`%P4vyHP+6@pNny&SzZ#_yxx8BM=A3COe6`o(bdH*6V1&VJ!DmeR+9_*9NJ$vkA{NB^x0~d>K3hnOW7$HKjc|3Qm zr{;_VN7$w&nV9G8WcNd#pGS+3ef~-e6Q+HJA&%2uEg&67mRr%oe&5Row;7{zzoK1g zLR&Z+LZi$mmd?oXSwXEkjBMSJVI}@>`H%Qx!yv=o5a(&`wr5|^rR)}>77$6!OphHu zJT^r~b5yg!)cEyq;ImN%cKB~vLYzQbOTFRWMUcl5*GY866(0Ar&?`gG*__+neLfa{ z)*IBij$YU)xv2J=IPcMQhY%SbO(Z`7Ul*!%iom+nO7kso_GmAb2saNFdl+4(p|QZr zc*__{_)Q%r4aU{$X3@R z*Ug9jfWPd_>^(idQ0@LE81Ha4JP>m;vt*V*(9=uHUM@ZDzx%@do=^;RHw7BH&?jD_ z;^{{iG*CSiOy#K`wZR`eo#|q>Xm9FRGr|@7b+>gJbVb=VaqqvwdrTH1BFC-7+D}T# zrHR;kYUtQ0y!i-UgG@sM^7w#aeQ9bA`N0D_bLmov|JDP7iatF*r@hRH5WxJc&*^zh z;6FC=5^Y!=Yk6KgT~|AA93CDGxn<;mE4{j(^P6b8?Yi!s$}-+PaqQ2K@~1ezKHzZu zk?6y>-u(MLSGAOK1f1&i1`r#pXhjyY1-(_4^O&=Jws`z0#dtmYSUDR>&XO`8vW zkHwk_jb_c|$hOa=$z9B8$sV!ft9q>sEe@=SS62{{^Lc&sLww3{a@i8WYi{%2ipNrS z#4}rNvTD-QN_qIjXwmR|sh3=(PDGJeR@dZ;_21EgQD~mqyF!iB5)pYJbs+yVw1T}|H zGJmRelCur_M?JDL+;d`gB6#U_`RC;P-_r0c&wwepO|s$iiqUCD3BHd5*ZkAr*M@J6 z%jT9F+L$Fj8GSY?f#Y__bX>{Q#RjQ1jXchJAd?%_5W4g5oO+N_pWAl{A=I5$9^~}# z&~=r)ySTfVDTm33iRddY6SGo-(y0=H-DI!qPN5$U zX&y?1vW6-|=Vj$)^*S}1Tw%9kA3R!lG{ImGRZY`W4pDYfCP+I?%lf`4$PaC$u;S(R zTMLi1ko9*vbnJ7P*>?7bAI$r#dMv{mAXjuvi-gT4_NkxvXzlBU_)h5=PFAi%dFSMwLHJ zByQwB^Z#x)zg8!C*omw|79ewRvWU!|eOLO$tgJX6i{xN=ZlyK4VA^Qv<*X2H-d`6# z8-JePmT#6%Z6Xt$BDi7vP4(OJVwSY{^tcxGW)IumX_$#nk5?ocjfY9o9x{J*NQLX$ z7k+rJ)ZeiAu&YS4C!} zv~don_{R!RrYl8#-g;RImIWqB(^WGij0Qwj3(TJRR_>;3xV5YR(Gefda+PbXLZV`q zw!vk0SY;voQ@T~LC-*p~c7QA6rAK#1N>NIx1zjb#MfE_<_hv))&F^J`ye=#*E}J)e zLuUFjO|2WbgAV$J6^4T!d&EMn-)j`9zF=>uANGKEOC#w=GHbf*Bc@*5Xt#6Lzui{T zR=xDXap()lkiFKuaJd~V%C`F>oMfk|tjnSAZzBCW{2TXPMP@}0Tb2f2{HuP2{#$Kc zr-=*P^YExbmcsQyA=WI`)Je@^t(J{rZTRdCOdQrY^`um%Y5&^$=8}!_9{;!l&x~fn z;TY3Mj!tR^AOHKg_Zqfz_AK*9t~ULXm91O>`3vS%#jbNndnY{%d+{?d>ryfcBXg+H``n!LpeM}72S`x1Zu1{OFZyWX@*RSQ! zN4biB=BiFlkHk-o`*W=rDpWcge~=sut)b-;({+lcsQ+BaETO;Ryc5%u8kl=sm- z9N5*`bD7!H z?fQ*|f`jfskjKTr_1+zbt2E(EFKfozr8XGOv$KcatpzB7e(|Att7G6H=C=d}pRKjz?*0Y5+5bR|im z!50JK$pR-HNL&-z_6Fbhd-^}lE-+_RqcjQ6;iX&BN!wou5MQmur6rAprCT#ReA3D2 zp00k1_jFR4uWWHI;!-kjZla#=j2)aISFs;Istcm+2|zm^`cn5wccljikEbSY)RZ6) zZzc#NFc<>41Rn)%K_G5i5Xd%AB+z6Cgv{}KgQ_t20@GMQRtj=^_w%DU_bd1W$MKDp z3j{*Sc=s1gPVE^2e2C>LrzDNFfll=B$#W95RALB(2_+{b{@!zTZ^29V{pn55;Z(|# zFVdZFb-5lWjf^*~w4mVz<=2kxF1{!w@OOBK=xCUOIUlb(4&j{i1?^D`RuDEht%VbKv}s%}36C8inwb660IsMLJ%rZt=}lP^COG1ubP7@R?vsSb{>whK!IoK}L1 zcUXf7LkdJt?_rGG+;P3VZ*-<$*mSrsA0#6aQ(R)AjIQqY|DGg|FIG7-P;S!G+tqC6cW-NFd7QbWBc>Wk5i~0Qd_sGbj zH098sAoTFeGMgz5ohdDBy0`M$11FQkI`=U#NAuq?517Bwsj-D0_Rw1FOct--eEBz) z`^sfwfZHP;n@-AYEa9YkvDs&)#ZPEvW+tzoA6)t5m3d_GAGTmZSmt!kVgwl{(c{OR ztv9EfP0h_-n`s_f3yu5?ve8v;ULk~;@{fHsJ%&Wi!Pv1BnTWGwV`Os24*&8m{g|+} z)6=I5&Lisjl#!ZBDH}tx-W$shNh8uwHYSNpw;e$=hoa!Ie4QccXR9X)P9&{}Y$j~y zkVE3o?Z$yf`5!vW`|5vvszF3dY^R5>8Y+hjgKln(WT6gjZ#b2{@szu%N>S$&q8Nko z1VQB3&Qyeym2rJfOS99X`tRAR$3tSwbaWrwb|(91Ml(mBp`_BvO#6r)OVjc2j895G zIt{M62-E&+%lFD|mPfnVx@$cD-QCqOZ;)ZyZ2bI58v0%#j~HYStK+@ZWI7tMdK|{-SuKFAUH37X$%S2Pj zZ4Rb+-Cm!wQRQU%e+?jMHwQOyXt`k8o2#|xk7pri=q}c&1?jWg9)bt*1>L~Fzh@Nk)sC7=rJos{c%4?^%ws&A)?W>At zTU*R9f)nZcm)>&cp zXKTLx*zQ2&V(qK#Y!yyGK!E5i%H=(b{ZyOjYuL_I=~#=O59DljrXvsy9r8IMf&qlf zOOx*DhW^Y%P#}3XmfL~`(u9&ucc#dEWch%HfI^`VhllXlYMV%Ep~OE)oJpP{SXfvz z-dl8;Sy^a1$QDpLT#nY{(tY*?B_t*PO%x^-7qVNk{M#eZZSssoB0Xt*cFV1=B5D1R z9}ZfsqCwPxiHH!9$r@Q+U++qOX`)$eO=4tZ`}%)6<%$>+8FP!xNH}U)kRutg)MWBPR#%A~P^u?@vgec*?#T8k|AhJ2Yf* zu-HtFkIA9e5Zu@(1i~Gj=CRneus~h5m?AABv&d`PFA0Nf3}wWGhGHEwpX;X#-mNGH z2Zu1?H~QTS$pH6G7XH4{})jcW?NwVg5FmXqJI9uPdN~x)-C3BnMg7xH}7IH6g6$uIo3j6%|senM; z`09S^?TvguVi_lqzgzH#rRm-1@G@U{&l0gt1 zy}Wi`S-C%;H7FwDV`d=0f%^J-#zd{p;cbBEf=P0hA7MJqt8J@*Z!FLfG zcrD5^^Hzbkh^{5ufsg>Ky!L6?ok zf`e02Nx{Lvjf0V8n#(6vco_lyiPbj8Rx{l3-BD3%o0L|I^AyVGOQplLrHRbxZxM(x zOs&=K$VGp32n&O?i$%IY!BUBlg538#l^k1 zU%XF7wlyr?)*~yqIY`mq_RbR_{$Ov`>IWafd!i5z>*XzSHHJ#SF%aPWlDjRK>f6hy*6`qrg7zUX zo$2xpMblp)epds(a&@+be=^1hJJb4IS%U|L45sncH7+%KT*Tngcoc`H3pxM7dSz~F zvfkfTo3PnWlF<2zR5?Rc&~LU~MNCVxb+J8Lft7<}I6LB#{Q}R)w%PUeq*B{Rxvv@V zX;QEOuNo}jU9Pebh%jFymv(WYsNW@G-em~Bx2Lz45pcQ<%TdWte*2aXAX(#HGnjA0 zZspKj&C=D?#Wgqwb#fr(6~xpuZ+G6xd{g-q96tH>--x6U!Vu5ODq^OL?z3uEK>%(_ zW#y}S4`EI=+$!7r;fie1Td~J*P!xQx-JS*9`Uq#q4s9TvG%LG%!-!cQJ_IZENj#_u zd11A+WDt~^8vL_aZM=~^O%($SXAnGb&-mmzflcEx$Ung4nwo^6Zrf5|8UrrJGOS&t z;dhmnU8knZ5#CND^d_20u-ziVPsY;H5^keWS`bOyNFx2Dt3;1fScFbMQwO=e`@O3( zj5tNui`(z=B)|L;)Q^!eqYnDgBpx3##Bv*t^>`kl?mJZiI@XKMZtc%eyu@S{bj(=3 ze!Tb;zuJa|D8kdzi9Vf4?4wv&*w`YHlga5B801t`a-}0k?_*$;`(8MI_YX)cd#~W(s=is>@U_Mat5BQ#^^^+NB9vo5d;y&d<+$%5N`?sK@&_@1mZOv5Q z_t1^>h34D3$oEPt*eya9rCYAh%ku++?Ol?xGa9V>mN?kM_Z}uIvdTv1?Jn9e@lCcD zmTr7a#B6E7l?=6h0)77{5RJl1v83l2s=v>a+X=BO-rmt62(qfAqy%|&1*oXvHy$hF zv5LK=){Jb0ByOKGTTo!NSDcq>GSvpN%8BDO+o; zNQE~_HH{>4gMK_^@AzcpxkbmmxHwv@oACyj&<5!7OLF{Y&z}8wj@#^WzVRCS*r#Wt2VgETKK?OS>9D9MMt*)WT3Xr@ zZxLc9#XL83J7Qwu6cHajVrJ#erKQ$s6Z-|VnTCnZa1uc(ky+g47TzJTQj_uL?1O_{ zw`e}z-auk??=LilMMs+h9NTi8I3DU!Z(h!Ja#z2b7Xp!AW;5J%j%^gdLhhQLD`CPf zNNses%8V#ZyxI3=@5aMGO`|#2=YFuLWNP|Mqf|c|pezW!mzKj%|HGP_i~(0{aN7}Z zUc<&C<9MO_Z9B&QW=29YIZP>45iAjxSvcitkI-nt>B4~~E+63mkQc8k28$4@W^!h? zs|&8;oOSx8oaJQmwtQL7<%_33af=x+LTB4sZ|#_b=Lh<}vQk5z3!fz?_Rx%t=EkUF z>QABZlKKNiLz3=jKn*zF9Y+KlQ@7q3gOigJ)O20j7-PG~w6p}8Q>|AF5nWKJb%fCl zK3)`4D3cQR6TTvh!9la7&a1V)uc)&*2N+-Oq$zi7LV~ojGdCHBE@b%w7R?V(T+MJN z0aN;#nyLWwq|LU5uKANEjwg9;&)>S&$aj3g)oZl2T%eerf80oGTU=ns7u08ZZC@VP zm1$hAxy0rv#*B4z^xLwE#)hYWJZkbBS4|#S z32s|k-@M0?Z;rBGA`l(%7Y$xsUWzH)Az+;jV**e?)izV=dA=APV~-v^(rt8)+W4D# z43x=B$K~I+3rdtxK!WQc&s%T*N)m{?XKd{NbpbG!J3dAYb4* z?{c3;>gu)q{X1Kg6zl$G5Azo>YTT(JPMadJ0`5^w*Ki9GIgUs=j561CtN}jj4 z2UV0jWrLR^)(P!HY;w(Z#|jDx48T5EX!Q8{^{eF7+1|*`Y12_ZTXo4o3BC8p)(21z z`r`bgWUr1o2+UE}yuL?a4E6N}kDkgeB zriacID2d5DR%+#&&lnl+K-?4YG(2;-(qhQEIn^Jq%(t+F@$vV7NvXqmQOe&1!~sXV zBf6lRnvedH6M#Z1d@qiySv)@M+9v2V4xkW0A+!NJE;mh=OFzn@S$5~9pkgzLw+?(M zMIO#Q(OLrD)GMY?Bg1I1 ziFY7FtabjU<4PyaqR)Yd=;c<{-2SMqpC8B*CHN4iYMH3axKo6-Jdshtk&Ep#E@@W@fSP&X^co zI5Xrb=hrnz#L!IE_ohLu0LT03=koxpENpsHO6F642p(%I${4YoaQuOCUe&mfwC!H)5x(|m?p{otkM^F;;VdJXTKlC*RKmI7qZ?o8!ojg{3` zCMAii5yY%bX>3}W9>ePVetf(sJcyuZ&x>7$RWYq#^?^<=L>m_E zm-zVi`v@VR9Y#h*LVT`2y6p^7ctTh+^@MDGG$B5oZVo8!JZ=iYV(;wH!6x8$=96O- z>Y0sR-zx@`{B&frqvLXY<=)`nkCFNGNSQ2o<%jc4`ObS3x{W!`rv=s2`zz83l>9|| zmSDEhgmlP$k#l!`;Lv;A;L38l^4Lwa8N2nzkEiNInyMuTwu7oF@gU4cl$1JVQF~_P z7BMMie-wR>qf**}czqA0(Pbi^LnBvGEYkCbPiyTL`fskUv|9Z|w2Mh2y4v9#p@0i~ zhq~c1GcyC_3#eh}xnI~@yD`dG0Je~DhB`p;`|T=kkK0*;1o8!Q5vB9c=V z4~^lS>aEk(CFw#|-Ik&_Fy|<*1cfC3gSP^1@{G)~jQmrxL6@=*A5Q*ub*bcF-q^EC z`#V~cB9S_YYq$T-azXp2c+e%SGh;p(e5%RI1!zHek#4}DmVcw+V@krSssaN8xm6VK z@bOJ&D$FKm{4w^t6q3FPxc0VxRk0TI91h!GC_M8rxj0$}s+CD4eE@iyGpzw)=9N!? z)k5QUq6#$Kr_ATTSm6XlSGIa^2e9$bET>ASZ&90K|3Prt7JeJ@qD}dr;q1p6k z%FD}V3%xCf$b4O>j$5p46)Q4bsNRe%76_~n$J6ZzdXWe@Ik^{_WsqWZoqQ`R;3P5d z@>b9G2OQ^Ai#V*y+xRT9m2M#~cQ9d#l*zHNZ2)6untk{|okCq71TYH-ByXnsl=Suw z8b0{uvN;ITX=ZEh=?VSy>z8vzQe2z_fM7QDqUF;m11v+}D^Ll0mT}>ST<7t_$;ipy z01~1ieAsDM%5Oc6t)ep2e&|S&0Pxw}oevO77<8SL7q%Wm5Lu*e*WMuskK%WYq|_Kr z*8rgC6`~-j*W#N3W{H@bycbAM-EDo!fVJ&y?UgvSniZgL_qj<1G|o;>*?spiZc)s@ z?JP+M%9TDX;D>8)~_@7}U>t;Xz$-KXQnQrq|U8ob4~z>}+FGf|uXi-j-KZRxUsA^7Q?1dr zCM4_WDS-8m>NO13;BhdRrTf;?Rd$*bjJ*CN#`ZC?4CT#lU4lyX;RdAKPlS zcpT^hx=SF6dAk=C%^@gWKvhKn2n#n|z)KL*SH$SMB`D%_WQ0`P1K@X*XB1b?Ob~ zS|gh+Iggbb-<*__kBGz!ASAn!(*s~K8`pXP^XVxoYk_0OkkpQjXkLCU6st z3pchbzpJzFB0e>`o0iqufVK1#X;tEqiR)Ea#ugXS+y2rI$$D$#UPbT zB)Pb*E)~FW;H)mg;cZ{&BsI$n#jC3<@>zXvn^U}wBdJ8J;UQ$MMS45E*XK#4o0FD2 zN1&|$fUeH-kUolnm;UL~AM*S!V#30V+CE~~&ear8*Vh6s<*A@x{bW@X3I1P}QXFC? z1fqjrb$i>oa=k)o?`>&&sl`w_aM)UUcfdVXrVIkW%mxDj$L)2rW_`_2qiJ~MPJFyc z2~;$c*yQRAv&vGnM)oZXCg^qa42&2!?!Z$2H&@FEjLgE5PwO82LqnS&IkQ258%X7k znW?m>H0_I{@A-G?O;1nHDx6cs$6{CSVLnhSxI|I)I`r3g<`@BuFs<#(t^sh|+ zfbRdPn7ZibG-&k?bnEVkrWPUx z2B2NfLvpUbFJHcBlla5xyML64owA~S&l36r^ML+pL_K$2HzA5rDfrX3-a&P*Yd0(= zrO}Z(r@nyiHS@?fFqnF=wzQfW5hb5(-CnF1m6llbZ6hID zIAP44QeYQirP?~t7{CjBUf}Fe_(lcZ@$n3l>9YH4m_XU}0^JUP>DsgC%nEAGKjIY1gW2R`>hL=(D!RJz zWcZzF#R`pr_c{bjb+(AcqyNvJ%D%UUp8-(%G7d$EOC(_;;|J_(Dr<1JUTdM`1Hr1X>4pt_O>0POIJU zwYAO!HrAg%WhP5_M}7viU!Fpnnq!C_87*fg4KDBQ4*U=gDlkQxC`io67(!hfk67~9 zE;iMVmvNKh1Dmznu=8h_ z+}Rg(S>(TSGwgE=S{+3%q=J$(FukumJiWV*Gj8*cmzPJ=CNU%BCbBvjl~Vu9UTC_x z+{rHS^4OVV-xy4b0Jb!c^BiMYUVv-@PraHRbq^igPVc^I=)@mH&=NYro)Xz~dvjHb zLHipx22#E#o%=h@|HPi+HS4P!EOJhS(UC0q9zW2jwKu!IJk2hdbzbXz1RU=n&|w5n zE1Ocfl%qt+q2FY=V~wk7b!v{DCA0@@&yy3^bl;;swHQjM>vQ1yN$Upy*7a|xH6SXX zwg9vbgFvBJ1}m}hGFK&oiIFh`P#TTAJ^P__krS{;>Qz^;5Ku!$0CUkHlN>Lb4Pbi* zD#!z4&>Qi1&l-9XczZAoG3bb778k10fkCc=`r33ds!-ePf4CxTKKQ*4j5B;- z`FRXre_11zo+l%+c6u;6)*_m{E>|HkGBRfumjmzlDyuPjA0G9`A;6VEppJ*;8gNPY zz^bgQt~yX2AVGCG9uzt#b6)S4$Dq~J=mRt&&jUW!OW+p)r9&>}FAPe?U4fP7BXpRrdkN%RO-UwUf|>)Z z5B&TIPIi<7`VM^((CL+KPKEKH?@-!&MiFrF$6&h7$t2{Aoka;0)(b!@tQH!oiwjw$ zLdJIxJ0n?GHd7_lZZB=khkuAmNlU}}B<^czX-Ub-5&h!mRq!5?C!KclVMbSYn}0n*=^kb!%&Dh&poEzKiaH*^@2E5p zk=nMmzr-|pBeod$V{B|pF6!%<60PwcQ}?dN7)>J*4tmnpSLYT$D0q5#k@!S|=>`i@ z*Vs5$RkAsh(PPL}7I5niO#YUZmPjhWH~=b>-_|!n@*eE|CzZ6CJe~648pAgv#KaxI z*TCf`{3h(>oWe(l3rGW!SCtmr4(+b{9v>f1Z>I+s&mCTg-ChilwA6GN!$C>v?CH_)s)xbW zfr}6v8tQ^VI#E$kZB3QZf<9}~wM9tY{YIR-A_&l#T*!Ubt`rLsvjP+@=g_4zI~(9$ zSb$i9sZ9c50opHjeM2#^3{cF0$K$mc#s6zT`7^6AKb+rj`E`zRn)Wjk&?IkER9bf~ zCT42fk2jS5S4RmDT`=oEr*`f-`MaugQoQA<2)60f>N=&`R9+kPwO9d8&KN)+!9eN& zKAz9J5P&8e5W!yAC0}$&K@$@l>mdL&+}vo4`}cdtU(#8BSqP?m4X|I(?~*4C>ejG# z`cZZTBsC!o&0ybGW+z8SL(m^l{4P-531?q-+8B5Od|tUXZ=QdLo(;Sj8m_U^(Ye(Q zE>mzP@dp?VppX7PF9LQ)`~fTs#E`(9Xm_2PP4husB9T9)c0jlLqarnNCTUqAVrQ{g zs8FMn(|3#l;2a1`jw*OL}^GwLO6WUFX3rD~gBW0Yy}fd}3XN<_los z-n|7GUtX~f5|=K#q7W37oVly>15P)wxw!9Q0qF>YBPcF01qF9Nq?#oIUtML8d>fo+ zvdeLoig#819dbdiscUYI613>>IGYS>BeHN+4W<_i9D&^<7;xiTez3m2PNGa*xD98Q z_&nRS-%juYq&)ER;oIB&hpRn6MN$d5&OC;l+yq;`3*e;U`3=J$$a^@Eno`7&U-uTPF!BH}Alvn8{ z9ZZ-h3|79NATBla4=5%;PGNzjEg#sj1GegLfs6t8{0}(GU=stQzN{(?R=nR1T%07( zN~|3J2)4p#0V$CPdOJi*Q09`>pK*5WQ+l!3$j9YSd zbWHrG&!1xh|NXHv%PI6|nk_i*-FsLzKv#T9z9L;C59az~Vu^7U08GI2N)+1Ne+Fj; zDFC)wU>hZYZ4`v=a^SxImqScJJi6^Fz*{Tr7oekKV<~)g6ks6q!ouJ6o4pT#my;ks z^L8KH!EF*W;&E_rjDRx(w*0Id91H;oc2{)XP0n_LRB^WC;5QBidHDFVfa{i`#D%kI(+~9u= z1r^W&xC0;yB1ze`zW}QhcvQ(yH&&1ctf#(Kck`qLw7^~42A>)lPb1<@NI@YF5ZMbX zk`-_e!QK9|F{tX||F%8oTI^BlD&EMROdHr5$LK1p>1t}^Y6dlNHUs}bxVgBw*|}b^ zzZ7`?k_XED3d+OB#>EBY;_}rT;{E?!VCP_JW$yKVT<}r=`s(F>FUT@m0Q>eJ&+nc9 zeuD^V;^64zY-M5T3X%NZM^D8a9c|5?%GsN6ym)%|b!`2PVBpt8*X literal 0 HcmV?d00001 From 12022571bf4d048c76d337ca136cb1493fea2f01 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 18 Mar 2022 16:12:09 -0700 Subject: [PATCH 45/87] add documentation on predict keyword + input_output_response list processing --- control/iosys.py | 88 +++++++++++++++++++++++++++++++++---- control/stochsys.py | 18 ++++++-- control/tests/iosys_test.py | 41 +++++++++++++++++ 3 files changed, 136 insertions(+), 11 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index ccfdba2ca..cc9666987 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1585,11 +1585,17 @@ def input_output_response( T : array-like Time steps at which the input is defined; values must be evenly spaced. - U : array-like or number, optional - Input array giving input at each time `T` (default = 0). - - X0 : array-like or number, optional - Initial condition (default = 0). + U : array-like, list, or number, optional + Input array giving input at each time `T` (default = 0). If a list + is specified, each element in the list will be treated as a portion + of the input and broadcast (if necessary) to match the time vector. + + X0 : array-like, list, or number, optional + Initial condition (default = 0). If a list is given, each element + in the list will be flattened and stacked into the initial + condition. If a smaller number of elements are given that the + number of states in the system, the initial condition will be padded + with zeros. return_x : bool, optional If True, return the state vector when assigning to a tuple (default = @@ -1641,6 +1647,16 @@ def input_output_response( ValueError If time step does not match sampling time (for discrete time systems). + Notes + ----- + 1. If a smaller number of initial conditions are given than the number of + states in the system, the initial conditions will be padded with + zeros. This is often useful for interconnected control systems where + the process dynamics are the first system and all other components + start with zero initial condition since this can be specified as + [xsys_0, 0]. A warning is issued if the initial conditions are padded + and and the final listed initial state is not zero. + """ # # Process keyword arguments @@ -1683,19 +1699,75 @@ def input_output_response( # Use the input time points as the output time points t_eval = T - # Check and convert the input, if needed - # TODO: improve MIMO ninputs check (choose from U) + # If we were passed a list of input, concatenate them (w/ broadcast) + if isinstance(U, (tuple, list)) and len(U) != ntimepts: + U_elements = [] + for i, u in enumerate(U): + u = np.array(u) # convert everyting to an array + # Process this input + if u.ndim == 0 or (u.ndim == 1 and u.shape[0] != T.shape[0]): + # Broadcast array to the length of the time input + u = np.outer(u, np.ones_like(T)) + + elif (u.ndim == 1 and u.shape[0] == T.shape[0]) or \ + (u.ndim == 2 and u.shape[1] == T.shape[0]): + # No processing necessary; just stack + pass + + else: + raise ValueError(f"Input element {i} has inconsistent shape") + + # Append this input to our list + U_elements.append(u) + + # Save the newly created input vector + U = np.vstack(U_elements) + + # Make sure the input has the right shape if sys.ninputs is None or sys.ninputs == 1: legal_shapes = [(ntimepts,), (1, ntimepts)] else: legal_shapes = [(sys.ninputs, ntimepts)] + U = _check_convert_array(U, legal_shapes, 'Parameter ``U``: ', squeeze=False) + + # Always store the input as a 2D array U = U.reshape(-1, ntimepts) ninputs = U.shape[0] - # create X0 if not given, test if X0 has correct shape + # If we were passed a list of initial states, concatenate them + if isinstance(X0, (tuple, list)): + X0_list = [] + for i, x0 in enumerate(X0): + x0 = np.array(x0).reshape(-1) # convert everyting to 1D array + X0_list += x0.tolist() # add elements to initial state + + # Save the newly created input vector + X0 = np.array(X0_list) + + # If the initial state is too short, make it longer (NB: sys.nstates + # could be None if nstates comes from size of initial condition) + if sys.nstates and isinstance(X0, np.ndarray) and X0.size < sys.nstates: + if X0[-1] != 0: + warn("initial state too short; padding with zeros") + X0 = np.hstack([X0, np.zeros(sys.nstates - X0.size)]) + + # Check to make sure this is not a static function nstates = _find_size(sys.nstates, X0) + if nstates == 0: + # No states => map input to output + u = U[0] if len(U.shape) == 1 else U[:, 0] + y = np.zeros((np.shape(sys._out(T[0], X0, u))[0], len(T))) + for i in range(len(T)): + u = U[i] if len(U.shape) == 1 else U[:, i] + y[:, i] = sys._out(T[i], [], u) + return TimeResponseData( + T, y, None, U, issiso=sys.issiso(), + output_labels=sys.output_index, input_labels=sys.input_index, + transpose=transpose, return_x=return_x, squeeze=squeeze) + + # create X0 if not given, test if X0 has correct shape X0 = _check_convert_array(X0, [(nstates,), (nstates, 1)], 'Parameter ``X0``: ', squeeze=True) diff --git a/control/stochsys.py b/control/stochsys.py index 02f19ebbf..609ab5804 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -323,9 +323,11 @@ def create_estimator_iosystem( estim = ct.create_estimator_iosystem(sys, QN, RN) - where ``sys`` is the process dynamics and QN and RN are the covariance of - the disturbance noise and sensor noise. The function returns the - estimator ``estim`` as I/O systems. + where ``sys`` is the process dynamics and QN and RN are the covariance + of the disturbance noise and sensor noise. The function returns the + estimator ``estim`` as I/O system with a parameter ``correct`` that can + be used to turn off the correction term in the estimation (for forward + predictions). Parameters ---------- @@ -368,6 +370,16 @@ def create_estimator_iosystem( est = ct.create_estimator_iosystem(sys, QN, RN, P0) ctrl, clsys = ct.create_statefbk_iosystem(sys, K, estimator=est) + The estimator can also be run on its own to process a noisy signal: + + resp = ct.input_output_response(est, T, [Y, U], [X0, P0]) + + If desired, the ``correct`` parameter can be set to ``False`` to allow + prediction with no additional sensor information: + + resp = ct.input_output_response( + est, T, 0, [X0, P0], param={'correct': False) + """ # Make sure that we were passed an I/O system as an input diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 93ce5df65..a80c64b1d 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1732,3 +1732,44 @@ def test_nonuniform_timepts(): t_even, y_even = ct.input_output_response( sys, noufpts, nonunif, t_eval=unifpts) np.testing.assert_almost_equal(y_unif, y_even, decimal=6) + +def test_input_output_broadcasting(): + # Create a system, time vector, and noisy input + sys = ct.rss(6, 2, 3) + T = np.linspace(0, 10, 10) + U = np.zeros((sys.ninputs, T.size)) + U[0, :] = np.sin(T) + U[1, :] = np.zeros_like(U[1, :]) + U[2, :] = np.ones_like(U[2, :]) + X0 = np.array([1, 2]) + P0 = np.array([[3.11, 3.12], [3.21, 3.3]]) + + # Simulate the system with nominal input to establish baseline + resp_base = ct.input_output_response( + sys, T, U, np.hstack([X0, P0.reshape(-1)])) + + # Split up the inputs into two pieces + resp_inp1 = ct.input_output_response(sys, T, [U[:1], U[1:]], [X0, P0]) + np.testing.assert_equal(resp_base.states, resp_inp1.states) + + # Specify two of the inputs as constants + resp_inp2 = ct.input_output_response(sys, T, [U[0], 0, 1], [X0, P0]) + np.testing.assert_equal(resp_base.states, resp_inp2.states) + + # Specify two of the inputs as constant vector + resp_inp3 = ct.input_output_response(sys, T, [U[0], [0, 1]], [X0, P0]) + np.testing.assert_equal(resp_base.states, resp_inp3.states) + + # Specify only some of the initial conditions + resp_init = ct.input_output_response(sys, T, [U[0], [0, 1]], [X0, 0]) + resp_cov0 = ct.input_output_response(sys, T, U, [X0, P0 * 0]) + np.testing.assert_equal(resp_cov0.states, resp_init.states) + + # Specify only some of the initial conditions + with pytest.warns(UserWarning, match="initial state too short; padding"): + resp_short = ct.input_output_response(sys, T, [U[0], [0, 1]], [X0, 1]) + + # Make sure that inconsistent settings don't work + with pytest.raises(ValueError, match="inconsistent"): + resp_bad = ct.input_output_response( + sys, T, (U[0, :], U[:2, :-1]), [X0, P0]) From a4567d4aeea90f05877aac2a55f529e28a000558 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 23 Mar 2022 08:05:44 -0700 Subject: [PATCH 46/87] rebase cleanup --- control/lti.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/control/lti.py b/control/lti.py index 174a7a3f8..b56c2bb44 100644 --- a/control/lti.py +++ b/control/lti.py @@ -16,13 +16,12 @@ from numpy import absolute, real, angle, abs from warnings import warn from . import config -from .namedio import _NamedIOSystem __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', 'isdtime', 'isctime', 'pole', 'zero', 'damp', 'evalfr', 'freqresp', 'dcgain'] -class LTI(_NamedIOSystem): +class LTI: """LTI is a parent class to linear time-invariant (LTI) system objects. LTI is the parent to the StateSpace and TransferFunction child classes. It From 051e19606f28bdf06be4c00bc513eaa31aa85abf Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 23 Mar 2022 08:45:42 -0700 Subject: [PATCH 47/87] retrigger checks From 34c2c7e3018a3537c97f3564368535d4158421a9 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 30 Mar 2022 08:35:16 -0700 Subject: [PATCH 48/87] add kwarg checks + rebase cleanup --- control/iosys.py | 8 +++---- control/stochsys.py | 6 ++--- control/tests/iosys_test.py | 44 ++++++++++++++++++------------------ control/tests/kwargs_test.py | 4 ++++ 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index cc9666987..161f06510 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1672,14 +1672,14 @@ def input_output_response( raise ValueError("ivp_method specified more than once") solve_ivp_kwargs['method'] = kwargs.pop('solve_ivp_method') - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - # Set the default method to 'RK45' if solve_ivp_kwargs.get('method', None) is None: solve_ivp_kwargs['method'] = 'RK45' + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keyword(s): ", str(kwargs)) + # Sanity checking on the input if not isinstance(sys, InputOutputSystem): raise TypeError("System of type ", type(sys), " not valid") diff --git a/control/stochsys.py b/control/stochsys.py index 609ab5804..fd276b92c 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -26,7 +26,7 @@ from .statesp import StateSpace, _ssmatrix from .exception import ControlArgument, ControlNotImplemented -__all__ = ['lqe','dlqe', 'create_estimator_iosystem', 'white_noise', +__all__ = ['lqe', 'dlqe', 'create_estimator_iosystem', 'white_noise', 'correlation'] @@ -134,7 +134,7 @@ def lqe(*args, **kwargs): # Get the method to use (if specified as a keyword) method = kwargs.pop('method', None) if kwargs: - raise TypeError("unrecognized kwargs: ", str(kwargs)) + raise TypeError("unrecognized keyword(s): ", str(kwargs)) # Get the system description if (len(args) < 3): @@ -255,7 +255,7 @@ def dlqe(*args, **kwargs): # Get the method to use (if specified as a keyword) method = kwargs.pop('method', None) if kwargs: - raise TypeError("unrecognized kwargs: ", str(kwargs)) + raise TypeError("unrecognized keyword(s): ", str(kwargs)) # Get the system description if (len(args) < 3): diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index a80c64b1d..4e0adfa03 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1711,28 +1711,6 @@ def test_interconnect_unused_output(): outputs=['y'], ignore_outputs=['v']) -def test_nonuniform_timepts(): - """Test non-uniform time points for simulations""" - sys = ct.LinearIOSystem(ct.rss(2, 1, 1)) - - # Start with a uniform set of times - unifpts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - uniform = [1, 2, 3, 2, 1, -1, -3, -5, -7, -3, 1] - t_unif, y_unif = ct.input_output_response(sys, unifpts, uniform) - - # Create a non-uniform set of inputs - noufpts = [0, 2, 4, 8, 10] - nonunif = [1, 3, 1, -7, 1] - t_nouf, y_nouf = ct.input_output_response(sys, noufpts, nonunif) - - # Make sure the outputs agree at common times - np.testing.assert_almost_equal(y_unif[noufpts], y_nouf, decimal=6) - - # Resimulate using a new set of evaluation points - t_even, y_even = ct.input_output_response( - sys, noufpts, nonunif, t_eval=unifpts) - np.testing.assert_almost_equal(y_unif, y_even, decimal=6) - def test_input_output_broadcasting(): # Create a system, time vector, and noisy input sys = ct.rss(6, 2, 3) @@ -1773,3 +1751,25 @@ def test_input_output_broadcasting(): with pytest.raises(ValueError, match="inconsistent"): resp_bad = ct.input_output_response( sys, T, (U[0, :], U[:2, :-1]), [X0, P0]) + +def test_nonuniform_timepts(): + """Test non-uniform time points for simulations""" + sys = ct.LinearIOSystem(ct.rss(2, 1, 1)) + + # Start with a uniform set of times + unifpts = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + uniform = [1, 2, 3, 2, 1, -1, -3, -5, -7, -3, 1] + t_unif, y_unif = ct.input_output_response(sys, unifpts, uniform) + + # Create a non-uniform set of inputs + noufpts = [0, 2, 4, 8, 10] + nonunif = [1, 3, 1, -7, 1] + t_nouf, y_nouf = ct.input_output_response(sys, noufpts, nonunif) + + # Make sure the outputs agree at common times + np.testing.assert_almost_equal(y_unif[noufpts], y_nouf, decimal=6) + + # Resimulate using a new set of evaluation points + t_even, y_even = ct.input_output_response( + sys, noufpts, nonunif, t_eval=unifpts) + np.testing.assert_almost_equal(y_unif, y_even, decimal=6) diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 0502114dc..818a906a5 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -81,9 +81,11 @@ def test_unrecognized_kwargs(): sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) table = [ + [control.dlqe, (sys, [[1]], [[1]]), {}], [control.dlqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], [control.drss, (2, 1, 1), {}], [control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}], + [control.lqe, (sys, [[1]], [[1]]), {}], [control.lqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], [control.linearize, (sys, 0, 0), {}], [control.pzmap, (sys,), {}], @@ -154,6 +156,7 @@ def test_matplotlib_kwargs(): 'bode': test_matplotlib_kwargs, 'bode_plot': test_matplotlib_kwargs, 'describing_function_plot': test_matplotlib_kwargs, + 'dlqe': test_unrecognized_kwargs, 'dlqr': statefbk_test.TestStatefbk.test_lqr_errors, 'drss': test_unrecognized_kwargs, 'gangof4': test_matplotlib_kwargs, @@ -161,6 +164,7 @@ def test_matplotlib_kwargs(): 'input_output_response': test_unrecognized_kwargs, 'interconnect': interconnect_test.test_interconnect_exceptions, 'linearize': test_unrecognized_kwargs, + 'lqe': test_unrecognized_kwargs, 'lqr': statefbk_test.TestStatefbk.test_lqr_errors, 'nyquist': test_matplotlib_kwargs, 'nyquist_plot': test_matplotlib_kwargs, From e5b4cb33208019c6baf7d119c7bdb6534f3dd90d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 18 Mar 2022 16:12:09 -0700 Subject: [PATCH 49/87] add documentation on predict keyword + input_output_response list processing --- control/iosys.py | 17 +++++++++++++++++ control/tests/iosys_test.py | 2 ++ 2 files changed, 19 insertions(+) diff --git a/control/iosys.py b/control/iosys.py index 161f06510..4af21b5cb 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1736,6 +1736,23 @@ def input_output_response( U = U.reshape(-1, ntimepts) ninputs = U.shape[0] + # If we were passed a list of initial states, concatenate them + if isinstance(X0, (tuple, list)): + X0_list = [] + for i, x0 in enumerate(X0): + x0 = np.array(x0).reshape(-1) # convert everyting to 1D array + X0_list += x0.tolist() # add elements to initial state + + # Save the newly created input vector + X0 = np.array(X0_list) + + # If the initial state is too short, make it longer (NB: sys.nstates + # could be None if nstates comes from size of initial condition) + if sys.nstates and isinstance(X0, np.ndarray) and X0.size < sys.nstates: + if X0[-1] != 0: + warn("initial state too short; padding with zeros") + X0 = np.hstack([X0, np.zeros(sys.nstates - X0.size)]) + # If we were passed a list of initial states, concatenate them if isinstance(X0, (tuple, list)): X0_list = [] diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 4e0adfa03..06d0f57ba 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1711,6 +1711,7 @@ def test_interconnect_unused_output(): outputs=['y'], ignore_outputs=['v']) + def test_input_output_broadcasting(): # Create a system, time vector, and noisy input sys = ct.rss(6, 2, 3) @@ -1752,6 +1753,7 @@ def test_input_output_broadcasting(): resp_bad = ct.input_output_response( sys, T, (U[0, :], U[:2, :-1]), [X0, P0]) + def test_nonuniform_timepts(): """Test non-uniform time points for simulations""" sys = ct.LinearIOSystem(ct.rss(2, 1, 1)) From 33a074417f30a8e4614b436eecb7758108bd4ccc Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 23 Mar 2022 08:45:42 -0700 Subject: [PATCH 50/87] retrigger checks From a4bb80e75e84a98244f8ccd4569e6c71feef5c1d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Mar 2022 06:20:19 -0700 Subject: [PATCH 51/87] check for unused keywords --- control/iosys.py | 1 - 1 file changed, 1 deletion(-) diff --git a/control/iosys.py b/control/iosys.py index 4af21b5cb..357876fd9 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -2224,7 +2224,6 @@ def _parse_signal_parameter(value, name, kwargs, end=False): if end and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) - return value From c2e3993a9efa28f7d12d40ef9a2e4c79313696e3 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 25 Mar 2022 08:32:30 -0700 Subject: [PATCH 52/87] add kwarg tests for lqe, dlqe --- control/tests/kwargs_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 818a906a5..2a4d24306 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -83,6 +83,7 @@ def test_unrecognized_kwargs(): table = [ [control.dlqe, (sys, [[1]], [[1]]), {}], [control.dlqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], + [control.dlqe, (sys, [[1]], [[1]]), {}], [control.drss, (2, 1, 1), {}], [control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}], [control.lqe, (sys, [[1]], [[1]]), {}], @@ -157,7 +158,7 @@ def test_matplotlib_kwargs(): 'bode_plot': test_matplotlib_kwargs, 'describing_function_plot': test_matplotlib_kwargs, 'dlqe': test_unrecognized_kwargs, - 'dlqr': statefbk_test.TestStatefbk.test_lqr_errors, + 'dlqr': test_unrecognized_kwargs, 'drss': test_unrecognized_kwargs, 'gangof4': test_matplotlib_kwargs, 'gangof4_plot': test_matplotlib_kwargs, @@ -165,7 +166,7 @@ def test_matplotlib_kwargs(): 'interconnect': interconnect_test.test_interconnect_exceptions, 'linearize': test_unrecognized_kwargs, 'lqe': test_unrecognized_kwargs, - 'lqr': statefbk_test.TestStatefbk.test_lqr_errors, + 'lqr': test_unrecognized_kwargs, 'nyquist': test_matplotlib_kwargs, 'nyquist_plot': test_matplotlib_kwargs, 'pzmap': test_matplotlib_kwargs, From 96d813cfdc02ac9f2d351058aafdcaa8c04be622 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 25 Mar 2022 21:01:46 -0700 Subject: [PATCH 53/87] remove _NamedIOStateSystem class --- control/iosys.py | 6 ++-- control/namedio.py | 69 ++++++++++++++++------------------------------ control/statesp.py | 4 +-- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 357876fd9..9dcf7c426 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -31,7 +31,7 @@ import copy from warnings import warn -from .namedio import _NamedIOStateSystem, _process_signal_list +from .namedio import _NamedIOSystem, _process_signal_list from .statesp import StateSpace, tf2ss, _convert_to_statespace from .statesp import _ss, _rss_generate from .xferfcn import TransferFunction @@ -55,7 +55,7 @@ } -class InputOutputSystem(_NamedIOStateSystem): +class InputOutputSystem(_NamedIOSystem): """A class for representing input/output systems. The InputOutputSystem class allows (possibly nonlinear) input/output @@ -139,7 +139,7 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, """ # Store the system name, inputs, outputs, and states - _NamedIOStateSystem.__init__( + _NamedIOSystem.__init__( self, inputs=inputs, outputs=outputs, states=states, name=name) # default parameters diff --git a/control/namedio.py b/control/namedio.py index 4ea82d819..8e541808b 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -1,10 +1,9 @@ # namedio.py - internal named I/O object class # RMM, 13 Mar 2022 # -# This file implements the _NamedIOSystem and _NamedIOStateSystem classes, -# which are used as a parent classes for FrequencyResponseData, -# InputOutputSystem, LTI, TimeResponseData, and other similar classes to -# allow naming of signals. +# This file implements the _NamedIOSystem class, which is used as a parent +# class for FrequencyResponseData, InputOutputSystem, LTI, TimeResponseData, +# and other similar classes to allow naming of signals. import numpy as np @@ -19,7 +18,7 @@ def _name_or_default(self, name=None): return name def __init__( - self, inputs=None, outputs=None, name=None): + self, name=None, inputs=None, outputs=None, states=None): # system name self.name = self._name_or_default(name) @@ -27,6 +26,7 @@ def __init__( # Parse and store the number of inputs and outputs self.set_inputs(inputs) self.set_outputs(outputs) + self.set_states(states) # # Class attributes @@ -38,12 +38,17 @@ def __init__( #: Number of system inputs. #: #: :meta hide-value: - ninputs = 0 + ninputs = None #: Number of system outputs. #: #: :meta hide-value: - noutputs = 0 + noutputs = None + + #: Number of system states. + #: + #: :meta hide-value: + nstates = None def __repr__(self): return str(type(self)) + ": " + self.name if self.name is not None \ @@ -58,6 +63,10 @@ def __str__(self): str += "\nOutputs (%s): " % self.noutputs for key in self.output_index: str += key + ", " + if self.nstates is not None: + str += "\nStates (%s): " % self.nstates + for key in self.state_index: + str += key + ", " return str # Find a signal by name @@ -122,44 +131,6 @@ def find_output(self, name): lambda self: list(self.output_index.keys()), # getter set_outputs) # setter - def issiso(self): - """Check to see if a system is single input, single output""" - return self.ninputs == 1 and self.noutputs == 1 - - -class _NamedIOStateSystem(_NamedIOSystem): - def __init__( - self, inputs=None, outputs=None, states=None, name=None): - # Parse and store the system name, inputs, and outputs - super().__init__(inputs=inputs, outputs=outputs, name=name) - - # Parse and store the number of states - self.set_states(states) - - # - # Class attributes - # - # These attributes are defined as class attributes so that they are - # documented properly. They are "overwritten" in __init__. - # - - #: Number of system states. - #: - #: :meta hide-value: - nstates = 0 - - def __str__(self): - """String representation of an input/output system""" - str = _NamedIOSystem.__str__(self) - str += "\nStates (%s): " % self.nstates - for key in self.state_index: - str += key + ", " - return str - - def _isstatic(self): - """Check to see if a system is a static system (no states)""" - return self.nstates == 0 - def set_states(self, states, prefix='x'): """Set the number/names of the system states. @@ -189,6 +160,14 @@ def find_state(self, name): lambda self: list(self.state_index.keys()), # getter set_states) # setter + def issiso(self): + """Check to see if a system is single input, single output""" + return self.ninputs == 1 and self.noutputs == 1 + + def _isstatic(self): + """Check to see if a system is a static system (no states)""" + return self.nstates == 0 + # Utility function to parse a list of signals def _process_signal_list(signals, prefix='s'): diff --git a/control/statesp.py b/control/statesp.py index 435ff702f..3cb53bf60 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -59,7 +59,7 @@ from scipy.signal import StateSpace as signalStateSpace from warnings import warn from .lti import LTI, common_timebase, isdtime, _process_frequency_response -from .namedio import _NamedIOStateSystem, _process_signal_list +from .namedio import _NamedIOSystem, _process_signal_list from . import config from copy import deepcopy @@ -153,7 +153,7 @@ def _f2s(f): return s -class StateSpace(LTI, _NamedIOStateSystem): +class StateSpace(LTI, _NamedIOSystem): """StateSpace(A, B, C, D[, dt]) A class for representing state-space models. From ad714fed47f15d8ed6927355afdaaaac3102a8f0 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 26 Mar 2022 13:58:16 -0700 Subject: [PATCH 54/87] allow creation of NonlinearIOSystem via ss() --- control/iosys.py | 20 ++++++++++++++++++-- control/tests/iosys_test.py | 25 ++++++++++++++++++++++--- control/tests/timeresp_test.py | 2 +- control/xferfcn.py | 3 ++- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 9dcf7c426..e6764cbf8 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -31,6 +31,7 @@ import copy from warnings import warn +from .lti import LTI from .namedio import _NamedIOSystem, _process_signal_list from .statesp import StateSpace, tf2ss, _convert_to_statespace from .statesp import _ss, _rss_generate @@ -2252,12 +2253,17 @@ def ss(*args, **kwargs): Create a state space system. - The function accepts either 1, 4 or 5 parameters: + The function accepts either 1, 2, 4 or 5 parameters: ``ss(sys)`` Convert a linear system into space system form. Always creates a new system, even if sys is already a state space system. + ``ss(updfcn, outfucn)``` + Create a nonlinear input/output system with update function ``updfcn`` + and output function ``outfcn``. See :class:`NonlinearIOSystem` for + more information. + ``ss(A, B, C, D)`` Create a state space system from the matrices of its state and output equations: @@ -2280,6 +2286,10 @@ def ss(*args, **kwargs): Everything that the constructor of :class:`numpy.matrix` accepts is permissible here too. + ``ss(args, inputs=['u1', ..., 'up'], outputs=['y1', ..., 'yq'], + states=['x1', ..., 'xn']) + Create a system with named input, output, and state signals. + Parameters ---------- sys : StateSpace or TransferFunction @@ -2326,6 +2336,12 @@ def ss(*args, **kwargs): >>> sys2 = ss(sys_tf) """ + # See if this is a nonlinear I/O system + if len(args) > 0 and hasattr(args[0], '__call__') and \ + not isinstance (args[0], (InputOutputSystem, LTI)): + # Function as first argument => assume nonlinear IO system + return NonlinearIOSystem(*args, **kwargs) + # Extract the keyword arguments needed for StateSpace (via _ss) ss_kwlist = ('dt', 'remove_useless_states') ss_kwargs = {} @@ -2334,7 +2350,7 @@ def ss(*args, **kwargs): ss_kwargs[kw] = kwargs.pop(kw) # Create the statespace system and then convert to I/O system - sys = _ss(*args, keywords=ss_kwargs) + sys = _ss(*args, **ss_kwargs) return LinearIOSystem(sys, **kwargs) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 06d0f57ba..f3377c0ab 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -130,9 +130,10 @@ def test_iosys_print(self, tsys, capsys): print(ios_linearized) @noscipy0 - def test_nonlinear_iosys(self, tsys): + @pytest.mark.parametrize("ss", [ios.NonlinearIOSystem, ct.ss]) + def test_nonlinear_iosys(self, tsys, ss): # Create a simple nonlinear I/O system - nlsys = ios.NonlinearIOSystem(predprey) + nlsys = ss(predprey) T = tsys.T # Start by simulating from an equilibrium point @@ -159,7 +160,7 @@ def test_nonlinear_iosys(self, tsys): np.reshape(linsys.C @ np.reshape(x, (-1, 1)) + linsys.D @ np.reshape(u, (-1, 1)), (-1,)) - nlsys = ios.NonlinearIOSystem(nlupd, nlout, inputs=1, outputs=1) + nlsys = ss(nlupd, nlout, inputs=1, outputs=1) # Make sure that simulations also line up T, U, X0 = tsys.T, tsys.U, tsys.X0 @@ -1775,3 +1776,21 @@ def test_nonuniform_timepts(): t_even, y_even = ct.input_output_response( sys, noufpts, nonunif, t_eval=unifpts) np.testing.assert_almost_equal(y_unif, y_even, decimal=6) + + +def test_ss_nonlinear(): + """Test ss() for creating nonlinear systems""" + secord = ct.ss(secord_update, secord_output, inputs='u', outputs='y', + states = ['x1', 'x2'], name='secord') + assert secord.name == 'secord' + assert secord.input_labels == ['u'] + assert secord.output_labels == ['y'] + assert secord.state_labels == ['x1', 'x2'] + + # Make sure that optional keywords are allowed + secord = ct.ss(secord_update, secord_output, dt=True) + assert ct.isdtime(secord) + + # Make sure that state space keywords are flagged + with pytest.raises(TypeError, match="unrecognized keyword"): + ct.ss(secord_update, remove_useless_states=True) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 61c0cae38..4273be772 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -875,7 +875,7 @@ def test_time_vector(self, tsystem, fun, squeeze, matarrayout): kw['U'] = np.vstack([np.sin(t) for i in range(sys.ninputs)]) elif fun == forced_response and isctime(sys, strict=True): pytest.skip("No continuous forced_response without time vector.") - if hasattr(sys, "nstates"): + if hasattr(sys, "nstates") and sys.nstates is not None: kw['X0'] = np.arange(sys.nstates) + 1 if sys.ninputs > 1 and fun in [step_response, impulse_response]: kw['input'] = 1 diff --git a/control/xferfcn.py b/control/xferfcn.py index a171a1143..87d6f533e 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -61,6 +61,7 @@ from re import sub from .lti import LTI, common_timebase, isdtime, _process_frequency_response from .exception import ControlMIMONotImplemented +from .namedio import _NamedIOSystem, _process_signal_list from . import config __all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata'] @@ -70,7 +71,7 @@ _xferfcn_defaults = {} -class TransferFunction(LTI): +class TransferFunction(LTI, _NamedIOSystem): """TransferFunction(num, den[, dt]) A class for representing transfer functions. From e1f8d3a90f6c60dc9c420652fb1a08ef77b19fb2 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 26 Mar 2022 15:50:08 -0700 Subject: [PATCH 55/87] allow TimeResponseData to be converted to pandas --- .github/workflows/python-package-conda.yml | 7 ++- control/exception.py | 13 ++++++ control/tests/timeresp_test.py | 54 +++++++++++++++++++++- control/timeresp.py | 18 ++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index 10cf2d1a9..3f1910697 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: test-linux: - name: Python ${{ matrix.python-version }}${{ matrix.slycot && format(' with Slycot from {0}', matrix.slycot) || ' without Slycot' }}${{ matrix.array-and-matrix == 1 && ', array and matrix' || '' }} + name: Python ${{ matrix.python-version }}${{ matrix.slycot && format(' with Slycot from {0}', matrix.slycot) || ' without Slycot' }}${{ matrix.pandas && ', with pandas' || '' }}${{ matrix.array-and-matrix == 1 && ', array and matrix' || '' }} runs-on: ubuntu-latest strategy: @@ -12,10 +12,12 @@ jobs: matrix: python-version: [3.7, 3.9] slycot: ["", "conda"] + pandas: [""] array-and-matrix: [0] include: - python-version: 3.9 slycot: conda + pandas: conda array-and-matrix: 1 steps: @@ -41,6 +43,9 @@ jobs: if [[ '${{matrix.slycot}}' == 'conda' ]]; then conda install -c conda-forge slycot fi + if [[ '${{matrix.pandas}}' == 'conda' ]]; then + conda install -c conda-forge pandas + fi - name: Test with pytest env: diff --git a/control/exception.py b/control/exception.py index e28ba8609..f66eb7f30 100644 --- a/control/exception.py +++ b/control/exception.py @@ -71,3 +71,16 @@ def slycot_check(): except: slycot_installed = False return slycot_installed + + +# Utility function to see if pandas is installed +pandas_installed = None +def pandas_check(): + global pandas_installed + if pandas_installed is None: + try: + import pandas + pandas_installed = True + except: + pandas_installed = False + return pandas_installed diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 4273be772..fe73ab4a9 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -9,7 +9,7 @@ import control as ct from control import StateSpace, TransferFunction, c2d, isctime, ss2tf, tf2ss -from control.exception import slycot_check +from control.exception import slycot_check, pandas_check from control.tests.conftest import slycotonly from control.timeresp import (_default_time_vector, _ideal_tfinal_and_dt, forced_response, impulse_response, @@ -1180,3 +1180,55 @@ def test_response_transpose( assert t.shape == (T.size, ) assert y.shape == ysh_no assert x.shape == (T.size, sys.nstates) + + +@pytest.mark.skipif(not pandas_check(), reason="pandas not installed") +def test_to_pandas(): + # Create a SISO time response + sys = ct.rss(2, 1, 1) + timepts = np.linspace(0, 10, 10) + resp = ct.input_output_response(sys, timepts, 1) + + # Convert to pandas + df = resp.to_pandas() + + # Check to make sure the data make senses + np.testing.assert_equal(df['time'], resp.time) + np.testing.assert_equal(df['u[0]'], resp.inputs) + np.testing.assert_equal(df['y[0]'], resp.outputs) + np.testing.assert_equal(df['x[0]'], resp.states[0]) + np.testing.assert_equal(df['x[1]'], resp.states[1]) + + # Create a MIMO time response + sys = ct.rss(2, 2, 1) + resp = ct.input_output_response(sys, timepts, np.sin(timepts)) + df = resp.to_pandas() + np.testing.assert_equal(df['time'], resp.time) + np.testing.assert_equal(df['u[0]'], resp.inputs[0]) + np.testing.assert_equal(df['y[0]'], resp.outputs[0]) + np.testing.assert_equal(df['y[1]'], resp.outputs[1]) + np.testing.assert_equal(df['x[0]'], resp.states[0]) + np.testing.assert_equal(df['x[1]'], resp.states[1]) + + # Change the time points + sys = ct.rss(2, 1, 1) + T = np.linspace(0, timepts[-1]/2, timepts.size * 2) + resp = ct.input_output_response(sys, timepts, np.sin(timepts), t_eval=T) + df = resp.to_pandas() + np.testing.assert_equal(df['time'], resp.time) + np.testing.assert_equal(df['u[0]'], resp.inputs) + np.testing.assert_equal(df['y[0]'], resp.outputs) + np.testing.assert_equal(df['x[0]'], resp.states[0]) + np.testing.assert_equal(df['x[1]'], resp.states[1]) + + +@pytest.mark.skipif(pandas_check(), reason="pandas installed") +def test_no_pandas(): + # Create a SISO time response + sys = ct.rss(2, 1, 1) + timepts = np.linspace(0, 10, 10) + resp = ct.input_output_response(sys, timepts, 1) + + # Convert to pandas + with pytest.raises(ImportError, match="pandas"): + df = resp.to_pandas() diff --git a/control/timeresp.py b/control/timeresp.py index bf826b539..87b5e52f7 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -79,6 +79,7 @@ from copy import copy from . import config +from .exception import pandas_check from .lti import isctime, isdtime from .statesp import StateSpace, _convert_to_statespace, _mimo2simo, _mimo2siso from .xferfcn import TransferFunction @@ -638,6 +639,23 @@ def __getitem__(self, index): def __len__(self): return 3 if self.return_x else 2 + # Convert to pandas + def to_pandas(self): + if not pandas_check(): + ImportError('pandas not installed') + import pandas + + # Create a dict for setting up the data frame + data = {'time': self.time} + data.update( + {name: self.u[i] for i, name in enumerate(self.input_labels)}) + data.update( + {name: self.y[i] for i, name in enumerate(self.output_labels)}) + data.update( + {name: self.x[i] for i, name in enumerate(self.state_labels)}) + + return pandas.DataFrame(data) + # Process signal labels def _process_labels(labels, signal, length): From 51f6bfc21ffb3faa9b7ce90d7a2c5afd68067c85 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 26 Mar 2022 22:34:00 -0700 Subject: [PATCH 56/87] allow FrequencyResponseData signal naming + pandas conversion --- control/frdata.py | 51 ++++++++++++++++++++++----- control/freqplot.py | 9 ++--- control/tests/frd_test.py | 41 +++++++++++++++++++++ control/tests/type_conversion_test.py | 6 ++++ 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 19e865821..1dad71fb7 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -50,12 +50,14 @@ real, imag, absolute, eye, linalg, where, sort from scipy.interpolate import splprep, splev from .lti import LTI, _process_frequency_response +from .exception import pandas_check +from .namedio import _NamedIOSystem from . import config __all__ = ['FrequencyResponseData', 'FRD', 'frd'] -class FrequencyResponseData(LTI): +class FrequencyResponseData(LTI, _NamedIOSystem): """FrequencyResponseData(d, w[, smooth]) A class for models defined by frequency response data (FRD). @@ -152,10 +154,6 @@ def __init__(self, *args, **kwargs): # TODO: discrete-time FRD systems? smooth = kwargs.pop('smooth', False) - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - if len(args) == 2: if not isinstance(args[0], FRD) and isinstance(args[0], LTI): # not an FRD, but still a system, second argument should be @@ -196,6 +194,23 @@ def __init__(self, *args, **kwargs): raise ValueError( "Needs 1 or 2 arguments; received %i." % len(args)) + # Set the size of the system + self.noutputs = self.fresp.shape[0] + self.ninputs = self.fresp.shape[1] + + # Process signal names + _NamedIOSystem.__init__( + self, name=kwargs.pop('name', None), + inputs=kwargs.pop('inputs', self.ninputs), + outputs=kwargs.pop('outputs', self.noutputs)) + + # Keep track of return type + self.return_magphase=kwargs.pop('return_magphase', False) + + # Make sure there were no extraneous keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + # create interpolation functions if smooth: self.ifunc = empty((self.fresp.shape[0], self.fresp.shape[1]), @@ -260,11 +275,13 @@ def __add__(self, other): # Check that the input-output sizes are consistent. if self.ninputs != other.ninputs: - raise ValueError("The first summand has %i input(s), but the \ -second has %i." % (self.ninputs, other.ninputs)) + raise ValueError( + "The first summand has %i input(s), but the " \ + "second has %i." % (self.ninputs, other.ninputs)) if self.noutputs != other.noutputs: - raise ValueError("The first summand has %i output(s), but the \ -second has %i." % (self.noutputs, other.noutputs)) + raise ValueError( + "The first summand has %i output(s), but the " \ + "second has %i." % (self.noutputs, other.noutputs)) return FRD(self.fresp + other.fresp, other.omega) @@ -551,6 +568,22 @@ def feedback(self, other=1, sign=-1): return FRD(fresp, other.omega, smooth=(self.ifunc is not None)) + # Convert to pandas + def to_pandas(self): + if not pandas_check(): + ImportError('pandas not installed') + import pandas + + # Create a dict for setting up the data frame + data = {'omega': self.omega} + data.update( + {'H_{%s, %s}' % (out, inp): self.fresp[i, j] \ + for i, out in enumerate(self.output_labels) \ + for j, inp in enumerate(self.input_labels)}) + + return pandas.DataFrame(data) + + # # Allow FRD as an alias for the FrequencyResponseData class # diff --git a/control/freqplot.py b/control/freqplot.py index 7a1243c6c..7eeea0b65 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -204,8 +204,9 @@ def bode_plot(syslist, omega=None, initial_phase = config._get_param( 'freqplot', 'initial_phase', kwargs, None, pop=True) omega_num = config._get_param('freqplot', 'number_of_samples', omega_num) + # If argument was a singleton, turn it into a tuple - if not hasattr(syslist, '__iter__'): + if not isinstance(syslist, (list, tuple)): syslist = (syslist,) omega, omega_range_given = _determine_omega_vector( @@ -678,8 +679,8 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, indent_direction = config._get_param( 'nyquist', 'indent_direction', kwargs, _nyquist_defaults, pop=True) - # If argument was a singleton, turn it into a list - if not hasattr(syslist, '__iter__'): + # If argument was a singleton, turn it into a tuple + if not isinstance(syslist, (list, tuple)): syslist = (syslist,) omega, omega_range_given = _determine_omega_vector( @@ -1109,7 +1110,7 @@ def singular_values_plot(syslist, omega=None, omega_num = config._get_param('freqplot', 'number_of_samples', omega_num) # If argument was a singleton, turn it into a tuple - if not hasattr(syslist, '__iter__'): + if not isinstance(syslist, (list, tuple)): syslist = (syslist,) omega, omega_range_given = _determine_omega_vector( diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index af7d18bc1..328803d53 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -15,6 +15,7 @@ from control.frdata import FRD, _convert_to_FRD, FrequencyResponseData from control import bdalg, evalfr, freqplot from control.tests.conftest import slycotonly +from control.exception import pandas_check class TestFRD: @@ -478,3 +479,43 @@ def test_unrecognized_keyword(self): omega = np.logspace(-1, 2, 10) with pytest.raises(TypeError, match="unrecognized keyword"): frd = FRD(h, omega, unknown=None) + + +def test_named_signals(): + ct.namedio._NamedIOSystem._idCounter = 0 + h1 = TransferFunction([1], [1, 2, 2]) + h2 = TransferFunction([1], [0.1, 1]) + omega = np.logspace(-1, 2, 10) + f1 = FRD(h1, omega) + f2 = FRD(h2, omega) + + # Make sure that systems were properly named + assert f1.name == 'sys[0]' + assert f2.name == 'sys[1]' + assert f1.ninputs == 1 + assert f1.input_labels == ['u[0]'] + assert f1.noutputs == 1 + assert f1.output_labels == ['y[0]'] + + # Change names + f1 = FRD(h1, omega, name='mysys', inputs='u0', outputs='y0') + assert f1.name == 'mysys' + assert f1.ninputs == 1 + assert f1.input_labels == ['u0'] + assert f1.noutputs == 1 + assert f1.output_labels == ['y0'] + + +@pytest.mark.skipif(not pandas_check(), reason="pandas not installed") +def test_to_pandas(): + # Create a SISO frequency response + h1 = TransferFunction([1], [1, 2, 2]) + omega = np.logspace(-1, 2, 10) + resp = FRD(h1, omega) + + # Convert to pandas + df = resp.to_pandas() + + # Check to make sure the data make senses + np.testing.assert_equal(df['omega'], resp.omega) + np.testing.assert_equal(df['H_{y[0], u[0]}'], resp.fresp[0, 0]) diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index d8c2d2b71..cc3b8ec88 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -185,3 +185,9 @@ def test_binary_op_type_conversions(opname, ltype, rtype, sys_dict): # Print out what we are testing in case something goes wrong assert isinstance(result, type_dict[expected]) + + # Make sure that input, output, and state names make sense + assert len(result.input_labels) == result.ninputs + assert len(result.output_labels) == result.outputs + if result.nstates is not None: + assert len(result.state_labels) == result.states From 6c3c630dcd6fffe102ec294fd4aca835a577fb46 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 27 Mar 2022 10:18:18 -0700 Subject: [PATCH 57/87] frequency_response() returns FRD; FRD allows return_magphase --- control/frdata.py | 71 +++++++++++++++++++++++++++++++++------ control/lti.py | 42 +++++++++++++---------- control/tests/lti_test.py | 2 +- control/timeresp.py | 2 +- control/xferfcn.py | 4 +++ 5 files changed, 91 insertions(+), 30 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 1dad71fb7..169d7c175 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -44,11 +44,14 @@ """ # External function declarations +from copy import copy from warnings import warn + import numpy as np from numpy import angle, array, empty, ones, \ real, imag, absolute, eye, linalg, where, sort from scipy.interpolate import splprep, splev + from .lti import LTI, _process_frequency_response from .exception import pandas_check from .namedio import _NamedIOSystem @@ -141,7 +144,7 @@ def __init__(self, *args, **kwargs): The default constructor is FRD(d, w), where w is an iterable of frequency points, and d is the matching frequency data. - If d is a single list, 1d array, or tuple, a SISO system description + If d is a single list, 1D array, or tuple, a SISO system description is assumed. d can also be To call the copy constructor, call FRD(sys), where sys is a @@ -170,13 +173,12 @@ def __init__(self, *args, **kwargs): else: # The user provided a response and a freq vector - self.fresp = array(args[0], dtype=complex) - if len(self.fresp.shape) == 1: - self.fresp = self.fresp.reshape(1, 1, len(args[0])) - self.omega = array(args[1], dtype=float) - if len(self.fresp.shape) != 3 or \ - self.fresp.shape[-1] != self.omega.shape[-1] or \ - len(self.omega.shape) != 1: + self.fresp = array(args[0], dtype=complex, ndmin=1) + if self.fresp.ndim == 1: + self.fresp = self.fresp.reshape(1, 1, -1) + self.omega = array(args[1], dtype=float, ndmin=1) + if self.fresp.ndim != 3 or self.omega.ndim != 1 or \ + self.fresp.shape[-1] != self.omega.shape[-1]: raise TypeError( "The frequency data constructor needs a 1-d or 3-d" " response data array and a matching frequency vector" @@ -206,6 +208,12 @@ def __init__(self, *args, **kwargs): # Keep track of return type self.return_magphase=kwargs.pop('return_magphase', False) + if self.return_magphase not in (True, False): + raise ValueError("unknown return_magphase value") + + self.squeeze=kwargs.pop('squeeze', None) + if self.squeeze not in (None, True, False): + raise ValueError("unknown squeeze value") # Make sure there were no extraneous keywords if kwargs: @@ -477,7 +485,7 @@ def eval(self, omega, squeeze=None): return _process_frequency_response(self, omega, out, squeeze=squeeze) - def __call__(self, s, squeeze=None): + def __call__(self, s=None, squeeze=None, return_magphase=None): """Evaluate system's transfer function at complex frequencies. Returns the complex frequency response `sys(s)` of system `sys` with @@ -490,17 +498,31 @@ def __call__(self, s, squeeze=None): For a frequency response data object, the argument must be an imaginary number (since only the frequency response is defined). + If ``s`` is not given, this function creates a copy of a frequency + response data object with a different set of output settings. + Parameters ---------- s : complex scalar or 1D array_like - Complex frequencies - squeeze : bool, optional (default=True) + Complex frequencies. If not specified, return a copy of the + frequency response data object with updated settings for output + processing (``squeeze``, ``return_magphase``). + + squeeze : bool, optional If squeeze=True, remove single-dimensional entries from the shape of the output even if the system is not SISO. If squeeze=False, keep all indices (output, input and, if omega is array_like, frequency) even if the system is SISO. The default value can be set using config.defaults['control.squeeze_frequency_response']. + return_magphase : bool, optional + If True, then a frequency response data object will enumerate as a + tuple of the form (mag, phase, omega) where where ``mag`` is the + magnitude (absolute value, not dB or log10) of the system + frequency response, ``phase`` is the wrapped phase in radians of + the system frequency response, and ``omega`` is the (sorted) + frequencies at which the response was evaluated. + Returns ------- fresp : complex ndarray @@ -519,6 +541,17 @@ def __call__(self, s, squeeze=None): frequency values. """ + if s is None: + # Create a copy of the response with new keywords + response = copy(self) + + # Update any keywords that we were passed + response.squeeze = self.squeeze if squeeze is None else squeeze + response.return_magphase = self.return_magphase \ + if return_magphase is None else return_magphase + + return response + # Make sure that we are operating on a simple list if len(np.atleast_1d(s).shape) > 1: raise ValueError("input list must be 1D") @@ -533,6 +566,22 @@ def __call__(self, s, squeeze=None): else: return self.eval(complex(s).imag, squeeze=squeeze) + # Implement iter to allow assigning to a tuple + def __iter__(self): + fresp = _process_frequency_response( + self, self.omega, self.fresp, squeeze=self.squeeze) + if not self.return_magphase: + return iter((self.omega, fresp)) + return iter((np.abs(fresp), np.angle(fresp), self.omega)) + + # Implement (thin) getitem to allow access via legacy indexing + def __getitem__(self, index): + return list(self.__iter__())[index] + + # Implement (thin) len to emulate legacy testing interface + def __len__(self): + return 3 if self.return_magphase else 2 + def freqresp(self, omega): """(deprecated) Evaluate transfer function at complex frequencies. diff --git a/control/lti.py b/control/lti.py index b56c2bb44..c6cea157c 100644 --- a/control/lti.py +++ b/control/lti.py @@ -77,9 +77,9 @@ def _set_inputs(self, value): """ Deprecated attribute; use :attr:`ninputs` instead. - The ``input`` attribute was used to store the number of system inputs. - It is no longer used. If you need access to the number of inputs for - an LTI system, use :attr:`ninputs`. + The ``inputs`` attribute was used to store the number of system + inputs. It is no longer used. If you need access to the number + of inputs for an LTI system, use :attr:`ninputs`. """) def _get_outputs(self): @@ -100,7 +100,7 @@ def _set_outputs(self, value): """ Deprecated attribute; use :attr:`noutputs` instead. - The ``output`` attribute was used to store the number of system + The ``outputs`` attribute was used to store the number of system outputs. It is no longer used. If you need access to the number of outputs for an LTI system, use :attr:`noutputs`. """) @@ -197,17 +197,21 @@ def frequency_response(self, omega, squeeze=None): Returns ------- - mag : ndarray - The magnitude (absolute value, not dB or log10) of the system - frequency response. If the system is SISO and squeeze is not - True, the array is 1D, indexed by frequency. If the system is not - SISO or squeeze is False, the array is 3D, indexed by the output, - input, and frequency. If ``squeeze`` is True then - single-dimensional axes are removed. - phase : ndarray - The wrapped phase in radians of the system frequency response. - omega : ndarray - The (sorted) frequencies at which the response was evaluated. + response : :class:`FrequencyReponseData` + Frequency response data object representing the frequency + response. This object can be assigned to a tuple using + + mag, phase, omega = response + + where ``mag`` is the magnitude (absolute value, not dB or log10) + of the system frequency response, ``phase`` is the wrapped phase + in radians of the system frequency response, and ``omega`` is the + (sorted) frequencies at which the response was evaluated. If the + system is SISO and squeeze is not True, ``mag`` and ``phase`` are + 1D, indexed by frequency. If the system is not SISO or squeeze is + False, the array is 3D, indexed by the output, input, and + frequency. If ``squeeze`` is True then single-dimensional axes + are removed. """ omega = np.sort(np.array(omega, ndmin=1)) @@ -218,8 +222,12 @@ def frequency_response(self, omega, squeeze=None): s = np.exp(1j * omega * self.dt) else: s = 1j * omega - response = self.__call__(s, squeeze=squeeze) - return abs(response), angle(response), omega + + # Return the data as a frequency response data object + from .frdata import FrequencyResponseData + response = self.__call__(s) + return FrequencyResponseData( + response, omega, return_magphase=True, squeeze=squeeze) def dcgain(self): """Return the zero-frequency gain""" diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index e2f7f2e03..28276fe27 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -267,7 +267,7 @@ def test_squeeze_exceptions(self, fcn): sys = fcn(ct.rss(2, 1, 1)) with pytest.raises(ValueError, match="unknown squeeze value"): - sys.frequency_response([1], squeeze=1) + resp = sys.frequency_response([1], squeeze='siso') with pytest.raises(ValueError, match="unknown squeeze value"): sys([1j], squeeze='siso') with pytest.raises(ValueError, match="unknown squeeze value"): diff --git a/control/timeresp.py b/control/timeresp.py index 87b5e52f7..fe62387dc 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -463,7 +463,7 @@ def __call__(self, **kwargs): # Update any keywords that we were passed response.transpose = kwargs.pop('transpose', self.transpose) response.squeeze = kwargs.pop('squeeze', self.squeeze) - response.return_x = kwargs.pop('return_x', self.squeeze) + response.return_x = kwargs.pop('return_x', self.return_x) # Check for new labels input_labels = kwargs.pop('input_labels', None) diff --git a/control/xferfcn.py b/control/xferfcn.py index 87d6f533e..6888e3858 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -61,6 +61,7 @@ from re import sub from .lti import LTI, common_timebase, isdtime, _process_frequency_response from .exception import ControlMIMONotImplemented +from .frdata import FrequencyResponseData from .namedio import _NamedIOSystem, _process_signal_list from . import config @@ -1382,6 +1383,9 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1): return TransferFunction(num, den) + elif isinstance(sys, FrequencyResponseData): + raise TypeError("Can't convert given FRD to TransferFunction system.") + # If this is array-like, try to create a constant feedthrough try: D = array(sys, ndmin=2) From ecdf1e7e6fa1516053b210300693570df6cbbfa3 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 29 Mar 2022 22:21:31 -0700 Subject: [PATCH 58/87] add frequency_reponse() + FRD properties mag, phase, freq --- control/frdata.py | 23 +++++++++++++++++++++++ control/lti.py | 38 +++++++++++++++++++++----------------- control/tests/frd_test.py | 13 +++++++++++++ 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 169d7c175..4d149a46b 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -233,6 +233,29 @@ def __init__(self, *args, **kwargs): self.ifunc = None super().__init__(self.fresp.shape[1], self.fresp.shape[0]) + # + # Frequency response properties + # + # Different properties of the frequency response that can be used for + # analysis and characterization. + # + + @property + def magnitude(self): + return np.abs(self.fresp) + + @property + def phase(self): + return np.angle(self.fresp) + + @property + def frequency(self): + return self.omega + + @property + def response(self): + return self.fresp + def __str__(self): """String representation of the transfer function.""" diff --git a/control/lti.py b/control/lti.py index c6cea157c..3e3901438 100644 --- a/control/lti.py +++ b/control/lti.py @@ -19,7 +19,7 @@ __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', 'isdtime', 'isctime', 'pole', 'zero', 'damp', 'evalfr', - 'freqresp', 'dcgain'] + 'frequency_response', 'freqresp', 'dcgain'] class LTI: """LTI is a parent class to linear time-invariant (LTI) system objects. @@ -172,16 +172,16 @@ def frequency_response(self, omega, squeeze=None): Reports the frequency response of the system, - G(j*omega) = mag*exp(j*phase) + G(j*omega) = mag * exp(j*phase) - for continuous time systems. For discrete time systems, the response is - evaluated around the unit circle such that + for continuous time systems. For discrete time systems, the response + is evaluated around the unit circle such that - G(exp(j*omega*dt)) = mag*exp(j*phase). + G(exp(j*omega*dt)) = mag * exp(j*phase). In general the system may be multiple input, multiple output (MIMO), - where `m = self.ninputs` number of inputs and `p = self.noutputs` number - of outputs. + where `m = self.ninputs` number of inputs and `p = self.noutputs` + number of outputs. Parameters ---------- @@ -203,15 +203,15 @@ def frequency_response(self, omega, squeeze=None): mag, phase, omega = response - where ``mag`` is the magnitude (absolute value, not dB or log10) - of the system frequency response, ``phase`` is the wrapped phase - in radians of the system frequency response, and ``omega`` is the - (sorted) frequencies at which the response was evaluated. If the - system is SISO and squeeze is not True, ``mag`` and ``phase`` are - 1D, indexed by frequency. If the system is not SISO or squeeze is - False, the array is 3D, indexed by the output, input, and - frequency. If ``squeeze`` is True then single-dimensional axes - are removed. + where ``mag`` is the magnitude (absolute value, not dB or + log10) of the system frequency response, ``phase`` is the wrapped + phase in radians of the system frequency response, and ``omega`` + is the (sorted) frequencies at which the response was evaluated. + If the system is SISO and squeeze is not True, ``magnitude`` and + ``phase`` are 1D, indexed by frequency. If the system is not SISO + or squeeze is False, the array is 3D, indexed by the output, + input, and frequency. If ``squeeze`` is True then + single-dimensional axes are removed. """ omega = np.sort(np.array(omega, ndmin=1)) @@ -597,7 +597,7 @@ def evalfr(sys, x, squeeze=None): """ return sys.__call__(x, squeeze=squeeze) -def freqresp(sys, omega, squeeze=None): +def frequency_response(sys, omega, squeeze=None): """Frequency response of an LTI system at multiple angular frequencies. In general the system may be multiple input, multiple output (MIMO), where @@ -671,6 +671,10 @@ def freqresp(sys, omega, squeeze=None): return sys.frequency_response(omega, squeeze=squeeze) +# Alternative name (legacy) +freqresp = frequency_response + + def dcgain(sys): """Return the zero-frequency (or DC) gain of the given system diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index 328803d53..864b771e4 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -519,3 +519,16 @@ def test_to_pandas(): # Check to make sure the data make senses np.testing.assert_equal(df['omega'], resp.omega) np.testing.assert_equal(df['H_{y[0], u[0]}'], resp.fresp[0, 0]) + + +def test_frequency_response(): + # Create an SISO frequence response + sys = ct.rss(2, 2, 2) + omega = np.logspace(-2, 2, 20) + resp = ct.frequency_response(sys, omega) + eval = sys(omega*1j) + + # Make sure we get the right answers in various ways + np.testing.assert_equal(resp.magnitude, np.abs(eval)) + np.testing.assert_equal(resp.phase, np.angle(eval)) + np.testing.assert_equal(resp.omega, omega) From 14fa89004b8e972e6b3654e5921b2d2dfbd32ded Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 30 Mar 2022 07:47:43 -0700 Subject: [PATCH 59/87] tweak iosys kwargs checking --- control/iosys.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index e6764cbf8..5623ee587 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1663,15 +1663,14 @@ def input_output_response( # Process keyword arguments # - # Allow method as an alternative to solve_ivp_method - if kwargs.get('method', None): - solve_ivp_kwargs['method'] = kwargs.pop('method') - # Figure out the method to be used if kwargs.get('solve_ivp_method', None): if kwargs.get('method', None): raise ValueError("ivp_method specified more than once") solve_ivp_kwargs['method'] = kwargs.pop('solve_ivp_method') + elif kwargs.get('method', None): + # Allow method as an alternative to solve_ivp_method + solve_ivp_kwargs['method'] = kwargs.pop('method') # Set the default method to 'RK45' if solve_ivp_kwargs.get('method', None) is None: From f3cda64ebabaed46f3080d8305474858f63d21c8 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 30 Mar 2022 22:34:07 -0700 Subject: [PATCH 60/87] change pole(), zero() to poles(), zeros(), with legacy interface available --- control/freqplot.py | 11 +++--- control/lti.py | 63 +++++++++++++++++++++------------- control/matlab/__init__.py | 3 ++ control/modelsimp.py | 2 +- control/pzmap.py | 4 +-- control/statesp.py | 12 +++---- control/tests/bdalg_test.py | 42 +++++++++++------------ control/tests/convert_test.py | 10 +++--- control/tests/freqresp_test.py | 16 ++++----- control/tests/iosys_test.py | 2 +- control/tests/lti_test.py | 39 +++++++++++++++------ control/tests/minreal_test.py | 6 ++-- control/tests/nyquist_test.py | 10 +++--- control/tests/rlocus_test.py | 2 +- control/tests/statefbk_test.py | 14 ++++---- control/tests/statesp_test.py | 22 ++++++------ control/tests/xferfcn_test.py | 6 ++-- control/xferfcn.py | 4 +-- doc/control.rst | 4 +-- examples/tfvis.py | 8 ++--- 20 files changed, 159 insertions(+), 121 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 7eeea0b65..7f29dce36 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -722,11 +722,11 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, if isinstance(sys, (StateSpace, TransferFunction)) \ and indent_direction != 'none': if sys.isctime(): - splane_poles = sys.pole() + splane_poles = sys.poles() else: # map z-plane poles to s-plane, ignoring any at the origin # because we don't need to indent for them - zplane_poles = sys.pole() + zplane_poles = sys.poles() zplane_poles = zplane_poles[~np.isclose(abs(zplane_poles), 0.)] splane_poles = np.log(zplane_poles)/sys.dt @@ -1328,8 +1328,8 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, try: # Add new features to the list if sys.isctime(): - features_ = np.concatenate((np.abs(sys.pole()), - np.abs(sys.zero()))) + features_ = np.concatenate( + (np.abs(sys.poles()), np.abs(sys.zeros()))) # Get rid of poles and zeros at the origin toreplace = np.isclose(features_, 0.0) if np.any(toreplace): @@ -1339,8 +1339,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, # TODO: What distance to the Nyquist frequency is appropriate? freq_interesting.append(fn * 0.9) - features_ = np.concatenate((sys.pole(), - sys.zero())) + features_ = np.concatenate((sys.poles(), sys.zeros())) # Get rid of poles and zeros on the real axis (imag==0) # * origin and real < 0 # * at 1.: would result in omega=0. (logaritmic plot!) diff --git a/control/lti.py b/control/lti.py index 3e3901438..45f7b3c54 100644 --- a/control/lti.py +++ b/control/lti.py @@ -18,8 +18,8 @@ from . import config __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', - 'isdtime', 'isctime', 'pole', 'zero', 'damp', 'evalfr', - 'frequency_response', 'freqresp', 'dcgain'] + 'isdtime', 'isctime', 'poles', 'zeros', 'damp', 'evalfr', + 'frequency_response', 'freqresp', 'dcgain', 'pole', 'zero'] class LTI: """LTI is a parent class to linear time-invariant (LTI) system objects. @@ -156,7 +156,7 @@ def damp(self): poles : array Array of system poles ''' - poles = self.pole() + poles = self.poles() if isdtime(self, strict=True): splane_poles = np.log(poles.astype(complex))/self.dt @@ -242,6 +242,21 @@ def _dcgain(self, warn_infinite): else: return zeroresp + # + # Deprecated functions + # + + def pole(self): + warn("pole() will be deprecated; use poles()", + PendingDeprecationWarning) + return self.poles() + + def zero(self): + warn("zero() will be deprecated; use zeros()", + PendingDeprecationWarning) + return self.zeros() + + # Test to see if a system is SISO def issiso(sys, strict=False): """ @@ -426,7 +441,8 @@ def isctime(sys, strict=False): # Got passed something we don't recognize return False -def pole(sys): + +def poles(sys): """ Compute system poles. @@ -440,23 +456,23 @@ def pole(sys): poles: ndarray Array that contains the system's poles. - Raises - ------ - NotImplementedError - when called on a TransferFunction object - See Also -------- - zero - TransferFunction.pole - StateSpace.pole + zeros + TransferFunction.poles + StateSpace.poles """ - return sys.pole() + return sys.poles() -def zero(sys): +def pole(sys): + warn("pole() will be deprecated; use poles()", PendingDeprecationWarning) + return poles(sys) + + +def zeros(sys): """ Compute system zeros. @@ -470,20 +486,21 @@ def zero(sys): zeros: ndarray Array that contains the system's zeros. - Raises - ------ - NotImplementedError - when called on a MIMO system - See Also -------- - pole - StateSpace.zero - TransferFunction.zero + poles + StateSpace.zeros + TransferFunction.zeros """ - return sys.zero() + return sys.zeros() + + +def zero(sys): + warn("zero() will be deprecated; use zeros()", PendingDeprecationWarning) + return zeros(sys) + def damp(sys, doprint=True): """ diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index f10a76c54..53c254189 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -84,6 +84,9 @@ from ..dtime import c2d from ..sisotool import sisotool +# Functions that are renamed in MATLAB +pole, zero = poles, zeros + # Import functions specific to Matlab compatibility package from .timeresp import * from .wrappers import * diff --git a/control/modelsimp.py b/control/modelsimp.py index f43acc2fd..2cd2745de 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -354,7 +354,7 @@ def minreal(sys, tol=None, verbose=True): sysr = sys.minreal(tol) if verbose: print("{nstates} states have been removed from the model".format( - nstates=len(sys.pole()) - len(sysr.pole()))) + nstates=len(sys.poles()) - len(sysr.poles()))) return sysr diff --git a/control/pzmap.py b/control/pzmap.py index 7d3836d7f..c528df4be 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -104,8 +104,8 @@ def pzmap(sys, plot=None, grid=None, title='Pole Zero Map', **kwargs): if not isinstance(sys, LTI): raise TypeError('Argument ``sys``: must be a linear system.') - poles = sys.pole() - zeros = sys.zero() + poles = sys.poles() + zeros = sys.zeros() if (plot): import matplotlib.pyplot as plt diff --git a/control/statesp.py b/control/statesp.py index 3cb53bf60..076b0ccea 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -933,7 +933,7 @@ def horner(self, x, warn_infinite=True): # Evaluating at a pole. Return value depends if there # is a zero at the same point or not. - if x_idx in self.zero(): + if x_idx in self.zeros(): out[:, :, idx] = complex(np.nan, np.nan) else: out[:, :, idx] = complex(np.inf, np.nan) @@ -955,12 +955,12 @@ def freqresp(self, omega): return self.frequency_response(omega) # Compute poles and zeros - def pole(self): + def poles(self): """Compute the poles of a state space system.""" return eigvals(self.A) if self.nstates else np.array([]) - def zero(self): + def zeros(self): """Compute the zeros of a state space system.""" if not self.nstates: @@ -982,9 +982,9 @@ def zero(self): except ImportError: # Slycot unavailable. Fall back to scipy. if self.C.shape[0] != self.D.shape[1]: - raise NotImplementedError("StateSpace.zero only supports " - "systems with the same number of " - "inputs as outputs.") + raise NotImplementedError( + "StateSpace.zero only supports systems with the same " + "number of inputs as outputs.") # This implements the QZ algorithm for finding transmission zeros # from diff --git a/control/tests/bdalg_test.py b/control/tests/bdalg_test.py index 433a584cc..2f6b5523f 100644 --- a/control/tests/bdalg_test.py +++ b/control/tests/bdalg_test.py @@ -11,7 +11,7 @@ from control.xferfcn import TransferFunction from control.statesp import StateSpace from control.bdalg import feedback, append, connect -from control.lti import zero, pole +from control.lti import zeros, poles class TestFeedback: @@ -188,52 +188,52 @@ def testLists(self, tsys): # Series sys1_2 = ctrl.series(sys1, sys2) - np.testing.assert_array_almost_equal(sort(pole(sys1_2)), [-4., -2.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_2)), [-3., -1.]) + np.testing.assert_array_almost_equal(sort(poles(sys1_2)), [-4., -2.]) + np.testing.assert_array_almost_equal(sort(zeros(sys1_2)), [-3., -1.]) sys1_3 = ctrl.series(sys1, sys2, sys3) - np.testing.assert_array_almost_equal(sort(pole(sys1_3)), + np.testing.assert_array_almost_equal(sort(poles(sys1_3)), [-6., -4., -2.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_3)), + np.testing.assert_array_almost_equal(sort(zeros(sys1_3)), [-5., -3., -1.]) sys1_4 = ctrl.series(sys1, sys2, sys3, sys4) - np.testing.assert_array_almost_equal(sort(pole(sys1_4)), + np.testing.assert_array_almost_equal(sort(poles(sys1_4)), [-8., -6., -4., -2.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_4)), + np.testing.assert_array_almost_equal(sort(zeros(sys1_4)), [-7., -5., -3., -1.]) sys1_5 = ctrl.series(sys1, sys2, sys3, sys4, sys5) - np.testing.assert_array_almost_equal(sort(pole(sys1_5)), + np.testing.assert_array_almost_equal(sort(poles(sys1_5)), [-8., -6., -4., -2., -0.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_5)), + np.testing.assert_array_almost_equal(sort(zeros(sys1_5)), [-9., -7., -5., -3., -1.]) # Parallel sys1_2 = ctrl.parallel(sys1, sys2) - np.testing.assert_array_almost_equal(sort(pole(sys1_2)), [-4., -2.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_2)), - sort(zero(sys1 + sys2))) + np.testing.assert_array_almost_equal(sort(poles(sys1_2)), [-4., -2.]) + np.testing.assert_array_almost_equal(sort(zeros(sys1_2)), + sort(zeros(sys1 + sys2))) sys1_3 = ctrl.parallel(sys1, sys2, sys3) - np.testing.assert_array_almost_equal(sort(pole(sys1_3)), + np.testing.assert_array_almost_equal(sort(poles(sys1_3)), [-6., -4., -2.]) - np.testing.assert_array_almost_equal(sort(zero(sys1_3)), - sort(zero(sys1 + sys2 + sys3))) + np.testing.assert_array_almost_equal(sort(zeros(sys1_3)), + sort(zeros(sys1 + sys2 + sys3))) sys1_4 = ctrl.parallel(sys1, sys2, sys3, sys4) - np.testing.assert_array_almost_equal(sort(pole(sys1_4)), + np.testing.assert_array_almost_equal(sort(poles(sys1_4)), [-8., -6., -4., -2.]) np.testing.assert_array_almost_equal( - sort(zero(sys1_4)), - sort(zero(sys1 + sys2 + sys3 + sys4))) + sort(zeros(sys1_4)), + sort(zeros(sys1 + sys2 + sys3 + sys4))) sys1_5 = ctrl.parallel(sys1, sys2, sys3, sys4, sys5) - np.testing.assert_array_almost_equal(sort(pole(sys1_5)), + np.testing.assert_array_almost_equal(sort(poles(sys1_5)), [-8., -6., -4., -2., -0.]) np.testing.assert_array_almost_equal( - sort(zero(sys1_5)), - sort(zero(sys1 + sys2 + sys3 + sys4 + sys5))) + sort(zeros(sys1_5)), + sort(zeros(sys1 + sys2 + sys3 + sys4 + sys5))) def testMimoSeries(self, tsys): """regression: bdalg.series reverses order of arguments""" diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index 36eac223c..6c4586471 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -225,7 +225,7 @@ def testTf2SsDuplicatePoles(self): [[1], [1, 0]]] g = tf(num, den) s = ss(g) - np.testing.assert_allclose(g.pole(), s.pole()) + np.testing.assert_allclose(g.poles(), s.poles()) @slycotonly def test_tf2ss_robustness(self): @@ -241,10 +241,10 @@ def test_tf2ss_robustness(self): sys2ss = tf2ss(sys2tf) # Make sure that the poles match for StateSpace and TransferFunction - np.testing.assert_array_almost_equal(np.sort(sys1tf.pole()), - np.sort(sys1ss.pole())) - np.testing.assert_array_almost_equal(np.sort(sys2tf.pole()), - np.sort(sys2ss.pole())) + np.testing.assert_array_almost_equal(np.sort(sys1tf.poles()), + np.sort(sys1ss.poles())) + np.testing.assert_array_almost_equal(np.sort(sys2tf.poles()), + np.sort(sys2ss.poles())) def test_tf2ss_nonproper(self): """Unit tests for non-proper transfer functions""" diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 4d1ac55e0..0e35a38ea 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -418,11 +418,11 @@ def test_dcgain_consistency(): """Test to make sure that DC gain is consistently evaluated""" # Set up transfer function with pole at the origin sys_tf = ctrl.tf([1], [1, 0]) - assert 0 in sys_tf.pole() + assert 0 in sys_tf.poles() # Set up state space system with pole at the origin sys_ss = ctrl.tf2ss(sys_tf) - assert 0 in sys_ss.pole() + assert 0 in sys_ss.poles() # Finite (real) numerator over 0 denominator => inf + nanj np.testing.assert_equal( @@ -440,8 +440,8 @@ def test_dcgain_consistency(): # Set up transfer function with pole, zero at the origin sys_tf = ctrl.tf([1, 0], [1, 0]) - assert 0 in sys_tf.pole() - assert 0 in sys_tf.zero() + assert 0 in sys_tf.poles() + assert 0 in sys_tf.zeros() # Pole and zero at the origin should give nan + nanj for the response np.testing.assert_equal( @@ -456,7 +456,7 @@ def test_dcgain_consistency(): ctrl.tf2ss(ctrl.tf([1], [1, 0])) # Different systems give different representations => test accordingly - if 0 in sys_ss.pole() and 0 in sys_ss.zero(): + if 0 in sys_ss.poles() and 0 in sys_ss.zeros(): # Pole and zero at the origin => should get (nan + nanj) np.testing.assert_equal( sys_ss(0, warn_infinite=False), complex(np.nan, np.nan)) @@ -464,7 +464,7 @@ def test_dcgain_consistency(): sys_ss(0j, warn_infinite=False), complex(np.nan, np.nan)) np.testing.assert_equal( sys_ss.dcgain(), np.nan) - elif 0 in sys_ss.pole(): + elif 0 in sys_ss.poles(): # Pole at the origin, but zero elsewhere => should get (inf + nanj) np.testing.assert_equal( sys_ss(0, warn_infinite=False), complex(np.inf, np.nan)) @@ -479,11 +479,11 @@ def test_dcgain_consistency(): # Pole with non-zero, complex numerator => inf + infj s = ctrl.tf('s') sys_tf = (s + 1) / (s**2 + 1) - assert 1j in sys_tf.pole() + assert 1j in sys_tf.poles() # Set up state space system with pole on imaginary axis sys_ss = ctrl.tf2ss(sys_tf) - assert 1j in sys_tf.pole() + assert 1j in sys_tf.poles() # Make sure we get correct response if evaluated at the pole np.testing.assert_equal( diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index f3377c0ab..87aa271ef 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1213,7 +1213,7 @@ def test_lineariosys_statespace(self, tsys): # Make sure that state space functions work for LinearIOSystems np.testing.assert_allclose( - iosys_siso.pole(), tsys.siso_linsys.pole()) + iosys_siso.poles(), tsys.siso_linsys.poles()) omega = np.logspace(.1, 10, 100) mag_io, phase_io, omega_io = iosys_siso.frequency_response(omega) mag_ss, phase_ss, omega_ss = tsys.siso_linsys.frequency_response(omega) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 28276fe27..36c1b100d 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -5,23 +5,42 @@ from .conftest import editsdefaults import control as ct -from control import c2d, tf, tf2ss, NonlinearIOSystem +from control import c2d, tf, ss, tf2ss, NonlinearIOSystem from control.lti import (LTI, common_timebase, evalfr, damp, dcgain, isctime, - isdtime, issiso, pole, timebaseEqual, zero) + isdtime, issiso, poles, timebaseEqual, zeros) from control.tests.conftest import slycotonly from control.exception import slycot_check class TestLTI: + @pytest.mark.parametrize("fun, args", [ + [tf, (126, [-1, 42])], + [ss, ([[42]], [[1]], [[1]], 0)] + ]) + def test_poles(self, fun, args): + sys = fun(*args) + np.testing.assert_allclose(sys.poles(), 42) + np.testing.assert_allclose(poles(sys), 42) - def test_pole(self): - sys = tf(126, [-1, 42]) - np.testing.assert_allclose(sys.pole(), 42) - np.testing.assert_allclose(pole(sys), 42) + with pytest.warns(PendingDeprecationWarning): + sys.pole() - def test_zero(self): - sys = tf([-1, 42], [1, 10]) - np.testing.assert_allclose(sys.zero(), 42) - np.testing.assert_allclose(zero(sys), 42) + with pytest.warns(PendingDeprecationWarning): + ct.pole(sys) + + @pytest.mark.parametrize("fun, args", [ + [tf, (126, [-1, 42])], + [ss, ([[42]], [[1]], [[1]], 0)] + ]) + def test_zero(self, fun, args): + sys = fun(*args) + np.testing.assert_allclose(sys.zeros(), 42) + np.testing.assert_allclose(zeros(sys), 42) + + with pytest.warns(PendingDeprecationWarning): + sys.zero() + + with pytest.warns(PendingDeprecationWarning): + ct.zero(sys) def test_issiso(self): assert issiso(1) diff --git a/control/tests/minreal_test.py b/control/tests/minreal_test.py index 466f9384d..10c56d4ca 100644 --- a/control/tests/minreal_test.py +++ b/control/tests/minreal_test.py @@ -7,7 +7,7 @@ from scipy.linalg import eigvals import pytest -from control import rss, ss, zero +from control import rss, ss, zeros from control.statesp import StateSpace from control.xferfcn import TransferFunction from itertools import permutations @@ -64,8 +64,8 @@ def testMinrealBrute(self): # Check that the zeros match # Note: sorting doesn't work => have to do the hard way - z1 = zero(s1) - z2 = zero(s2) + z1 = zeros(s1) + z2 = zeros(s2) # Start by making sure we have the same # of zeros assert len(z1) == len(z2) diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index c77d94c86..a001598a6 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -19,11 +19,11 @@ # Utility function for counting unstable poles of open loop (P in FBS) def _P(sys, indent='right'): if indent == 'right': - return (sys.pole().real > 0).sum() + return (sys.poles().real > 0).sum() elif indent == 'left': - return (sys.pole().real >= 0).sum() + return (sys.poles().real >= 0).sum() elif indent == 'none': - if any(sys.pole().real == 0): + if any(sys.poles().real == 0): raise ValueError("indent must be left or right for imaginary pole") else: raise TypeError("unknown indent value") @@ -31,7 +31,7 @@ def _P(sys, indent='right'): # Utility function for counting unstable poles of closed loop (Z in FBS) def _Z(sys): - return (sys.feedback().pole().real >= 0).sum() + return (sys.feedback().poles().real >= 0).sum() # Basic tests @@ -308,6 +308,6 @@ def test_nyquist_exceptions(): print("Unusual Nyquist plot") sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1]) plt.figure() - plt.title("Poles: %s" % np.array2string(sys.pole(), precision=2, separator=',')) + plt.title("Poles: %s" % np.array2string(sys.poles(), precision=2, separator=',')) count = ct.nyquist_plot(sys) assert _Z(sys) == count + _P(sys) diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index ef9bd7ecb..a0ecebb15 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -41,7 +41,7 @@ def sys(self, request): def check_cl_poles(self, sys, pole_list, k_list): for k, poles in zip(k_list, pole_list): - poles_expected = np.sort(feedback(sys, k).pole()) + poles_expected = np.sort(feedback(sys, k).poles()) poles = np.sort(poles) np.testing.assert_array_almost_equal(poles, poles_expected) diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 9f04b3723..13f164e1f 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -7,7 +7,7 @@ import pytest import control as ct -from control import lqe, dlqe, pole, rss, ss, tf +from control import lqe, dlqe, poles, rss, ss, tf from control.exception import ControlDimension, ControlSlycot, \ ControlArgument, slycot_check from control.mateqn import care, dare @@ -167,12 +167,12 @@ def testAcker(self, fixedseed): # Place the poles at random locations des = rss(states, 1, 1) - poles = pole(des) + desired = poles(des) # Now place the poles using acker - K = acker(sys.A, sys.B, poles) + K = acker(sys.A, sys.B, desired) new = ss(sys.A - sys.B * K, sys.B, sys.C, sys.D) - placed = pole(new) + placed = poles(new) # Debugging code # diff = np.sort(poles) - np.sort(placed) @@ -181,8 +181,8 @@ def testAcker(self, fixedseed): # print(sys) # print("desired = ", poles) - np.testing.assert_array_almost_equal(np.sort(poles), - np.sort(placed), decimal=4) + np.testing.assert_array_almost_equal( + np.sort(desired), np.sort(placed), decimal=4) def checkPlaced(self, P_expected, P_placed): """Check that placed poles are correct""" @@ -679,7 +679,7 @@ def test_lqr_integral_continuous(self): np.testing.assert_array_almost_equal(clsys.D, D_clsys) # Check the poles of the closed loop system - assert all(np.real(clsys.pole()) < 0) + assert all(np.real(clsys.poles()) < 0) # Make sure controller infinite zero frequency gain if slycot_check(): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index be6cd9a6b..d2e5a9a70 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -230,7 +230,7 @@ def test_D_broadcast(self, sys623): def test_pole(self, sys322): """Evaluate the poles of a MIMO system.""" - p = np.sort(sys322.pole()) + p = np.sort(sys322.poles()) true_p = np.sort([3.34747678408874, -3.17373839204437 + 1.47492908003839j, -3.17373839204437 - 1.47492908003839j]) @@ -240,7 +240,7 @@ def test_pole(self, sys322): def test_zero_empty(self): """Test to make sure zero() works with no zeros in system.""" sys = _convert_to_statespace(TransferFunction([1], [1, 2, 1])) - np.testing.assert_array_equal(sys.zero(), np.array([])) + np.testing.assert_array_equal(sys.zeros(), np.array([])) @slycotonly def test_zero_siso(self, sys222): @@ -252,9 +252,9 @@ def test_zero_siso(self, sys222): # compute zeros as root of the characteristic polynomial at the numerator of tf111 # this method is simple and assumed as valid in this test - true_z = np.sort(tf111[0, 0].zero()) + true_z = np.sort(tf111[0, 0].zeros()) # Compute the zeros through ab08nd, which is tested here - z = np.sort(sys111.zero()) + z = np.sort(sys111.zeros()) np.testing.assert_almost_equal(true_z, z) @@ -262,7 +262,7 @@ def test_zero_siso(self, sys222): def test_zero_mimo_sys322_square(self, sys322): """Evaluate the zeros of a square MIMO system.""" - z = np.sort(sys322.zero()) + z = np.sort(sys322.zeros()) true_z = np.sort([44.41465, -0.490252, -5.924398]) np.testing.assert_array_almost_equal(z, true_z) @@ -270,7 +270,7 @@ def test_zero_mimo_sys322_square(self, sys322): def test_zero_mimo_sys222_square(self, sys222): """Evaluate the zeros of a square MIMO system.""" - z = np.sort(sys222.zero()) + z = np.sort(sys222.zeros()) true_z = np.sort([-10.568501, 3.368501]) np.testing.assert_array_almost_equal(z, true_z) @@ -278,7 +278,7 @@ def test_zero_mimo_sys222_square(self, sys222): def test_zero_mimo_sys623_non_square(self, sys623): """Evaluate the zeros of a non square MIMO system.""" - z = np.sort(sys623.zero()) + z = np.sort(sys623.zeros()) true_z = np.sort([2., -1.]) np.testing.assert_array_almost_equal(z, true_z) @@ -749,9 +749,9 @@ def test_str(self, sys322): assert str(sysdt1) == tref + "\ndt = {}\n".format(1.) def test_pole_static(self): - """Regression: pole() of static gain is empty array.""" + """Regression: poles() of static gain is empty array.""" np.testing.assert_array_equal(np.array([]), - StateSpace([], [], [], [[1]]).pole()) + StateSpace([], [], [], [[1]]).poles()) def test_horner(self, sys322): """Test horner() function""" @@ -853,7 +853,7 @@ def test_shape(self, states, outputs, inputs): def test_pole(self, states, outputs, inputs): """Test that the poles of rss outputs have a negative real part.""" sys = rss(states, outputs, inputs) - p = sys.pole() + p = sys.poles() for z in p: assert z.real < 0 @@ -905,7 +905,7 @@ def test_shape(self, states, outputs, inputs): def test_pole(self, states, outputs, inputs): """Test that the poles of drss outputs have less than unit magnitude.""" sys = drss(states, outputs, inputs) - p = sys.pole() + p = sys.poles() for z in p: assert abs(z) < 1 diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 7821ce54d..f2eb33f6a 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -596,7 +596,7 @@ def test_pole_mimo(self): sys = TransferFunction( [[[1.], [1.]], [[1.], [1.]]], [[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]]) - p = sys.pole() + p = sys.poles() np.testing.assert_array_almost_equal(p, [-2., -2., -7., -3., -2.]) @@ -604,14 +604,14 @@ def test_pole_mimo(self): sys2 = TransferFunction( [[[1., 2., 3., 4.], [1.]], [[1.], [1.]]], [[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]]) - p2 = sys2.pole() + p2 = sys2.poles() np.testing.assert_array_almost_equal(p2, [-2., -2., -7., -3., -2.]) def test_double_cancelling_poles_siso(self): H = TransferFunction([1, 1], [1, 2, 1]) - p = H.pole() + p = H.poles() np.testing.assert_array_almost_equal(p, [-1, -1]) # Tests for TransferFunction.feedback diff --git a/control/xferfcn.py b/control/xferfcn.py index 6888e3858..069a90926 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -772,7 +772,7 @@ def freqresp(self, omega): "MATLAB compatibility module instead", DeprecationWarning) return self.frequency_response(omega) - def pole(self): + def poles(self): """Compute the poles of a transfer function.""" _, den, denorder = self._common_den(allow_nonproper=True) rts = [] @@ -780,7 +780,7 @@ def pole(self): rts.extend(roots(d[:o + 1])) return np.array(rts) - def zero(self): + def zeros(self): """Compute the zeros of a transfer function.""" if self.ninputs > 1 or self.noutputs > 1: raise NotImplementedError( diff --git a/doc/control.rst b/doc/control.rst index 20f363a1e..fc6618d24 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -83,8 +83,8 @@ Control system analysis margin stability_margins phase_crossover_frequencies - pole - zero + poles + zeros pzmap root_locus sisotool diff --git a/examples/tfvis.py b/examples/tfvis.py index 30a084ffb..0cb789db4 100644 --- a/examples/tfvis.py +++ b/examples/tfvis.py @@ -270,8 +270,8 @@ def button_release(self, event): tfcn = self.tfi.get_tf() if (tfcn): - self.zeros = tfcn.zero() - self.poles = tfcn.pole() + self.zeros = tfcn.zeros() + self.poles = tfcn.poles() self.sys = tfcn self.redraw() @@ -314,8 +314,8 @@ def apply(self): tfcn = self.tfi.get_tf() if (tfcn): - self.zeros = tfcn.zero() - self.poles = tfcn.pole() + self.zeros = tfcn.zeros() + self.poles = tfcn.poles() self.sys = tfcn self.redraw() From fb38fd3d68c8adc1d33e08de7c5c85fe4dbe300a Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 1 Apr 2022 21:26:56 -0700 Subject: [PATCH 61/87] rebase cleanup --- control/iosys.py | 1 + control/tests/kwargs_test.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/control/iosys.py b/control/iosys.py index 5623ee587..5e9d3ebe2 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -2224,6 +2224,7 @@ def _parse_signal_parameter(value, name, kwargs, end=False): if end and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) + return value diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 2a4d24306..7de944c49 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -83,7 +83,6 @@ def test_unrecognized_kwargs(): table = [ [control.dlqe, (sys, [[1]], [[1]]), {}], [control.dlqr, (sys, [[1, 0], [0, 1]], [[1]]), {}], - [control.dlqe, (sys, [[1]], [[1]]), {}], [control.drss, (2, 1, 1), {}], [control.input_output_response, (sys, [0, 1, 2], [1, 1, 1]), {}], [control.lqe, (sys, [[1]], [[1]]), {}], From 2264c768e121a90a357463a6c801ef49f5c94b2f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 2 Apr 2022 08:23:33 -0700 Subject: [PATCH 62/87] update repr, str representations on NamedIOSystem objects --- control/flatsys/flatsys.py | 5 +++++ control/iosys.py | 11 ++++++++++- control/namedio.py | 18 ++++++------------ control/tests/namedio_test.py | 6 ++++++ 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py index 2f20aa1e9..c01eb9127 100644 --- a/control/flatsys/flatsys.py +++ b/control/flatsys/flatsys.py @@ -156,6 +156,11 @@ def __init__(self, # Save the length of the flat flag + def __str__(self): + return f"{NonlinearIOSystem.__str__(self)}\n\n" \ + + f"Forward: {self.forward}\n" \ + + f"Reverse: {self.reverse}" + def forward(self, x, u, params={}): """Compute the flat flag given the states and input. diff --git a/control/iosys.py b/control/iosys.py index 5e9d3ebe2..ab7c43a62 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -706,6 +706,10 @@ def _out(self, t, x, u): + self.D @ np.reshape(u, (-1, 1)) return np.array(y).reshape((-1,)) + def __repr__(self): + # Need to define so that I/O system gets used instead of StateSpace + return InputOutputSystem.__repr__(self) + def __str__(self): return InputOutputSystem.__str__(self) + "\n\n" \ + StateSpace.__str__(self) @@ -786,7 +790,7 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, # Initialize the rest of the structure dt = kwargs.pop('dt', config.defaults['control.default_dt']) - super(NonlinearIOSystem, self).__init__( + super().__init__( inputs=inputs, outputs=outputs, states=states, params=params, dt=dt, name=name ) @@ -816,6 +820,11 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, # Initialize current parameters to default parameters self._current_params = params.copy() + def __str__(self): + return f"{InputOutputSystem.__str__(self)}\n\n" + \ + f"Update: {self.updfcn}\n" + \ + f"Output: {self.outfcn}" + # Return the value of a static nonlinear system def __call__(sys, u, params=None, squeeze=None): """Evaluate a (static) nonlinearity at a given input value diff --git a/control/namedio.py b/control/namedio.py index 8e541808b..ee08d00f2 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -51,22 +51,16 @@ def __init__( nstates = None def __repr__(self): - return str(type(self)) + ": " + self.name if self.name is not None \ - else str(type(self)) + return f'<{self.__class__.__name__}:{self.name}:' + \ + f'{list(self.input_labels)}->{list(self.output_labels)}>' def __str__(self): """String representation of an input/output object""" - str = "Object: " + (self.name if self.name else "(None)") + "\n" - str += "Inputs (%s): " % self.ninputs - for key in self.input_index: - str += key + ", " - str += "\nOutputs (%s): " % self.noutputs - for key in self.output_index: - str += key + ", " + str = f"<{self.__class__.__name__}>: {self.name}\n" + str += f"Inputs ({self.ninputs}): {self.input_labels}\n" + str += f"Outputs ({self.noutputs}): {self.output_labels}\n" if self.nstates is not None: - str += "\nStates (%s): " % self.nstates - for key in self.state_index: - str += key + ", " + str += f"States ({self.nstates}): {self.state_labels}" return str # Find a signal by name diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 9278136b5..2966ab4e8 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -31,6 +31,8 @@ def test_named_ss(): assert sys.input_labels == ['u[0]', 'u[1]'] assert sys.output_labels == ['y[0]', 'y[1]'] assert sys.state_labels == ['x[0]', 'x[1]'] + assert repr(sys) == \ + "['y[0]', 'y[1]']>" # Pass the names as arguments sys = ct.ss( @@ -41,6 +43,8 @@ def test_named_ss(): assert sys.input_labels == ['u1', 'u2'] assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2'] + assert repr(sys) == \ + "['y1', 'y2']>" # Do the same with rss sys = ct.rss(['x1', 'x2', 'x3'], ['y1', 'y2'], 'u1', name='random') @@ -49,3 +53,5 @@ def test_named_ss(): assert sys.input_labels == ['u1'] assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2', 'x3'] + assert repr(sys) == \ + "['y1', 'y2']>" From e2f76df5e6ae2f562a8040ad33f1ea40838994d3 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 2 Apr 2022 17:05:26 -0700 Subject: [PATCH 63/87] refactor system classes (frdata, iosys, namedio, statesp, xferfcn) * LTI is NamedIOSystem instead of StateSpace, TransferFunction, and FRD * implement _process_namedio_keywords and use for system/signal name, dt * move timebase functions from lti to namedio * move statesp/_ss code to iosys/ss and iosys/copy to namedio/copy * clean up duplicate object/sysname warnings * updated unit tests --- control/__init__.py | 1 + control/canonical.py | 2 +- control/dtime.py | 2 +- control/frdata.py | 38 +- control/iosys.py | 536 ++++++++++++++------------ control/lti.py | 248 +----------- control/margins.py | 3 +- control/matlab/__init__.py | 1 + control/modelsimp.py | 2 +- control/namedio.py | 403 ++++++++++++++++++- control/pzmap.py | 3 +- control/rlocus.py | 2 +- control/sisotool.py | 4 +- control/statefbk.py | 3 +- control/statesp.py | 235 +++++------ control/stochsys.py | 3 +- control/tests/config_test.py | 8 +- control/tests/frd_test.py | 6 +- control/tests/iosys_test.py | 107 ++++- control/tests/kwargs_test.py | 10 +- control/tests/lti_test.py | 10 +- control/tests/namedio_test.py | 204 +++++++++- control/tests/statesp_test.py | 16 +- control/tests/type_conversion_test.py | 54 +-- control/tests/xferfcn_test.py | 7 +- control/timeresp.py | 2 +- control/xferfcn.py | 171 +++++--- doc/conventions.rst | 56 +-- doc/iosys.rst | 47 ++- doc/optimal.rst | 5 +- examples/pvtol-lqr.py | 127 +++--- examples/pvtol-nested.py | 57 ++- examples/steering-gainsched.py | 20 +- 33 files changed, 1465 insertions(+), 928 deletions(-) diff --git a/control/__init__.py b/control/__init__.py index 386fa91c1..ad2685273 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -55,6 +55,7 @@ from .margins import * from .mateqn import * from .modelsimp import * +from .namedio import * from .nichols import * from .phaseplot import * from .pzmap import * diff --git a/control/canonical.py b/control/canonical.py index 7b2b58ef7..e714e5b8d 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -2,7 +2,7 @@ # RMM, 10 Nov 2012 from .exception import ControlNotImplemented, ControlSlycot -from .lti import issiso +from .namedio import issiso from .statesp import StateSpace, _convert_to_statespace from .statefbk import ctrb, obsv diff --git a/control/dtime.py b/control/dtime.py index c60778d00..b05d22b96 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -47,7 +47,7 @@ """ -from .lti import isctime +from .namedio import isctime from .statesp import StateSpace __all__ = ['sample_system', 'c2d'] diff --git a/control/frdata.py b/control/frdata.py index 4d149a46b..13813d775 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -54,13 +54,13 @@ from .lti import LTI, _process_frequency_response from .exception import pandas_check -from .namedio import _NamedIOSystem +from .namedio import NamedIOSystem, _process_namedio_keywords from . import config __all__ = ['FrequencyResponseData', 'FRD', 'frd'] -class FrequencyResponseData(LTI, _NamedIOSystem): +class FrequencyResponseData(LTI): """FrequencyResponseData(d, w[, smooth]) A class for models defined by frequency response data (FRD). @@ -117,7 +117,7 @@ class FrequencyResponseData(LTI, _NamedIOSystem): # Allow NDarray * StateSpace to give StateSpace._rmul_() priority # https://docs.scipy.org/doc/numpy/reference/arrays.classes.html - __array_priority__ = 11 # override ndarray and matrix types + __array_priority__ = 13 # override ndarray, StateSpace, I/O sys # # Class attributes @@ -157,6 +157,9 @@ def __init__(self, *args, **kwargs): # TODO: discrete-time FRD systems? smooth = kwargs.pop('smooth', False) + # + # Process positional arguments + # if len(args) == 2: if not isinstance(args[0], FRD) and isinstance(args[0], LTI): # not an FRD, but still a system, second argument should be @@ -196,28 +199,28 @@ def __init__(self, *args, **kwargs): raise ValueError( "Needs 1 or 2 arguments; received %i." % len(args)) - # Set the size of the system - self.noutputs = self.fresp.shape[0] - self.ninputs = self.fresp.shape[1] - - # Process signal names - _NamedIOSystem.__init__( - self, name=kwargs.pop('name', None), - inputs=kwargs.pop('inputs', self.ninputs), - outputs=kwargs.pop('outputs', self.noutputs)) - + # + # Process key word arguments + # # Keep track of return type self.return_magphase=kwargs.pop('return_magphase', False) if self.return_magphase not in (True, False): raise ValueError("unknown return_magphase value") + # Determine whether to squeeze the output self.squeeze=kwargs.pop('squeeze', None) if self.squeeze not in (None, True, False): raise ValueError("unknown squeeze value") - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) + # Process namedio keywords + defaults = { + 'inputs': self.fresp.shape[1], 'outputs': self.fresp.shape[0]} + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, defaults, end=True) + + # Process signal names + NamedIOSystem.__init__( + self, name=name, inputs=inputs, outputs=outputs, dt=dt) # create interpolation functions if smooth: @@ -231,7 +234,6 @@ def __init__(self, *args, **kwargs): w=1.0/(absolute(self.fresp[i, j, :]) + 0.001), s=0.0) else: self.ifunc = None - super().__init__(self.fresp.shape[1], self.fresp.shape[0]) # # Frequency response properties @@ -666,8 +668,6 @@ def to_pandas(self): # FrequenceResponseData and then assigning FRD to point to the same object # fixes this problem. # - - FRD = FrequencyResponseData diff --git a/control/iosys.py b/control/iosys.py index ab7c43a62..e3719614b 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -32,13 +32,13 @@ from warnings import warn from .lti import LTI -from .namedio import _NamedIOSystem, _process_signal_list +from .namedio import NamedIOSystem, _process_signal_list, \ + _process_namedio_keywords, isctime, isdtime, common_timebase from .statesp import StateSpace, tf2ss, _convert_to_statespace -from .statesp import _ss, _rss_generate +from .statesp import _rss_generate from .xferfcn import TransferFunction from .timeresp import _check_convert_array, _process_time_response, \ TimeResponseData -from .lti import isctime, isdtime, common_timebase from . import config __all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem', @@ -56,7 +56,7 @@ } -class InputOutputSystem(_NamedIOSystem): +class InputOutputSystem(NamedIOSystem): """A class for representing input/output systems. The InputOutputSystem class allows (possibly nonlinear) input/output @@ -69,7 +69,7 @@ class for a set of subclasses that are used to implement specific ---------- inputs : int, list of str, or None Description of the system inputs. This can be given as an integer - count or as a list of strings that name the individual signals. If an + count or a list of strings that name the individual signals. If an integer count is specified, the names of the signal will be of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If this parameter is not given or given as `None`, the relevant quantity will be @@ -80,17 +80,16 @@ class for a set of subclasses that are used to implement specific states : int, list of str, or None Description of the system states. Same format as `inputs`. dt : None, True or float, optional - System timebase. 0 (default) indicates continuous - time, True indicates discrete time with unspecified sampling - time, positive number is discrete time with specified - sampling time, None indicates unspecified timebase (either - continuous or discrete time). - params : dict, optional - Parameter values for the systems. Passed to the evaluation functions - for the system as default values, overriding internal defaults. + System timebase. 0 (default) indicates continuous time, True + indicates discrete time with unspecified sampling time, positive + number is discrete time with specified sampling time, None indicates + unspecified timebase (either continuous or discrete time). name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. + params : dict, optional + Parameter values for the systems. Passed to the evaluation functions + for the system as default values, overriding internal defaults. Attributes ---------- @@ -127,8 +126,7 @@ class for a set of subclasses that are used to implement specific # Allow ndarray * InputOutputSystem to give IOSystem._rmul_() priority __array_priority__ = 12 # override ndarray, matrix, SS types - def __init__(self, inputs=None, outputs=None, states=None, params={}, - name=None, **kwargs): + def __init__(self, params={}, **kwargs): """Create an input/output system. The InputOutputSystem constructor is used to create an input/output @@ -140,19 +138,18 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, """ # Store the system name, inputs, outputs, and states - _NamedIOSystem.__init__( - self, inputs=inputs, outputs=outputs, states=states, name=name) + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, end=True) + + # Initialize the data structure + # Note: don't use super() to override LinearIOSystem/StateSpace MRO + NamedIOSystem.__init__( + self, inputs=inputs, outputs=outputs, + states=states, name=name, dt=dt) # default parameters self.params = params.copy() - # timebase - self.dt = kwargs.pop('dt', config.defaults['control.default_dt']) - - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - def __mul__(sys2, sys1): """Multiply two input/output systems (series interconnection)""" # Note: order of arguments is flipped so that self = sys2, @@ -166,7 +163,8 @@ def __mul__(sys2, sys1): elif isinstance(sys1, np.ndarray): sys1 = LinearIOSystem(StateSpace([], [], [], sys1)) - elif isinstance(sys1, (StateSpace, TransferFunction)): + elif isinstance(sys1, (StateSpace, TransferFunction)) and \ + not isinstance(sys1, LinearIOSystem): sys1 = LinearIOSystem(sys1) elif not isinstance(sys1, InputOutputSystem): @@ -212,7 +210,8 @@ def __rmul__(sys1, sys2): elif isinstance(sys2, np.ndarray): sys2 = LinearIOSystem(StateSpace([], [], [], sys2)) - elif isinstance(sys2, (StateSpace, TransferFunction)): + elif isinstance(sys2, (StateSpace, TransferFunction)) and \ + not isinstance(sys2, LinearIOSystem): sys2 = LinearIOSystem(sys2) elif not isinstance(sys2, InputOutputSystem): @@ -230,7 +229,8 @@ def __add__(sys1, sys2): elif isinstance(sys2, np.ndarray): sys2 = LinearIOSystem(StateSpace([], [], [], sys2)) - elif isinstance(sys2, (StateSpace, TransferFunction)): + elif isinstance(sys2, (StateSpace, TransferFunction)) and \ + not isinstance(sys2, LinearIOSystem): sys2 = LinearIOSystem(sys2) elif not isinstance(sys2, InputOutputSystem): @@ -267,7 +267,8 @@ def __radd__(sys1, sys2): elif isinstance(sys2, np.ndarray): sys2 = LinearIOSystem(StateSpace([], [], [], sys2)) - elif isinstance(sys2, (StateSpace, TransferFunction)): + elif isinstance(sys2, (StateSpace, TransferFunction)) and \ + not isinstance(sys2, LinearIOSystem): sys2 = LinearIOSystem(sys2) elif not isinstance(sys2, InputOutputSystem): @@ -285,7 +286,8 @@ def __sub__(sys1, sys2): elif isinstance(sys2, np.ndarray): sys2 = LinearIOSystem(StateSpace([], [], [], sys2)) - elif isinstance(sys2, (StateSpace, TransferFunction)): + elif isinstance(sys2, (StateSpace, TransferFunction)) and \ + not isinstance(sys2, LinearIOSystem): sys2 = LinearIOSystem(sys2) elif not isinstance(sys2, InputOutputSystem): @@ -322,7 +324,8 @@ def __rsub__(sys1, sys2): elif isinstance(sys2, np.ndarray): sys2 = LinearIOSystem(StateSpace([], [], [], sys2)) - elif isinstance(sys2, (StateSpace, TransferFunction)): + elif isinstance(sys2, (StateSpace, TransferFunction)) and \ + not isinstance(sys2, LinearIOSystem): sys2 = LinearIOSystem(sys2) elif not isinstance(sys2, InputOutputSystem): @@ -580,15 +583,6 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, return linsys - def copy(self, newname=None): - """Make a copy of an input/output system.""" - dup_prefix = config.defaults['iosys.duplicate_system_name_prefix'] - dup_suffix = config.defaults['iosys.duplicate_system_name_suffix'] - newsys = copy.copy(self) - newsys.name = self._name_or_default( - dup_prefix + self.name + dup_suffix if not newname else newname) - return newsys - class LinearIOSystem(InputOutputSystem, StateSpace): """Input/output representation of a linear (state space) system. @@ -617,12 +611,12 @@ class LinearIOSystem(InputOutputSystem, StateSpace): discrete time with unspecified sampling time, positive number is discrete time with specified sampling time, None indicates unspecified timebase (either continuous or discrete time). - params : dict, optional - Parameter values for the systems. Passed to the evaluation functions - for the system as default values, overriding internal defaults. name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. + params : dict, optional + Parameter values for the systems. Passed to the evaluation functions + for the system as default values, overriding internal defaults. Attributes ---------- @@ -633,8 +627,7 @@ class LinearIOSystem(InputOutputSystem, StateSpace): See :class:`~control.StateSpace` for inherited attributes. """ - def __init__(self, linsys, inputs=None, outputs=None, states=None, - name=None, **kwargs): + def __init__(self, linsys, **kwargs): """Create an I/O system from a state space linear system. Converts a :class:`~control.StateSpace` system into an @@ -650,33 +643,19 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, raise TypeError("Linear I/O system must be a state space " "or transfer function object") - # Look for 'input' and 'output' parameter name variants - states = _parse_signal_parameter(states, 'state', kwargs) - inputs = _parse_signal_parameter(inputs, 'input', kwargs) - outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) + # Process keyword arguments + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, linsys, end=True) # Create the I/O system object - super(LinearIOSystem, self).__init__( - inputs=linsys.ninputs, outputs=linsys.noutputs, - states=linsys.nstates, params={}, dt=linsys.dt, name=name) + # Note: don't use super() to override StateSpace MRO + InputOutputSystem.__init__( + self, inputs=inputs, outputs=outputs, states=states, + params={}, dt=dt, name=name) # Initalize additional state space variables - StateSpace.__init__(self, linsys, remove_useless_states=False) - - # Process input, output, state lists, if given - # Make sure they match the size of the linear system - ninputs, self.input_index = _process_signal_list( - inputs if inputs is not None else linsys.ninputs, prefix='u') - if ninputs is not None and linsys.ninputs != ninputs: - raise ValueError("Wrong number/type of inputs given.") - noutputs, self.output_index = _process_signal_list( - outputs if outputs is not None else linsys.noutputs, prefix='y') - if noutputs is not None and linsys.noutputs != noutputs: - raise ValueError("Wrong number/type of outputs given.") - nstates, self.state_index = _process_signal_list( - states if states is not None else linsys.nstates, prefix='x') - if nstates is not None and linsys.nstates != nstates: - raise ValueError("Wrong number/type of states given.") + StateSpace.__init__( + self, linsys, remove_useless_states=False, init_namedio=False) # The following text needs to be replicated from StateSpace in order for # this entry to show up properly in sphinx doccumentation (not sure why, @@ -757,11 +736,6 @@ class NonlinearIOSystem(InputOutputSystem): states : int, list of str, or None, optional Description of the system states. Same format as `inputs`. - params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. - dt : timebase, optional The timebase for the system, used to specify whether the system is operating in continuous or discrete time. It can have the @@ -776,28 +750,27 @@ class NonlinearIOSystem(InputOutputSystem): System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + """ - def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, - states=None, params={}, name=None, **kwargs): + def __init__(self, updfcn, outfcn=None, params={}, **kwargs): """Create a nonlinear I/O system given update and output functions.""" - # Look for 'input' and 'output' parameter name variants - inputs = _parse_signal_parameter(inputs, 'input', kwargs) - outputs = _parse_signal_parameter(outputs, 'output', kwargs) - - # Store the update and output functions - self.updfcn = updfcn - self.outfcn = outfcn + # Process keyword arguments + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, end=True) # Initialize the rest of the structure - dt = kwargs.pop('dt', config.defaults['control.default_dt']) super().__init__( inputs=inputs, outputs=outputs, states=states, params=params, dt=dt, name=name ) - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) + # Store the update and output functions + self.updfcn = updfcn + self.outfcn = outfcn # Check to make sure arguments are consistent if updfcn is None: @@ -890,31 +863,33 @@ class InterconnectedSystem(InputOutputSystem): """ def __init__(self, syslist, connections=[], inplist=[], outlist=[], - inputs=None, outputs=None, states=None, - params={}, dt=None, name=None, **kwargs): + params={}, warn_duplicate=None, **kwargs): """Create an I/O system from a list of systems + connection info.""" - - # Look for 'input' and 'output' parameter name variants - inputs = _parse_signal_parameter(inputs, 'input', kwargs) - outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) - # Convert input and output names to lists if they aren't already if not isinstance(inplist, (list, tuple)): inplist = [inplist] if not isinstance(outlist, (list, tuple)): outlist = [outlist] - # Check to make sure all systems are consistent + # Process keyword arguments + defaults = {'inputs': len(inplist), 'outputs': len(outlist)} + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, defaults, end=True) + + # Initialize the system list and index self.syslist = syslist self.syslist_index = {} - nstates = 0 - self.state_offset = [] - ninputs = 0 - self.input_offset = [] - noutputs = 0 - self.output_offset = [] + + # Initialize the input, output, and state counts, indices + nstates, self.state_offset = 0, [] + ninputs, self.input_offset = 0, [] + noutputs, self.output_offset = 0, [] + + # Keep track of system objects and names we have already seen sysobj_name_dct = {} sysname_count_dct = {} + + # Go through the system list and keep track of counts, offsets for sysidx, sys in enumerate(syslist): # Make sure time bases are consistent dt = common_timebase(dt, sys.dt) @@ -939,17 +914,32 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[], # Check for duplicate systems or duplicate names # Duplicates are renamed sysname_1, sysname_2, etc. if sys in sysobj_name_dct: - sys = sys.copy() - warn("Duplicate object found in system list: %s. " - "Making a copy" % str(sys.name)) + # Make a copy of the object using a new name + if warn_duplicate is None and sys._generic_name_check(): + # Make a copy w/out warning, using generic format + sys = sys.copy(use_prefix_suffix=False) + warn_flag = False + else: + sys = sys.copy() + warn_flag = warn_duplicate + + # Warn the user about the new object + if warn_flag is not False: + warn("duplicate object found in system list; " + "created copy: %s" % str(sys.name), stacklevel=2) + + # Check to see if the system name shows up more than once if sys.name is not None and sys.name in sysname_count_dct: count = sysname_count_dct[sys.name] sysname_count_dct[sys.name] += 1 sysname = sys.name + "_" + str(count) sysobj_name_dct[sys] = sysname self.syslist_index[sysname] = sysidx - warn("Duplicate name found in system list. " - "Renamed to {}".format(sysname)) + + if warn_duplicate is not False: + warn("duplicate name found in system list; " + "renamed to {}".format(sysname), stacklevel=2) + else: sysname_count_dct[sys.name] = 1 sysobj_name_dct[sys] = sys.name @@ -962,23 +952,18 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[], states += [sysname + state_name_delim + statename for statename in sys.state_index.keys()] + # Make sure we the state list is the right length (internal check) + if isinstance(states, list) and len(states) != nstates: + raise RuntimeError( + f"construction of state labels failed; found: " + f"{len(states)} labels; expecting {nstates}") + # Create the I/O system - super(InterconnectedSystem, self).__init__( - inputs=len(inplist), outputs=len(outlist), + # Note: don't use super() to override LinearICSystem/StateSpace MRO + InputOutputSystem.__init__( + self, inputs=inputs, outputs=outputs, states=states, params=params, dt=dt, name=name) - # If input or output list was specified, update it - if inputs is not None: - nsignals, self.input_index = \ - _process_signal_list(inputs, prefix='u') - if nsignals is not None and len(inplist) != nsignals: - raise ValueError("Wrong number/type of inputs given.") - if outputs is not None: - nsignals, self.output_index = \ - _process_signal_list(outputs, prefix='y') - if nsignals is not None and len(outlist) != nsignals: - raise ValueError("Wrong number/type of outputs given.") - # Convert the list of interconnections to a connection map (matrix) self.connect_map = np.zeros((ninputs, noutputs)) for connection in connections: @@ -1515,8 +1500,8 @@ class LinearICSystem(InterconnectedSystem, LinearIOSystem): :class:`StateSpace` class structure, allowing it to be passed to functions that expect a :class:`StateSpace` system. - This class is usually generated using :func:`~control.interconnect` and - not called directly + This class is generated using :func:`~control.interconnect` and + not called directly. """ @@ -1524,18 +1509,15 @@ def __init__(self, io_sys, ss_sys=None): if not isinstance(io_sys, InterconnectedSystem): raise TypeError("First argument must be an interconnected system.") - # Create the I/O system object + # Create the (essentially empty) I/O system object InputOutputSystem.__init__( self, name=io_sys.name, params=io_sys.params) - # Copy over the I/O systems attributes + # Copy over the named I/O system attributes self.syslist = io_sys.syslist - self.ninputs = io_sys.ninputs - self.noutputs = io_sys.noutputs - self.nstates = io_sys.nstates - self.input_index = io_sys.input_index - self.output_index = io_sys.output_index - self.state_index = io_sys.state_index + self.ninputs, self.input_index = io_sys.ninputs, io_sys.input_index + self.noutputs, self.output_index = io_sys.noutputs, io_sys.output_index + self.nstates, self.state_index = io_sys.nstates, io_sys.state_index self.dt = io_sys.dt # Copy over the attributes from the interconnected system @@ -1555,13 +1537,14 @@ def __init__(self, io_sys, ss_sys=None): # Initialize the state space attributes if isinstance(ss_sys, StateSpace): - # Make sure the dimension match + # Make sure the dimensions match if io_sys.ninputs != ss_sys.ninputs or \ io_sys.noutputs != ss_sys.noutputs or \ io_sys.nstates != ss_sys.nstates: raise ValueError("System dimensions for first and second " "arguments must match.") - StateSpace.__init__(self, ss_sys, remove_useless_states=False) + StateSpace.__init__( + self, ss_sys, remove_useless_states=False, init_namedio=False) else: raise TypeError("Second argument must be a state space system.") @@ -2219,24 +2202,23 @@ def linearize(sys, xeq, ueq=[], t=0, params={}, **kw): The linearization of the system, as a :class:`~control.LinearIOSystem` object (which is also a :class:`~control.StateSpace` object. + Additional Parameters + --------------------- + inputs : int, list of str or None, optional + Description of the system inputs. If not specified, the origional + system inputs are used. See :class:`InputOutputSystem` for more + information. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. + """ if not isinstance(sys, InputOutputSystem): raise TypeError("Can only linearize InputOutputSystem types") return sys.linearize(xeq, ueq, t=t, params=params, **kw) -# Utility function to parse a signal parameter -def _parse_signal_parameter(value, name, kwargs, end=False): - # Check kwargs for a variant of the parameter name - if value is None and name in kwargs: - value = kwargs.pop(name) - - if end and kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - - return value - - def _find_size(sysval, vecval): """Utility function to find the size of a system parameter @@ -2314,7 +2296,8 @@ def ss(*args, **kwargs): inputs, outputs, states : str, or list of str, optional List of strings that name the individual signals. If this parameter is not given or given as `None`, the signal names will be of the - form `s[i]` (where `s` is one of `u`, `y`, or `x`). + form `s[i]` (where `s` is one of `u`, `y`, or `x`). See + :class:`InputOutputSystem` for more information. name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. @@ -2346,37 +2329,60 @@ def ss(*args, **kwargs): """ # See if this is a nonlinear I/O system - if len(args) > 0 and hasattr(args[0], '__call__') and \ - not isinstance (args[0], (InputOutputSystem, LTI)): - # Function as first argument => assume nonlinear IO system + if len(args) > 0 and (hasattr(args[0], '__call__') or args[0] is None) \ + and not isinstance(args[0], (InputOutputSystem, LTI)): + # Function as first (or second) argument => assume nonlinear IO system return NonlinearIOSystem(*args, **kwargs) - # Extract the keyword arguments needed for StateSpace (via _ss) - ss_kwlist = ('dt', 'remove_useless_states') - ss_kwargs = {} - for kw in ss_kwlist: - if kw in kwargs: - ss_kwargs[kw] = kwargs.pop(kw) + elif len(args) == 4 or len(args) == 5: + # Create a state space function from A, B, C, D[, dt] + sys = LinearIOSystem(StateSpace(*args, **kwargs)) - # Create the statespace system and then convert to I/O system - sys = _ss(*args, **ss_kwargs) - return LinearIOSystem(sys, **kwargs) + elif len(args) == 1: + sys = args[0] + if isinstance(sys, LTI): + # Check for system with no states and specified state names + if sys.nstates is None and 'states' in kwargs: + warn("state labels specified for " + "non-unique state space realization") + + # Create a state space system from an LTI system + sys = LinearIOSystem(_convert_to_statespace(sys), **kwargs) + else: + raise TypeError("ss(sys): sys must be a StateSpace or " + "TransferFunction object. It is %s." % type(sys)) + else: + raise TypeError( + "Needs 1, 4, or 5 arguments; received %i." % len(args)) + + return sys def rss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs): - """ - Create a stable *continuous* random state space object. + """Create a stable random state space object. Parameters ---------- - states : int - Number of state variables - outputs : int - Number of system outputs - inputs : int - Number of system inputs + inputs : int, list of str, or None + Description of the system inputs. This can be given as an integer + count or as a list of strings that name the individual signals. If an + integer count is specified, the names of the signal will be of the + form `s[i]` (where `s` is one of `u`, `y`, or `x`). + outputs : int, list of str, or None + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None + Description of the system states. Same format as `inputs`. strictly_proper : bool, optional If set to 'True', returns a proper system (no direct term). + dt : None, True or float, optional + System timebase. 0 (default) indicates continuous + time, True indicates discrete time with unspecified sampling + time, positive number is discrete time with specified + sampling time, None indicates unspecified timebase (either + continuous or discrete time). + name : string, optional + System name (used for specifying signals). If unspecified, a generic + name is generated with a unique integer id. Returns ------- @@ -2388,85 +2394,136 @@ def rss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs): ValueError if any input is not a positive integer - See Also - -------- - drss - Notes ----- If the number of states, inputs, or outputs is not specified, then the - missing numbers are assumed to be 1. The poles of the returned system - will always have a negative real part. + missing numbers are assumed to be 1. If dt is not specified or is given + as 0 or None, the poles of the returned system will always have a + negative real part. If dt is True or a postive float, the poles of the + returned system will have magnitude less than 1. """ - # Process states, inputs, outputs (ignoring names) + # Process keyword arguments + kwargs.update({'states': states, 'outputs': outputs, 'inputs': inputs}) + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, end=True) + + # Figure out the size of the sytem nstates, _ = _process_signal_list(states) ninputs, _ = _process_signal_list(inputs) noutputs, _ = _process_signal_list(outputs) sys = _rss_generate( - nstates, ninputs, noutputs, 'c', strictly_proper=strictly_proper) + nstates, ninputs, noutputs, 'c' if not dt else 'd', name=name, + strictly_proper=strictly_proper) + return LinearIOSystem( - sys, states=states, inputs=inputs, outputs=outputs, **kwargs) + sys, name=name, states=states, inputs=inputs, outputs=outputs, dt=dt) -def drss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs): +def drss(*args, **kwargs): + """Create a stable, discrete-time, random state space system + + Create a stable *discrete time* random state space object. This + function calls :func:`rss` using either the `dt` keyword provided by + the user or `dt=True` if not specified. + """ - Create a stable *discrete* random state space object. + # Make sure the timebase makes sense + if 'dt' in kwargs: + dt = kwargs['dt'] + + if dt == 0: + raise ValueError("drss called with continuous timebase") + elif dt is None: + warn("drss called with unspecified timebase; " + "system may be interpreted as continuous time") + kwargs['dt'] = True # force rss to generate discrete time sys + else: + dt = True + kwargs['dt'] = True + + # Create the system + sys = rss(*args, **kwargs) + + # Reset the timebase (in case it was specified as None) + sys.dt = dt + + return sys + + +# Convert a state space system into an input/output system (wrapper) +def ss2io(*args, **kwargs): + return LinearIOSystem(*args, **kwargs) +ss2io.__doc__ = LinearIOSystem.__init__.__doc__ + + +# Convert a transfer function into an input/output system (wrapper) +def tf2io(*args, **kwargs): + """tf2io(sys) + + Convert a transfer function into an I/O system + + The function accepts either 1 or 2 parameters: + + ``tf2io(sys)`` + Convert a linear system into space space form. Always creates + a new system, even if sys is already a StateSpace object. + + ``tf2io(num, den)`` + Create a linear I/O system from its numerator and denominator + polynomial coefficients. + + For details see: :func:`tf` Parameters ---------- - states : int - Number of state variables - inputs : integer - Number of system inputs - outputs : int - Number of system outputs - strictly_proper: bool, optional - If set to 'True', returns a proper system (no direct term). + sys : LTI (StateSpace or TransferFunction) + A linear system. + num : array_like, or list of list of array_like + Polynomial coefficients of the numerator. + den : array_like, or list of list of array_like + Polynomial coefficients of the denominator. Returns ------- - sys : StateSpace - The randomly created linear system + out : LinearIOSystem + New I/O system (in state space form). + + Other Parameters + ---------------- + inputs, outputs : str, or list of str, optional + List of strings that name the individual signals of the transformed + system. If not given, the inputs and outputs are the same as the + original system. + name : string, optional + System name. If unspecified, a generic name is generated + with a unique integer id. Raises ------ ValueError - if any input is not a positive integer + if `num` and `den` have invalid or unequal dimensions, or if an + invalid number of arguments is passed in. + TypeError + if `num` or `den` are of incorrect type, or if sys is not a + TransferFunction object. See Also -------- - rss - - Notes - ----- - If the number of states, inputs, or outputs is not specified, then the - missing numbers are assumed to be 1. The poles of the returned system - will always have a magnitude less than 1. - - """ - # Process states, inputs, outputs (ignoring names) - nstates, _ = _process_signal_list(states) - ninputs, _ = _process_signal_list(inputs) - noutputs, _ = _process_signal_list(outputs) - - sys = _rss_generate( - nstates, ninputs, noutputs, 'd', strictly_proper=strictly_proper) - return LinearIOSystem( - sys, states=states, inputs=inputs, outputs=outputs, **kwargs) - + ss2io + tf2ss -# Convert a state space system into an input/output system (wrapper) -def ss2io(*args, **kwargs): - return LinearIOSystem(*args, **kwargs) -ss2io.__doc__ = LinearIOSystem.__init__.__doc__ + Examples + -------- + >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]] + >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]] + >>> sys1 = tf2ss(num, den) + >>> sys_tf = tf(num, den) + >>> sys2 = tf2ss(sys_tf) -# Convert a transfer function into an input/output system (wrapper) -def tf2io(*args, **kwargs): - """Convert a transfer function into an I/O system""" - # TODO: add remaining documentation + """ # Convert the system to a state space system linsys = tf2ss(*args) @@ -2475,11 +2532,9 @@ def tf2io(*args, **kwargs): # Function to create an interconnected system -def interconnect(syslist, connections=None, inplist=[], outlist=[], - inputs=None, outputs=None, states=None, - params={}, dt=None, name=None, +def interconnect(syslist, connections=None, inplist=[], outlist=[], params={}, check_unused=True, ignore_inputs=None, ignore_outputs=None, - **kwargs): + warn_duplicate=None, **kwargs): """Interconnect a set of input/output systems. This function creates a new system that is an interconnection of a set of @@ -2629,6 +2684,12 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], outputs from all sub-systems with that base name are considered ignored. + warn_duplicate : None, True, or False + Control how warnings are generated if duplicate objects or names are + detected. In `None` (default), then warnings are generated for + systems that have non-generic names. If `False`, warnings are not + generated and if `True` then warnings are always generated. + Example ------- @@ -2651,7 +2712,7 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], >>> P = control.tf2io(control.tf(1, [1, 0]), inputs='u', outputs='y') >>> C = control.tf2io(control.tf(10, [1, 1]), inputs='e', outputs='u') >>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e') - >>> T = control.interconnect([P, C, sumblk], input='r', output='y') + >>> T = control.interconnect([P, C, sumblk], inputs='r', outputs='y') Notes ----- @@ -2680,9 +2741,9 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], `outputs`, for more natural naming of SISO systems. """ - # Look for 'input' and 'output' parameter name variants - inputs = _parse_signal_parameter(inputs, 'input', kwargs) - outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) + dt = kwargs.pop('dt', None) # by pass normal 'dt' processing + name, inputs, outputs, states, _ = _process_namedio_keywords( + kwargs, end=True) if not check_unused and (ignore_inputs or ignore_outputs): raise ValueError('check_unused is False, but either ' @@ -2699,10 +2760,10 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], # For each system input, look for outputs with the same name connections = [] for input_sys in syslist: - for input_name in input_sys.input_index.keys(): + for input_name in input_sys.input_labels: connect = [input_sys.name + "." + input_name] for output_sys in syslist: - if input_name in output_sys.output_index.keys(): + if input_name in output_sys.output_labels: connect.append(output_sys.name + "." + input_name) if len(connect) > 1: connections.append(connect) @@ -2714,11 +2775,6 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], # Use an empty connections list connections = [] - if isinstance(inputs, str): - inputs = [inputs] - if isinstance(outputs, str): - outputs = [outputs] - # If inplist/outlist is not present, try using inputs/outputs instead if not inplist and inputs is not None: inplist = list(inputs) @@ -2784,11 +2840,12 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], newsys = InterconnectedSystem( syslist, connections=connections, inplist=inplist, outlist=outlist, inputs=inputs, outputs=outputs, states=states, - params=params, dt=dt, name=name) + params=params, dt=dt, name=name, warn_duplicate=warn_duplicate) # check for implicity dropped signals if check_unused: newsys.check_unused_signals(ignore_inputs, ignore_outputs) + # If all subsystems are linear systems, maintain linear structure if all([isinstance(sys, LinearIOSystem) for sys in syslist]): return LinearICSystem(newsys, None) @@ -2798,8 +2855,7 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], # Summing junction def summing_junction( - inputs=None, output=None, dimension=None, name=None, - prefix='u', **kwargs): + inputs=None, output=None, dimension=None, prefix='u', **kwargs): """Create a summing junction as an input/output system. This function creates a static input/output system that outputs the sum of @@ -2838,10 +2894,10 @@ def summing_junction( Example ------- - >>> P = control.tf2io(ct.tf(1, [1, 0]), input='u', output='y') - >>> C = control.tf2io(ct.tf(10, [1, 1]), input='e', output='u') + >>> P = control.tf2io(ct.tf(1, [1, 0]), inputs='u', outputs='y') + >>> C = control.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='u') >>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e') - >>> T = control.interconnect((P, C, sumblk), input='r', output='y') + >>> T = control.interconnect((P, C, sumblk), inputs='r', outputs='y') """ # Utility function to parse input and output signal lists @@ -2874,15 +2930,15 @@ def _parse_list(signals, signame='input', prefix='u'): # Return the parsed list return nsignals, names, gains - # Look for 'input' and 'output' parameter name variants - inputs = _parse_signal_parameter(inputs, 'input', kwargs) - output = _parse_signal_parameter(output, 'outputs', kwargs, end=True) - - # Default values for inputs and output + # Parse system and signal names (with some minor pre-processing) + if input is not None: + kwargs['inputs'] = inputs # positional/keyword -> keyword + if output is not None: + kwargs['output'] = output # positional/keyword -> keyword + name, inputs, output, states, dt = _process_namedio_keywords( + kwargs, {'inputs': None, 'outputs': 'y'}, end=True) if inputs is None: raise TypeError("input specification is required") - if output is None: - output = 'y' # Read the input list ninputs, input_names, input_gains = _parse_list( diff --git a/control/lti.py b/control/lti.py index 45f7b3c54..9d60f0526 100644 --- a/control/lti.py +++ b/control/lti.py @@ -16,12 +16,12 @@ from numpy import absolute, real, angle, abs from warnings import warn from . import config +from .namedio import NamedIOSystem, isdtime -__all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', - 'isdtime', 'isctime', 'poles', 'zeros', 'damp', 'evalfr', - 'frequency_response', 'freqresp', 'dcgain', 'pole', 'zero'] +__all__ = ['poles', 'zeros', 'damp', 'evalfr', 'frequency_response', + 'freqresp', 'dcgain', 'pole', 'zero'] -class LTI: +class LTI(NamedIOSystem): """LTI is a parent class to linear time-invariant (LTI) system objects. LTI is the parent to the StateSpace and TransferFunction child classes. It @@ -41,15 +41,13 @@ class LTI: with timebase None can be combined with a system having a specified timebase, and the result will have the timebase of the latter system. - """ + Note: dt processing has been moved to the NamedIOSystem class. - def __init__(self, inputs=1, outputs=1, dt=None): + """ + def __init__(self, inputs=1, outputs=1, states=None, name=None, **kwargs): """Assign the LTI object's numbers of inputs and ouputs.""" - - # Data members common to StateSpace and TransferFunction. - self.ninputs = inputs - self.noutputs = outputs - self.dt = dt + super().__init__( + name=name, inputs=inputs, outputs=outputs, states=states, **kwargs) # # Getter and setter functions for legacy state attributes @@ -105,45 +103,6 @@ def _set_outputs(self, value): outputs for an LTI system, use :attr:`noutputs`. """) - def isdtime(self, strict=False): - """ - Check to see if a system is a discrete-time system - - Parameters - ---------- - strict: bool, optional - If strict is True, make sure that timebase is not None. Default - is False. - """ - - # If no timebase is given, answer depends on strict flag - if self.dt == None: - return True if not strict else False - - # Look for dt > 0 (also works if dt = True) - return self.dt > 0 - - def isctime(self, strict=False): - """ - Check to see if a system is a continuous-time system - - Parameters - ---------- - sys : LTI system - System to be checked - strict: bool, optional - If strict is True, make sure that timebase is not None. Default - is False. - """ - # If no timebase is given, answer depends on strict flag - if self.dt is None: - return True if not strict else False - return self.dt == 0 - - def issiso(self): - '''Check to see if a system is single input, single output''' - return self.ninputs == 1 and self.noutputs == 1 - def damp(self): '''Natural frequency, damping ratio of system poles @@ -158,7 +117,7 @@ def damp(self): ''' poles = self.poles() - if isdtime(self, strict=True): + if self.isdtime(strict=True): splane_poles = np.log(poles.astype(complex))/self.dt else: splane_poles = poles @@ -215,7 +174,7 @@ def frequency_response(self, omega, squeeze=None): """ omega = np.sort(np.array(omega, ndmin=1)) - if isdtime(self, strict=True): + if self.isdtime(strict=True): # Convert the frequency to discrete time if np.any(omega * self.dt > np.pi): warn("__call__: evaluation above Nyquist frequency") @@ -257,191 +216,6 @@ def zero(self): return self.zeros() -# Test to see if a system is SISO -def issiso(sys, strict=False): - """ - Check to see if a system is single input, single output - - Parameters - ---------- - sys : LTI system - System to be checked - strict: bool (default = False) - If strict is True, do not treat scalars as SISO - """ - if isinstance(sys, (int, float, complex, np.number)) and not strict: - return True - elif not isinstance(sys, LTI): - raise ValueError("Object is not an LTI system") - - # Done with the tricky stuff... - return sys.issiso() - -# Return the timebase (with conversion if unspecified) -def timebase(sys, strict=True): - """Return the timebase for an LTI system - - dt = timebase(sys) - - returns the timebase for a system 'sys'. If the strict option is - set to False, dt = True will be returned as 1. - """ - # System needs to be either a constant or an LTI system - if isinstance(sys, (int, float, complex, np.number)): - return None - elif not isinstance(sys, LTI): - raise ValueError("Timebase not defined") - - # Return the sample time, with converstion to float if strict is false - if (sys.dt == None): - return None - elif (strict): - return float(sys.dt) - - return sys.dt - -def common_timebase(dt1, dt2): - """ - Find the common timebase when interconnecting systems - - Parameters - ---------- - dt1, dt2: number or system with a 'dt' attribute (e.g. TransferFunction - or StateSpace system) - - Returns - ------- - dt: number - The common timebase of dt1 and dt2, as specified in - :ref:`conventions-ref`. - - Raises - ------ - ValueError - when no compatible time base can be found - """ - # explanation: - # if either dt is None, they are compatible with anything - # if either dt is True (discrete with unspecified time base), - # use the timebase of the other, if it is also discrete - # otherwise both dts must be equal - if hasattr(dt1, 'dt'): - dt1 = dt1.dt - if hasattr(dt2, 'dt'): - dt2 = dt2.dt - - if dt1 is None: - return dt2 - elif dt2 is None: - return dt1 - elif dt1 is True: - if dt2 > 0: - return dt2 - else: - raise ValueError("Systems have incompatible timebases") - elif dt2 is True: - if dt1 > 0: - return dt1 - else: - raise ValueError("Systems have incompatible timebases") - elif np.isclose(dt1, dt2): - return dt1 - else: - raise ValueError("Systems have incompatible timebases") - -# Check to see if two timebases are equal -def timebaseEqual(sys1, sys2): - """ - Check to see if two systems have the same timebase - - timebaseEqual(sys1, sys2) - - returns True if the timebases for the two systems are compatible. By - default, systems with timebase 'None' are compatible with either - discrete or continuous timebase systems. If two systems have a discrete - timebase (dt > 0) then their timebases must be equal. - """ - warn("timebaseEqual will be deprecated in a future release of " - "python-control; use :func:`common_timebase` instead", - PendingDeprecationWarning) - - if (type(sys1.dt) == bool or type(sys2.dt) == bool): - # Make sure both are unspecified discrete timebases - return type(sys1.dt) == type(sys2.dt) and sys1.dt == sys2.dt - elif (sys1.dt is None or sys2.dt is None): - # One or the other is unspecified => the other can be anything - return True - else: - return sys1.dt == sys2.dt - - -# Check to see if a system is a discrete time system -def isdtime(sys, strict=False): - """ - Check to see if a system is a discrete time system - - Parameters - ---------- - sys : LTI system - System to be checked - strict: bool (default = False) - If strict is True, make sure that timebase is not None - """ - - # Check to see if this is a constant - if isinstance(sys, (int, float, complex, np.number)): - # OK as long as strict checking is off - return True if not strict else False - - # Check for a transfer function or state-space object - if isinstance(sys, LTI): - return sys.isdtime(strict) - - # Check to see if object has a dt object - if hasattr(sys, 'dt'): - # If no timebase is given, answer depends on strict flag - if sys.dt == None: - return True if not strict else False - - # Look for dt > 0 (also works if dt = True) - return sys.dt > 0 - - # Got passed something we don't recognize - return False - -# Check to see if a system is a continuous time system -def isctime(sys, strict=False): - """ - Check to see if a system is a continuous-time system - - Parameters - ---------- - sys : LTI system - System to be checked - strict: bool (default = False) - If strict is True, make sure that timebase is not None - """ - - # Check to see if this is a constant - if isinstance(sys, (int, float, complex, np.number)): - # OK as long as strict checking is off - return True if not strict else False - - # Check for a transfer function or state space object - if isinstance(sys, LTI): - return sys.isctime(strict) - - # Check to see if object has a dt object - if hasattr(sys, 'dt'): - # If no timebase is given, answer depends on strict flag - if sys.dt is None: - return True if not strict else False - return sys.dt == 0 - - # Got passed something we don't recognize - return False - - def poles(sys): """ Compute system poles. diff --git a/control/margins.py b/control/margins.py index 41739704e..662634086 100644 --- a/control/margins.py +++ b/control/margins.py @@ -52,7 +52,8 @@ import numpy as np import scipy as sp from . import xferfcn -from .lti import issiso, evalfr +from .lti import evalfr +from .namedio import issiso from . import frdata from . import freqplot from .exception import ControlMIMONotImplemented diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index 53c254189..80f2a0a65 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -65,6 +65,7 @@ from ..iosys import ss, rss, drss # moved from .statesp from ..xferfcn import * from ..lti import * +from ..namedio import * from ..frdata import * from ..dtime import * from ..exception import ControlArgument diff --git a/control/modelsimp.py b/control/modelsimp.py index 2cd2745de..432b76b96 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -45,7 +45,7 @@ import warnings from .exception import ControlSlycot, ControlMIMONotImplemented, \ ControlDimension -from .lti import isdtime, isctime +from .namedio import isdtime, isctime from .statesp import StateSpace from .statefbk import gram diff --git a/control/namedio.py b/control/namedio.py index ee08d00f2..254f310ff 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -1,24 +1,21 @@ -# namedio.py - internal named I/O object class +# namedio.py - named I/O system class and helper functions # RMM, 13 Mar 2022 # -# This file implements the _NamedIOSystem class, which is used as a parent +# This file implements the NamedIOSystem class, which is used as a parent # class for FrequencyResponseData, InputOutputSystem, LTI, TimeResponseData, # and other similar classes to allow naming of signals. import numpy as np +from copy import copy +from warnings import warn +from . import config +__all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', + 'isdtime', 'isctime'] -class _NamedIOSystem(object): - _idCounter = 0 - - def _name_or_default(self, name=None): - if name is None: - name = "sys[{}]".format(_NamedIOSystem._idCounter) - _NamedIOSystem._idCounter += 1 - return name - +class NamedIOSystem(object): def __init__( - self, name=None, inputs=None, outputs=None, states=None): + self, name=None, inputs=None, outputs=None, states=None, **kwargs): # system name self.name = self._name_or_default(name) @@ -28,6 +25,30 @@ def __init__( self.set_outputs(outputs) self.set_states(states) + # Process timebase: if not given use default, but allow None as value + self.dt = _process_dt_keyword(kwargs) + + # Make sure there were no other keywords + if kwargs: + raise TypeError("unrecognized keywords: ", str(kwargs)) + + # + # Functions to manipulate the system name + # + _idCounter = 0 # Counter for creating generic system name + + # Return system name + def _name_or_default(self, name=None): + if name is None: + name = "sys[{}]".format(NamedIOSystem._idCounter) + NamedIOSystem._idCounter += 1 + return name + + # Check if system name is generic + def _generic_name_check(self): + import re + return re.match(r'^sys\[\d*\]$', self.name) is not None + # # Class attributes # @@ -67,7 +88,35 @@ def __str__(self): def _find_signal(self, name, sigdict): return sigdict.get(name, None) + def copy(self, name=None, use_prefix_suffix=True): + """Make a copy of an input/output system + + A copy of the system is made, with a new name. The `name` keyword + can be used to specify a specific name for the system. If no name + is given and `use_prefix_suffix` is True, the name is constructed + by prepending config.defaults['iosys.duplicate_system_name_prefix'] + and appending config.defaults['iosys.duplicate_system_name_suffix']. + Otherwise, a generic system name of the form `sys[]` is used, + where `` is based on an internal counter. + + """ + # Create a copy of the system + newsys = copy(self) + + # Update the system name + if name is None and use_prefix_suffix: + # Get the default prefix and suffix to use + dup_prefix = config.defaults['iosys.duplicate_system_name_prefix'] + dup_suffix = config.defaults['iosys.duplicate_system_name_suffix'] + newsys.name = self._name_or_default( + dup_prefix + self.name + dup_suffix) + else: + newsys.name = self._name_or_default(name) + + return newsys + def set_inputs(self, inputs, prefix='u'): + """Set the number/names of the system inputs. Parameters @@ -154,6 +203,41 @@ def find_state(self, name): lambda self: list(self.state_index.keys()), # getter set_states) # setter + def isctime(self, strict=False): + """ + Check to see if a system is a continuous-time system + + Parameters + ---------- + sys : Named I/O system + System to be checked + strict: bool, optional + If strict is True, make sure that timebase is not None. Default + is False. + """ + # If no timebase is given, answer depends on strict flag + if self.dt is None: + return True if not strict else False + return self.dt == 0 + + def isdtime(self, strict=False): + """ + Check to see if a system is a discrete-time system + + Parameters + ---------- + strict: bool, optional + If strict is True, make sure that timebase is not None. Default + is False. + """ + + # If no timebase is given, answer depends on strict flag + if self.dt == None: + return True if not strict else False + + # Look for dt > 0 (also works if dt = True) + return self.dt > 0 + def issiso(self): """Check to see if a system is single input, single output""" return self.ninputs == 1 and self.noutputs == 1 @@ -163,6 +247,301 @@ def _isstatic(self): return self.nstates == 0 +# Test to see if a system is SISO +def issiso(sys, strict=False): + """ + Check to see if a system is single input, single output + + Parameters + ---------- + sys : I/O or LTI system + System to be checked + strict: bool (default = False) + If strict is True, do not treat scalars as SISO + """ + if isinstance(sys, (int, float, complex, np.number)) and not strict: + return True + elif not isinstance(sys, NamedIOSystem): + raise ValueError("Object is not an I/O or LTI system") + + # Done with the tricky stuff... + return sys.issiso() + +# Return the timebase (with conversion if unspecified) +def timebase(sys, strict=True): + """Return the timebase for a system + + dt = timebase(sys) + + returns the timebase for a system 'sys'. If the strict option is + set to False, dt = True will be returned as 1. + """ + # System needs to be either a constant or an I/O or LTI system + if isinstance(sys, (int, float, complex, np.number)): + return None + elif not isinstance(sys, NamedIOSystem): + raise ValueError("Timebase not defined") + + # Return the sample time, with converstion to float if strict is false + if (sys.dt == None): + return None + elif (strict): + return float(sys.dt) + + return sys.dt + +def common_timebase(dt1, dt2): + """ + Find the common timebase when interconnecting systems + + Parameters + ---------- + dt1, dt2: number or system with a 'dt' attribute (e.g. TransferFunction + or StateSpace system) + + Returns + ------- + dt: number + The common timebase of dt1 and dt2, as specified in + :ref:`conventions-ref`. + + Raises + ------ + ValueError + when no compatible time base can be found + """ + # explanation: + # if either dt is None, they are compatible with anything + # if either dt is True (discrete with unspecified time base), + # use the timebase of the other, if it is also discrete + # otherwise both dts must be equal + if hasattr(dt1, 'dt'): + dt1 = dt1.dt + if hasattr(dt2, 'dt'): + dt2 = dt2.dt + + if dt1 is None: + return dt2 + elif dt2 is None: + return dt1 + elif dt1 is True: + if dt2 > 0: + return dt2 + else: + raise ValueError("Systems have incompatible timebases") + elif dt2 is True: + if dt1 > 0: + return dt1 + else: + raise ValueError("Systems have incompatible timebases") + elif np.isclose(dt1, dt2): + return dt1 + else: + raise ValueError("Systems have incompatible timebases") + +# Check to see if two timebases are equal +def timebaseEqual(sys1, sys2): + """ + Check to see if two systems have the same timebase + + timebaseEqual(sys1, sys2) + + returns True if the timebases for the two systems are compatible. By + default, systems with timebase 'None' are compatible with either + discrete or continuous timebase systems. If two systems have a discrete + timebase (dt > 0) then their timebases must be equal. + """ + warn("timebaseEqual will be deprecated in a future release of " + "python-control; use :func:`common_timebase` instead", + PendingDeprecationWarning) + + if (type(sys1.dt) == bool or type(sys2.dt) == bool): + # Make sure both are unspecified discrete timebases + return type(sys1.dt) == type(sys2.dt) and sys1.dt == sys2.dt + elif (sys1.dt is None or sys2.dt is None): + # One or the other is unspecified => the other can be anything + return True + else: + return sys1.dt == sys2.dt + + +# Check to see if a system is a discrete time system +def isdtime(sys, strict=False): + """ + Check to see if a system is a discrete time system + + Parameters + ---------- + sys : I/O or LTI system + System to be checked + strict: bool (default = False) + If strict is True, make sure that timebase is not None + """ + + # Check to see if this is a constant + if isinstance(sys, (int, float, complex, np.number)): + # OK as long as strict checking is off + return True if not strict else False + + # Check for a transfer function or state-space object + if isinstance(sys, NamedIOSystem): + return sys.isdtime(strict) + + # Check to see if object has a dt object + if hasattr(sys, 'dt'): + # If no timebase is given, answer depends on strict flag + if sys.dt == None: + return True if not strict else False + + # Look for dt > 0 (also works if dt = True) + return sys.dt > 0 + + # Got passed something we don't recognize + return False + +# Check to see if a system is a continuous time system +def isctime(sys, strict=False): + """ + Check to see if a system is a continuous-time system + + Parameters + ---------- + sys : I/O or LTI system + System to be checked + strict: bool (default = False) + If strict is True, make sure that timebase is not None + """ + + # Check to see if this is a constant + if isinstance(sys, (int, float, complex, np.number)): + # OK as long as strict checking is off + return True if not strict else False + + # Check for a transfer function or state space object + if isinstance(sys, NamedIOSystem): + return sys.isctime(strict) + + # Check to see if object has a dt object + if hasattr(sys, 'dt'): + # If no timebase is given, answer depends on strict flag + if sys.dt is None: + return True if not strict else False + return sys.dt == 0 + + # Got passed something we don't recognize + return False + + +# Utility function to parse nameio keywords +def _process_namedio_keywords( + keywords={}, defaults={}, static=False, end=False): + """Process namedio specification + + This function processes the standard keywords used in initializing a named + I/O system. It first looks in the `keyword` dictionary to see if a value + is specified. If not, the `default` dictionary is used. The `default` + dictionary can also be set to a NamedIOSystem object, which is useful for + copy constructors that change system and signal names. + + If `end` is True, then generate an error if there are any remaining + keywords. + + """ + # If default is a system, redefine as a dictionary + if isinstance(defaults, NamedIOSystem): + sys = defaults + defaults = { + 'name': sys.name, 'inputs': sys.input_labels, + 'outputs': sys.output_labels, 'dt': sys.dt} + + if sys.nstates is not None: + defaults['states'] = sys.state_labels + + elif not isinstance(defaults, dict): + raise TypeError("default must be dict or sys") + + else: + sys = None + + # Sort out singular versus plural signal names + for singular in ['input', 'output', 'state']: + kw = singular + 's' + if singular in keywords and kw in keywords: + raise TypeError(f"conflicting keywords '{singular}' and '{kw}'") + + if singular in keywords: + keywords[kw] = keywords.pop(singular) + + # Utility function to get keyword with defaults, processing + def pop_with_default(kw, defval=None, return_list=True): + val = keywords.pop(kw, None) + if val is None: + val = defaults.get(kw, defval) + if return_list and isinstance(val, str): + val = [val] # make sure to return a list + return val + + # Process system and signal names + name = pop_with_default('name', return_list=False) + inputs = pop_with_default('inputs') + outputs = pop_with_default('outputs') + states = pop_with_default('states') + + # If we were given a system, make sure sizes match list lengths + if sys: + if isinstance(inputs, list) and sys.ninputs != len(inputs): + raise ValueError("Wrong number of input labels given.") + if isinstance(outputs, list) and sys.noutputs != len(outputs): + raise ValueError("Wrong number of output labels given.") + if sys.nstates is not None and \ + isinstance(states, list) and sys.nstates != len(states): + raise ValueError("Wrong number of state labels given.") + + # Process timebase: if not given use default, but allow None as value + dt = _process_dt_keyword(keywords, defaults, static=static) + + # If desired, make sure we processed all keywords + if end and keywords: + raise TypeError("unrecognized keywords: ", str(keywords)) + + # Return the processed keywords + return name, inputs, outputs, states, dt + +# +# Parse 'dt' in for named I/O system +# +# The 'dt' keyword is used to set the timebase for a system. Its +# processing is a bit unusual: if it is not specified at all, then the +# value is pulled from config.defaults['control.default_dt']. But +# since 'None' is an allowed value, we can't just use the default if +# dt is None. Instead, we have to look to see if it was listed as a +# variable keyword. +# +# In addition, if a system is static and dt is not specified, we set dt = +# None to allow static systems to be combined with either discrete-time or +# continuous-time systems. +# +# TODO: update all 'dt' processing to call this function, so that +# everything is done consistently. +# +def _process_dt_keyword(keywords, defaults={}, static=False): + if static and 'dt' not in keywords and 'dt' not in defaults: + dt = None + elif 'dt' in keywords: + dt = keywords.pop('dt') + elif 'dt' in defaults: + dt = defaults.pop('dt') + else: + dt = config.defaults['control.default_dt'] + + # Make sure that the value for dt is valid + if dt is not None and not isinstance(dt, (bool, int, float)) or \ + isinstance(dt, (bool, int, float)) and dt < 0: + raise ValueError(f"invalid timebase, dt = {dt}") + + return dt + + # Utility function to parse a list of signals def _process_signal_list(signals, prefix='s'): if signals is None: diff --git a/control/pzmap.py b/control/pzmap.py index c528df4be..09f58b79c 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -41,7 +41,8 @@ from numpy import real, imag, linspace, exp, cos, sin, sqrt from math import pi -from .lti import LTI, isdtime, isctime +from .lti import LTI +from .namedio import isdtime, isctime from .grid import sgrid, zgrid, nogrid from . import config diff --git a/control/rlocus.py b/control/rlocus.py index 5cf7983a3..9d531de94 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -55,7 +55,7 @@ import matplotlib.pyplot as plt from numpy import array, poly1d, row_stack, zeros_like, real, imag import scipy.signal # signal processing toolbox -from .lti import isdtime +from .namedio import isdtime from .xferfcn import _convert_to_transfer_function from .exception import ControlMIMONotImplemented from .sisotool import _SisotoolUpdate diff --git a/control/sisotool.py b/control/sisotool.py index 41f21ecbe..52c061249 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -3,14 +3,12 @@ from control.exception import ControlMIMONotImplemented from .freqplot import bode_plot from .timeresp import step_response -from .lti import issiso, isdtime +from .namedio import issiso, common_timebase, isctime, isdtime from .xferfcn import tf from .iosys import ss from .bdalg import append, connect from .iosys import tf2io, ss2io, summing_junction, interconnect from control.statesp import _convert_to_statespace, StateSpace -from control.lti import common_timebase, isctime -import matplotlib import matplotlib.pyplot as plt import warnings diff --git a/control/statefbk.py b/control/statefbk.py index 0aaf49f61..97f314da5 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -45,7 +45,8 @@ from . import statesp from .mateqn import care, dare, _check_shape from .statesp import StateSpace, _ssmatrix, _convert_to_statespace -from .lti import LTI, isdtime, isctime +from .lti import LTI +from .namedio import isdtime, isctime from .iosys import InputOutputSystem, NonlinearIOSystem, LinearIOSystem, \ interconnect, ss from .exception import ControlSlycot, ControlArgument, ControlDimension, \ diff --git a/control/statesp.py b/control/statesp.py index 076b0ccea..58412e57a 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -58,8 +58,10 @@ from scipy.signal import cont2discrete from scipy.signal import StateSpace as signalStateSpace from warnings import warn -from .lti import LTI, common_timebase, isdtime, _process_frequency_response -from .namedio import _NamedIOSystem, _process_signal_list +from .frdata import FrequencyResponseData +from .lti import LTI, _process_frequency_response +from .namedio import common_timebase, isdtime +from .namedio import _process_namedio_keywords from . import config from copy import deepcopy @@ -153,7 +155,7 @@ def _f2s(f): return s -class StateSpace(LTI, _NamedIOSystem): +class StateSpace(LTI): """StateSpace(A, B, C, D[, dt]) A class for representing state-space models. @@ -161,7 +163,9 @@ class StateSpace(LTI, _NamedIOSystem): The StateSpace class is used to represent state-space realizations of linear time-invariant (LTI) systems: + .. math:: dx/dt = A x + B u + y = C x + D u where u is the input, y is the output, and x is the state. @@ -217,6 +221,8 @@ class StateSpace(LTI, _NamedIOSystem): The default value of dt can be changed by changing the value of ``control.config.defaults['control.default_dt']``. + Note: timebase processing has moved to namedio. + A state space system is callable and returns the value of the transfer function evaluated at a point in the complex plane. See :meth:`~control.StateSpace.__call__` for a more detailed description. @@ -244,7 +250,7 @@ class StateSpace(LTI, _NamedIOSystem): # Allow ndarray * StateSpace to give StateSpace._rmul_() priority __array_priority__ = 11 # override ndarray and matrix types - def __init__(self, *args, keywords=None, **kwargs): + def __init__(self, *args, init_namedio=True, **kwargs): """StateSpace(A, B, C, D[, dt]) Construct a state space object. @@ -262,18 +268,27 @@ def __init__(self, *args, keywords=None, **kwargs): value is read from `config.defaults['statesp.remove_useless_states']` (default = False). - """ - # Use keywords object if we received one (and pop keywords we use) - if keywords is None: - keywords = kwargs + The `init_namedio` keyword can be used to turn off initialization of + system and signal names. This is used internally by the + :class:`LinearIOSystem` class to avoid renaming. - # first get A, B, C, D matrices + """ + # + # Process positional arguments + # if len(args) == 4: # The user provided A, B, C, and D matrices. (A, B, C, D) = args + elif len(args) == 5: # Discrete time system - (A, B, C, D, _) = args + (A, B, C, D, dt) = args + if 'dt' in kwargs: + warn("received multiple dt arguments, " + "using positional arg dt = %s" % dt) + kwargs['dt'] = dt + args = args[:-1] + elif len(args) == 1: # Use the copy constructor. if not isinstance(args[0], StateSpace): @@ -284,15 +299,11 @@ def __init__(self, *args, keywords=None, **kwargs): B = args[0].B C = args[0].C D = args[0].D + else: - raise ValueError( + raise TypeError( "Expected 1, 4, or 5 arguments; received %i." % len(args)) - # Process keyword arguments - remove_useless_states = keywords.pop( - 'remove_useless_states', - config.defaults['statesp.remove_useless_states']) - # Convert all matrices to standard form A = _ssmatrix(A) # if B is a 1D array, turn it into a column vector if it fits @@ -309,41 +320,38 @@ def __init__(self, *args, keywords=None, **kwargs): D = np.zeros((C.shape[0], B.shape[1])) D = _ssmatrix(D) - super().__init__(inputs=D.shape[1], outputs=D.shape[0]) + # Matrices definining the linear system self.A = A self.B = B self.C = C self.D = D - # now set dt - if len(args) == 4: - if 'dt' in keywords: - dt = keywords.pop('dt') - elif self._isstatic(): - dt = None - else: - dt = config.defaults['control.default_dt'] - elif len(args) == 5: - dt = args[4] - if 'dt' in keywords: - warn("received multiple dt arguments, " - "using positional arg dt = %s" % dt) - keywords.pop('dt') - elif len(args) == 1: - try: - dt = args[0].dt - except AttributeError: - if self._isstatic(): - dt = None - else: - dt = config.defaults['control.default_dt'] - self.dt = dt - self.nstates = A.shape[1] - - # Make sure there were no extraneous keywords - if keywords: - raise TypeError("unrecognized keywords: ", str(keywords)) + # + # Process keyword arguments + # + remove_useless_states = kwargs.pop( + 'remove_useless_states', + config.defaults['statesp.remove_useless_states']) + + # Initialize the instance variables + if init_namedio: + # Process namedio keywords + defaults = args[0] if len(args) == 1 else \ + {'inputs': D.shape[1], 'outputs': D.shape[0], + 'states': A.shape[0]} + static = (A.size == 0) + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, defaults, static=static, end=True) + + # Initialize LTI (NamedIOSystem) object + super().__init__( + name=name, inputs=inputs, outputs=outputs, + states=states, dt=dt) + elif kwargs: + raise TypeError("unrecognized keyword(s): ", str(kwargs)) + + # Reset shapes (may not be needed once np.matrix support is removed) if 0 == self.nstates: # static gain # matrix's default "empty" shape is 1x0 @@ -351,8 +359,11 @@ def __init__(self, *args, keywords=None, **kwargs): B.shape = (0, self.ninputs) C.shape = (self.noutputs, 0) - # Check that the matrix sizes are consistent. - if self.nstates != A.shape[0]: + # + # Check to make sure everything is consistent + # + # Check that the matrix sizes are consistent + if A.shape[0] != A.shape[1] or self.nstates != A.shape[0]: raise ValueError("A must be square.") if self.nstates != B.shape[0]: raise ValueError("A and B must have the same number of rows.") @@ -363,7 +374,10 @@ def __init__(self, *args, keywords=None, **kwargs): if self.noutputs != C.shape[0]: raise ValueError("C and D must have the same number of rows.") - # Check for states that don't do anything, and remove them. + # + # Final processing + # + # Check for states that don't do anything, and remove them if remove_useless_states: self._remove_useless_states() @@ -464,9 +478,10 @@ def _remove_useless_states(self): self.B = delete(self.B, useless, 0) self.C = delete(self.C, useless, 1) - self.nstates = self.A.shape[0] - self.ninputs = self.B.shape[1] - self.noutputs = self.C.shape[0] + # Remove any state names that we don't need + self.set_states( + [self.state_labels[i] for i in range(self.nstates) + if i not in useless]) def __str__(self): """Return string representation of the state space system.""" @@ -655,6 +670,13 @@ def __add__(self, other): D = self.D + other dt = self.dt else: + # Check to see if the right operator has priority + if getattr(other, '__array_priority__', None) and \ + getattr(self, '__array_priority__', None) and \ + other.__array_priority__ > self.__array_priority__: + return other.__radd__(self) + + # Convert the other argument to state space other = _convert_to_statespace(other) # Check to make sure the dimensions are OK @@ -705,6 +727,13 @@ def __mul__(self, other): D = self.D * other dt = self.dt else: + # Check to see if the right operator has priority + if getattr(other, '__array_priority__', None) and \ + getattr(self, '__array_priority__', None) and \ + other.__array_priority__ > self.__array_priority__: + return other.__rmul__(self) + + # Convert the other argument to state space other = _convert_to_statespace(other) # Check to make sure the dimensions are OK @@ -1369,7 +1398,7 @@ def dynamics(self, t, x, u=None): The first argument `t` is ignored because :class:`StateSpace` systems are time-invariant. It is included so that the dynamics can be passed - to most numerical integrators, such as :func:`scipy.integrate.solve_ivp` + to numerical integrators, such as :func:`scipy.integrate.solve_ivp` and for consistency with :class:`IOSystem` systems. Parameters @@ -1384,6 +1413,7 @@ def dynamics(self, t, x, u=None): Returns ------- dx/dt or x[t+dt] : ndarray + """ x = np.reshape(x, (-1, 1)) # force to a column in case matrix if np.size(x) != self.nstates: @@ -1447,7 +1477,7 @@ def _isstatic(self): # TODO: add discrete time check -def _convert_to_statespace(sys, **kw): +def _convert_to_statespace(sys): """Convert a system to state space form (if needed). If sys is already a state space, then it is returned. If sys is a @@ -1460,16 +1490,15 @@ def _convert_to_statespace(sys, **kw): In the latter example, A = B = C = 0 and D = [[1., 1., 1.] [1., 1., 1.]]. + + Note: no renaming of inputs and outputs is performed; this should be done + by the calling function. + """ from .xferfcn import TransferFunction import itertools if isinstance(sys, StateSpace): - if len(kw): - raise TypeError("If sys is a StateSpace, _convert_to_statespace " - "cannot take keywords.") - - # Already a state space system; just return it return sys elif isinstance(sys, TransferFunction): @@ -1478,11 +1507,9 @@ def _convert_to_statespace(sys, **kw): [[len(num) for num in col] for col in sys.den]): raise ValueError("Transfer function is non-proper; can't " "convert to StateSpace system.") + try: from slycot import td04ad - if len(kw): - raise TypeError("If sys is a TransferFunction, " - "_convert_to_statespace cannot take keywords.") # Change the numerator and denominator arrays so that the transfer # function matrix has a common denominator. @@ -1494,10 +1521,10 @@ def _convert_to_statespace(sys, **kw): denorder, den, num, tol=0) states = ssout[0] - return StateSpace(ssout[1][:states, :states], - ssout[2][:states, :sys.ninputs], - ssout[3][:sys.noutputs, :states], ssout[4], - sys.dt) + return StateSpace( + ssout[1][:states, :states], ssout[2][:states, :sys.ninputs], + ssout[3][:sys.noutputs, :states], ssout[4], sys.dt, + inputs=sys.input_labels, outputs=sys.output_labels) except ImportError: # No Slycot. Scipy tf->ss can't handle MIMO, but static # MIMO is an easy special case we can check for here @@ -1520,34 +1547,25 @@ def _convert_to_statespace(sys, **kw): # the squeeze A, B, C, D = \ sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den)) - return StateSpace(A, B, C, D, sys.dt) + return StateSpace( + A, B, C, D, sys.dt, inputs=sys.input_labels, + outputs=sys.output_labels) - elif isinstance(sys, (int, float, complex, np.number)): - if "inputs" in kw: - inputs = kw["inputs"] - else: - inputs = 1 - if "outputs" in kw: - outputs = kw["outputs"] - else: - outputs = 1 - - # Generate a simple state space system of the desired dimension - # The following Doesn't work due to inconsistencies in ltisys: - # return StateSpace([[]], [[]], [[]], eye(outputs, inputs)) - return StateSpace([], zeros((0, inputs)), zeros((outputs, 0)), - sys * ones((outputs, inputs))) + elif isinstance(sys, FrequencyResponseData): + raise TypeError("Can't convert FRD to StateSpace system.") # If this is a matrix, try to create a constant feedthrough try: - D = _ssmatrix(sys) - return StateSpace([], [], [], D) + D = _ssmatrix(np.atleast_2d(sys)) + return StateSpace([], [], [], D, dt=None) + except Exception: raise TypeError("Can't convert given type to StateSpace system.") # TODO: add discrete time option -def _rss_generate(states, inputs, outputs, cdtype, strictly_proper=False): +def _rss_generate( + states, inputs, outputs, cdtype, strictly_proper=False, name=None): """Generate a random state space. This does the actual random state space generation expected from rss and @@ -1665,7 +1683,7 @@ def _rss_generate(states, inputs, outputs, cdtype, strictly_proper=False): ss_args = (A, B, C, D) else: ss_args = (A, B, C, D, True) - return StateSpace(*ss_args) + return StateSpace(*ss_args, name=name) # Convert a MIMO system to a SISO system @@ -1776,31 +1794,7 @@ def _mimo2simo(sys, input, warn_conversion=False): return sys -def _ss(*args, keywords=None, **kwargs): - """Internal function to create StateSpace system""" - if len(args) == 4 or len(args) == 5: - return StateSpace(*args, keywords=keywords, **kwargs) - - elif len(args) == 1: - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - - from .xferfcn import TransferFunction - sys = args[0] - if isinstance(sys, StateSpace): - return deepcopy(sys) - elif isinstance(sys, TransferFunction): - return tf2ss(sys) - else: - raise TypeError("ss(sys): sys must be a StateSpace or " - "TransferFunction object. It is %s." % type(sys)) - else: - raise ValueError( - "Needs 1, 4, or 5 arguments; received %i." % len(args)) - - -def tf2ss(*args): +def tf2ss(*args, **kwargs): """tf2ss(sys) Transform a transfer function to a state space system. @@ -1808,11 +1802,11 @@ def tf2ss(*args): The function accepts either 1 or 2 parameters: ``tf2ss(sys)`` - Convert a linear system into transfer function form. Always creates - a new system, even if sys is already a TransferFunction object. + Convert a linear system into space space form. Always creates + a new system, even if sys is already a StateSpace object. ``tf2ss(num, den)`` - Create a transfer function system from its numerator and denominator + Create a state space system from its numerator and denominator polynomial coefficients. For details see: :func:`tf` @@ -1831,6 +1825,16 @@ def tf2ss(*args): out : StateSpace New linear system in state space form + Other Parameters + ---------------- + inputs, outputs : str, or list of str, optional + List of strings that name the individual signals of the transformed + system. If not given, the inputs and outputs are the same as the + original system. + name : string, optional + System name. If unspecified, a generic name is generated + with a unique integer id. + Raises ------ ValueError @@ -1860,14 +1864,15 @@ def tf2ss(*args): from .xferfcn import TransferFunction if len(args) == 2 or len(args) == 3: # Assume we were given the num, den - return _convert_to_statespace(TransferFunction(*args)) + return StateSpace( + _convert_to_statespace(TransferFunction(*args)), **kwargs) elif len(args) == 1: sys = args[0] if not isinstance(sys, TransferFunction): raise TypeError("tf2ss(sys): sys must be a TransferFunction " "object.") - return _convert_to_statespace(sys) + return StateSpace(_convert_to_statespace(sys), **kwargs) else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) diff --git a/control/stochsys.py b/control/stochsys.py index fd276b92c..2b8233070 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -21,7 +21,8 @@ from math import sqrt from .iosys import InputOutputSystem, NonlinearIOSystem -from .lti import LTI, isctime, isdtime +from .lti import LTI +from .namedio import isctime, isdtime from .mateqn import care, dare, _check_shape from .statesp import StateSpace, _ssmatrix from .exception import ControlArgument, ControlNotImplemented diff --git a/control/tests/config_test.py b/control/tests/config_test.py index b495a0f6f..295c68bdd 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -292,8 +292,12 @@ def test_change_default_dt_static(self): """Test that static gain systems always have dt=None""" ct.set_defaults('control', default_dt=0) assert ct.tf(1, 1).dt is None - assert ct.ss(0, 0, 0, 1).dt is None - # TODO: add in test for static gain iosys + assert ct.ss([], [], [], 1).dt is None + + # Make sure static gain is preserved for the I/O system + sys = ct.ss([], [], [], 1) + sys_io = ct.ss2io(sys) + assert sys_io.dt is None def test_get_param_last(self): """Test _get_param last keyword""" diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index 864b771e4..00425565f 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -482,7 +482,7 @@ def test_unrecognized_keyword(self): def test_named_signals(): - ct.namedio._NamedIOSystem._idCounter = 0 + ct.namedio.NamedIOSystem._idCounter = 0 h1 = TransferFunction([1], [1, 2, 2]) h2 = TransferFunction([1], [0.1, 1]) omega = np.logspace(-1, 2, 10) @@ -490,8 +490,8 @@ def test_named_signals(): f2 = FRD(h2, omega) # Make sure that systems were properly named - assert f1.name == 'sys[0]' - assert f2.name == 'sys[1]' + assert f1.name == 'sys[2]' + assert f2.name == 'sys[3]' assert f1.ninputs == 1 assert f1.input_labels == ['u[0]'] assert f1.noutputs == 1 diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 87aa271ef..ecb30c316 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -133,7 +133,7 @@ def test_iosys_print(self, tsys, capsys): @pytest.mark.parametrize("ss", [ios.NonlinearIOSystem, ct.ss]) def test_nonlinear_iosys(self, tsys, ss): # Create a simple nonlinear I/O system - nlsys = ss(predprey) + nlsys = ios.NonlinearIOSystem(predprey) T = tsys.T # Start by simulating from an equilibrium point @@ -160,7 +160,7 @@ def test_nonlinear_iosys(self, tsys, ss): np.reshape(linsys.C @ np.reshape(x, (-1, 1)) + linsys.D @ np.reshape(u, (-1, 1)), (-1,)) - nlsys = ss(nlupd, nlout, inputs=1, outputs=1) + nlsys = ios.NonlinearIOSystem(nlupd, nlout, inputs=1, outputs=1) # Make sure that simulations also line up T, U, X0 = tsys.T, tsys.U, tsys.X0 @@ -240,9 +240,9 @@ def test_linearize_named_signals(self, kincar): def test_connect(self, tsys): # Define a couple of (linear) systems to interconnection linsys1 = tsys.siso_linsys - iosys1 = ios.LinearIOSystem(linsys1) + iosys1 = ios.LinearIOSystem(linsys1, name='iosys1') linsys2 = tsys.siso_linsys - iosys2 = ios.LinearIOSystem(linsys2) + iosys2 = ios.LinearIOSystem(linsys2, name='iosys2') # Connect systems in different ways and compare to StateSpace linsys_series = linsys2 * linsys1 @@ -408,8 +408,8 @@ def test_algebraic_loop(self, tsys): lnios = ios.LinearIOSystem(linsys) nlios = ios.NonlinearIOSystem(None, \ lambda t, x, u, params: u*u, inputs=1, outputs=1) - nlios1 = nlios.copy() - nlios2 = nlios.copy() + nlios1 = nlios.copy(name='nlios1') + nlios2 = nlios.copy(name='nlios2') # Set up parameters for simulation T, U, X0 = tsys.T, tsys.U, tsys.X0 @@ -474,8 +474,8 @@ def test_algebraic_loop(self, tsys): def test_summer(self, tsys): # Construct a MIMO system for testing linsys = tsys.mimo_linsys1 - linio1 = ios.LinearIOSystem(linsys) - linio2 = ios.LinearIOSystem(linsys) + linio1 = ios.LinearIOSystem(linsys, name='linio1') + linio2 = ios.LinearIOSystem(linsys, name='linio2') linsys_parallel = linsys + linsys iosys_parallel = linio1 + linio2 @@ -1043,8 +1043,12 @@ def test_sys_naming_convention(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.namedio._NamedIOSystem._idCounter = 0 - sys = ct.LinearIOSystem(tsys.mimo_linsys1) + + # Create a system with a known ID + ct.namedio.NamedIOSystem._idCounter = 0 + sys = ct.ss( + tsys.mimo_linsys1.A, tsys.mimo_linsys1.B, + tsys.mimo_linsys1.C, tsys.mimo_linsys1.D) assert sys.name == "sys[0]" assert sys.copy().name == "copy of sys[0]" @@ -1094,7 +1098,7 @@ def test_sys_naming_convention(self, tsys): # Same system conflict with pytest.warns(UserWarning): - unnamedsys1 * unnamedsys1 + namedsys * namedsys @pytest.mark.usefixtures("editsdefaults") def test_signals_naming_convention_0_8_4(self, tsys): @@ -1107,8 +1111,13 @@ def test_signals_naming_convention_0_8_4(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.namedio._NamedIOSystem._idCounter = 0 - sys = ct.LinearIOSystem(tsys.mimo_linsys1) + + # Create a system with a known ID + ct.namedio.NamedIOSystem._idCounter = 0 + sys = ct.ss( + tsys.mimo_linsys1.A, tsys.mimo_linsys1.B, + tsys.mimo_linsys1.C, tsys.mimo_linsys1.D) + for statename in ["x[0]", "x[1]"]: assert statename in sys.state_index for inputname in ["u[0]", "u[1]"]: @@ -1155,9 +1164,9 @@ def test_signals_naming_convention_0_8_4(self, tsys): # Same system conflict with pytest.warns(UserWarning): - same_name_series = unnamedsys * unnamedsys - assert "sys[1].x[0]" in same_name_series.state_index - assert "copy of sys[1].x[0]" in same_name_series.state_index + same_name_series = namedsys * namedsys + assert "namedsys.x0" in same_name_series.state_index + assert "copy of namedsys.x0" in same_name_series.state_index def test_named_signals_linearize_inconsistent(self, tsys): """Mare sure that providing inputs or outputs not consistent with @@ -1207,8 +1216,8 @@ def outfcn(t, x, u, params): def test_lineariosys_statespace(self, tsys): """Make sure that a LinearIOSystem is also a StateSpace object""" - iosys_siso = ct.LinearIOSystem(tsys.siso_linsys) - iosys_siso2 = ct.LinearIOSystem(tsys.siso_linsys) + iosys_siso = ct.LinearIOSystem(tsys.siso_linsys, name='siso') + iosys_siso2 = ct.LinearIOSystem(tsys.siso_linsys, name='siso2') assert isinstance(iosys_siso, ct.StateSpace) # Make sure that state space functions work for LinearIOSystems @@ -1391,7 +1400,7 @@ def test_duplicates(self, tsys): name="sys") # Duplicate objects - with pytest.warns(UserWarning, match="Duplicate object"): + with pytest.warns(UserWarning, match="duplicate object"): ios_series = nlios * nlios # Nonduplicate objects @@ -1399,7 +1408,7 @@ def test_duplicates(self, tsys): ct.config.use_numpy_matrix(False) # np.matrix deprecated nlios1 = nlios.copy() nlios2 = nlios.copy() - with pytest.warns(UserWarning, match="Duplicate name"): + with pytest.warns(UserWarning, match="duplicate name"): ios_series = nlios1 * nlios2 assert "copy of sys_1.x[0]" in ios_series.state_index.keys() assert "copy of sys.x[0]" in ios_series.state_index.keys() @@ -1413,7 +1422,7 @@ def test_duplicates(self, tsys): lambda t, x, u, params: u * u, inputs=1, outputs=1, name="sys") - with pytest.warns(UserWarning, match="Duplicate name"): + with pytest.warns(UserWarning, match="duplicate name"): ct.InterconnectedSystem([nlios1, iosys_siso, nlios2], inputs=0, outputs=0, states=0) @@ -1787,6 +1796,18 @@ def test_ss_nonlinear(): assert secord.output_labels == ['y'] assert secord.state_labels == ['x1', 'x2'] + # Make sure we get the same answer for simulations + T = np.linspace(0, 10, 100) + U = np.sin(T) + X0 = np.array([1, -1]) + secord_nlio = ct.NonlinearIOSystem( + secord_update, secord_output, inputs=1, outputs=1, states=2) + ss_response = ct.input_output_response(secord, T, U, X0) + io_response = ct.input_output_response(secord_nlio, T, U, X0) + np.testing.assert_almost_equal(ss_response.time, io_response.time) + np.testing.assert_almost_equal(ss_response.inputs, io_response.inputs) + np.testing.assert_almost_equal(ss_response.outputs, io_response.outputs) + # Make sure that optional keywords are allowed secord = ct.ss(secord_update, secord_output, dt=True) assert ct.isdtime(secord) @@ -1794,3 +1815,47 @@ def test_ss_nonlinear(): # Make sure that state space keywords are flagged with pytest.raises(TypeError, match="unrecognized keyword"): ct.ss(secord_update, remove_useless_states=True) + + +def test_rss(): + # Basic call, with no arguments + sys = ct.rss() + assert sys.ninputs == 1 + assert sys.noutputs == 1 + assert sys.nstates == 1 + assert sys.dt == 0 + assert np.all(np.real(sys.poles()) < 0) + + # Set the timebase explicitly + sys = ct.rss(inputs=2, outputs=3, states=4, dt=None, name='sys') + assert sys.name == 'sys' + assert sys.ninputs == 2 + assert sys.noutputs == 3 + assert sys.nstates == 4 + assert sys.dt == None + assert np.all(np.real(sys.poles()) < 0) + + # Discrete time + sys = ct.rss(inputs=['a', 'b'], outputs=1, states=1, dt=True) + assert sys.ninputs == 2 + assert sys.input_labels == ['a', 'b'] + assert sys.noutputs == 1 + assert sys.nstates == 1 + assert sys.dt == True + assert np.all(np.abs(sys.poles()) < 1) + + # Call drss directly + sys = ct.drss(inputs=['a', 'b'], outputs=1, states=1, dt=True) + assert sys.ninputs == 2 + assert sys.input_labels == ['a', 'b'] + assert sys.noutputs == 1 + assert sys.nstates == 1 + assert sys.dt == True + assert np.all(np.abs(sys.poles()) < 1) + + with pytest.raises(ValueError, match="continuous timebase"): + sys = ct.drss(2, 1, 1, dt=0) + + with pytest.warns(UserWarning, match="may be interpreted as continuous"): + sys = ct.drss(2, 1, 1, dt=None) + assert np.all(np.abs(sys.poles()) < 1) diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 7de944c49..62887301d 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -99,7 +99,9 @@ def test_unrecognized_kwargs(): [control.summing_junction, (2,), {}], [control.tf, ([1], [1, 1]), {}], [control.tf2io, (control.tf([1], [1, 1]),), {}], - [control.InputOutputSystem, (1, 1, 1), {}], + [control.tf2ss, (control.tf([1], [1, 1]),), {}], + [control.InputOutputSystem, (), + {'inputs': 1, 'outputs': 1, 'states': 1}], [control.InputOutputSystem.linearize, (sys, 0, 0), {}], [control.StateSpace, ([[-1, 0], [0, -1]], [[1], [1]], [[1, 1]], 0), {}], [control.TransferFunction, ([1], [1, 1]), {}], @@ -117,14 +119,15 @@ def test_unrecognized_kwargs(): def test_matplotlib_kwargs(): # Create a SISO system for use in parameterized tests sys = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) + ctl = control.ss([[-1, 1], [0, -1]], [[0], [1]], [[1, 0]], 0, dt=None) table = [ [control.bode, (sys, ), {}], [control.bode_plot, (sys, ), {}], [control.describing_function_plot, (sys, control.descfcn.saturation_nonlinearity(1), [1, 2, 3, 4]), {}], - [control.gangof4, (sys, sys), {}], - [control.gangof4_plot, (sys, sys), {}], + [control.gangof4, (sys, ctl), {}], + [control.gangof4_plot, (sys, ctl), {}], [control.nyquist, (sys, ), {}], [control.nyquist_plot, (sys, ), {}], [control.singular_values_plot, (sys, ), {}], @@ -180,6 +183,7 @@ def test_matplotlib_kwargs(): 'summing_junction': interconnect_test.test_interconnect_exceptions, 'tf': test_unrecognized_kwargs, 'tf2io' : test_unrecognized_kwargs, + 'tf2ss' : test_unrecognized_kwargs, 'flatsys.point_to_point': flatsys_test.TestFlatSys.test_point_to_point_errors, 'FrequencyResponseData.__init__': diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 36c1b100d..8e45ea482 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -6,8 +6,8 @@ import control as ct from control import c2d, tf, ss, tf2ss, NonlinearIOSystem -from control.lti import (LTI, common_timebase, evalfr, damp, dcgain, isctime, - isdtime, issiso, poles, timebaseEqual, zeros) +from control.lti import LTI, evalfr, damp, dcgain, zeros, poles +from control import common_timebase, isctime, isdtime, issiso, timebaseEqual from control.tests.conftest import slycotonly from control.exception import slycot_check @@ -22,10 +22,12 @@ def test_poles(self, fun, args): np.testing.assert_allclose(poles(sys), 42) with pytest.warns(PendingDeprecationWarning): - sys.pole() + pole_list = sys.pole() + assert pole_list == sys.poles() with pytest.warns(PendingDeprecationWarning): - ct.pole(sys) + pole_list = ct.pole(sys) + assert pole_list == sys.poles() @pytest.mark.parametrize("fun, args", [ [tf, (126, [-1, 42])], diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 2966ab4e8..3a96203a8 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -9,11 +9,13 @@ """ import re +from copy import copy import numpy as np import control as ct import pytest + def test_named_ss(): # Create a system to play with sys = ct.rss(2, 2, 2) @@ -23,9 +25,9 @@ def test_named_ss(): # Get the state matrices for later use A, B, C, D = sys.A, sys.B, sys.C, sys.D - + # Set up a named state space systems with default names - ct.namedio._NamedIOSystem._idCounter = 0 + ct.namedio.NamedIOSystem._idCounter = 0 sys = ct.ss(A, B, C, D) assert sys.name == 'sys[0]' assert sys.input_labels == ['u[0]', 'u[1]'] @@ -39,7 +41,7 @@ def test_named_ss(): A, B, C, D, name='system', inputs=['u1', 'u2'], outputs=['y1', 'y2'], states=['x1', 'x2']) assert sys.name == 'system' - assert ct.namedio._NamedIOSystem._idCounter == 1 + assert ct.namedio.NamedIOSystem._idCounter == 1 assert sys.input_labels == ['u1', 'u2'] assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2'] @@ -49,9 +51,203 @@ def test_named_ss(): # Do the same with rss sys = ct.rss(['x1', 'x2', 'x3'], ['y1', 'y2'], 'u1', name='random') assert sys.name == 'random' - assert ct.namedio._NamedIOSystem._idCounter == 1 + assert ct.namedio.NamedIOSystem._idCounter == 1 assert sys.input_labels == ['u1'] assert sys.output_labels == ['y1', 'y2'] assert sys.state_labels == ['x1', 'x2', 'x3'] assert repr(sys) == \ "['y1', 'y2']>" + + +# List of classes that are expected +fun_instance = { + ct.rss: (ct.InputOutputSystem, ct.LinearIOSystem, ct.StateSpace), + ct.drss: (ct.InputOutputSystem, ct.LinearIOSystem, ct.StateSpace), + ct.FRD: (ct.lti.LTI), + ct.NonlinearIOSystem: (ct.InputOutputSystem), + ct.ss: (ct.InputOutputSystem, ct.LinearIOSystem, ct.StateSpace), + ct.StateSpace: (ct.StateSpace), + ct.tf: (ct.TransferFunction), + ct.TransferFunction: (ct.TransferFunction), +} + +# List of classes that are not expected +fun_notinstance = { + ct.FRD: (ct.InputOutputSystem, ct.LinearIOSystem, ct.StateSpace), + ct.StateSpace: (ct.InputOutputSystem, ct.TransferFunction), + ct.TransferFunction: (ct.InputOutputSystem, ct.StateSpace), +} + + +@pytest.mark.parametrize("fun, args, kwargs", [ + [ct.rss, (4, 1, 1), {}], + [ct.rss, (3, 2, 1), {}], + [ct.drss, (4, 1, 1), {}], + [ct.drss, (3, 2, 1), {}], + [ct.FRD, ([1, 2, 3,], [1, 2, 3]), {}], + [ct.NonlinearIOSystem, + (lambda t, x, u, params: -x, None), + {'inputs': 2, 'outputs':2, 'states':2}], + [ct.ss, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}], + [ct.StateSpace, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}], + [ct.tf, ([1, 2], [3, 4, 5]), {}], + [ct.TransferFunction, ([1, 2], [3, 4, 5]), {}], +]) +def test_io_naming(fun, args, kwargs): + # Reset the ID counter to get uniform generic names + ct.namedio.NamedIOSystem._idCounter = 0 + + # Create the system w/out any names + sys_g = fun(*args, **kwargs) + + # Make sure the class are what we expect + if fun in fun_instance: + assert isinstance(sys_g, fun_instance[fun]) + + if fun in fun_notinstance: + assert not isinstance(sys_g, fun_notinstance[fun]) + + # Make sure the names make sense + assert sys_g.name == 'sys[0]' + assert sys_g.input_labels == [f'u[{i}]' for i in range(sys_g.ninputs)] + assert sys_g.output_labels == [f'y[{i}]' for i in range(sys_g.noutputs)] + if sys_g.nstates: + assert sys_g.state_labels == [f'x[{i}]' for i in range(sys_g.nstates)] + + # + # Reset the names to something else and make sure they stick + # + sys_r = copy(sys_g) + + input_labels = [f'u{i}' for i in range(sys_g.ninputs)] + sys_r.set_inputs(input_labels) + assert sys_r.input_labels == input_labels + + output_labels = [f'y{i}' for i in range(sys_g.noutputs)] + sys_r.set_outputs(output_labels) + assert sys_r.output_labels == output_labels + + if sys_g.nstates: + state_labels = [f'x{i}' for i in range(sys_g.nstates)] + sys_r.set_states(state_labels) + assert sys_r.state_labels == state_labels + + # + # Set names using keywords and make sure they stick + # + + # How the keywords are used depends on the type of system + if fun in (ct.rss, ct.drss): + # Pass the labels instead of the numbers + sys_k = fun(state_labels, output_labels, input_labels, name='mysys') + + elif sys_g.nstates is None: + # Don't pass state labels + sys_k = fun( + *args, inputs=input_labels, outputs=output_labels, name='mysys') + + else: + sys_k = fun( + *args, inputs=input_labels, outputs=output_labels, + states=state_labels, name='mysys') + + assert sys_k.name == 'mysys' + assert sys_k.input_labels == input_labels + assert sys_k.output_labels == output_labels + if sys_g.nstates: + assert sys_k.state_labels == state_labels + + # + # Convert the system to state space and make sure labels transfer + # + if ct.slycot_check() and not isinstance( + sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)): + sys_ss = ct.ss(sys_r) + assert sys_ss != sys_r + assert sys_ss.input_labels == input_labels + assert sys_ss.output_labels == output_labels + + # Reassign system and signal names + sys_ss = ct.ss( + sys_g, inputs=input_labels, outputs=output_labels, name='new') + assert sys_ss.name == 'new' + assert sys_ss.input_labels == input_labels + assert sys_ss.output_labels == output_labels + + # + # Convert the system to a transfer function and make sure labels transfer + # + if not isinstance( + sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)) and \ + ct.slycot_check(): + sys_tf = ct.tf(sys_r) + assert sys_tf != sys_r + assert sys_tf.input_labels == input_labels + assert sys_tf.output_labels == output_labels + + # Reassign system and signal names + sys_tf = ct.tf( + sys_g, inputs=input_labels, outputs=output_labels, name='new') + assert sys_tf.name == 'new' + assert sys_tf.input_labels == input_labels + assert sys_tf.output_labels == output_labels + + +# Internal testing of StateSpace initialization +def test_init_namedif(): + # Set up the initial system + sys = ct.rss(2, 1, 1) + + # Rename the system, inputs, and outouts + sys_new = sys.copy() + ct.StateSpace.__init__( + sys_new, sys, inputs='u', outputs='y', name='new') + assert sys_new.name == 'new' + assert sys_new.input_labels == ['u'] + assert sys_new.output_labels == ['y'] + + # Call constructor without re-initialization + sys_keep = sys.copy() + ct.StateSpace.__init__(sys_keep, sys, init_namedio=False) + assert sys_keep.name == sys_keep.name + assert sys_keep.input_labels == sys_keep.input_labels + assert sys_keep.output_labels == sys_keep.output_labels + + # Make sure that passing an unrecognized keyword generates an error + with pytest.raises(TypeError, match="unrecognized keyword"): + ct.StateSpace.__init__( + sys_keep, sys, inputs='u', outputs='y', init_namedio=False) + +# Test state space conversion +def test_convert_to_statespace(): + # Set up the initial system + sys = ct.tf(ct.rss(2, 1, 1)) + + # Make sure we can rename system name, inputs, outputs + sys_new = ct.ss(sys, inputs='u', outputs='y', name='new') + assert sys_new.name == 'new' + assert sys_new.input_labels == ['u'] + assert sys_new.output_labels == ['y'] + + # Try specifying the state names (via low level test) + with pytest.warns(UserWarning, match="non-unique state space realization"): + sys_new = ct.ss(sys, inputs='u', outputs='y', states=['x1', 'x2']) + assert sys_new.input_labels == ['u'] + assert sys_new.output_labels == ['y'] + assert sys_new.state_labels == ['x1', 'x2'] + + +# Duplicate name warnings +def test_duplicate_sysname(): + # Start with an unnamed system + sys = ct.rss(4, 1, 1) + + # No warnings should be generated if we reuse an an unnamed system + with pytest.warns(None) as record: + res = sys * sys + assert not any([type(msg) == UserWarning for msg in record]) + + # Generate a warning if the system is named + sys = ct.rss(4, 1, 1, name='sys') + with pytest.warns(UserWarning, match="duplicate object found"): + res = sys * sys diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index d2e5a9a70..f7757f2e9 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -125,7 +125,7 @@ def test_constructor(self, sys322ABCD, dt, argfun): @pytest.mark.parametrize("args, exc, errmsg", [((True, ), TypeError, "(can only take in|sys must be) a StateSpace"), - ((1, 2), ValueError, "1, 4, or 5 arguments"), + ((1, 2), TypeError, "1, 4, or 5 arguments"), ((np.ones((3, 2)), np.ones((3, 2)), np.ones((2, 2)), np.ones((2, 2))), ValueError, "A must be square"), @@ -180,16 +180,17 @@ def test_copy_constructor(self): linsys.A[0, 0] = -3 np.testing.assert_allclose(cpysys.A, [[-1]]) # original value + @pytest.mark.skip("obsolete test") def test_copy_constructor_nodt(self, sys322): """Test the copy constructor when an object without dt is passed""" sysin = sample_system(sys322, 1.) - del sysin.dt + del sysin.dt # this is a nonsensical thing to do sys = StateSpace(sysin) assert sys.dt == defaults['control.default_dt'] # test for static gain sysin = StateSpace([], [], [], [[1, 2], [3, 4]], 1.) - del sysin.dt + del sysin.dt # this is a nonsensical thing to do sys = StateSpace(sysin) assert sys.dt is None @@ -570,15 +571,24 @@ def test_scalar_static_gain(self): """ g1 = StateSpace([], [], [], [2]) g2 = StateSpace([], [], [], [3]) + assert g1.dt == None + assert g2.dt == None g3 = g1 * g2 assert 6 == g3.D[0, 0] + assert g3.dt == None + g4 = g1 + g2 assert 5 == g4.D[0, 0] + assert g4.dt == None + g5 = g1.feedback(g2) np.testing.assert_allclose(2. / 7, g5.D[0, 0]) + assert g5.dt == None + g6 = g1.append(g2) np.testing.assert_allclose(np.diag([2, 3]), g6.D) + assert g6.dt == None def test_matrix_static_gain(self): """Regression: can we create matrix static gains?""" diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index cc3b8ec88..cdf302015 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -13,13 +13,13 @@ @pytest.fixture() def sys_dict(): sdict = {} - sdict['ss'] = ct.ss([[-1]], [[1]], [[1]], [[0]]) - sdict['tf'] = ct.tf([1],[0.5, 1]) - sdict['tfx'] = ct.tf([1, 1],[1]) # non-proper transfer function - sdict['frd'] = ct.frd([10+0j, 9 + 1j, 8 + 2j], [1,2,3]) + sdict['ss'] = ct.StateSpace([[-1]], [[1]], [[1]], [[0]]) + sdict['tf'] = ct.TransferFunction([1],[0.5, 1]) + sdict['tfx'] = ct.TransferFunction([1, 1], [1]) # non-proper TF + sdict['frd'] = ct.frd([10+0j, 9 + 1j, 8 + 2j, 7 + 3j], [1, 2, 3, 4]) sdict['lio'] = ct.LinearIOSystem(ct.ss([[-1]], [[5]], [[5]], [[0]])) sdict['ios'] = ct.NonlinearIOSystem( - sdict['lio']._rhs, sdict['lio']._out, 1, 1, 1) + sdict['lio']._rhs, sdict['lio']._out, inputs=1, outputs=1, states=1) sdict['arr'] = np.array([[2.0]]) sdict['flt'] = 3. return sdict @@ -59,39 +59,39 @@ def sys_dict(): rtype_list = ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'flt'] conversion_table = [ # op left ss tf frd lio ios arr flt - ('add', 'ss', ['ss', 'ss', 'xrd', 'ss', 'ios', 'ss', 'ss' ]), - ('add', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), - ('add', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'xrd']), + ('add', 'ss', ['ss', 'ss', 'frd', 'ss', 'ios', 'ss', 'ss' ]), + ('add', 'tf', ['tf', 'tf', 'frd', 'lio', 'ios', 'tf', 'tf' ]), + ('add', 'frd', ['frd', 'frd', 'frd', 'frd', 'E', 'frd', 'frd']), ('add', 'lio', ['lio', 'lio', 'xrd', 'lio', 'ios', 'lio', 'lio']), ('add', 'ios', ['ios', 'ios', 'E', 'ios', 'ios', 'ios', 'ios']), - ('add', 'arr', ['ss', 'tf', 'xrd', 'lio', 'ios', 'arr', 'arr']), - ('add', 'flt', ['ss', 'tf', 'xrd', 'lio', 'ios', 'arr', 'flt']), + ('add', 'arr', ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'arr']), + ('add', 'flt', ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'flt']), # op left ss tf frd lio ios arr flt - ('sub', 'ss', ['ss', 'ss', 'xrd', 'ss', 'ios', 'ss', 'ss' ]), - ('sub', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), - ('sub', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'xrd']), + ('sub', 'ss', ['ss', 'ss', 'frd', 'ss', 'ios', 'ss', 'ss' ]), + ('sub', 'tf', ['tf', 'tf', 'frd', 'lio', 'ios', 'tf', 'tf' ]), + ('sub', 'frd', ['frd', 'frd', 'frd', 'frd', 'E', 'frd', 'frd']), ('sub', 'lio', ['lio', 'lio', 'xrd', 'lio', 'ios', 'lio', 'lio']), ('sub', 'ios', ['ios', 'ios', 'E', 'ios', 'ios', 'ios', 'ios']), - ('sub', 'arr', ['ss', 'tf', 'xrd', 'lio', 'ios', 'arr', 'arr']), - ('sub', 'flt', ['ss', 'tf', 'xrd', 'lio', 'ios', 'arr', 'flt']), + ('sub', 'arr', ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'arr']), + ('sub', 'flt', ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'flt']), # op left ss tf frd lio ios arr flt - ('mul', 'ss', ['ss', 'ss', 'xrd', 'ss', 'ios', 'ss', 'ss' ]), - ('mul', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), - ('mul', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'frd']), + ('mul', 'ss', ['ss', 'ss', 'frd', 'ss', 'ios', 'ss', 'ss' ]), + ('mul', 'tf', ['tf', 'tf', 'frd', 'lio', 'ios', 'tf', 'tf' ]), + ('mul', 'frd', ['frd', 'frd', 'frd', 'frd', 'E', 'frd', 'frd']), ('mul', 'lio', ['lio', 'lio', 'xrd', 'lio', 'ios', 'lio', 'lio']), ('mul', 'ios', ['ios', 'ios', 'E', 'ios', 'ios', 'ios', 'ios']), - ('mul', 'arr', ['ss', 'tf', 'xrd', 'lio', 'ios', 'arr', 'arr']), + ('mul', 'arr', ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'arr']), ('mul', 'flt', ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'flt']), # op left ss tf frd lio ios arr flt - ('truediv', 'ss', ['xs', 'tf', 'xrd', 'xio', 'xos', 'xs', 'xs' ]), + ('truediv', 'ss', ['xs', 'tf', 'frd', 'xio', 'xos', 'xs', 'xs' ]), ('truediv', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), - ('truediv', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'frd']), - ('truediv', 'lio', ['xio', 'tf', 'xrd', 'xio', 'xio', 'xio', 'xio']), + ('truediv', 'frd', ['frd', 'frd', 'frd', 'frd', 'E', 'frd', 'frd']), + ('truediv', 'lio', ['xio', 'tf', 'frd', 'xio', 'xio', 'xio', 'xio']), ('truediv', 'ios', ['xos', 'xos', 'E', 'xos', 'xos' 'xos', 'xos']), - ('truediv', 'arr', ['xs', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), + ('truediv', 'arr', ['xs', 'tf', 'frd', 'xio', 'xos', 'arr', 'arr']), ('truediv', 'flt', ['xs', 'tf', 'frd', 'xio', 'xos', 'arr', 'flt'])] # Now create list of the tests we actually want to run @@ -147,9 +147,8 @@ def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): # Note: tfx = non-proper transfer function, order(num) > order(den) # -type_list = ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] +type_list = ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] conversion_table = [ - # L \ R ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] ('ss', ['ss', 'ss', 'tf' 'frd', 'lio', 'ios', 'ss', 'ss' ]), ('tf', ['tf', 'tf', 'tf' 'frd', 'lio', 'ios', 'tf', 'tf' ]), ('tfx', ['tf', 'tf', 'tf', 'frd', 'E', 'E', 'tf', 'tf' ]), @@ -161,6 +160,7 @@ def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): @pytest.mark.skip(reason="future test; conversions not yet fully implemented") # @pytest.mark.parametrize("opname", ['add', 'sub', 'mul', 'truediv']) +# @pytest.mark.parametrize("opname", ['add', 'sub', 'mul']) # @pytest.mark.parametrize("ltype", type_list) # @pytest.mark.parametrize("rtype", type_list) def test_binary_op_type_conversions(opname, ltype, rtype, sys_dict): @@ -188,6 +188,6 @@ def test_binary_op_type_conversions(opname, ltype, rtype, sys_dict): # Make sure that input, output, and state names make sense assert len(result.input_labels) == result.ninputs - assert len(result.output_labels) == result.outputs + assert len(result.output_labels) == result.noutputs if result.nstates is not None: - assert len(result.state_labels) == result.states + assert len(result.state_labels) == result.nstates diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index f2eb33f6a..79273f31b 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -39,7 +39,7 @@ def test_constructor_bad_input_type(self): TransferFunction([1]) # Too many arguments - with pytest.raises(ValueError): + with pytest.raises(TypeError): TransferFunction(1, 2, 3, 4) # Different numbers of elements in numerator rows @@ -85,18 +85,19 @@ def test_constructor_zero_denominator(self): TransferFunction([[[1.], [2., 3.]], [[-1., 4.], [3., 2.]]], [[[1., 0.], [0.]], [[0., 0.], [2.]]]) + @pytest.mark.skip("outdated test") def test_constructor_nodt(self): """Test the constructor when an object without dt is passed""" sysin = TransferFunction([[[0., 1.], [2., 3.]]], [[[5., 2.], [3., 0.]]]) - del sysin.dt + del sysin.dt # this doesn't make sense and now breaks sys = TransferFunction(sysin) assert sys.dt == defaults['control.default_dt'] # test for static gain sysin = TransferFunction([[[2.], [3.]]], [[[1.], [.1]]]) - del sysin.dt + del sysin.dt # this doesn't make sense and now breaks sys = TransferFunction(sysin) assert sys.dt is None diff --git a/control/timeresp.py b/control/timeresp.py index fe62387dc..aa1261ccd 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -80,7 +80,7 @@ from . import config from .exception import pandas_check -from .lti import isctime, isdtime +from .namedio import isctime, isdtime from .statesp import StateSpace, _convert_to_statespace, _mimo2simo, _mimo2siso from .xferfcn import TransferFunction diff --git a/control/xferfcn.py b/control/xferfcn.py index 069a90926..93a66ce9d 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -59,10 +59,10 @@ from warnings import warn from itertools import chain from re import sub -from .lti import LTI, common_timebase, isdtime, _process_frequency_response +from .lti import LTI, _process_frequency_response +from .namedio import common_timebase, isdtime, _process_namedio_keywords from .exception import ControlMIMONotImplemented from .frdata import FrequencyResponseData -from .namedio import _NamedIOSystem, _process_signal_list from . import config __all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata'] @@ -72,7 +72,7 @@ _xferfcn_defaults = {} -class TransferFunction(LTI, _NamedIOSystem): +class TransferFunction(LTI): """TransferFunction(num, den[, dt]) A class for representing transfer functions. @@ -163,13 +163,22 @@ def __init__(self, *args, **kwargs): (continuous or discrete). """ - args = deepcopy(args) + # + # Process positional arguments + # if len(args) == 2: # The user provided a numerator and a denominator. - (num, den) = args + num, den = args + elif len(args) == 3: # Discrete time transfer function - (num, den, dt) = args + num, den, dt = args + if 'dt' in kwargs: + warn("received multiple dt arguments, " + "using positional arg dt = %s" % dt) + kwargs['dt'] = dt + args = args[:-1] + elif len(args) == 1: # Use the copy constructor. if not isinstance(args[0], TransferFunction): @@ -178,43 +187,68 @@ def __init__(self, *args, **kwargs): % type(args[0])) num = args[0].num den = args[0].den + else: - raise ValueError("Needs 1, 2 or 3 arguments; received %i." + raise TypeError("Needs 1, 2 or 3 arguments; received %i." % len(args)) num = _clean_part(num) den = _clean_part(den) - inputs = len(num[0]) - outputs = len(num) + # + # Process keyword arguments + # + + # Determine if the transfer function is static (needed for dt) + static = True + for col in num + den: + for poly in col: + if len(poly) > 1: + static = False + defaults = args[0] if len(args) == 1 else \ + {'inputs': len(num[0]), 'outputs': len(num)} + + name, inputs, outputs, states, dt = _process_namedio_keywords( + kwargs, defaults, static=static, end=True) + if states: + raise TypeError( + "states keyword not allowed for transfer functions") + + # Initialize LTI (NamedIOSystem) object + super().__init__( + name=name, inputs=inputs, outputs=outputs, dt=dt) + + # + # Check to make sure everything is consistent + # # Make sure numerator and denominator matrices have consistent sizes - if inputs != len(den[0]): + if self.ninputs != len(den[0]): raise ValueError( "The numerator has %i input(s), but the denominator has " - "%i input(s)." % (inputs, len(den[0]))) - if outputs != len(den): + "%i input(s)." % (self.ninputs, len(den[0]))) + if self.noutputs != len(den): raise ValueError( "The numerator has %i output(s), but the denominator has " - "%i output(s)." % (outputs, len(den))) + "%i output(s)." % (self.noutputs, len(den))) # Additional checks/updates on structure of the transfer function - for i in range(outputs): + for i in range(self.noutputs): # Make sure that each row has the same number of columns - if len(num[i]) != inputs: + if len(num[i]) != self.ninputs: raise ValueError( "Row 0 of the numerator matrix has %i elements, but row " - "%i has %i." % (inputs, i, len(num[i]))) - if len(den[i]) != inputs: + "%i has %i." % (self.ninputs, i, len(num[i]))) + if len(den[i]) != self.ninputs: raise ValueError( "Row 0 of the denominator matrix has %i elements, but row " - "%i has %i." % (inputs, i, len(den[i]))) + "%i has %i." % (self.ninputs, i, len(den[i]))) # Check for zeros in numerator or denominator # TODO: Right now these checks are only done during construction. # It might be worthwhile to think of a way to perform checks if the # user modifies the transfer function after construction. - for j in range(inputs): + for j in range(self.ninputs): # Check that we don't have any zero denominators. zeroden = True for k in den[i][j]: @@ -235,42 +269,16 @@ def __init__(self, *args, **kwargs): if zeronum: den[i][j] = ones(1) - super().__init__(inputs, outputs) + # Store the numerator and denominator self.num = num self.den = den + # + # Final processing + # + # Truncate leading zeros self._truncatecoeff() - # get dt - if len(args) == 2: - # no dt given in positional arguments - if 'dt' in kwargs: - dt = kwargs.pop('dt') - elif self._isstatic(): - dt = None - else: - dt = config.defaults['control.default_dt'] - elif len(args) == 3: - # Discrete time transfer function - if 'dt' in kwargs: - warn('received multiple dt arguments, ' - 'using positional arg dt=%s' % dt) - kwargs.pop('dt') - elif len(args) == 1: - # TODO: not sure this can ever happen since dt is always present - try: - dt = args[0].dt - except AttributeError: - if self._isstatic(): - dt = None - else: - dt = config.defaults['control.default_dt'] - self.dt = dt - - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - # # Class attributes # @@ -530,6 +538,12 @@ def __add__(self, other): """Add two LTI objects (parallel connection).""" from .statesp import StateSpace + # Check to see if the right operator has priority + if getattr(other, '__array_priority__', None) and \ + getattr(self, '__array_priority__', None) and \ + other.__array_priority__ > self.__array_priority__: + return other.__radd__(self) + # Convert the second argument to a transfer function. if isinstance(other, StateSpace): other = _convert_to_transfer_function(other) @@ -575,6 +589,12 @@ def __rsub__(self, other): def __mul__(self, other): """Multiply two LTI objects (serial connection).""" + # Check to see if the right operator has priority + if getattr(other, '__array_priority__', None) and \ + getattr(self, '__array_priority__', None) and \ + other.__array_priority__ > self.__array_priority__: + return other.__rmul__(self) + # Convert the second argument to a transfer function. if isinstance(other, (int, float, complex, np.number)): other = _convert_to_transfer_function(other, inputs=self.ninputs, @@ -1227,10 +1247,9 @@ def _c2d_matched(sysC, Ts): sysDnum, sysDden = zpk2tf(zzeros, zpoles, gain) return TransferFunction(sysDnum, sysDden, Ts) + # Utility function to convert a transfer function polynomial to a string # Borrowed from poly1d library - - def _tf_polynomial_to_string(coeffs, var='s'): """Convert a transfer function polynomial to a string""" @@ -1320,6 +1339,9 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1): If sys is an array-like type, then it is converted to a constant-gain transfer function. + Note: no renaming of inputs and outputs is performed; this should be done + by the calling function. + >>> sys = _convert_to_transfer_function([[1., 0.], [2., 3.]]) In this example, the numerator matrix will be @@ -1328,6 +1350,7 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1): """ from .statesp import StateSpace + kwargs = {} if isinstance(sys, TransferFunction): return sys @@ -1375,13 +1398,16 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1): num = squeeze(num) # Convert to 1D array den = squeeze(den) # Probably not needed - return TransferFunction(num, den, sys.dt) + return TransferFunction( + num, den, sys.dt, inputs=sys.input_labels, + outputs=sys.output_labels) elif isinstance(sys, (int, float, complex, np.number)): num = [[[sys] for j in range(inputs)] for i in range(outputs)] den = [[[1] for j in range(inputs)] for i in range(outputs)] - return TransferFunction(num, den) + return TransferFunction( + num, den, inputs=inputs, outputs=outputs) elif isinstance(sys, FrequencyResponseData): raise TypeError("Can't convert given FRD to TransferFunction system.") @@ -1393,6 +1419,7 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1): num = [[[D[i, j]] for j in range(inputs)] for i in range(outputs)] den = [[[1] for j in range(inputs)] for i in range(outputs)] return TransferFunction(num, den) + except Exception: raise TypeError("Can't convert given type to TransferFunction system.") @@ -1442,6 +1469,16 @@ def tf(*args, **kwargs): out: :class:`TransferFunction` The new linear system + Other Parameters + ---------------- + inputs, outputs : str, or list of str, optional + List of strings that name the individual signals of the transformed + system. If not given, the inputs and outputs are the same as the + original system. + name : string, optional + System name. If unspecified, a generic name is generated + with a unique integer id. + Raises ------ ValueError @@ -1488,7 +1525,8 @@ def tf(*args, **kwargs): if len(args) == 2 or len(args) == 3: return TransferFunction(*args, **kwargs) - elif len(args) == 1: + + elif len(args) == 1 and isinstance(args[0], str): # Make sure there were no extraneous keywords if kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) @@ -1499,12 +1537,14 @@ def tf(*args, **kwargs): elif args[0] == 'z': return TransferFunction.z + elif len(args) == 1: from .statesp import StateSpace sys = args[0] if isinstance(sys, StateSpace): - return ss2tf(sys) + return ss2tf(sys, **kwargs) elif isinstance(sys, TransferFunction): - return deepcopy(sys) + # Use copy constructor + return TransferFunction(sys, **kwargs) else: raise TypeError("tf(sys): sys must be a StateSpace or " "TransferFunction object. It is %s." % type(sys)) @@ -1547,6 +1587,16 @@ def ss2tf(*args, **kwargs): out: TransferFunction New linear system in transfer function form + Other Parameters + ---------------- + inputs, outputs : str, or list of str, optional + List of strings that name the individual signals of the transformed + system. If not given, the inputs and outputs are the same as the + original system. + name : string, optional + System name. If unspecified, a generic name is generated + with a unique integer id. + Raises ------ ValueError @@ -1579,14 +1629,11 @@ def ss2tf(*args, **kwargs): # Assume we were given the A, B, C, D matrix and (optional) dt return _convert_to_transfer_function(StateSpace(*args, **kwargs)) - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - if len(args) == 1: sys = args[0] if isinstance(sys, StateSpace): - return _convert_to_transfer_function(sys) + return TransferFunction( + _convert_to_transfer_function(sys), **kwargs) else: raise TypeError( "ss2tf(sys): sys must be a StateSpace object. It is %s." diff --git a/doc/conventions.rst b/doc/conventions.rst index 462a71408..de1fc5f57 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -29,9 +29,9 @@ of linear time-invariant (LTI) systems: where u is the input, y is the output, and x is the state. -To create a state space system, use the :class:`StateSpace` constructor: +To create a state space system, use the :fun:`ss` function: - sys = StateSpace(A, B, C, D) + sys = ct.ss(A, B, C, D) State space systems can be manipulated using standard arithmetic operations as well as the :func:`feedback`, :func:`parallel`, and :func:`series` @@ -51,10 +51,9 @@ transfer functions where n is generally greater than or equal to m (for a proper transfer function). -To create a transfer function, use the :class:`TransferFunction` -constructor: +To create a transfer function, use the :func:`tf` function: - sys = TransferFunction(num, den) + sys = ct.tf(num, den) Transfer functions can be manipulated using standard arithmetic operations as well as the :func:`feedback`, :func:`parallel`, and :func:`series` @@ -89,14 +88,16 @@ Only the :class:`StateSpace`, :class:`TransferFunction`, and :class:`InputOutputSystem` classes allow explicit representation of discrete time systems. -Systems must have compatible timebases in order to be combined. A discrete time -system with unspecified sampling time (`dt = True`) can be combined with a system -having a specified sampling time; the result will be a discrete time system with the sample time of the latter -system. Similarly, a system with timebase `None` can be combined with a system having a specified -timebase; the result will have the timebase of the latter system. For continuous -time systems, the :func:`sample_system` function or the :meth:`StateSpace.sample` and :meth:`TransferFunction.sample` methods -can be used to create a discrete time system from a continuous time system. -See :ref:`utility-and-conversions`. The default value of 'dt' can be changed by +Systems must have compatible timebases in order to be combined. A discrete +time system with unspecified sampling time (`dt = True`) can be combined with +a system having a specified sampling time; the result will be a discrete time +system with the sample time of the latter system. Similarly, a system with +timebase `None` can be combined with a system having a specified timebase; the +result will have the timebase of the latter system. For continuous time +systems, the :func:`sample_system` function or the :meth:`StateSpace.sample` +and :meth:`TransferFunction.sample` methods can be used to create a discrete +time system from a continuous time system. See +:ref:`utility-and-conversions`. The default value of 'dt' can be changed by changing the value of ``control.config.defaults['control.default_dt']``. Conversion between representations @@ -129,11 +130,6 @@ and :func:`initial_response`. Thus, all 2D values must be transposed when they are used with functions from `scipy.signal`_. -Types: - - * **Arguments** can be **arrays**, **matrices**, or **nested lists**. - * **Return values** are **arrays** (not matrices). - The time vector is a 1D array with shape (n, ):: T = [t1, t2, t3, ..., tn ] @@ -170,8 +166,8 @@ Functions that return time responses (e.g., :func:`forced_response`, response. These data can be accessed via the ``time``, ``outputs``, ``states`` and ``inputs`` properties:: - sys = rss(4, 1, 1) - response = step_response(sys) + sys = ct.rss(4, 1, 1) + response = ct.step_response(sys) plot(response.time, response.outputs) The dimensions of the response properties depend on the function being @@ -185,12 +181,12 @@ The time response functions can also be assigned to a tuple, which extracts the time and output (and optionally the state, if the `return_x` keyword is used). This allows simple commands for plotting:: - t, y = step_response(sys) + t, y = ct.step_response(sys) plot(t, y) -The output of a MIMO system can be plotted like this:: +The output of a MIMO LTI system can be plotted like this:: - t, y = forced_response(sys, t, u) + t, y = ct.forced_response(sys, t, u) plot(t, y[0], label='y_0') plot(t, y[1], label='y_1') @@ -201,6 +197,16 @@ response, can be computed like this:: ft = D @ U +Finally, the `to_pandas()` function can be used to create a pandas dataframe: + + df = response.to_pandas() + +The column labels for the data frame are `time` and the labels for the input, +output, and state signals (`u[i]`, `y[i]`, and `x[i]` by default, but these +can be changed using the `inputs`, `outputs`, and `states` keywords when +constructing the system, as described in :func:`ss`, :func:`tf`, and other +system creation function. Note that when exporting to pandas, "rows" in the +data frame correspond to time and "cols" (DataSeries) correspond to signals. .. currentmodule:: control .. _package-configuration-parameters: @@ -218,14 +224,14 @@ element of the `control.config.defaults` dictionary: .. code-block:: python - control.config.defaults['module.parameter'] = value + ct.config.defaults['module.parameter'] = value The `~control.config.set_defaults` function can also be used to set multiple configuration parameters at the same time: .. code-block:: python - control.config.set_defaults('module', param1=val1, param2=val2, ...] + ct.config.set_defaults('module', param1=val1, param2=val2, ...] Finally, there are also functions available set collections of variables based on standard configurations. diff --git a/doc/iosys.rst b/doc/iosys.rst index 41e37cfec..1da7f5884 100644 --- a/doc/iosys.rst +++ b/doc/iosys.rst @@ -13,25 +13,22 @@ The dynamics of the system can be in continuous or discrete time. To simulate an input/output system, use the :func:`~control.input_output_response` function:: - t, y = input_output_response(io_sys, T, U, X0, params) + t, y = ct.input_output_response(io_sys, T, U, X0, params) An input/output system can be linearized around an equilibrium point to obtain a :class:`~control.StateSpace` linear system. Use the :func:`~control.find_eqpt` function to obtain an equilibrium point and the :func:`~control.linearize` function to linearize about that equilibrium point:: - xeq, ueq = find_eqpt(io_sys, X0, U0) - ss_sys = linearize(io_sys, xeq, ueq) + xeq, ueq = ct.find_eqpt(io_sys, X0, U0) + ss_sys = ct.linearize(io_sys, xeq, ueq) -Input/output systems can be created from state space LTI systems by using the -:class:`~control.LinearIOSystem` class`:: - - io_sys = LinearIOSystem(ss_sys) - -Nonlinear input/output systems can be created using the -:class:`~control.NonlinearIOSystem` class, which requires the definition of an -update function (for the right hand side of the differential or different -equation) and and output function (computes the outputs from the state):: +Input/output systems are automatically created for state space LTI systems +when using the :func:`ss` function. Nonlinear input/output systems can be +created using the :class:`~control.NonlinearIOSystem` class, which requires +the definition of an update function (for the right hand side of the +differential or different equation) and an output function (computes the +outputs from the state):: io_sys = NonlinearIOSystem(updfcn, outfcn, inputs=M, outputs=P, states=N) @@ -64,7 +61,7 @@ We begin by defining the dynamics of the system .. code-block:: python - import control + import control as ct import numpy as np import matplotlib.pyplot as plt @@ -94,7 +91,7 @@ We now create an input/output system using these dynamics: .. code-block:: python - io_predprey = control.NonlinearIOSystem( + io_predprey = ct.NonlinearIOSystem( predprey_rhs, None, inputs=('u'), outputs=('H', 'L'), states=('H', 'L'), name='predprey') @@ -110,7 +107,7 @@ of the system: T = np.linspace(0, 70, 500) # Simulation 70 years of time # Simulate the system - t, y = control.input_output_response(io_predprey, T, 0, X0) + t, y = ct.input_output_response(io_predprey, T, 0, X0) # Plot the response plt.figure(1) @@ -125,9 +122,9 @@ system and computing the linearization about that point. .. code-block:: python - eqpt = control.find_eqpt(io_predprey, X0, 0) + eqpt = ct.find_eqpt(io_predprey, X0, 0) xeq = eqpt[0] # choose the nonzero equilibrium point - lin_predprey = control.linearize(io_predprey, xeq, 0) + lin_predprey = ct.linearize(io_predprey, xeq, 0) We next compute a controller that stabilizes the equilibrium point using eigenvalue placement and computing the feedforward gain using the number of @@ -135,7 +132,7 @@ lynxes as the desired output (following FBS2e, Example 7.5): .. code-block:: python - K = control.place(lin_predprey.A, lin_predprey.B, [-0.1, -0.2]) + K = ct.place(lin_predprey.A, lin_predprey.B, [-0.1, -0.2]) A, B = lin_predprey.A, lin_predprey.B C = np.array([[0, 1]]) # regulated output = number of lynxes kf = -1/(C @ np.linalg.inv(A - B @ K) @ B) @@ -147,7 +144,7 @@ constructed using the `~control.ios.NonlinearIOSystem` class: .. code-block:: python - io_controller = control.NonlinearIOSystem( + io_controller = ct.NonlinearIOSystem( None, lambda t, x, u, params: -K @ (u[1:] - xeq) + kf * (u[0] - xeq[1]), inputs=('Ld', 'u1', 'u2'), outputs=1, name='control') @@ -161,7 +158,7 @@ function: .. code-block:: python - io_closed = control.interconnect( + io_closed = ct.interconnect( [io_predprey, io_controller], # systems connections=[ ['predprey.u', 'control.y[0]'], @@ -177,7 +174,7 @@ Finally, we simulate the closed loop system: .. code-block:: python # Simulate the system - t, y = control.input_output_response(io_closed, T, 30, [15, 20]) + t, y = ct.input_output_response(io_closed, T, 30, [15, 20]) # Plot the response plt.figure(2) @@ -245,10 +242,10 @@ interconnecting systems, especially when combined with the :func:`~control.summing_junction` function. For example, the following code will create a unity gain, negative feedback system:: - P = control.tf2io(control.tf(1, [1, 0]), inputs='u', outputs='y') - C = control.tf2io(control.tf(10, [1, 1]), inputs='e', outputs='u') - sumblk = control.summing_junction(inputs=['r', '-y'], output='e') - T = control.interconnect([P, C, sumblk], inplist='r', outlist='y') + P = ct.tf2io([1], [1, 0], inputs='u', outputs='y') + C = ct.tf2io([10], [1, 1], inputs='e', outputs='u') + sumblk = ct.summing_junction(inputs=['r', '-y'], output='e') + T = ct.interconnect([P, C, sumblk], inplist='r', outlist='y') If a signal name appears in multiple outputs then that signal will be summed when it is interconnected. Similarly, if a signal name appears in multiple diff --git a/doc/optimal.rst b/doc/optimal.rst index 8da08e7af..bb952e9cc 100644 --- a/doc/optimal.rst +++ b/doc/optimal.rst @@ -99,7 +99,10 @@ The optimal control module provides a means of computing optimal trajectories for nonlinear systems and implementing optimization-based controllers, including model predictive control. It follows the basic problem setup described above, but carries out all computations in *discrete -time* (so that integrals become sums) and over a *finite horizon*. +time* (so that integrals become sums) and over a *finite horizon*. To local +the optimal control modules, import `control.optimal`: + + import control.optimal as obc To describe an optimal control problem we need an input/output system, a time horizon, a cost function, and (optionally) a set of constraints on the diff --git a/examples/pvtol-lqr.py b/examples/pvtol-lqr.py index 8654c77ad..8a9ff55d9 100644 --- a/examples/pvtol-lqr.py +++ b/examples/pvtol-lqr.py @@ -9,8 +9,8 @@ import os import numpy as np -import matplotlib.pyplot as plt # MATLAB plotting functions -from control.matlab import * # MATLAB-like functions +import matplotlib.pyplot as plt # MATLAB-like plotting functions +import control as ct # # System dynamics @@ -28,14 +28,13 @@ # State space dynamics xe = [0, 0, 0, 0, 0, 0] # equilibrium point of interest -ue = [0, m*g] # (note these are lists, not matrices) +ue = [0, m * g] # (note these are lists, not matrices) # TODO: The following objects need converting from np.matrix to np.array # This will involve re-working the subsequent equations as the shapes # See below. -# Dynamics matrix (use matrix type so that * works for multiplication) -A = np.matrix( +A = np.array( [[0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1], @@ -45,7 +44,7 @@ ) # Input matrix -B = np.matrix( +B = np.array( [[0, 0], [0, 0], [0, 0], [np.cos(xe[2])/m, -np.sin(xe[2])/m], [np.sin(xe[2])/m, np.cos(xe[2])/m], @@ -53,8 +52,8 @@ ) # Output matrix -C = np.matrix([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]]) -D = np.matrix([[0, 0], [0, 0]]) +C = np.array([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]]) +D = np.array([[0, 0], [0, 0]]) # # Construct inputs and outputs corresponding to steps in xy position @@ -74,8 +73,8 @@ # so that xd corresponds to the desired steady state. # -xd = np.matrix([[1], [0], [0], [0], [0], [0]]) -yd = np.matrix([[0], [1], [0], [0], [0], [0]]) +xd = np.array([[1], [0], [0], [0], [0], [0]]) +yd = np.array([[0], [1], [0], [0], [0], [0]]) # # Extract the relevant dynamics for use with SISO library @@ -93,14 +92,14 @@ # Decoupled dynamics Ax = A[np.ix_(lat, lat)] -Bx = B[lat, 0] -Cx = C[0, lat] -Dx = D[0, 0] +Bx = B[np.ix_(lat, [0])] +Cx = C[np.ix_([0], lat)] +Dx = D[np.ix_([0], [0])] Ay = A[np.ix_(alt, alt)] -By = B[alt, 1] -Cy = C[1, alt] -Dy = D[1, 1] +By = B[np.ix_(alt, [1])] +Cy = C[np.ix_([1], alt)] +Dy = D[np.ix_([1], [1])] # Label the plot plt.clf() @@ -113,44 +112,24 @@ # Start with a diagonal weighting Qx1 = np.diag([1, 1, 1, 1, 1, 1]) Qu1a = np.diag([1, 1]) -K, X, E = lqr(A, B, Qx1, Qu1a) -K1a = np.matrix(K) +K1a, X, E = ct.lqr(A, B, Qx1, Qu1a) # Close the loop: xdot = Ax - B K (x-xd) +# # Note: python-control requires we do this 1 input at a time # H1a = ss(A-B*K1a, B*K1a*concatenate((xd, yd), axis=1), C, D); -# (T, Y) = step(H1a, T=np.linspace(0,10,100)); - -# TODO: The following equations will need modifying when converting from np.matrix to np.array -# because the results and even intermediate calculations will be different with numpy arrays -# For example: -# Bx = B[lat, 0] -# Will need to be changed to: -# Bx = B[lat, 0].reshape(-1, 1) -# (if we want it to have the same shape as before) - -# For reference, here is a list of the correct shapes of these objects: -# A: (6, 6) -# B: (6, 2) -# C: (2, 6) -# D: (2, 2) -# xd: (6, 1) -# yd: (6, 1) -# Ax: (4, 4) -# Bx: (4, 1) -# Cx: (1, 4) -# Dx: () -# Ay: (2, 2) -# By: (2, 1) -# Cy: (1, 2) +# (T, Y) = step_response(H1a, T=np.linspace(0,10,100)); +# # Step response for the first input -H1ax = ss(Ax - Bx*K1a[0, lat], Bx*K1a[0, lat]*xd[lat, :], Cx, Dx) -Yx, Tx = step(H1ax, T=np.linspace(0, 10, 100)) +H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)], + Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx) +Tx, Yx = ct.step_response(H1ax, T=np.linspace(0, 10, 100)) # Step response for the second input -H1ay = ss(Ay - By*K1a[1, alt], By*K1a[1, alt]*yd[alt, :], Cy, Dy) -Yy, Ty = step(H1ay, T=np.linspace(0, 10, 100)) +H1ay = ct.ss(Ay - By @ K1a[np.ix_([1], alt)], + By @ K1a[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy) +Ty, Yy = ct.step_response(H1ay, T=np.linspace(0, 10, 100)) plt.subplot(221) plt.title("Identity weights") @@ -164,20 +143,23 @@ # Look at different input weightings Qu1a = np.diag([1, 1]) -K1a, X, E = lqr(A, B, Qx1, Qu1a) -H1ax = ss(Ax - Bx*K1a[0, lat], Bx*K1a[0, lat]*xd[lat, :], Cx, Dx) +K1a, X, E = ct.lqr(A, B, Qx1, Qu1a) +H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)], + Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx) Qu1b = (40 ** 2)*np.diag([1, 1]) -K1b, X, E = lqr(A, B, Qx1, Qu1b) -H1bx = ss(Ax - Bx*K1b[0, lat], Bx*K1b[0, lat]*xd[lat, :], Cx, Dx) +K1b, X, E = ct.lqr(A, B, Qx1, Qu1b) +H1bx = ct.ss(Ax - Bx @ K1b[np.ix_([0], lat)], + Bx @ K1b[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx) Qu1c = (200 ** 2)*np.diag([1, 1]) -K1c, X, E = lqr(A, B, Qx1, Qu1c) -H1cx = ss(Ax - Bx*K1c[0, lat], Bx*K1c[0, lat]*xd[lat, :], Cx, Dx) +K1c, X, E = ct.lqr(A, B, Qx1, Qu1c) +H1cx = ct.ss(Ax - Bx @ K1c[np.ix_([0], lat)], + Bx @ K1c[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx) -[Y1, T1] = step(H1ax, T=np.linspace(0, 10, 100)) -[Y2, T2] = step(H1bx, T=np.linspace(0, 10, 100)) -[Y3, T3] = step(H1cx, T=np.linspace(0, 10, 100)) +T1, Y1 = ct.step_response(H1ax, T=np.linspace(0, 10, 100)) +T2, Y2 = ct.step_response(H1bx, T=np.linspace(0, 10, 100)) +T3, Y3 = ct.step_response(H1cx, T=np.linspace(0, 10, 100)) plt.subplot(222) plt.title("Effect of input weights") @@ -189,21 +171,22 @@ plt.axis([0, 10, -0.1, 1.4]) # arcarrow([1.3, 0.8], [5, 0.45], -6) -plt.text(5.3, 0.4, 'rho') +plt.text(5.3, 0.4, r'$\rho$') # Output weighting - change Qx to use outputs -Qx2 = C.T*C -Qu2 = 0.1*np.diag([1, 1]) -K, X, E = lqr(A, B, Qx2, Qu2) -K2 = np.matrix(K) +Qx2 = C.T @ C +Qu2 = 0.1 * np.diag([1, 1]) +K2, X, E = ct.lqr(A, B, Qx2, Qu2) -H2x = ss(Ax - Bx*K2[0, lat], Bx*K2[0, lat]*xd[lat, :], Cx, Dx) -H2y = ss(Ay - By*K2[1, alt], By*K2[1, alt]*yd[alt, :], Cy, Dy) +H2x = ct.ss(Ax - Bx @ K2[np.ix_([0], lat)], + Bx @ K2[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx) +H2y = ct.ss(Ay - By @ K2[np.ix_([1], alt)], + By @ K2[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy) plt.subplot(223) plt.title("Output weighting") -[Y2x, T2x] = step(H2x, T=np.linspace(0, 10, 100)) -[Y2y, T2y] = step(H2y, T=np.linspace(0, 10, 100)) +T2x, Y2x = ct.step_response(H2x, T=np.linspace(0, 10, 100)) +T2y, Y2y = ct.step_response(H2y, T=np.linspace(0, 10, 100)) plt.plot(T2x.T, Y2x.T, T2y.T, Y2y.T) plt.ylabel('position') plt.xlabel('time') @@ -220,19 +203,21 @@ Qx3 = np.diag([100, 10, 2*np.pi/5, 0, 0, 0]) Qu3 = 0.1*np.diag([1, 10]) -(K, X, E) = lqr(A, B, Qx3, Qu3) -K3 = np.matrix(K) +K3, X, E = ct.lqr(A, B, Qx3, Qu3) -H3x = ss(Ax - Bx*K3[0, lat], Bx*K3[0, lat]*xd[lat, :], Cx, Dx) -H3y = ss(Ay - By*K3[1, alt], By*K3[1, alt]*yd[alt, :], Cy, Dy) +H3x = ct.ss(Ax - Bx @ K3[np.ix_([0], lat)], + Bx @ K3[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx) +H3y = ct.ss(Ay - By @ K3[np.ix_([1], alt)], + By @ K3[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy) plt.subplot(224) -# step(H3x, H3y, 10) -[Y3x, T3x] = step(H3x, T=np.linspace(0, 10, 100)) -[Y3y, T3y] = step(H3y, T=np.linspace(0, 10, 100)) +# step_response(H3x, H3y, 10) +T3x, Y3x = ct.step_response(H3x, T=np.linspace(0, 10, 100)) +T3y, Y3y = ct.step_response(H3y, T=np.linspace(0, 10, 100)) plt.plot(T3x.T, Y3x.T, T3y.T, Y3y.T) plt.title("Physically motivated weights") plt.xlabel('time') plt.legend(('x', 'y'), loc='lower right') +plt.tight_layout() if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: plt.show() diff --git a/examples/pvtol-nested.py b/examples/pvtol-nested.py index 24cd7d1c5..040b4a1f4 100644 --- a/examples/pvtol-nested.py +++ b/examples/pvtol-nested.py @@ -9,8 +9,8 @@ # import os -import matplotlib.pyplot as plt # MATLAB plotting functions -from control.matlab import * # MATLAB-like functions +import matplotlib.pyplot as plt # MATLAB-like plotting functions +import control as ct import numpy as np # System parameters @@ -21,8 +21,8 @@ c = 0.05 # damping factor (estimated) # Transfer functions for dynamics -Pi = tf([r], [J, 0, 0]) # inner loop (roll) -Po = tf([1], [m, c, 0]) # outer loop (position) +Pi = ct.tf([r], [J, 0, 0]) # inner loop (roll) +Po = ct.tf([1], [m, c, 0]) # outer loop (position) # # Inner loop control design @@ -34,59 +34,58 @@ # Design a simple lead controller for the system k, a, b = 200, 2, 50 -Ci = k*tf([1, a], [1, b]) # lead compensator -Li = Pi*Ci +Ci = k * ct.tf([1, a], [1, b]) # lead compensator +Li = Pi * Ci # Bode plot for the open loop process plt.figure(1) -bode(Pi) +ct.bode_plot(Pi) # Bode plot for the loop transfer function, with margins plt.figure(2) -bode(Li) +ct.bode_plot(Li) # Compute out the gain and phase margins -#! Not implemented -# gm, pm, wcg, wcp = margin(Li) +gm, pm, wcg, wcp = ct.margin(Li) # Compute the sensitivity and complementary sensitivity functions -Si = feedback(1, Li) -Ti = Li*Si +Si = ct.feedback(1, Li) +Ti = Li * Si # Check to make sure that the specification is met plt.figure(3) -gangof4(Pi, Ci) +ct.gangof4(Pi, Ci) # Compute out the actual transfer function from u1 to v1 (see L8.2 notes) # Hi = Ci*(1-m*g*Pi)/(1+Ci*Pi) -Hi = parallel(feedback(Ci, Pi), -m*g*feedback(Ci*Pi, 1)) +Hi = ct.parallel(ct.feedback(Ci, Pi), -m * g *ct.feedback(Ci * Pi, 1)) plt.figure(4) plt.clf() plt.subplot(221) -bode(Hi) +ct.bode_plot(Hi) # Now design the lateral control system a, b, K = 0.02, 5, 2 -Co = -K*tf([1, 0.3], [1, 10]) # another lead compensator +Co = -K * ct.tf([1, 0.3], [1, 10]) # another lead compensator Lo = -m*g*Po*Co plt.figure(5) -bode(Lo) # margin(Lo) +ct.bode_plot(Lo) # margin(Lo) # Finally compute the real outer-loop loop gain + responses -L = Co*Hi*Po -S = feedback(1, L) -T = feedback(L, 1) +L = Co * Hi * Po +S = ct.feedback(1, L) +T = ct.feedback(L, 1) # Compute stability margins -gm, pm, wgc, wpc = margin(L) +gm, pm, wgc, wpc = ct.margin(L) print("Gain margin: %g at %g" % (gm, wgc)) print("Phase margin: %g at %g" % (pm, wpc)) plt.figure(6) plt.clf() -bode(L, np.logspace(-4, 3)) +ct.bode_plot(L, np.logspace(-4, 3)) # Add crossover line to the magnitude plot # @@ -113,7 +112,7 @@ break # Recreate the frequency response and shift the phase -mag, phase, w = freqresp(L, np.logspace(-4, 3)) +mag, phase, w = ct.freqresp(L, np.logspace(-4, 3)) phase = phase - 360 # Replot the phase by hand @@ -130,7 +129,7 @@ # plt.figure(7) plt.clf() -nyquist(L, (0.0001, 1000)) +ct.nyquist_plot(L, (0.0001, 1000)) # Add a box in the region we are going to expand plt.plot([-2, -2, 1, 1, -2], [-4, 4, 4, -4, -4], 'r-') @@ -138,7 +137,7 @@ # Expanded region plt.figure(8) plt.clf() -nyquist(L) +ct.nyquist_plot(L) plt.axis([-2, 1, -4, 4]) # set up the color @@ -154,21 +153,21 @@ # 'EdgeColor', color, 'FaceColor', color); plt.figure(9) -Yvec, Tvec = step(T, np.linspace(0, 20)) +Tvec, Yvec = ct.step_response(T, np.linspace(0, 20)) plt.plot(Tvec.T, Yvec.T) -Yvec, Tvec = step(Co*S, np.linspace(0, 20)) +Tvec, Yvec = ct.step_response(Co*S, np.linspace(0, 20)) plt.plot(Tvec.T, Yvec.T) plt.figure(10) plt.clf() -P, Z = pzmap(T, plot=True, grid=True) +P, Z = ct.pzmap(T, plot=True, grid=True) print("Closed loop poles and zeros: ", P, Z) # Gang of Four plt.figure(11) plt.clf() -gangof4(Hi*Po, Co) +ct.gangof4_plot(Hi * Po, Co) if 'PYCONTROL_TEST_EXAMPLES' not in os.environ: plt.show() diff --git a/examples/steering-gainsched.py b/examples/steering-gainsched.py index 7db2d9a73..7ddc6b5b8 100644 --- a/examples/steering-gainsched.py +++ b/examples/steering-gainsched.py @@ -10,7 +10,7 @@ import numpy as np import control as ct from cmath import sqrt -import matplotlib.pyplot as mpl +import matplotlib.pyplot as plt # # Vehicle steering dynamics @@ -137,7 +137,7 @@ def trajgen_output(t, x, u, params): # We construct the system using the InterconnectedSystem constructor and using # signal labels to keep track of everything. -steering = ct.InterconnectedSystem( +steering = ct.interconnect( # List of subsystems (trajgen, controller, vehicle), name='steering', @@ -167,10 +167,10 @@ def trajgen_output(t, x, u, params): T = np.linspace(0, 5, 100) # Set up a figure for plotting the results -mpl.figure(); +plt.figure(); # Plot the reference trajectory for the y position -mpl.plot([0, 5], [yref, yref], 'k--') +plt.plot([0, 5], [yref, yref], 'k--') # Find the signals we want to plot y_index = steering.find_output('y') @@ -183,13 +183,13 @@ def trajgen_output(t, x, u, params): steering, T, [vref * np.ones(len(T)), yref * np.ones(len(T))]) # Plot the reference speed - mpl.plot([0, 5], [vref, vref], 'k--') + plt.plot([0, 5], [vref, vref], 'k--') # Plot the system output - y_line, = mpl.plot(tout, yout[y_index, :], 'r') # lateral position - v_line, = mpl.plot(tout, yout[v_index, :], 'b') # vehicle velocity + y_line, = plt.plot(tout, yout[y_index, :], 'r') # lateral position + v_line, = plt.plot(tout, yout[v_index, :], 'b') # vehicle velocity # Add axis labels -mpl.xlabel('Time (s)') -mpl.ylabel('x vel (m/s), y pos (m)') -mpl.legend((v_line, y_line), ('v', 'y'), loc='center right', frameon=False) +plt.xlabel('Time (s)') +plt.ylabel('x vel (m/s), y pos (m)') +plt.legend((v_line, y_line), ('v', 'y'), loc='center right', frameon=False) From dc5a39299eb920e1a42854550abe6a84c9ffaa1c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 9 Apr 2022 13:41:18 -0700 Subject: [PATCH 64/87] add documentation on system class structure --- doc/.gitignore | 1 + doc/Makefile | 8 ++- doc/classes.fig | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/classes.pdf | Bin 0 -> 12108 bytes doc/classes.rst | 20 ++++++-- 5 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 doc/.gitignore create mode 100644 doc/classes.fig create mode 100644 doc/classes.pdf diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 000000000..d948f64d2 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +*.fig.bak diff --git a/doc/Makefile b/doc/Makefile index b72312be4..3f372684c 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -14,7 +14,11 @@ help: .PHONY: help Makefile +# Rules to create figures +FIGS = classes.pdf +classes.pdf: classes.fig; fig2dev -Lpdf $< $@ + # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file +html pdf: Makefile $(FIGS) + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/classes.fig b/doc/classes.fig new file mode 100644 index 000000000..6e996a4c7 --- /dev/null +++ b/doc/classes.fig @@ -0,0 +1,132 @@ +#FIG 3.2 Produced by xfig version 3.2.8b +Landscape +Center +Inches +Letter +100.00 +Single +-2 +1200 2 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 5400 3375 6600 3375 6600 3825 5400 3825 5400 3375 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 6900 2175 8100 2175 8100 2625 6900 2625 6900 2175 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 7275 3375 8925 3375 8925 3825 7275 3825 7275 3375 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 9750 3375 12075 3375 12075 4725 9750 4725 9750 3375 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 9750 6000 12075 6000 12075 7350 9750 7350 9750 6000 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 4425 975 6525 975 6525 1425 4425 1425 4425 975 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 7875 2550 10875 3450 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 5850 6075 5850 6975 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 4350 6075 5625 6975 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 5925 3750 5925 5775 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 8925 3600 9750 3600 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 10875 3750 10875 4350 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 6375 3750 9975 6150 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 10875 6375 10875 6975 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 6750 6225 9975 6225 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 6000 6075 6000 6975 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 2700 5400 3075 5850 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 4125 4875 5400 5775 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 1650 4500 6750 4500 6750 7425 1650 7425 1650 4500 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 1650 7950 6150 7950 6150 8550 1650 8550 1650 7950 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 -1 0 0 2 + 2400 5400 2400 8025 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 5250 1350 3825 4575 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 3300 4875 3000 5100 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 4350 4875 5625 5775 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 -1 1 0 2 + 1 1 1.00 60.00 120.00 + 2775 8175 4200 8175 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 7575 2550 8025 3450 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 -1 0 0 2 + 9075 7800 9675 7800 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 -1 1 0 2 + 1 1 1.00 60.00 120.00 + 9075 8100 9675 8100 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 9075 8250 9675 8250 9675 8550 9075 8550 9075 8250 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 1 1.00 60.00 120.00 + 4725 5925 5175 5925 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 7350 2550 6225 3450 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 5775 1350 7575 2250 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 1 2 + 1 1 1.00 60.00 120.00 + 1 1 1.00 60.00 120.00 + 6525 3600 7275 3600 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 + 3825 4875 3825 5775 +4 0 0 50 -1 0 12 0.0000 4 165 885 5400 3300 statesp.py\001 +4 0 0 50 -1 0 12 0.0000 4 195 420 8175 2325 lti.py\001 +4 2 0 50 -1 0 12 0.0000 4 195 885 8925 3300 xferfcn.py\001 +4 2 0 50 -1 0 12 0.0000 4 195 780 12075 3300 frdata.py\001 +4 2 0 50 -1 0 12 0.0000 4 195 780 12075 5925 trdata.py\001 +4 1 1 50 -1 0 12 0.0000 4 150 345 7575 2475 LTI\001 +4 1 1 50 -1 0 12 0.0000 4 195 1440 5925 6000 LinearIOSystem\001 +4 0 0 50 -1 0 12 0.0000 4 195 615 1650 7875 flatsys/\001 +4 0 0 50 -1 0 12 0.0000 4 195 705 1650 4425 iosys.py\001 +4 0 0 50 -1 0 12 0.0000 4 195 720 8700 7575 Legend:\001 +4 1 1 50 -1 16 12 0.0000 4 210 1590 5475 1275 NamedIOSystem\001 +4 1 1 50 -1 16 12 0.0000 4 210 1770 3975 4800 InputOutputSystem\001 +4 1 1 50 -1 16 12 0.0000 4 210 1830 2625 5325 NonlinearIOSystem\001 +4 0 0 50 -1 0 12 0.0000 4 195 1005 6600 1125 namedio.py\001 +4 0 4 50 -1 16 12 0.0000 4 210 945 4800 5100 linearize()\001 +4 1 1 50 -1 16 12 0.0000 4 210 2115 3750 6000 InterconnectedSystem\001 +4 0 4 50 -1 16 12 0.0000 4 210 1875 3000 6750 ic() = interconnect()\001 +4 1 1 50 -1 16 12 0.0000 4 210 1500 5925 7200 LinearICSystem\001 +4 1 1 50 -1 16 12 0.0000 4 210 1035 2250 8250 FlatSystem\001 +4 1 4 50 -1 16 12 0.0000 4 210 1500 3525 8400 point_to_point()\001 +4 1 1 50 -1 16 12 0.0000 4 210 1095 6000 3675 StateSpace\001 +4 1 1 50 -1 16 12 0.0000 4 165 1605 8100 3675 TransferFunction\001 +4 1 1 50 -1 16 12 0.0000 4 210 2400 10875 3675 FrequencyResponseData\001 +4 0 4 50 -1 16 12 0.0000 4 210 1155 10950 4050 to_pandas()\001 +4 1 1 50 -1 16 12 0.0000 4 210 1800 10875 4575 pandas.DataFrame\001 +4 0 4 50 -1 16 12 0.0000 4 210 1560 7950 4725 step_response()\001 +4 0 4 50 -1 16 12 0.0000 4 210 1635 8400 5025 initial_response()\001 +4 0 4 50 -1 16 12 0.0000 4 210 1755 8850 5325 forced_response()\001 +4 1 1 50 -1 16 12 0.0000 4 210 1875 10875 6300 TimeResponseData\001 +4 0 4 50 -1 16 12 0.0000 4 210 1155 10950 6675 to_pandas()\001 +4 1 1 50 -1 16 12 0.0000 4 210 1800 10875 7200 pandas.DataFrame\001 +4 1 4 50 -1 16 12 0.0000 4 210 2295 8325 6450 input_output_response()\001 +4 0 1 50 -1 16 12 0.0000 4 210 1755 9750 7875 Class dependency\001 +4 0 4 50 -1 16 12 0.0000 4 210 2475 9750 8175 Conversion [via function()]\001 +4 0 0 50 -1 0 12 0.0000 4 150 1380 9750 8475 Source code file\001 +4 1 4 50 -1 16 12 0.0000 4 210 300 3150 5625 ic()\001 +4 0 4 50 -1 16 12 0.0000 4 210 300 6075 6600 ic()\001 +4 1 1 50 -1 16 12 0.0000 4 210 1650 4950 8250 SystemTrajectory\001 +4 1 4 50 -1 16 12 0.0000 4 210 945 9375 3825 freqresp()\001 +4 1 4 50 -1 16 12 0.0000 4 210 600 6975 3825 tf2ss()\001 +4 1 4 50 -1 16 12 0.0000 4 210 600 6975 3450 ss2tf()\001 +4 1 4 50 -1 16 12 0.0000 4 210 300 5025 6150 ic()\001 diff --git a/doc/classes.pdf b/doc/classes.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6ec48154291ab1406d8023d34ba48fed9049c71c GIT binary patch literal 12108 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 H9rXVI!9NP} literal 0 HcmV?d00001 diff --git a/doc/classes.rst b/doc/classes.rst index 0753271c4..87ce457de 100644 --- a/doc/classes.rst +++ b/doc/classes.rst @@ -5,20 +5,30 @@ Control system classes ********************** -The classes listed below are used to represent models of linear time-invariant -(LTI) systems. They are usually created from factory functions such as -:func:`tf` and :func:`ss`, so the user should normally not need to instantiate -these directly. +The classes listed below are used to represent models of input/output +systems (both linear time-invariant and nonlinear). They are usually +created from factory functions such as :func:`tf` and :func:`ss`, so the +user should normally not need to instantiate these directly. .. autosummary:: :toctree: generated/ :template: custom-class-template.rst - TransferFunction StateSpace + TransferFunction + InputOutputSystem FrequencyResponseData TimeResponseData +The following figure illustrates the relationship between the classes and +some of the functions that can be used to convert objects from one class to +another: + +.. image:: classes.pdf + :width: 800 + +| + Input/output system subclasses ============================== Input/output systems are accessed primarily via a set of subclasses From afa4967e2899f10e811f41045f07a0b886872943 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 9 Apr 2022 15:33:25 -0700 Subject: [PATCH 65/87] add flatsys.systraj.response() to create TimeResponseData object --- control/flatsys/linflat.py | 10 ++++ control/flatsys/systraj.py | 73 +++++++++++++++++++++++++++ control/tests/flatsys_test.py | 26 ++++++++++ control/tests/kwargs_test.py | 8 ++- doc/classes.fig | 91 ++++++++++++++++++++-------------- doc/classes.pdf | Bin 12108 -> 12798 bytes 6 files changed, 169 insertions(+), 39 deletions(-) diff --git a/control/flatsys/linflat.py b/control/flatsys/linflat.py index 94523cc0b..e4a31c6de 100644 --- a/control/flatsys/linflat.py +++ b/control/flatsys/linflat.py @@ -140,3 +140,13 @@ def reverse(self, zflag, params): x = self.Tinv @ z u = zflag[0][-1] - self.F @ z return np.reshape(x, self.nstates), np.reshape(u, self.ninputs) + + # Update function + def _rhs(self, t, x, u, params={}): + # Use LinearIOSystem._rhs instead of default (MRO) NonlinearIOSystem + return LinearIOSystem._rhs(self, t, x, u) + + # output function + def _out(self, t, x, u, params={}): + # Use LinearIOSystem._out instead of default (MRO) NonlinearIOSystem + return LinearIOSystem._out(self, t, x, u) diff --git a/control/flatsys/systraj.py b/control/flatsys/systraj.py index 5e390a7b5..9d425295b 100644 --- a/control/flatsys/systraj.py +++ b/control/flatsys/systraj.py @@ -37,6 +37,7 @@ # SUCH DAMAGE. import numpy as np +from ..timeresp import TimeResponseData class SystemTrajectory: """Class representing a system trajectory. @@ -117,3 +118,75 @@ def eval(self, tlist): self.system.reverse(zflag, self.params) return xd, ud + + # Return the system trajectory as a TimeResponseData object + def response(self, tlist, transpose=False, return_x=False, squeeze=None): + """Return the trajectory of a system as a TimeResponseData object + + Evaluate the trajectory at a list of time points, returning the state + and input vectors for the trajectory: + + response = traj.response(tlist) + time, yd, ud = response.time, response.outputs, response.inputs + + Parameters + ---------- + tlist : 1D array + List of times to evaluate the trajectory. + + transpose : bool, optional + If True, transpose all input and output arrays (for backward + compatibility with MATLAB and :func:`scipy.signal.lsim`). + Default value is False. + + return_x : bool, optional + If True, return the state vector when assigning to a tuple + (default = False). See :func:`forced_response` for more details. + + squeeze : bool, optional + By default, if a system is single-input, single-output (SISO) then + the output response is returned as a 1D array (indexed by time). + If squeeze=True, remove single-dimensional entries from the shape + of the output even if the system is not SISO. If squeeze=False, + keep the output as a 3D array (indexed by the output, input, and + time) even if the system is SISO. The default value can be set + using config.defaults['control.squeeze_time_response']. + + Returns + ------- + results : TimeResponseData + Time response represented as a :class:`TimeResponseData` object + containing the following properties: + + * time (array): Time values of the output. + + * outputs (array): Response of the system. If the system is SISO + and squeeze is not True, the array is 1D (indexed by time). If + the system is not SISO or ``squeeze`` is False, the array is 3D + (indexed by the output, trace, and time). + + * states (array): Time evolution of the state vector, represented + as either a 2D array indexed by state and time (if SISO) or a 3D + array indexed by state, trace, and time. Not affected by + ``squeeze``. + + * inputs (array): Input(s) to the system, indexed in the same + manner as ``outputs``. + + The return value of the system can also be accessed by assigning + the function to a tuple of length 2 (time, output) or of length 3 + (time, output, state) if ``return_x`` is ``True``. + + """ + # Compute the state and input response using the eval function + sys = self.system + xout, uout = self.eval(tlist) + yout = np.array([ + sys.output(tlist[i], xout[:, i], uout[:, i]) + for i in range(len(tlist))]).transpose() + + return TimeResponseData( + tlist, yout, xout, uout, issiso=sys.issiso(), + input_labels=sys.input_labels, output_labels=sys.output_labels, + state_labels=sys.state_labels, + transpose=transpose, return_x=return_x, squeeze=squeeze) diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py index 8b182a17a..a12852759 100644 --- a/control/tests/flatsys_test.py +++ b/control/tests/flatsys_test.py @@ -378,3 +378,29 @@ def test_point_to_point_errors(self): with pytest.raises(TypeError, match="unrecognized keyword"): traj_method = fs.point_to_point( flat_sys, timepts, x0, u0, xf, uf, solve_ivp_method=None) + + @pytest.mark.parametrize( + "xf, uf, Tf", + [([1, 0], [0], 2), + ([0, 1], [0], 3), + ([1, 1], [1], 4)]) + def test_response(self, xf, uf, Tf): + # Define a second order integrator + sys = ct.StateSpace([[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], 0) + flatsys = fs.LinearFlatSystem(sys) + + # Define the basis set + poly = fs.PolyFamily(6) + + x1, u1, = [0, 0], [0] + traj = fs.point_to_point(flatsys, Tf, x1, u1, xf, uf, basis=poly) + + # Compute the response the regular way + T = np.linspace(0, Tf, 10) + x, u = traj.eval(T) + + # Recompute using response() + response = traj.response(T, squeeze=False) + np.testing.assert_equal(T, response.time) + np.testing.assert_equal(u, response.inputs) + np.testing.assert_equal(x, response.states) diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 62887301d..ada16a46a 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -115,6 +115,10 @@ def test_unrecognized_kwargs(): with pytest.raises(TypeError, match="unrecognized keyword"): function(*args, **kwargs, unknown=None) + # If we opened any figures, close them to avoid matplotlib warnings + if plt.gca(): + plt.close('all') + def test_matplotlib_kwargs(): # Create a SISO system for use in parameterized tests @@ -141,7 +145,7 @@ def test_matplotlib_kwargs(): with pytest.raises(AttributeError, match="has no property"): function(*args, **kwargs, unknown=None) - # If we opened any figures, close them + # If we opened any figures, close them to avoid matplotlib warnings if plt.gca(): plt.close('all') @@ -171,7 +175,7 @@ def test_matplotlib_kwargs(): 'lqr': test_unrecognized_kwargs, 'nyquist': test_matplotlib_kwargs, 'nyquist_plot': test_matplotlib_kwargs, - 'pzmap': test_matplotlib_kwargs, + 'pzmap': test_unrecognized_kwargs, 'rlocus': test_unrecognized_kwargs, 'root_locus': test_unrecognized_kwargs, 'rss': test_unrecognized_kwargs, diff --git a/doc/classes.fig b/doc/classes.fig index 6e996a4c7..950510c01 100644 --- a/doc/classes.fig +++ b/doc/classes.fig @@ -7,26 +7,10 @@ Letter Single -2 1200 2 -2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 - 5400 3375 6600 3375 6600 3825 5400 3825 5400 3375 -2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 - 6900 2175 8100 2175 8100 2625 6900 2625 6900 2175 -2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 - 7275 3375 8925 3375 8925 3825 7275 3825 7275 3375 2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 9750 3375 12075 3375 12075 4725 9750 4725 9750 3375 2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 9750 6000 12075 6000 12075 7350 9750 7350 9750 6000 -2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 - 4425 975 6525 975 6525 1425 4425 1425 4425 975 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 7875 2550 10875 3450 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 5850 6075 5850 6975 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 4350 6075 5625 6975 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 5925 3750 5925 5775 2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 1 1 1.00 60.00 120.00 8925 3600 9750 3600 @@ -48,28 +32,13 @@ Single 2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 1 1 1.00 60.00 120.00 2700 5400 3075 5850 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 4125 4875 5400 5775 2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 1650 4500 6750 4500 6750 7425 1650 7425 1650 4500 2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 1650 7950 6150 7950 6150 8550 1650 8550 1650 7950 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 -1 0 0 2 - 2400 5400 2400 8025 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 5250 1350 3825 4575 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 3300 4875 3000 5100 -2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 - 1 1 1.00 60.00 120.00 - 4350 4875 5625 5775 2 1 0 2 4 7 50 -1 -1 0.000 0 0 -1 1 0 2 1 1 1.00 60.00 120.00 2775 8175 4200 8175 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 7575 2550 8025 3450 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 -1 0 0 2 - 9075 7800 9675 7800 2 1 0 2 4 7 50 -1 -1 0.000 0 0 -1 1 0 2 1 1 1.00 60.00 120.00 9075 8100 9675 8100 @@ -78,16 +47,63 @@ Single 2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 0 1 2 1 1 1.00 60.00 120.00 4725 5925 5175 5925 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 7350 2550 6225 3450 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 - 5775 1350 7575 2250 2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 1 2 1 1 1.00 60.00 120.00 1 1 1.00 60.00 120.00 6525 3600 7275 3600 -2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 0 2 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 5775 8175 9975 6300 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 5400 3375 6600 3375 6600 3900 5400 3900 5400 3375 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 7050 2175 8100 2175 8100 2700 7050 2700 7050 2175 +2 2 1 1 1 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 4500 975 6525 975 6525 1500 4500 1500 4500 975 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 5250 1350 3825 4575 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 5775 1350 7575 2250 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 7875 2550 10875 3450 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 7575 2550 8025 3450 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 7350 2550 6225 3450 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 3300 4875 3000 5100 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 3825 4875 3825 5775 +2 1 0 2 4 7 50 -1 -1 0.000 0 0 7 1 0 2 + 1 1 1.00 60.00 120.00 + 4350 4875 5625 5775 +2 2 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 5 + 7350 3375 8925 3375 8925 3900 7350 3900 7350 3375 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 -1 0 1 2 + 1 0 1.00 60.00 90.00 + 9075 7800 9675 7800 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 4350 6075 5625 6975 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 -1 0 1 2 + 1 0 1.00 60.00 90.00 + 2400 5400 2400 8025 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 5850 6075 5850 6975 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 4125 4875 5400 5775 +2 1 0 2 1 7 50 -1 -1 0.000 0 0 7 0 1 2 + 1 0 1.00 60.00 90.00 + 5925 3750 5925 5775 4 0 0 50 -1 0 12 0.0000 4 165 885 5400 3300 statesp.py\001 4 0 0 50 -1 0 12 0.0000 4 195 420 8175 2325 lti.py\001 4 2 0 50 -1 0 12 0.0000 4 195 885 8925 3300 xferfcn.py\001 @@ -119,7 +135,6 @@ Single 4 1 1 50 -1 16 12 0.0000 4 210 1875 10875 6300 TimeResponseData\001 4 0 4 50 -1 16 12 0.0000 4 210 1155 10950 6675 to_pandas()\001 4 1 1 50 -1 16 12 0.0000 4 210 1800 10875 7200 pandas.DataFrame\001 -4 1 4 50 -1 16 12 0.0000 4 210 2295 8325 6450 input_output_response()\001 4 0 1 50 -1 16 12 0.0000 4 210 1755 9750 7875 Class dependency\001 4 0 4 50 -1 16 12 0.0000 4 210 2475 9750 8175 Conversion [via function()]\001 4 0 0 50 -1 0 12 0.0000 4 150 1380 9750 8475 Source code file\001 @@ -130,3 +145,5 @@ Single 4 1 4 50 -1 16 12 0.0000 4 210 600 6975 3825 tf2ss()\001 4 1 4 50 -1 16 12 0.0000 4 210 600 6975 3450 ss2tf()\001 4 1 4 50 -1 16 12 0.0000 4 210 300 5025 6150 ic()\001 +4 1 4 50 -1 16 12 0.0000 4 210 2295 8325 6075 input_output_response()\001 +4 2 4 50 -1 16 12 0.0000 4 210 1035 8175 6975 response()\001 diff --git a/doc/classes.pdf b/doc/classes.pdf index 6ec48154291ab1406d8023d34ba48fed9049c71c..66ef25e1034a69cabf6d2fb7a6df7c55ae950752 100644 GIT binary patch delta 3019 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 MhN!9 Date: Fri, 15 Apr 2022 13:31:45 -0700 Subject: [PATCH 66/87] added more documentation on return_magphase per @sawyerbfuller --- control/frdata.py | 2 +- control/lti.py | 23 +++++++++++++---------- control/tests/frd_test.py | 31 +++++++++++++++++++++++++++++++ doc/conventions.rst | 22 ++++++++++++++++++++++ 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 13813d775..a33775afb 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -151,7 +151,7 @@ def __init__(self, *args, **kwargs): FRD object. To construct frequency response data for an existing LTI - object, other than an FRD, call FRD(sys, omega) + object, other than an FRD, call FRD(sys, omega). """ # TODO: discrete-time FRD systems? diff --git a/control/lti.py b/control/lti.py index 9d60f0526..fdb4946cd 100644 --- a/control/lti.py +++ b/control/lti.py @@ -412,18 +412,21 @@ def frequency_response(sys, omega, squeeze=None): Returns ------- - mag : ndarray - The magnitude (absolute value, not dB or log10) of the system - frequency response. If the system is SISO and squeeze is not True, - the array is 1D, indexed by frequency. If the system is not SISO or - squeeze is False, the array is 3D, indexed by the output, input, and + response : FrequencyResponseData + Frequency response data object representing the frequency response. + This object can be assigned to a tuple using + + mag, phase, omega = response + + where ``mag`` is the magnitude (absolute value, not dB or log10) of + the system frequency response, ``phase`` is the wrapped phase in + radians of the system frequency response, and ``omega`` is the + (sorted) frequencies at which the response was evaluated. If the + system is SISO and squeeze is not True, ``magnitude`` and ``phase`` + are 1D, indexed by frequency. If the system is not SISO or squeeze + is False, the array is 3D, indexed by the output, input, and frequency. If ``squeeze`` is True then single-dimensional axes are removed. - phase : ndarray - The wrapped phase in radians of the system frequency response. - omega : ndarray - The list of sorted frequencies at which the response was - evaluated. See Also -------- diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index 00425565f..ff88c3dea 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -532,3 +532,34 @@ def test_frequency_response(): np.testing.assert_equal(resp.magnitude, np.abs(eval)) np.testing.assert_equal(resp.phase, np.angle(eval)) np.testing.assert_equal(resp.omega, omega) + + # Make sure that we can change the properties of the response + sys = ct.rss(2, 1, 1) + resp_default = ct.frequency_response(sys, omega) + mag_default, phase_default, omega_default = resp_default + assert mag_default.ndim == 1 + assert phase_default.ndim == 1 + assert omega_default.ndim == 1 + assert mag_default.shape[0] == omega_default.shape[0] + assert phase_default.shape[0] == omega_default.shape[0] + + resp_nosqueeze = ct.frequency_response(sys, omega, squeeze=False) + mag_nosqueeze, phase_nosqueeze, omega_nosqueeze = resp_nosqueeze + assert mag_nosqueeze.ndim == 3 + assert phase_nosqueeze.ndim == 3 + assert omega_nosqueeze.ndim == 1 + assert mag_nosqueeze.shape[2] == omega_nosqueeze.shape[0] + assert phase_nosqueeze.shape[2] == omega_nosqueeze.shape[0] + + # Try changing the response + resp_def_nosq = resp_default(squeeze=False) + mag_def_nosq, phase_def_nosq, omega_def_nosq = resp_def_nosq + assert mag_def_nosq.shape == mag_nosqueeze.shape + assert phase_def_nosq.shape == phase_nosqueeze.shape + assert omega_def_nosq.shape == omega_nosqueeze.shape + + resp_nosq_sq = resp_nosqueeze(squeeze=True) + mag_nosq_sq, phase_nosq_sq, omega_nosq_sq = resp_nosq_sq + assert mag_nosq_sq.shape == mag_default.shape + assert phase_nosq_sq.shape == phase_default.shape + assert omega_nosq_sq.shape == omega_default.shape diff --git a/doc/conventions.rst b/doc/conventions.rst index de1fc5f57..1832b9525 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -74,6 +74,28 @@ FRD systems have a somewhat more limited set of functions that are available, although all of the standard algebraic manipulations can be performed. +The FRD class is also used as the return type for the +:func:`frequency_response` function (and the equivalent method for the +:class:`StateSpace` and :class:`TransferFunction` classes). This +object can be assigned to a tuple using + + mag, phase, omega = response + +where `mag` is the magnitude (absolute value, not dB or log10) of the +system frequency response, `phase` is the wrapped phase in radians of +the system frequency response, and `omega` is the (sorted) frequencies +at which the response was evaluated. If the system is SISO and the +`squeeze` argument to :func:`frequency_response` is not True, +`magnitude` and `phase` are 1D, indexed by frequency. If the system +is not SISO or `squeeze` is False, the array is 3D, indexed by the +output, input, and frequency. If `squeeze` is True then +single-dimensional axes are removed. The processing of the `squeeze` +keyword can be changed by calling the response function with a new +argument: + + mag, phase, omega = response(squeeze=False) + + Discrete time systems --------------------- A discrete time system is created by specifying a nonzero 'timebase', dt. From 18e997c037db361076c4fb9c61250ef7139d03f9 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 10 Apr 2022 07:10:48 -0700 Subject: [PATCH 67/87] add warning for nyquist_plot() when indent_radius is too large --- control/freqplot.py | 45 +++++++++++++++++++++++++++++++---- control/tests/nyquist_test.py | 27 +++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 7f29dce36..27a457d15 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -723,6 +723,7 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, and indent_direction != 'none': if sys.isctime(): splane_poles = sys.poles() + splane_cl_poles = sys.feedback().poles() else: # map z-plane poles to s-plane, ignoring any at the origin # because we don't need to indent for them @@ -730,28 +731,64 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, zplane_poles = zplane_poles[~np.isclose(abs(zplane_poles), 0.)] splane_poles = np.log(zplane_poles)/sys.dt + zplane_cl_poles = sys.feedback().poles() + zplane_cl_poles = zplane_cl_poles[ + ~np.isclose(abs(zplane_poles), 0.)] + splane_cl_poles = np.log(zplane_cl_poles)/sys.dt + + # + # Check to make sure indent radius is small enough + # + # If there is a closed loop pole that is near the imaginary access + # at a point that is near an open loop pole, it is possible that + # indentation might skip or create an extraneous encirclement. + # We check for that situation here and generate a warning if that + # could happen. + # + for p_cl in splane_cl_poles: + # See if any closed loop poles are near the imaginary axis + if abs(p_cl.real) <= indent_radius: + # See if any open loop poles are close to closed loop poles + p_ol = splane_poles[ + (np.abs(splane_poles - p_cl)).argmin()] + + if abs(p_ol - p_cl) <= indent_radius: + warnings.warn( + "indented contour may miss closed loop pole; " + "consider reducing indent_radius to be less than " + f"{abs(p_ol - p_cl):5.2g}", stacklevel=2) + + # See if we should add some frequency points near the origin if splane_contour[1].imag > indent_radius \ and np.any(np.isclose(abs(splane_poles), 0)) \ and not omega_range_given: # add some points for quarter circle around poles at origin + # (these will get indented left or right below) splane_contour = np.concatenate( (1j * np.linspace(0., indent_radius, 50), splane_contour[1:])) + for i, s in enumerate(splane_contour): # Find the nearest pole p = splane_poles[(np.abs(splane_poles - s)).argmin()] + # See if we need to indent around it if abs(s - p) < indent_radius: + # Figure out how much to offset (simple trigonometry) + offset = np.sqrt(indent_radius ** 2 - (s-p).imag ** 2) \ + -(s-p).real + + # Figure out which way to offset the contour point if p.real < 0 or (np.isclose(p.real, 0) \ and indent_direction == 'right'): # Indent to the right - splane_contour[i] += \ - np.sqrt(indent_radius ** 2 - (s-p).imag ** 2) + splane_contour[i] += offset + elif p.real > 0 or (np.isclose(p.real, 0) \ and indent_direction == 'left'): # Indent to the left - splane_contour[i] -= \ - np.sqrt(indent_radius ** 2 - (s-p).imag ** 2) + splane_contour[i] -= offset + else: ValueError("unknown value for indent_direction") diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index a001598a6..e93fef9ac 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -41,6 +41,33 @@ def test_nyquist_basic(): N_sys = ct.nyquist_plot(sys) assert _Z(sys) == N_sys + _P(sys) + # Previously identified bug + # + # This example has an open loop pole at -0.06 and a closed loop pole at + # 0.06, so if you use an indent_radius of larger than 0.12, then the + # encirclements computed by nyquist_plot() will not properly predict + # stability. A new warning messages was added to catch this case. + # + A = np.array([ + [-3.56355873, -1.22980795, -1.5626527 , -0.4626829 , -0.16741484], + [-8.52361371, -3.60331459, -3.71574266, -0.43839201, 0.41893656], + [-2.50458726, -0.72361335, -1.77795489, -0.4038419 , 0.52451147], + [-0.281183 , 0.23391825, 0.19096003, -0.9771515 , 0.66975606], + [-3.04982852, -1.1091943 , -1.40027242, -0.1974623 , -0.78930791]]) + B = np.array([[-0.], [-1.42827213], [ 0.76806551], [-1.07987454], [0.]]) + C = np.array([[-0., 0.35557249, 0.35941791, -0., -1.42320969]]) + D = np.array([[0]]) + sys = ct.ss(A, B, C, D) + + # With a small indent_radius, all should be fine + N_sys = ct.nyquist_plot(sys, indent_radius=0.001) + assert _Z(sys) == N_sys + _P(sys) + + # With a larger indent_radius, we get a warning message + wrong answer + with pytest.warns(UserWarning, match="contour may miss closed loop pole"): + N_sys = ct.nyquist_plot(sys, indent_radius=0.2) + assert _Z(sys) != N_sys + _P(sys) + # Unstable system sys = ct.tf([10], [1, 2, 2, 1]) N_sys = ct.nyquist_plot(sys) From 1e12a51f457a7ada030f5b80d25dabe27ad3f68a Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 10 Apr 2022 08:58:11 -0700 Subject: [PATCH 68/87] add warning for nyquist_plot() when Nyquist criterion isn't met --- control/freqplot.py | 23 +++++++++++++++++++++++ control/tests/nyquist_test.py | 21 ++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 27a457d15..bfb2c7c6e 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -47,6 +47,7 @@ import matplotlib.pyplot as plt import numpy as np import warnings +from math import nan from .ctrlutil import unwrap from .bdalg import feedback @@ -805,6 +806,28 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, phase = -unwrap(np.angle(resp + 1)) count = int(np.round(np.sum(np.diff(phase)) / np.pi, 0)) + # + # Make sure that the enciriclements match the Nyquist criterion + # + # If the user specifies the frequency points to use, it is possible + # to miss enciriclements, so we check here to make sure that the + # Nyquist criterion is actually satisfied. + # + if isinstance(sys, (StateSpace, TransferFunction)): + P = (sys.pole().real > 0).sum() if indent_direction == 'right' \ + else (sys.pole().real >= 0).sum() + Z = (sys.feedback().pole().real >= 0).sum() + if Z != count + P: + warnings.warn( + "number of encirclements does not match Nyquist criterion;" + " check frequency range and indent radius/direction", + UserWarning, stacklevel=2) + elif indent_direction == 'none' and any(sys.pole().real == 0): + warnings.warn( + "system has pure imaginary poles but indentation is" + " turned off; results may be meaningless", + RuntimeWarning, stacklevel=2) + counts.append(count) contours.append(contour) diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index e93fef9ac..fe4119666 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -98,9 +98,10 @@ def test_nyquist_basic(): count, contour_indented = ct.nyquist_plot( sys, np.linspace(1e-4, 1e2, 100), return_contour=True) assert not all(contour_indented.real == 0) - count, contour = ct.nyquist_plot( - sys, np.linspace(1e-4, 1e2, 100), return_contour=True, - indent_direction='none') + with pytest.warns(UserWarning, match="encirclements does not match"): + count, contour = ct.nyquist_plot( + sys, np.linspace(1e-4, 1e2, 100), return_contour=True, + indent_direction='none') np.testing.assert_almost_equal(contour, 1j*np.linspace(1e-4, 1e2, 100)) # Nyquist plot with poles at the origin, omega unspecified @@ -166,10 +167,11 @@ def test_nyquist_fbs_examples(): plt.figure() plt.title("Figure 10.10: L(s) = 3 (s+6)^2 / (s (s+1)^2) [zoom]") - count = ct.nyquist_plot(sys, omega_limits=[1.5, 1e3]) - # Frequency limits for zoom give incorrect encirclement count - # assert _Z(sys) == count + _P(sys) - assert count == -1 + with pytest.warns(UserWarning, match="encirclements does not match"): + count = ct.nyquist_plot(sys, omega_limits=[1.5, 1e3]) + # Frequency limits for zoom give incorrect encirclement count + # assert _Z(sys) == count + _P(sys) + assert count == -1 @pytest.mark.parametrize("arrows", [ @@ -276,8 +278,9 @@ def test_nyquist_indent_im(): # Imaginary poles with no indentation plt.figure(); - count = ct.nyquist_plot( - sys, np.linspace(0, 1e3, 1000), indent_direction='none') + with pytest.warns(UserWarning, match="encirclements does not match"): + count = ct.nyquist_plot( + sys, np.linspace(0, 1e3, 1000), indent_direction='none') plt.title( "Imaginary poles; indent_direction='none'; encirclements = %d" % count) assert _Z(sys) == count + _P(sys) From 6da87066cf065bde32b23222519e653b65a34df7 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 10 Apr 2022 14:32:12 -0700 Subject: [PATCH 69/87] add code to limit magnitude of response and enhance contour near poles * use plot_curve_magnitude to scale response at large magnitudes * add new line styles of scaled points on the curve * add points to contour near imaginary poles to avoid missing encirclements * add offsets to primary/mirror curve at large amplitude to avoid overlap * (still needs some unit tests, documention updates, and comparisons) --- control/freqplot.py | 206 ++++++++++++++++++++++++++-------- control/tests/nyquist_test.py | 60 +++++++--- 2 files changed, 200 insertions(+), 66 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index bfb2c7c6e..d19a75ab1 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -519,11 +519,15 @@ def gen_zero_centered_series(val_min, val_max, period): # Default values for module parameter variables _nyquist_defaults = { - 'nyquist.mirror_style': '--', + 'nyquist.primary_style': ['-', ':'], # style for primary curve + 'nyquist.mirror_style': ['--', '-.'], # style for mirror curve 'nyquist.arrows': 2, 'nyquist.arrow_size': 8, - 'nyquist.indent_radius': 1e-1, - 'nyquist.indent_direction': 'right', + 'nyquist.indent_radius': 1e-6, # indentation radius + 'nyquist.indent_direction': 'right', # indentation direction + 'nyquist.indent_points': 50, # number of points to insert + 'nyquist.max_curve_magnitude': 20, + 'nyquist.max_curve_offset': 0.02, # percent offset of curves } @@ -563,19 +567,32 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, color : string Used to specify the color of the line and arrowhead. - mirror_style : string or False - Linestyle for mirror image of the Nyquist curve. If `False` then - omit completely. Default linestyle ('--') is determined by - config.defaults['nyquist.mirror_style']. - - return_contour : bool + return_contour : bool, optional If 'True', return the contour used to evaluate the Nyquist plot. - label_freq : int + *args : :func:`matplotlib.pyplot.plot` positional properties, optional + Additional arguments for `matplotlib` plots (color, linestyle, etc) + + **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional + Additional keywords (passed to `matplotlib`) + + Returns + ------- + count : int (or list of int if len(syslist) > 1) + Number of encirclements of the point -1 by the Nyquist curve. If + multiple systems are given, an array of counts is returned. + + contour : ndarray (or list of ndarray if len(syslist) > 1)), optional + The contour used to create the primary Nyquist curve segment. To + obtain the Nyquist curve values, evaluate system(s) along contour. + + Additional Parameters + --------------------- + label_freq : int, optiona Label every nth frequency on the plot. If not specified, no labels are generated. - arrows : int or 1D/2D array of floats + arrows : int or 1D/2D array of floats, optional Specify the number of arrows to plot on the Nyquist curve. If an integer is passed. that number of equally spaced arrows will be plotted on each of the primary segment and the mirror image. If a 1D @@ -585,39 +602,51 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, locations for the primary curve and the second row will be used for the mirror image. - arrow_size : float + arrow_size : float, optional Arrowhead width and length (in display coordinates). Default value is 8 and can be set using config.defaults['nyquist.arrow_size']. - arrow_style : matplotlib.patches.ArrowStyle + arrow_style : matplotlib.patches.ArrowStyle, optional Define style used for Nyquist curve arrows (overrides `arrow_size`). - indent_radius : float - Amount to indent the Nyquist contour around poles that are at or near - the imaginary axis. - - indent_direction : str + indent_direction : str, optional For poles on the imaginary axis, set the direction of indentation to be 'right' (default), 'left', or 'none'. - warn_nyquist : bool, optional - If set to 'False', turn off warnings about frequencies above Nyquist. - - *args : :func:`matplotlib.pyplot.plot` positional properties, optional - Additional arguments for `matplotlib` plots (color, linestyle, etc) + indent_points : int, optional + Number of points to insert in the Nyquist contour around poles that + are at or near the imaginary axis. - **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional - Additional keywords (passed to `matplotlib`) + indent_radius : float, optional + Amount to indent the Nyquist contour around poles that are at or near + the imaginary axis. - Returns - ------- - count : int (or list of int if len(syslist) > 1) - Number of encirclements of the point -1 by the Nyquist curve. If - multiple systems are given, an array of counts is returned. + max_curve_magnitude : float, optional + Restrict the maximum magnitude of the Nyquist plot to this value. + Portions of the Nyquist plot whose magnitude is restricted are + plotted using a different line style. + + max_curve_offset : float, optional + When plotting scaled portion of the Nyquist plot, increase/decrease + the magnitude by this fraction of the max_curve_magnitude to allow + any overlaps between the primary and mirror curves to be avoided. + + mirror_style : [str, str] or False + Linestyles for mirror image of the Nyquist curve. The first element + is used for unscaled portions of the Nyquist curve, the second element + is used for portions that are scaled (using max_curve_magnitude). If + `False` then omit completely. Default linestyle (['--', '-.']) is + determined by config.defaults['nyquist.mirror_style']. + + primary_style : [str, str] + Linestyles for primary image of the Nyquist curve. The first element + is used for unscaled portions of the Nyquist curve, the second + element is used for scaled portions that are scaled (using + max_curve_magnitude). Default linestyle (['-', ':']) is determined by + config.defaults['nyquist.mirror_style']. - contour : ndarray (or list of ndarray if len(syslist) > 1)), optional - The contour used to create the primary Nyquist curve segment. To - obtain the Nyquist curve values, evaluate system(s) along contour. + warn_nyquist : bool, optional + If set to 'False', turn off warnings about frequencies above Nyquist. Notes ----- @@ -668,8 +697,6 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, # Get values for params (and pop from list to allow keyword use in plot) omega_num = config._get_param('freqplot', 'number_of_samples', omega_num) - mirror_style = config._get_param( - 'nyquist', 'mirror_style', kwargs, _nyquist_defaults, pop=True) arrows = config._get_param( 'nyquist', 'arrows', kwargs, _nyquist_defaults, pop=True) arrow_size = config._get_param( @@ -679,6 +706,28 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, 'nyquist', 'indent_radius', kwargs, _nyquist_defaults, pop=True) indent_direction = config._get_param( 'nyquist', 'indent_direction', kwargs, _nyquist_defaults, pop=True) + indent_points = config._get_param( + 'nyquist', 'indent_points', kwargs, _nyquist_defaults, pop=True) + max_curve_magnitude = config._get_param( + 'nyquist', 'max_curve_magnitude', kwargs, _nyquist_defaults, pop=True) + max_curve_offset = config._get_param( + 'nyquist', 'max_curve_offset', kwargs, _nyquist_defaults, pop=True) + + # Set line styles for the curves + def _parse_linestyle(style_name, allow_false=False): + style = config._get_param( + 'nyquist', style_name, kwargs, _nyquist_defaults, pop=True) + if isinstance(style, str): + # Only one style provided, use the default for the other + style = [style, _nyquist_defaults['nyquist.' + style_name][1]] + if (allow_false and style is False) or \ + (isinstance(style, list) and len(style) == 2): + return style + else: + raise ValueError(f"invalid '{style_name}': {style}") + + primary_style = _parse_linestyle('primary_style') + mirror_style = _parse_linestyle('mirror_style', allow_false=True) # If argument was a singleton, turn it into a tuple if not isinstance(syslist, (list, tuple)): @@ -759,15 +808,39 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, "consider reducing indent_radius to be less than " f"{abs(p_ol - p_cl):5.2g}", stacklevel=2) - # See if we should add some frequency points near the origin - if splane_contour[1].imag > indent_radius \ - and np.any(np.isclose(abs(splane_poles), 0)) \ - and not omega_range_given: - # add some points for quarter circle around poles at origin + # + # See if we should add some frequency points near imaginary poles + # + for p in splane_poles: + # See if we need to process this pole (skip any that is on + # the not near or on the negative omega axis + user override) + if p.imag < 0 or abs(p.real) > indent_radius or \ + omega_range_given: + continue + + # Find the frequencies before the pole frequency + below_points = np.argwhere( + splane_contour.imag - abs(p.imag) < -indent_radius) + if below_points.size > 0: + first_point = below_points[-1].item() + start_freq = p.imag - indent_radius + else: + # Add the points starting at the beginning of the contour + assert splane_contour[0] == 0 + first_point = 0 + start_freq = 0 + + above_points = np.argwhere( + splane_contour.imag - abs(p.imag) > indent_radius) + last_point = above_points[0].item() + + # Add points for half/quarter circle around pole frequency # (these will get indented left or right below) - splane_contour = np.concatenate( - (1j * np.linspace(0., indent_radius, 50), - splane_contour[1:])) + splane_contour = np.concatenate(( + splane_contour[0:first_point+1], + (1j * np.linspace( + start_freq, p.imag + indent_radius, indent_points)), + splane_contour[last_point:])) for i, s in enumerate(splane_contour): # Find the nearest pole @@ -849,19 +922,55 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, arrow_style = mpl.patches.ArrowStyle( 'simple', head_width=arrow_size, head_length=arrow_size) - # Save the components of the response - x, y = resp.real, resp.imag + # Find the different portions of the curve (with scaled pts marked) + reg_mask = abs(resp) > max_curve_magnitude + scale_mask = ~reg_mask \ + & np.concatenate((~reg_mask[1:], ~reg_mask[-1:])) \ + & np.concatenate((~reg_mask[0:1], ~reg_mask[:-1])) + + # Rescale the points with large magnitude + resp[reg_mask] /= (np.abs(resp[reg_mask]) / max_curve_magnitude) - # Plot the primary curve - p = plt.plot(x, y, '-', color=color, *args, **kwargs) + # Plot the regular portions of the curve (and grab the color) + x_reg = np.ma.masked_where(reg_mask, resp.real) + y_reg = np.ma.masked_where(reg_mask, resp.imag) + p = plt.plot( + x_reg, y_reg, primary_style[0], color=color, *args, **kwargs) c = p[0].get_color() + + # Plot the scaled sections of the curve (changing linestyle) + x_scl = np.ma.masked_where(scale_mask, resp.real) + y_scl = np.ma.masked_where(scale_mask, resp.imag) + plt.plot( + x_scl * (1 + max_curve_offset), y_scl * (1 + max_curve_offset), + primary_style[1], color=c, *args, **kwargs) + + # Plot the primary curve (invisible) for setting arrows + x, y = resp.real.copy(), resp.imag.copy() + x[reg_mask] *= (1 + max_curve_offset) + y[reg_mask] *= (1 + max_curve_offset) + p = plt.plot(x, y, linestyle='None', color=c, *args, **kwargs) + + # Add arrows ax = plt.gca() _add_arrows_to_line2D( ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=1) # Plot the mirror image if mirror_style is not False: - p = plt.plot(x, -y, mirror_style, color=c, *args, **kwargs) + # Plot the regular and scaled segments + plt.plot( + x_reg, -y_reg, mirror_style[0], color=c, *args, **kwargs) + plt.plot( + x_scl * (1 - max_curve_offset), + -y_scl * (1 - max_curve_offset), + mirror_style[1], color=c, *args, **kwargs) + + # Add the arrows (on top of an invisible contour) + x, y = resp.real.copy(), resp.imag.copy() + x[reg_mask] *= (1 - max_curve_offset) + y[reg_mask] *= (1 - max_curve_offset) + p = plt.plot(x, -y, linestyle='None', color=c, *args, **kwargs) _add_arrows_to_line2D( ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=-1) @@ -982,7 +1091,6 @@ def _add_arrows_to_line2D( # # Gang of Four plot # - # TODO: think about how (and whether) to handle lists of systems def gangof4_plot(P, C, omega=None, **kwargs): """Plot the "Gang of 4" transfer functions for a system diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index fe4119666..4f142e901 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -94,14 +94,18 @@ def test_nyquist_basic(): contour[contour.real == 0], 1j*np.linspace(0, 1e2, 100)[contour.real == 0]) + # # Make sure that we can turn off frequency modification + # + # Start with a case where indentation should occur count, contour_indented = ct.nyquist_plot( - sys, np.linspace(1e-4, 1e2, 100), return_contour=True) + sys, np.linspace(1e-4, 1e2, 100), indent_radius=1e-2, + return_contour=True) assert not all(contour_indented.real == 0) with pytest.warns(UserWarning, match="encirclements does not match"): count, contour = ct.nyquist_plot( - sys, np.linspace(1e-4, 1e2, 100), return_contour=True, - indent_direction='none') + sys, np.linspace(1e-4, 1e2, 100), indent_radius=1e-2, + return_contour=True, indent_direction='none') np.testing.assert_almost_equal(contour, 1j*np.linspace(1e-4, 1e2, 100)) # Nyquist plot with poles at the origin, omega unspecified @@ -115,15 +119,20 @@ def test_nyquist_basic(): assert _Z(sys) == count + _P(sys) # Nyquist plot with poles on imaginary axis, omega specified + # (can miss encirclements due to the imaginary poles at +/- 1j) sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1]) - count = ct.nyquist_plot(sys, np.linspace(1e-3, 1e1, 1000)) - assert _Z(sys) == count + _P(sys) + with pytest.warns(UserWarning, match="does not match") as records: + count = ct.nyquist_plot(sys, np.linspace(1e-3, 1e1, 1000)) + if len(records) == 0: + assert _Z(sys) == count + _P(sys) # Nyquist plot with poles on imaginary axis, omega specified, with contour sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1]) - count, contour = ct.nyquist_plot( - sys, np.linspace(1e-3, 1e1, 1000), return_contour=True) - assert _Z(sys) == count + _P(sys) + with pytest.warns(UserWarning, match="does not match") as records: + count, contour = ct.nyquist_plot( + sys, np.linspace(1e-3, 1e1, 1000), return_contour=True) + if len(records) == 0: + assert _Z(sys) == count + _P(sys) # Nyquist plot with poles on imaginary axis, return contour sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1]) @@ -229,10 +238,10 @@ def test_nyquist_indent_default(indentsys): def test_nyquist_indent_dont(indentsys): # first value of default omega vector was 0.1, replaced by 0. for contour # indent_radius is larger than 0.1 -> no extra quater circle around origin - count, contour = ct.nyquist_plot(indentsys, - plot=False, - indent_radius=.1007, - return_contour=True) + with pytest.warns(UserWarning, match="encirclements does not match"): + count, contour = ct.nyquist_plot( + indentsys, omega=[0, 0.2, 0.3, 0.4], indent_radius=.1007, + plot=False, return_contour=True) np.testing.assert_allclose(contour[0], .1007+0.j) # second value of omega_vector is larger than indent_radius: not indented assert np.all(contour.real[2:] == 0.) @@ -240,9 +249,8 @@ def test_nyquist_indent_dont(indentsys): def test_nyquist_indent_do(indentsys): plt.figure(); - count, contour = ct.nyquist_plot(indentsys, - indent_radius=0.01, - return_contour=True) + count, contour = ct.nyquist_plot( + indentsys, indent_radius=0.01, return_contour=True) plt.title("Pole at origin; indent_radius=0.01; encirclements = %d" % count) assert _Z(indentsys) == count + _P(indentsys) # indent radius is smaller than the start of the default omega vector @@ -305,6 +313,19 @@ def test_nyquist_exceptions(): with pytest.warns(UserWarning, match="above Nyquist"): ct.nyquist_plot(sys, np.logspace(-2, 3)) +def test_linestyle_checks(): + sys = ct.rss(2, 1, 1) + + # Things that should work + ct.nyquist_plot(sys, primary_style=['-', '-'], mirror_style=['-', '-']) + ct.nyquist_plot(sys, mirror_style=None) + + with pytest.raises(ValueError, match="invalid 'primary_style'"): + ct.nyquist_plot(sys, primary_style=False) + + with pytest.raises(ValueError, match="invalid 'mirror_style'"): + ct.nyquist_plot(sys, mirror_style=0.2) + if __name__ == "__main__": # @@ -333,11 +354,16 @@ def test_nyquist_exceptions(): test_nyquist_encirclements() print("Indentation checks") - test_nyquist_indent() + s = ct.tf('s') + indentsys = 3 * (s+6)**2 / (s * (s+1)**2) + test_nyquist_indent_default(indentsys) + test_nyquist_indent_do(indentsys) + test_nyquist_indent_left(indentsys) print("Unusual Nyquist plot") sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1]) plt.figure() - plt.title("Poles: %s" % np.array2string(sys.poles(), precision=2, separator=',')) + plt.title("Poles: %s" % + np.array2string(sys.poles(), precision=2, separator=',')) count = ct.nyquist_plot(sys) assert _Z(sys) == count + _P(sys) From 3d3a5593742c938f58a69a11a64406eadcd5dfd5 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 10 Apr 2022 23:01:22 -0700 Subject: [PATCH 70/87] smooth curve offsets to avoid discontinuities --- control/freqplot.py | 90 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index d19a75ab1..05ed7cf4f 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -519,8 +519,8 @@ def gen_zero_centered_series(val_min, val_max, period): # Default values for module parameter variables _nyquist_defaults = { - 'nyquist.primary_style': ['-', ':'], # style for primary curve - 'nyquist.mirror_style': ['--', '-.'], # style for mirror curve + 'nyquist.primary_style': ['-', '-.'], # style for primary curve + 'nyquist.mirror_style': ['--', ':'], # style for mirror curve 'nyquist.arrows': 2, 'nyquist.arrow_size': 8, 'nyquist.indent_radius': 1e-6, # indentation radius @@ -696,6 +696,7 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, kwargs.pop('arrow_length', False) # Get values for params (and pop from list to allow keyword use in plot) + omega_num_given = omega_num is not None omega_num = config._get_param('freqplot', 'number_of_samples', omega_num) arrows = config._get_param( 'nyquist', 'arrows', kwargs, _nyquist_defaults, pop=True) @@ -736,8 +737,13 @@ def _parse_linestyle(style_name, allow_false=False): omega, omega_range_given = _determine_omega_vector( syslist, omega, omega_limits, omega_num) if not omega_range_given: - # Start contour at zero frequency - omega[0] = 0. + if omega_num_given: + # Just reset the starting point + omega[0] = 0.0 + else: + # Insert points between the origin and the first frequency point + omega = np.concatenate(( + np.linspace(0, omega[0], indent_points), omega[1:])) # Go through each system and keep track of the results counts, contours = [], [] @@ -938,17 +944,23 @@ def _parse_linestyle(style_name, allow_false=False): x_reg, y_reg, primary_style[0], color=color, *args, **kwargs) c = p[0].get_color() + # Figure out how much to offset the curve: the offset goes from + # zero at the start of the scaled section to max_curve_offset as + # we move along the curve + curve_offset = _compute_curve_offset( + resp, scale_mask, max_curve_offset) + # Plot the scaled sections of the curve (changing linestyle) x_scl = np.ma.masked_where(scale_mask, resp.real) y_scl = np.ma.masked_where(scale_mask, resp.imag) plt.plot( - x_scl * (1 + max_curve_offset), y_scl * (1 + max_curve_offset), + x_scl * (1 + curve_offset), y_scl * (1 + curve_offset), primary_style[1], color=c, *args, **kwargs) # Plot the primary curve (invisible) for setting arrows x, y = resp.real.copy(), resp.imag.copy() - x[reg_mask] *= (1 + max_curve_offset) - y[reg_mask] *= (1 + max_curve_offset) + x[reg_mask] *= (1 + curve_offset[reg_mask]) + y[reg_mask] *= (1 + curve_offset[reg_mask]) p = plt.plot(x, y, linestyle='None', color=c, *args, **kwargs) # Add arrows @@ -962,14 +974,14 @@ def _parse_linestyle(style_name, allow_false=False): plt.plot( x_reg, -y_reg, mirror_style[0], color=c, *args, **kwargs) plt.plot( - x_scl * (1 - max_curve_offset), - -y_scl * (1 - max_curve_offset), + x_scl * (1 - curve_offset), + -y_scl * (1 - curve_offset), mirror_style[1], color=c, *args, **kwargs) # Add the arrows (on top of an invisible contour) x, y = resp.real.copy(), resp.imag.copy() - x[reg_mask] *= (1 - max_curve_offset) - y[reg_mask] *= (1 - max_curve_offset) + x[reg_mask] *= (1 - curve_offset[reg_mask]) + y[reg_mask] *= (1 - curve_offset[reg_mask]) p = plt.plot(x, -y, linestyle='None', color=c, *args, **kwargs) _add_arrows_to_line2D( ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=-1) @@ -1087,6 +1099,62 @@ def _add_arrows_to_line2D( arrows.append(p) return arrows +# +# Function to compute Nyquist curve offsets +# +# This function computes a smoothly varying offset that starts and ends at +# zero at the ends of a scaled segment. +# +def _compute_curve_offset(resp, mask, max_offset): + # Compute the arc length along the curve + s_curve = np.cumsum( + np.sqrt(np.diff(resp.real) ** 2 + np.diff(resp.imag) ** 2)) + + # Initialize the offset + offset = np.zeros(resp.size) + arclen = np.zeros(resp.size) + + # Walk through the response and keep track of each continous component + i, nsegs = 0, 0 + while i < resp.size: + # Skip the regular segment + while i < resp.size and mask[i]: + i += 1 # Increment the counter + if i == resp.size: + break; + # Keep track of the arclength + arclen[i] = arclen[i-1] + np.abs(resp[i] - resp[i-1]) + + nsegs += 0.5 + if i == resp.size: + break; + + # Save the starting offset of this segment + seg_start = i + + # Walk through the scaled segment + while i < resp.size and not mask[i]: + i += 1 + if i == resp.size: # See if we are done with this segment + break; + # Keep track of the arclength + arclen[i] = arclen[i-1] + np.abs(resp[i] - resp[i-1]) + + nsegs += 0.5 + if i == resp.size: + break; + + # Save the ending offset of this segment + seg_end = i + + # Now compute the scaling for this segment + s_segment = arclen[seg_end-1] - arclen[seg_start] + offset[seg_start:seg_end] = max_offset * s_segment/s_curve[-1] * \ + np.sin(np.pi * (arclen[seg_start:seg_end] + - arclen[seg_start])/s_segment) + + return offset + # # Gang of Four plot From aef3b0618faa2af26372219ea0c20e0ea34a16b4 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 12 Apr 2022 23:04:22 -0700 Subject: [PATCH 71/87] tweak max_curve_magnitude + add start point --- control/freqplot.py | 54 +++++++++++++++++++++++++++-------- control/tests/nyquist_test.py | 7 +++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 05ed7cf4f..3b16b935c 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -519,15 +519,17 @@ def gen_zero_centered_series(val_min, val_max, period): # Default values for module parameter variables _nyquist_defaults = { - 'nyquist.primary_style': ['-', '-.'], # style for primary curve - 'nyquist.mirror_style': ['--', ':'], # style for mirror curve - 'nyquist.arrows': 2, - 'nyquist.arrow_size': 8, - 'nyquist.indent_radius': 1e-6, # indentation radius + 'nyquist.primary_style': ['-', '-.'], # style for primary curve + 'nyquist.mirror_style': ['--', ':'], # style for mirror curve + 'nyquist.arrows': 2, # number of arrors around curve + 'nyquist.arrow_size': 8, # pixel size for arrows + 'nyquist.indent_radius': 1e-4, # indentation radius 'nyquist.indent_direction': 'right', # indentation direction 'nyquist.indent_points': 50, # number of points to insert - 'nyquist.max_curve_magnitude': 20, - 'nyquist.max_curve_offset': 0.02, # percent offset of curves + 'nyquist.max_curve_magnitude': 20, # clip large values + 'nyquist.max_curve_offset': 0.02, # offset of primary/mirror + 'nyquist.start_marker': 'o', # marker at start of curve + 'nyquist.start_marker_size': 4, # size of the maker } @@ -618,8 +620,9 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, are at or near the imaginary axis. indent_radius : float, optional - Amount to indent the Nyquist contour around poles that are at or near - the imaginary axis. + Amount to indent the Nyquist contour around poles on or near the + imaginary axis. Portions of the Nyquist plot corresponding to indented + portions of the contour are plotted using a different line style. max_curve_magnitude : float, optional Restrict the maximum magnitude of the Nyquist plot to this value. @@ -638,13 +641,22 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, `False` then omit completely. Default linestyle (['--', '-.']) is determined by config.defaults['nyquist.mirror_style']. - primary_style : [str, str] + primary_style : [str, str], optional Linestyles for primary image of the Nyquist curve. The first element is used for unscaled portions of the Nyquist curve, the second element is used for scaled portions that are scaled (using max_curve_magnitude). Default linestyle (['-', ':']) is determined by config.defaults['nyquist.mirror_style']. + start_marker : str, optional + Matplotlib marker to use to mark the starting point of the Nyquist + plot. Defaults value is 'o' and can be set using + config.defaults['nyquist.start_marker']. + + start_marker_size : float, optional + Start marker size (in display coordinates). Default value is + 4 and can be set using config.defaults['nyquist.start_marker_size']. + warn_nyquist : bool, optional If set to 'False', turn off warnings about frequencies above Nyquist. @@ -713,6 +725,10 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, 'nyquist', 'max_curve_magnitude', kwargs, _nyquist_defaults, pop=True) max_curve_offset = config._get_param( 'nyquist', 'max_curve_offset', kwargs, _nyquist_defaults, pop=True) + start_marker = config._get_param( + 'nyquist', 'start_marker', kwargs, _nyquist_defaults, pop=True) + start_marker_size = config._get_param( + 'nyquist', 'start_marker_size', kwargs, _nyquist_defaults, pop=True) # Set line styles for the curves def _parse_linestyle(style_name, allow_false=False): @@ -848,6 +864,7 @@ def _parse_linestyle(style_name, allow_false=False): start_freq, p.imag + indent_radius, indent_points)), splane_contour[last_point:])) + # Indent points that are too close to a pole for i, s in enumerate(splane_contour): # Find the nearest pole p = splane_poles[(np.abs(splane_poles - s)).argmin()] @@ -929,13 +946,21 @@ def _parse_linestyle(style_name, allow_false=False): 'simple', head_width=arrow_size, head_length=arrow_size) # Find the different portions of the curve (with scaled pts marked) - reg_mask = abs(resp) > max_curve_magnitude + reg_mask = np.logical_or( + np.abs(resp) > max_curve_magnitude, + contour.real != 0) + # reg_mask = np.logical_or( + # np.abs(resp.real) > max_curve_magnitude, + # np.abs(resp.imag) > max_curve_magnitude) + scale_mask = ~reg_mask \ & np.concatenate((~reg_mask[1:], ~reg_mask[-1:])) \ & np.concatenate((~reg_mask[0:1], ~reg_mask[:-1])) # Rescale the points with large magnitude - resp[reg_mask] /= (np.abs(resp[reg_mask]) / max_curve_magnitude) + rescale = np.logical_and( + reg_mask, abs(resp) > max_curve_magnitude) + resp[rescale] *= max_curve_magnitude / abs(resp[rescale]) # Plot the regular portions of the curve (and grab the color) x_reg = np.ma.masked_where(reg_mask, resp.real) @@ -986,6 +1011,11 @@ def _parse_linestyle(style_name, allow_false=False): _add_arrows_to_line2D( ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=-1) + # Mark the start of the curve + if start_marker: + plt.plot(resp[0].real, resp[0].imag, start_marker, + color=c, markersize=start_marker_size) + # Mark the -1 point plt.plot([-1], [0], 'r+') diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 4f142e901..730b36696 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -360,6 +360,13 @@ def test_linestyle_checks(): test_nyquist_indent_do(indentsys) test_nyquist_indent_left(indentsys) + # Generate a figuring showing effects of different parameters + sys = 3 * (s+6)**2 / (s * (s**2 + 1e-4 * s + 1)) + plt.figure() + ct.nyquist_plot(sys) + ct.nyquist_plot(sys, max_curve_magnitude=15) + ct.nyquist_plot(sys, indent_radius=1e-6, max_curve_magnitude=25) + print("Unusual Nyquist plot") sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1]) plt.figure() From c25c0cb39a00c22078d83a288302a21e4fc9e634 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 13 Apr 2022 22:38:07 -0700 Subject: [PATCH 72/87] add warning if encirclements is not near an integer --- control/freqplot.py | 24 +++++++++++++++++++++++- control/tests/descfcn_test.py | 4 ++-- control/tests/freqresp_test.py | 5 +++-- control/tests/nyquist_test.py | 11 +++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 3b16b935c..7710c0d53 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -523,6 +523,7 @@ def gen_zero_centered_series(val_min, val_max, period): 'nyquist.mirror_style': ['--', ':'], # style for mirror curve 'nyquist.arrows': 2, # number of arrors around curve 'nyquist.arrow_size': 8, # pixel size for arrows + 'nyquist.encirclement_threshold': 0.05, # warning threshold 'nyquist.indent_radius': 1e-4, # indentation radius 'nyquist.indent_direction': 'right', # indentation direction 'nyquist.indent_points': 50, # number of points to insert @@ -611,6 +612,11 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, arrow_style : matplotlib.patches.ArrowStyle, optional Define style used for Nyquist curve arrows (overrides `arrow_size`). + encirclement_threshold : float, optional + Define the threshold for generating a warning if the number of net + encirclements is a non-integer value. Default value is 0.05 and can + be set using config.defaults['nyquist.encirclement_threshold']. + indent_direction : str, optional For poles on the imaginary axis, set the direction of indentation to be 'right' (default), 'left', or 'none'. @@ -717,6 +723,9 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, arrow_style = config._get_param('nyquist', 'arrow_style', kwargs, None) indent_radius = config._get_param( 'nyquist', 'indent_radius', kwargs, _nyquist_defaults, pop=True) + encirclement_threshold = config._get_param( + 'nyquist', 'encirclement_threshold', kwargs, + _nyquist_defaults, pop=True) indent_direction = config._get_param( 'nyquist', 'indent_direction', kwargs, _nyquist_defaults, pop=True) indent_points = config._get_param( @@ -750,8 +759,11 @@ def _parse_linestyle(style_name, allow_false=False): if not isinstance(syslist, (list, tuple)): syslist = (syslist,) + # Determine the range of frequencies to use, based on args/features omega, omega_range_given = _determine_omega_vector( syslist, omega, omega_limits, omega_num) + + # If omega was not specified explicitly, start at omega = 0 if not omega_range_given: if omega_num_given: # Just reset the starting point @@ -852,6 +864,7 @@ def _parse_linestyle(style_name, allow_false=False): first_point = 0 start_freq = 0 + # Find the frequencies after the pole frequency above_points = np.argwhere( splane_contour.imag - abs(p.imag) > indent_radius) last_point = above_points[0].item() @@ -900,7 +913,15 @@ def _parse_linestyle(style_name, allow_false=False): # Compute CW encirclements of -1 by integrating the (unwrapped) angle phase = -unwrap(np.angle(resp + 1)) - count = int(np.round(np.sum(np.diff(phase)) / np.pi, 0)) + encirclements = np.sum(np.diff(phase)) / np.pi + count = int(np.round(encirclements, 0)) + + # Let the user know if the count might not make sense + if abs(encirclements - count) > encirclement_threshold: + warnings.warn( + "number of encirclements was a non-integer value; this can" + " happen is contour is not closed, possibly based on a" + " frequency range that does not include zero.") # # Make sure that the enciriclements match the Nyquist criterion @@ -1534,6 +1555,7 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num, num=omega_num, endpoint=True) else: omega_out = np.copy(omega_in) + return omega_out, omega_range_given diff --git a/control/tests/descfcn_test.py b/control/tests/descfcn_test.py index 796ad9034..0ebfda446 100644 --- a/control/tests/descfcn_test.py +++ b/control/tests/descfcn_test.py @@ -140,7 +140,7 @@ def test_describing_function(fcn, amin, amax): def test_describing_function_plot(): # Simple linear system with at most 1 intersection H_simple = ct.tf([1], [1, 2, 2, 1]) - omega = np.logspace(-1, 2, 100) + omega = np.logspace(-2, 2, 100) # Saturation nonlinearity F_saturation = ct.descfcn.saturation_nonlinearity(1) @@ -160,7 +160,7 @@ def test_describing_function_plot(): # Multiple intersections H_multiple = H_simple * ct.tf(*ct.pade(5, 4)) * 4 - omega = np.logspace(-1, 3, 50) + omega = np.logspace(-2, 3, 50) F_backlash = ct.descfcn.friction_backlash_nonlinearity(1) amp = np.linspace(0.6, 5, 50) xsects = ct.describing_function_plot(H_multiple, F_backlash, amp, omega) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 0e35a38ea..573fd6359 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -81,8 +81,9 @@ def test_nyquist_basic(ss_siso): tf_siso, plot=False, return_contour=True, omega_num=20) assert len(contour) == 20 - count, contour = nyquist_plot( - tf_siso, plot=False, omega_limits=(1, 100), return_contour=True) + with pytest.warns(UserWarning, match="encirclements was a non-integer"): + count, contour = nyquist_plot( + tf_siso, plot=False, omega_limits=(1, 100), return_contour=True) assert_allclose(contour[0], 1j) assert_allclose(contour[-1], 100j) diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 730b36696..3b383f655 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -219,6 +219,17 @@ def test_nyquist_encirclements(): plt.title("Pole at the origin; encirclements = %d" % count) assert _Z(sys) == count + _P(sys) + # Non-integer number of encirclements + plt.figure(); + sys = 1 / (s**2 + s + 1) + with pytest.warns(UserWarning, match="encirclements was a non-integer"): + count = ct.nyquist_plot(sys, omega_limits=[0.5, 1e3]) + with pytest.warns(None) as records: + count = ct.nyquist_plot( + sys, omega_limits=[0.5, 1e3], encirclement_threshold=0.2) + assert len(records) == 0 + plt.title("Non-integer number of encirclements [%g]" % count) + @pytest.fixture def indentsys(): From 67c1f3f68944feecae6e9028744f2c007746423b Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 14 Apr 2022 18:59:33 -0700 Subject: [PATCH 73/87] add legacy settings for nyquist --- control/config.py | 9 +++++++++ control/tests/nyquist_test.py | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/control/config.py b/control/config.py index 605fbcb23..32f5f2eef 100644 --- a/control/config.py +++ b/control/config.py @@ -267,6 +267,15 @@ def use_legacy_defaults(version): # reset_defaults() # start from a clean slate + # Version 0.9.2: + if major == 0 and minor < 9 or (minor == 9 and patch < 2): + from math import inf + + # Reset Nyquist defaults + set_defaults('nyquist', indent_radius=0.1, max_curve_magnitude=inf, + max_curve_offset=0, primary_style=['-', '-'], + mirror_style=['--', '--'], start_marker_size=0) + # Version 0.9.0: if major == 0 and minor < 9: # switched to 'array' as default for state space objects diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 3b383f655..8ac083535 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -337,6 +337,17 @@ def test_linestyle_checks(): with pytest.raises(ValueError, match="invalid 'mirror_style'"): ct.nyquist_plot(sys, mirror_style=0.2) +@pytest.mark.usefixtures("editsdefaults") +def test_nyquist_legacy(): + ct.use_legacy_defaults('0.9.1') + + # Example that generated a warning using earlier defaults + s = ct.tf('s') + sys = (0.02 * s**3 - 0.1 * s) / (s**4 + s**3 + s**2 + 0.25 * s + 0.04) + + with pytest.warns(UserWarning, match="indented contour may miss"): + count = ct.nyquist_plot(sys) + if __name__ == "__main__": # From 9a5759fb2809db1ca8c8f7ed42168646cc42be3a Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 14 Apr 2022 22:11:28 -0700 Subject: [PATCH 74/87] increase coverage; PEP8 cleanup --- control/freqplot.py | 82 +++++++++++++++++++---------------- control/tests/nyquist_test.py | 15 +++++++ 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 7710c0d53..006bf4830 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -149,12 +149,13 @@ def bode_plot(syslist, omega=None, the `deg` parameter. Default is -180 if wrap_phase is False, 0 if wrap_phase is True. wrap_phase : bool or float - If wrap_phase is `False`, then the phase will be unwrapped so that it - is continuously increasing or decreasing. If wrap_phase is `True` the - phase will be restricted to the range [-180, 180) (or [:math:`-\\pi`, - :math:`\\pi`) radians). If `wrap_phase` is specified as a float, the - phase will be offset by 360 degrees if it falls below the specified - value. Default to `False`, set by config.defaults['freqplot.wrap_phase']. + If wrap_phase is `False` (default), then the phase will be unwrapped + so that it is continuously increasing or decreasing. If wrap_phase is + `True` the phase will be restricted to the range [-180, 180) (or + [:math:`-\\pi`, :math:`\\pi`) radians). If `wrap_phase` is specified + as a float, the phase will be offset by 360 degrees if it falls below + the specified value. Default value is `False` and can be set using + config.defaults['freqplot.wrap_phase']. The default values for Bode plot configuration parameters can be reset using the `config.defaults` dictionary, with module name 'bode'. @@ -573,9 +574,6 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, return_contour : bool, optional If 'True', return the contour used to evaluate the Nyquist plot. - *args : :func:`matplotlib.pyplot.plot` positional properties, optional - Additional arguments for `matplotlib` plots (color, linestyle, etc) - **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional Additional keywords (passed to `matplotlib`) @@ -586,15 +584,12 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, multiple systems are given, an array of counts is returned. contour : ndarray (or list of ndarray if len(syslist) > 1)), optional - The contour used to create the primary Nyquist curve segment. To - obtain the Nyquist curve values, evaluate system(s) along contour. + The contour used to create the primary Nyquist curve segment, returned + if `return_contour` is Tue. To obtain the Nyquist curve values, + evaluate system(s) along contour. Additional Parameters --------------------- - label_freq : int, optiona - Label every nth frequency on the plot. If not specified, no labels - are generated. - arrows : int or 1D/2D array of floats, optional Specify the number of arrows to plot on the Nyquist curve. If an integer is passed. that number of equally spaced arrows will be @@ -630,6 +625,10 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, imaginary axis. Portions of the Nyquist plot corresponding to indented portions of the contour are plotted using a different line style. + label_freq : int, optiona + Label every nth frequency on the plot. If not specified, no labels + are generated. + max_curve_magnitude : float, optional Restrict the maximum magnitude of the Nyquist plot to this value. Portions of the Nyquist plot whose magnitude is restricted are @@ -666,6 +665,10 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, warn_nyquist : bool, optional If set to 'False', turn off warnings about frequencies above Nyquist. + warn_encirclements : bool, optional + If set to 'False', turn off warnings about number of encirclements not + meeting the Nyquist criterion. + Notes ----- 1. If a discrete time model is given, the frequency response is computed @@ -885,22 +888,22 @@ def _parse_linestyle(style_name, allow_false=False): # See if we need to indent around it if abs(s - p) < indent_radius: # Figure out how much to offset (simple trigonometry) - offset = np.sqrt(indent_radius ** 2 - (s-p).imag ** 2) \ - -(s-p).real + offset = np.sqrt(indent_radius ** 2 - (s - p).imag ** 2) \ + - (s - p).real # Figure out which way to offset the contour point - if p.real < 0 or (np.isclose(p.real, 0) \ - and indent_direction == 'right'): + if p.real < 0 or (np.isclose(p.real, 0) + and indent_direction == 'right'): # Indent to the right splane_contour[i] += offset - elif p.real > 0 or (np.isclose(p.real, 0) \ - and indent_direction == 'left'): + elif p.real > 0 or (np.isclose(p.real, 0) + and indent_direction == 'left'): # Indent to the left splane_contour[i] -= offset else: - ValueError("unknown value for indent_direction") + raise ValueError("unknown value for indent_direction") # change contour to z-plane if necessary if sys.isctime(): @@ -939,7 +942,8 @@ def _parse_linestyle(style_name, allow_false=False): "number of encirclements does not match Nyquist criterion;" " check frequency range and indent radius/direction", UserWarning, stacklevel=2) - elif indent_direction == 'none' and any(sys.pole().real == 0): + elif indent_direction == 'none' and \ + any(np.isclose(sys.pole().real, 0)): warnings.warn( "system has pure imaginary poles but indentation is" " turned off; results may be meaningless", @@ -950,14 +954,14 @@ def _parse_linestyle(style_name, allow_false=False): if plot: # Parse the arrows keyword - if isinstance(arrows, int): + if not arrows: + arrow_pos = [] + elif isinstance(arrows, int): N = arrows # Space arrows out, starting midway along each "region" arrow_pos = np.linspace(0.5/N, 1 + 0.5/N, N, endpoint=False) elif isinstance(arrows, (list, np.ndarray)): arrow_pos = np.sort(np.atleast_1d(arrows)) - elif not arrows: - arrow_pos = [] else: raise ValueError("unknown or unsupported arrow location") @@ -1150,6 +1154,7 @@ def _add_arrows_to_line2D( arrows.append(p) return arrows + # # Function to compute Nyquist curve offsets # @@ -1172,13 +1177,13 @@ def _compute_curve_offset(resp, mask, max_offset): while i < resp.size and mask[i]: i += 1 # Increment the counter if i == resp.size: - break; + break # Keep track of the arclength arclen[i] = arclen[i-1] + np.abs(resp[i] - resp[i-1]) nsegs += 0.5 if i == resp.size: - break; + break # Save the starting offset of this segment seg_start = i @@ -1187,13 +1192,13 @@ def _compute_curve_offset(resp, mask, max_offset): while i < resp.size and not mask[i]: i += 1 if i == resp.size: # See if we are done with this segment - break; + break # Keep track of the arclength arclen[i] = arclen[i-1] + np.abs(resp[i] - resp[i-1]) nsegs += 0.5 if i == resp.size: - break; + break # Save the ending offset of this segment seg_end = i @@ -1333,7 +1338,8 @@ def singular_values_plot(syslist, omega=None, *args, **kwargs): """Singular value plot for a system - Plots a Singular Value plot for the system over a (optional) frequency range. + Plots a singular value plot for the system over a (optional) frequency + range. Parameters ---------- @@ -1347,11 +1353,11 @@ def singular_values_plot(syslist, omega=None, Limits of the frequency vector to generate. If Hz=True the limits are in Hz otherwise in rad/s. omega_num : int - Number of samples to plot. - Default value (1000) set by config.defaults['freqplot.number_of_samples']. + Number of samples to plot. Default value (1000) set by + config.defaults['freqplot.number_of_samples']. dB : bool - If True, plot result in dB. - Default value (False) set by config.defaults['freqplot.dB']. + If True, plot result in dB. Default value (False) set by + config.defaults['freqplot.dB']. Hz : bool If True, plot frequency in Hz (omega must be provided in rad/sec). Default value (False) set by config.defaults['freqplot.Hz'] @@ -1373,7 +1379,8 @@ def singular_values_plot(syslist, omega=None, -------- >>> import numpy as np >>> den = [75, 1] - >>> sys = TransferFunction([[[87.8], [-86.4]], [[108.2], [-109.6]]], [[den, den], [den, den]]) + >>> sys = TransferFunction( + [[[87.8], [-86.4]], [[108.2], [-109.6]]], [[den, den], [den, den]]) >>> omega = np.logspace(-4, 1, 1000) >>> sigma, omega = singular_values_plot(sys, plot=True) >>> singular_values_plot(sys, 0.0, plot=False) @@ -1480,7 +1487,8 @@ def singular_values_plot(syslist, omega=None, # Add a grid to the plot + labeling if plot: ax_sigma.grid(grid, which='both') - ax_sigma.set_ylabel("Singular Values (dB)" if dB else "Singular Values") + ax_sigma.set_ylabel( + "Singular Values (dB)" if dB else "Singular Values") ax_sigma.set_xlabel("Frequency (Hz)" if Hz else "Frequency (rad/sec)") if len(syslist) == 1: diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 8ac083535..4a9f662b3 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -185,6 +185,7 @@ def test_nyquist_fbs_examples(): @pytest.mark.parametrize("arrows", [ None, # default argument + False, # no arrows 1, 2, 3, 4, # specified number of arrows [0.1, 0.5, 0.9], # specify arc lengths ]) @@ -318,12 +319,22 @@ def test_nyquist_exceptions(): with pytest.warns(FutureWarning, match="use `arrow_size` instead"): ct.nyquist_plot(sys, arrow_width=8, arrow_length=6) + # Unknown arrow keyword + with pytest.raises(ValueError, match="unsupported arrow location"): + ct.nyquist_plot(sys, arrows='uniform') + + # Bad value for indent direction + sys = ct.tf([1], [1, 0, 1]) + with pytest.raises(ValueError, match="unknown value for indent"): + ct.nyquist_plot(sys, indent_direction='up') + # Discrete time system sampled above Nyquist frequency sys = ct.drss(2, 1, 1) sys.dt = 0.01 with pytest.warns(UserWarning, match="above Nyquist"): ct.nyquist_plot(sys, np.logspace(-2, 3)) + def test_linestyle_checks(): sys = ct.rss(2, 1, 1) @@ -337,6 +348,10 @@ def test_linestyle_checks(): with pytest.raises(ValueError, match="invalid 'mirror_style'"): ct.nyquist_plot(sys, mirror_style=0.2) + # If only one line style is given use, the default value for the other + # TODO: for now, just make sure the signature works; no correct check yet + ct.nyquist_plot(sys, primary_style=':', mirror_style='-.') + @pytest.mark.usefixtures("editsdefaults") def test_nyquist_legacy(): ct.use_legacy_defaults('0.9.1') From 5dd6a70ba731983188e40e99a0519aef39b9767c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 15 Apr 2022 11:08:47 -0700 Subject: [PATCH 75/87] turn off encirclement warnings for describing function plot --- control/descfcn.py | 15 +++++++++++-- control/freqplot.py | 41 +++++++++++++++++++---------------- control/tests/descfcn_test.py | 4 ++-- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/control/descfcn.py b/control/descfcn.py index 2ebb18569..149db1bd2 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -199,7 +199,8 @@ def describing_function( def describing_function_plot( - H, F, A, omega=None, refine=True, label="%5.2g @ %-5.2g", **kwargs): + H, F, A, omega=None, refine=True, label="%5.2g @ %-5.2g", + warn=None, **kwargs): """Plot a Nyquist plot with a describing function for a nonlinear system. This function generates a Nyquist plot for a closed loop system consisting @@ -220,6 +221,10 @@ def describing_function_plot( label : str, optional Formatting string used to label intersection points on the Nyquist plot. Defaults to "%5.2g @ %-5.2g". Set to `None` to omit labels. + warn : bool, optional + Set to True to turn on warnings generated by `nyquist_plot` or False + to turn off warnings. If not set (or set to None), warnings are + turned off if omega is specified, otherwise they are turned on. Returns ------- @@ -240,9 +245,15 @@ def describing_function_plot( [(3.344008947853124, 1.414213099755523)] """ + # Decide whether to turn on warnings or not + if warn is None: + # Turn warnings on unless omega was specified + warn = omega is None + # Start by drawing a Nyquist curve count, contour = nyquist_plot( - H, omega, plot=True, return_contour=True, **kwargs) + H, omega, plot=True, return_contour=True, + warn_encirclements=warn, warn_nyquist=warn, **kwargs) H_omega, H_vals = contour.imag, H(contour) # Compute the describing function diff --git a/control/freqplot.py b/control/freqplot.py index 006bf4830..56e67e91d 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -535,9 +535,10 @@ def gen_zero_centered_series(val_min, val_max, period): } -def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, - omega_num=None, label_freq=0, color=None, - return_contour=False, warn_nyquist=True, *args, **kwargs): +def nyquist_plot( + syslist, omega=None, plot=True, omega_limits=None, omega_num=None, + label_freq=0, color=None, return_contour=False, + warn_encirclements=True, warn_nyquist=True, **kwargs): """Nyquist plot for a system Plots a Nyquist plot for the system over a (optional) frequency range. @@ -647,11 +648,11 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, determined by config.defaults['nyquist.mirror_style']. primary_style : [str, str], optional - Linestyles for primary image of the Nyquist curve. The first element - is used for unscaled portions of the Nyquist curve, the second - element is used for scaled portions that are scaled (using - max_curve_magnitude). Default linestyle (['-', ':']) is determined by - config.defaults['nyquist.mirror_style']. + Linestyles for primary image of the Nyquist curve. The first + element is used for unscaled portions of the Nyquist curve, + the second element is used for portions that are scaled (using + max_curve_magnitude). Default linestyle (['-', ':']) is + determined by config.defaults['nyquist.mirror_style']. start_marker : str, optional Matplotlib marker to use to mark the starting point of the Nyquist @@ -839,7 +840,8 @@ def _parse_linestyle(style_name, allow_false=False): p_ol = splane_poles[ (np.abs(splane_poles - p_cl)).argmin()] - if abs(p_ol - p_cl) <= indent_radius: + if abs(p_ol - p_cl) <= indent_radius and \ + warn_encirclements: warnings.warn( "indented contour may miss closed loop pole; " "consider reducing indent_radius to be less than " @@ -920,7 +922,8 @@ def _parse_linestyle(style_name, allow_false=False): count = int(np.round(encirclements, 0)) # Let the user know if the count might not make sense - if abs(encirclements - count) > encirclement_threshold: + if abs(encirclements - count) > encirclement_threshold and \ + warn_encirclements: warnings.warn( "number of encirclements was a non-integer value; this can" " happen is contour is not closed, possibly based on a" @@ -937,13 +940,13 @@ def _parse_linestyle(style_name, allow_false=False): P = (sys.pole().real > 0).sum() if indent_direction == 'right' \ else (sys.pole().real >= 0).sum() Z = (sys.feedback().pole().real >= 0).sum() - if Z != count + P: + if Z != count + P and warn_encirclements: warnings.warn( "number of encirclements does not match Nyquist criterion;" " check frequency range and indent radius/direction", UserWarning, stacklevel=2) - elif indent_direction == 'none' and \ - any(np.isclose(sys.pole().real, 0)): + elif indent_direction == 'none' and any(sys.pole().real == 0) and \ + warn_encirclements: warnings.warn( "system has pure imaginary poles but indentation is" " turned off; results may be meaningless", @@ -991,7 +994,7 @@ def _parse_linestyle(style_name, allow_false=False): x_reg = np.ma.masked_where(reg_mask, resp.real) y_reg = np.ma.masked_where(reg_mask, resp.imag) p = plt.plot( - x_reg, y_reg, primary_style[0], color=color, *args, **kwargs) + x_reg, y_reg, primary_style[0], color=color, **kwargs) c = p[0].get_color() # Figure out how much to offset the curve: the offset goes from @@ -1005,13 +1008,13 @@ def _parse_linestyle(style_name, allow_false=False): y_scl = np.ma.masked_where(scale_mask, resp.imag) plt.plot( x_scl * (1 + curve_offset), y_scl * (1 + curve_offset), - primary_style[1], color=c, *args, **kwargs) + primary_style[1], color=c, **kwargs) # Plot the primary curve (invisible) for setting arrows x, y = resp.real.copy(), resp.imag.copy() x[reg_mask] *= (1 + curve_offset[reg_mask]) y[reg_mask] *= (1 + curve_offset[reg_mask]) - p = plt.plot(x, y, linestyle='None', color=c, *args, **kwargs) + p = plt.plot(x, y, linestyle='None', color=c, **kwargs) # Add arrows ax = plt.gca() @@ -1022,17 +1025,17 @@ def _parse_linestyle(style_name, allow_false=False): if mirror_style is not False: # Plot the regular and scaled segments plt.plot( - x_reg, -y_reg, mirror_style[0], color=c, *args, **kwargs) + x_reg, -y_reg, mirror_style[0], color=c, **kwargs) plt.plot( x_scl * (1 - curve_offset), -y_scl * (1 - curve_offset), - mirror_style[1], color=c, *args, **kwargs) + mirror_style[1], color=c, **kwargs) # Add the arrows (on top of an invisible contour) x, y = resp.real.copy(), resp.imag.copy() x[reg_mask] *= (1 - curve_offset[reg_mask]) y[reg_mask] *= (1 - curve_offset[reg_mask]) - p = plt.plot(x, -y, linestyle='None', color=c, *args, **kwargs) + p = plt.plot(x, -y, linestyle='None', color=c, **kwargs) _add_arrows_to_line2D( ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=-1) diff --git a/control/tests/descfcn_test.py b/control/tests/descfcn_test.py index 0ebfda446..796ad9034 100644 --- a/control/tests/descfcn_test.py +++ b/control/tests/descfcn_test.py @@ -140,7 +140,7 @@ def test_describing_function(fcn, amin, amax): def test_describing_function_plot(): # Simple linear system with at most 1 intersection H_simple = ct.tf([1], [1, 2, 2, 1]) - omega = np.logspace(-2, 2, 100) + omega = np.logspace(-1, 2, 100) # Saturation nonlinearity F_saturation = ct.descfcn.saturation_nonlinearity(1) @@ -160,7 +160,7 @@ def test_describing_function_plot(): # Multiple intersections H_multiple = H_simple * ct.tf(*ct.pade(5, 4)) * 4 - omega = np.logspace(-2, 3, 50) + omega = np.logspace(-1, 3, 50) F_backlash = ct.descfcn.friction_backlash_nonlinearity(1) amp = np.linspace(0.6, 5, 50) xsects = ct.describing_function_plot(H_multiple, F_backlash, amp, omega) From 5d9c7f0e890e1db7f41d5d2c4fbe93aece9cbdf9 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 15 Apr 2022 17:59:07 -0700 Subject: [PATCH 76/87] rebase cleanup --- control/freqplot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 56e67e91d..06a85a6e1 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -937,15 +937,15 @@ def _parse_linestyle(style_name, allow_false=False): # Nyquist criterion is actually satisfied. # if isinstance(sys, (StateSpace, TransferFunction)): - P = (sys.pole().real > 0).sum() if indent_direction == 'right' \ - else (sys.pole().real >= 0).sum() - Z = (sys.feedback().pole().real >= 0).sum() + P = (sys.poles().real > 0).sum() if indent_direction == 'right' \ + else (sys.poles().real >= 0).sum() + Z = (sys.feedback().poles().real >= 0).sum() if Z != count + P and warn_encirclements: warnings.warn( "number of encirclements does not match Nyquist criterion;" " check frequency range and indent radius/direction", UserWarning, stacklevel=2) - elif indent_direction == 'none' and any(sys.pole().real == 0) and \ + elif indent_direction == 'none' and any(sys.poles().real == 0) and \ warn_encirclements: warnings.warn( "system has pure imaginary poles but indentation is" From 782e526059f17dc15ec68f2d1eb7855ece740969 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 16 Apr 2022 15:12:25 +0200 Subject: [PATCH 77/87] Improvements to Nichols chart plotting Clip closed-loop contour labels. Add labels for constant closed-loop phase contours. Use smaller of data or view extents when deciding on how big, in phase, a chart to create. Use more uniformly spaced closed-loop phase contours, and use more widely-spaced contours when phase extent is large. Add optional `ax` argument, for axes to add grid to. --- control/nichols.py | 81 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/control/nichols.py b/control/nichols.py index a643d8580..88ff22974 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -51,6 +51,8 @@ import numpy as np import matplotlib.pyplot as plt +import matplotlib.transforms + from .ctrlutil import unwrap from .freqplot import _default_frequency_range from . import config @@ -119,7 +121,18 @@ def nichols_plot(sys_list, omega=None, grid=None): nichols_grid() -def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): +def _inner_extents(ax): + # intersection of data and view extents + # if intersection empty, return view extents + _inner = matplotlib.transforms.Bbox.intersection(ax.viewLim, ax.dataLim) + if _inner is None: + return ax.ViewLim.extents + else: + return _inner.extents + + +def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted', ax=None, + label_cl_phases=True): """Nichols chart grid Plots a Nichols chart grid on the current axis, or creates a new chart @@ -136,8 +149,14 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): line_style : string, optional :doc:`Matplotlib linestyle \ ` - + ax : matplotlib.axes.Axes, optional + Axes to add grid to. If ``None``, use ``plt.gca()``. + label_cl_phases: bool, optional + If True, closed-loop phase lines will be labelled. """ + if ax is None: + ax = plt.gca() + # Default chart size ol_phase_min = -359.99 ol_phase_max = 0.0 @@ -145,8 +164,8 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): ol_mag_max = default_ol_mag_max = 50.0 # Find bounds of the current dataset, if there is one. - if plt.gcf().gca().has_data(): - ol_phase_min, ol_phase_max, ol_mag_min, ol_mag_max = plt.axis() + if ax.has_data(): + ol_phase_min, ol_mag_min, ol_phase_max, ol_mag_max = _inner_extents(ax) # M-circle magnitudes. if cl_mags is None: @@ -165,17 +184,18 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): ol_mag_min + cl_mag_step, cl_mag_step) cl_mags = np.concatenate((extended_cl_mags, key_cl_mags)) + phase_offset_min = 360.0*np.ceil(ol_phase_min/360.0) + phase_offset_max = 360.0*np.ceil(ol_phase_max/360.0) + 360.0 + # N-circle phases (should be in the range -360 to 0) if cl_phases is None: - # Choose a reasonable set of default phases (denser if the open-loop - # data is restricted to a relatively small range of phases). - key_cl_phases = np.array([-0.25, -45.0, -90.0, -180.0, -270.0, - -325.0, -359.75]) - if np.abs(ol_phase_max - ol_phase_min) < 90.0: - other_cl_phases = np.arange(-10.0, -360.0, -10.0) - else: - other_cl_phases = np.arange(-10.0, -360.0, -20.0) - cl_phases = np.concatenate((key_cl_phases, other_cl_phases)) + # aim for 9 lines, but always show (-360+eps, -180, -eps) + # smallest spacing is 45, biggest is 180 + phase_span = phase_offset_max - phase_offset_min + spacing = np.clip(round(phase_span / 8 / 45) * 45, 45, 180) + key_cl_phases = np.array([-0.25, -359.75]) + other_cl_phases = np.arange(-spacing, -360.0, -spacing) + cl_phases = np.unique(np.concatenate((key_cl_phases, other_cl_phases))) else: assert ((-360.0 < np.min(cl_phases)) and (np.max(cl_phases) < 0.0)) @@ -196,27 +216,46 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): # over the range -360 < phase < 0. Given the range # the base chart is computed over, the phase offset should be 0 # for -360 < ol_phase_min < 0. - phase_offset_min = 360.0*np.ceil(ol_phase_min/360.0) - phase_offset_max = 360.0*np.ceil(ol_phase_max/360.0) + 360.0 phase_offsets = np.arange(phase_offset_min, phase_offset_max, 360.0) for phase_offset in phase_offsets: # Draw M and N contours - plt.plot(m_phase + phase_offset, m_mag, color='lightgray', + ax.plot(m_phase + phase_offset, m_mag, color='lightgray', linestyle=line_style, zorder=0) - plt.plot(n_phase + phase_offset, n_mag, color='lightgray', + ax.plot(n_phase + phase_offset, n_mag, color='lightgray', linestyle=line_style, zorder=0) # Add magnitude labels for x, y, m in zip(m_phase[:][-1] + phase_offset, m_mag[:][-1], cl_mags): align = 'right' if m < 0.0 else 'left' - plt.text(x, y, str(m) + ' dB', size='small', ha=align, - color='gray') + ax.text(x, y, str(m) + ' dB', size='small', ha=align, + color='gray', clip_on=True) + + # phase labels + if label_cl_phases: + for x, y, p in zip(n_phase[:][0] + phase_offset, + n_mag[:][0], + cl_phases): + if p > -175: + align = 'right' + elif p > -185: + align = 'center' + else: + align = 'left' + ax.text(x, y, f'{round(p)}\N{DEGREE SIGN}', + size='small', + ha=align, + va='bottom', + color='gray', + clip_on=True) + # Fit axes to generated chart - plt.axis([phase_offset_min - 360.0, phase_offset_max - 360.0, - np.min(cl_mags), np.max([ol_mag_max, default_ol_mag_max])]) + ax.axis([phase_offset_min - 360.0, + phase_offset_max - 360.0, + np.min(np.concatenate([cl_mags,[ol_mag_min]])), + np.max([ol_mag_max, default_ol_mag_max])]) # # Utility functions From 7407265496ecf03de95d31416a663afb09aa7d3c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 16 Apr 2022 13:49:13 -0700 Subject: [PATCH 78/87] Fixed docstrings per roryyorke plus discrete-time corrections * changed return type for poles() and zeros() to complex * updated (missing) discrete-time tests for stability warnings * changed processing of near imaginary poles to pure imaginary poles --- control/freqplot.py | 57 ++++++++++++++++++++++------------- control/statesp.py | 8 +++-- control/tests/nyquist_test.py | 23 +++++++++++--- control/xferfcn.py | 4 +-- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 06a85a6e1..05ae9da55 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -644,14 +644,14 @@ def nyquist_plot( Linestyles for mirror image of the Nyquist curve. The first element is used for unscaled portions of the Nyquist curve, the second element is used for portions that are scaled (using max_curve_magnitude). If - `False` then omit completely. Default linestyle (['--', '-.']) is + `False` then omit completely. Default linestyle (['--', ':']) is determined by config.defaults['nyquist.mirror_style']. primary_style : [str, str], optional Linestyles for primary image of the Nyquist curve. The first element is used for unscaled portions of the Nyquist curve, the second element is used for portions that are scaled (using - max_curve_magnitude). Default linestyle (['-', ':']) is + max_curve_magnitude). Default linestyle (['-', '-.']) is determined by config.defaults['nyquist.mirror_style']. start_marker : str, optional @@ -750,6 +750,9 @@ def _parse_linestyle(style_name, allow_false=False): if isinstance(style, str): # Only one style provided, use the default for the other style = [style, _nyquist_defaults['nyquist.' + style_name][1]] + warnings.warn( + "use of a single string for linestyle will be deprecated " + " in a future release", PendingDeprecationWarning) if (allow_false and style is False) or \ (isinstance(style, list) and len(style) == 2): return style @@ -765,7 +768,7 @@ def _parse_linestyle(style_name, allow_false=False): # Determine the range of frequencies to use, based on args/features omega, omega_range_given = _determine_omega_vector( - syslist, omega, omega_limits, omega_num) + syslist, omega, omega_limits, omega_num, feature_periphery_decades=2) # If omega was not specified explicitly, start at omega = 0 if not omega_range_given: @@ -790,7 +793,7 @@ def _parse_linestyle(style_name, allow_false=False): # Determine the contour used to evaluate the Nyquist curve if sys.isdtime(strict=True): - # Transform frequencies in for discrete-time systems + # Restrict frequencies for discrete-time systems nyquistfrq = math.pi / sys.dt if not omega_range_given: # limit up to and including nyquist frequency @@ -817,12 +820,12 @@ def _parse_linestyle(style_name, allow_false=False): # because we don't need to indent for them zplane_poles = sys.poles() zplane_poles = zplane_poles[~np.isclose(abs(zplane_poles), 0.)] - splane_poles = np.log(zplane_poles)/sys.dt + splane_poles = np.log(zplane_poles) / sys.dt zplane_cl_poles = sys.feedback().poles() zplane_cl_poles = zplane_cl_poles[ ~np.isclose(abs(zplane_poles), 0.)] - splane_cl_poles = np.log(zplane_cl_poles)/sys.dt + splane_cl_poles = np.log(zplane_cl_poles) / sys.dt # # Check to make sure indent radius is small enough @@ -851,8 +854,8 @@ def _parse_linestyle(style_name, allow_false=False): # See if we should add some frequency points near imaginary poles # for p in splane_poles: - # See if we need to process this pole (skip any that is on - # the not near or on the negative omega axis + user override) + # See if we need to process this pole (skip if on the negative + # imaginary axis or not near imaginary axis + user override) if p.imag < 0 or abs(p.real) > indent_radius or \ omega_range_given: continue @@ -894,13 +897,13 @@ def _parse_linestyle(style_name, allow_false=False): - (s - p).real # Figure out which way to offset the contour point - if p.real < 0 or (np.isclose(p.real, 0) - and indent_direction == 'right'): + if p.real < 0 or (p.real == 0 and + indent_direction == 'right'): # Indent to the right splane_contour[i] += offset - elif p.real > 0 or (np.isclose(p.real, 0) - and indent_direction == 'left'): + elif p.real > 0 or (p.real == 0 and + indent_direction == 'left'): # Indent to the left splane_contour[i] -= offset @@ -937,9 +940,21 @@ def _parse_linestyle(style_name, allow_false=False): # Nyquist criterion is actually satisfied. # if isinstance(sys, (StateSpace, TransferFunction)): - P = (sys.poles().real > 0).sum() if indent_direction == 'right' \ - else (sys.poles().real >= 0).sum() - Z = (sys.feedback().poles().real >= 0).sum() + # Count the number of open/closed loop RHP poles + if sys.isctime(): + if indent_direction == 'right': + P = (sys.poles().real > 0).sum() + else: + P = (sys.poles().real >= 0).sum() + Z = (sys.feedback().poles().real >= 0).sum() + else: + if indent_direction == 'right': + P = (np.abs(sys.poles()) > 1).sum() + else: + P = (np.abs(sys.poles()) >= 1).sum() + Z = (np.abs(sys.feedback().poles()) >= 1).sum() + + # Check to make sure the results make sense; warn if not if Z != count + P and warn_encirclements: warnings.warn( "number of encirclements does not match Nyquist criterion;" @@ -976,7 +991,7 @@ def _parse_linestyle(style_name, allow_false=False): # Find the different portions of the curve (with scaled pts marked) reg_mask = np.logical_or( np.abs(resp) > max_curve_magnitude, - contour.real != 0) + splane_contour.real != 0) # reg_mask = np.logical_or( # np.abs(resp.real) > max_curve_magnitude, # np.abs(resp.imag) > max_curve_magnitude) @@ -1508,7 +1523,7 @@ def singular_values_plot(syslist, omega=None, # Determine the frequency range to be used def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num, - Hz=None): + Hz=None, feature_periphery_decades=None): """Determine the frequency range for a frequency-domain plot according to a standard logic. @@ -1554,9 +1569,9 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num, if omega_limits is None: omega_range_given = False # Select a default range if none is provided - omega_out = _default_frequency_range(syslist, - number_of_samples=omega_num, - Hz=Hz) + omega_out = _default_frequency_range( + syslist, number_of_samples=omega_num, Hz=Hz, + feature_periphery_decades=feature_periphery_decades) else: omega_limits = np.asarray(omega_limits) if len(omega_limits) != 2: @@ -1640,7 +1655,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None, features_ = np.concatenate((sys.poles(), sys.zeros())) # Get rid of poles and zeros on the real axis (imag==0) - # * origin and real < 0 + # * origin and real < 0 # * at 1.: would result in omega=0. (logaritmic plot!) toreplace = np.isclose(features_.imag, 0.0) & ( (features_.real <= 0.) | diff --git a/control/statesp.py b/control/statesp.py index 58412e57a..c04174d25 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -987,7 +987,8 @@ def freqresp(self, omega): def poles(self): """Compute the poles of a state space system.""" - return eigvals(self.A) if self.nstates else np.array([]) + return eigvals(self.A).astype(complex) if self.nstates \ + else np.array([]) def zeros(self): """Compute the zeros of a state space system.""" @@ -1006,8 +1007,9 @@ def zeros(self): if nu == 0: return np.array([]) else: + # Use SciPy generalized eigenvalue fucntion return sp.linalg.eigvals(out[8][0:nu, 0:nu], - out[9][0:nu, 0:nu]) + out[9][0:nu, 0:nu]).astype(complex) except ImportError: # Slycot unavailable. Fall back to scipy. if self.C.shape[0] != self.D.shape[1]: @@ -1031,7 +1033,7 @@ def zeros(self): (0, self.B.shape[1])), "constant") return np.array([x for x in sp.linalg.eigvals(L, M, overwrite_a=True) - if not isinf(x)]) + if not isinf(x)], dtype=complex) # Feedback around a state space system def feedback(self, other=1, sign=-1): diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 4a9f662b3..b1aa00577 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -350,7 +350,8 @@ def test_linestyle_checks(): # If only one line style is given use, the default value for the other # TODO: for now, just make sure the signature works; no correct check yet - ct.nyquist_plot(sys, primary_style=':', mirror_style='-.') + with pytest.warns(PendingDeprecationWarning, match="single string"): + ct.nyquist_plot(sys, primary_style=':', mirror_style='-.') @pytest.mark.usefixtures("editsdefaults") def test_nyquist_legacy(): @@ -363,13 +364,17 @@ def test_nyquist_legacy(): with pytest.warns(UserWarning, match="indented contour may miss"): count = ct.nyquist_plot(sys) - +def test_discrete_nyquist(): + # Make sure we can handle discrete time systems with negative poles + sys = ct.tf(1, [1, -0.1], dt=1) * ct.tf(1, [1, 0.1], dt=1) + ct.nyquist_plot(sys) + if __name__ == "__main__": # # Interactive mode: generate plots for manual viewing # - # Running this script in python (or better ipython) will show a collection of - # figures that should all look OK on the screeen. + # Running this script in python (or better ipython) will show a + # collection of figures that should all look OK on the screeen. # # In interactive mode, turn on ipython interactive graphics @@ -411,3 +416,13 @@ def test_nyquist_legacy(): np.array2string(sys.poles(), precision=2, separator=',')) count = ct.nyquist_plot(sys) assert _Z(sys) == count + _P(sys) + + print("Discrete time systems") + sys = ct.c2d(sys, 0.01) + plt.figure() + plt.title("Discrete-time; poles: %s" % + np.array2string(sys.poles(), precision=2, separator=',')) + count = ct.nyquist_plot(sys) + + + diff --git a/control/xferfcn.py b/control/xferfcn.py index 93a66ce9d..d3671c533 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -798,7 +798,7 @@ def poles(self): rts = [] for d, o in zip(den, denorder): rts.extend(roots(d[:o + 1])) - return np.array(rts) + return np.array(rts).astype(complex) def zeros(self): """Compute the zeros of a transfer function.""" @@ -808,7 +808,7 @@ def zeros(self): "for SISO systems.") else: # for now, just give zeros of a SISO tf - return roots(self.num[0][0]) + return roots(self.num[0][0]).astype(complex) def feedback(self, other=1, sign=-1): """Feedback interconnection between two LTI objects.""" From 02b34512736f06989cc764af080336d81286aca6 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 24 Apr 2022 16:41:17 +0200 Subject: [PATCH 79/87] ENH: add `linform` to compute linear system L-infinity norm linfnorm requires Slycot routine ab13dd, which does the actual work. Added tests to check that wrapping of ab13dd is correct, that static systems are handled, and that discrete-time system frequencies are scaled correctly. --- control/statesp.py | 66 ++++++++++++++++++++++++++++++++++- control/tests/statesp_test.py | 53 +++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 435ff702f..639210649 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -55,6 +55,7 @@ from numpy.linalg import solve, eigvals, matrix_rank from numpy.linalg.linalg import LinAlgError import scipy as sp +import scipy.linalg from scipy.signal import cont2discrete from scipy.signal import StateSpace as signalStateSpace from warnings import warn @@ -63,7 +64,12 @@ from . import config from copy import deepcopy -__all__ = ['StateSpace', 'tf2ss', 'ssdata'] +try: + from slycot import ab13dd +except ImportError: + ab13dd = None + +__all__ = ['StateSpace', 'tf2ss', 'ssdata', 'linfnorm'] # Define module default parameter values _statesp_defaults = { @@ -1888,3 +1894,61 @@ def ssdata(sys): """ ss = _convert_to_statespace(sys) return ss.A, ss.B, ss.C, ss.D + + +def linfnorm(sys, tol=1e-10): + """L-infinity norm of a linear system + + Parameters + ---------- + sys : LTI (StateSpace or TransferFunction) + system to evalute L-infinity norm of + tol : real scalar + tolerance on norm estimate + + Returns + ------- + gpeak : non-negative scalar + L-infinity norm + fpeak : non-negative scalar + Frequency, in rad/s, at which gpeak occurs + + For stable systems, the L-infinity and H-infinity norms are equal; + for unstable systems, the H-infinity norm is infinite, while the + L-infinity norm is finite if the system has no poles on the + imaginary axis. + + See also + -------- + slycot.ab13dd : the Slycot routine linfnorm that does the calculation + """ + + if ab13dd is None: + raise ControlSlycot("Can't find slycot module 'ab13dd'") + + a, b, c, d = ssdata(_convert_to_statespace(sys)) + e = np.eye(a.shape[0]) + + n = a.shape[0] + m = b.shape[1] + p = c.shape[0] + + if n == 0: + # ab13dd doesn't accept empty A, B, C, D; + # static gain case is easy enough to compute + gpeak = scipy.linalg.svdvals(d)[0] + # max svd is constant with freq; arbitrarily choose 0 as peak + fpeak = 0 + return gpeak, fpeak + + dico = 'C' if sys.isctime() else 'D' + jobe = 'I' + equil = 'S' + jobd = 'Z' if all(0 == d.flat) else 'D' + + gpeak, fpeak = ab13dd(dico, jobe, equil, jobd, n, m, p, a, e, b, c, d, tol) + + if dico=='D': + fpeak /= sys.dt + + return gpeak, fpeak diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index be6cd9a6b..7db5586ed 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -19,13 +19,15 @@ from control.dtime import sample_system from control.lti import evalfr from control.statesp import StateSpace, _convert_to_statespace, tf2ss, \ - _statesp_defaults, _rss_generate + _statesp_defaults, _rss_generate, linfnorm from control.iosys import ss, rss, drss from control.tests.conftest import ismatarrayout, slycotonly from control.xferfcn import TransferFunction, ss2tf + from .conftest import editsdefaults + class TestStateSpace: """Tests for the StateSpace class.""" @@ -1097,3 +1099,52 @@ def test_latex_repr_testsize(editsdefaults): gstatic = ss([], [], [], 1) assert gstatic._repr_latex_() is None + + +class TestLinfnorm: + # these are simple tests; we assume ab13dd is correct + # python-control specific behaviour is: + # - checking for continuous- and discrete-time + # - scaling fpeak for discrete-time + # - handling static gains + + # the underdamped gpeak and fpeak are found from + # gpeak = 1/(2*zeta*(1-zeta**2)**0.5) + # fpeak = wn*(1-2*zeta**2)**0.5 + @pytest.fixture(params=[ + ('static', ct.tf, ([1.23],[1]), 1.23, 0), + ('underdamped', ct.tf, ([100],[1, 2*0.5*10, 100]), 1.1547005, 7.0710678), + ]) + def ct_siso(self, request): + name, systype, sysargs, refgpeak, reffpeak = request.param + return systype(*sysargs), refgpeak, reffpeak + + @pytest.fixture(params=[ + ('underdamped', ct.tf, ([100],[1, 2*0.5*10, 100]), 1e-4, 1.1547005, 7.0710678), + ]) + def dt_siso(self, request): + name, systype, sysargs, dt, refgpeak, reffpeak = request.param + return ct.c2d(systype(*sysargs), dt), refgpeak, reffpeak + + @slycotonly + def test_linfnorm_ct_siso(self, ct_siso): + sys, refgpeak, reffpeak = ct_siso + gpeak, fpeak = linfnorm(sys) + np.testing.assert_allclose(gpeak, refgpeak) + np.testing.assert_allclose(fpeak, reffpeak) + + @slycotonly + def test_linfnorm_dt_siso(self, dt_siso): + sys, refgpeak, reffpeak = dt_siso + gpeak, fpeak = linfnorm(sys) + # c2d pole-mapping has round-off + np.testing.assert_allclose(gpeak, refgpeak) + np.testing.assert_allclose(fpeak, reffpeak) + + @slycotonly + def test_linfnorm_ct_mimo(self, ct_siso): + siso, refgpeak, reffpeak = ct_siso + sys = ct.append(siso, siso) + gpeak, fpeak = linfnorm(sys) + np.testing.assert_allclose(gpeak, refgpeak) + np.testing.assert_allclose(fpeak, reffpeak) From 3281a8d6eafe8980eb1cf967cb357bd86f292912 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Wed, 27 Apr 2022 10:05:23 +0200 Subject: [PATCH 80/87] nichols_grid returns artists of grid elements; further tighten phase extent --- control/nichols.py | 68 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/control/nichols.py b/control/nichols.py index 88ff22974..69546678b 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -153,6 +153,19 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted', ax=None, Axes to add grid to. If ``None``, use ``plt.gca()``. label_cl_phases: bool, optional If True, closed-loop phase lines will be labelled. + + Returns + ------- + cl_mag_lines: list of `matplotlib.line.Line2D` + The constant closed-loop gain contours + cl_phase_lines: list of `matplotlib.line.Line2D` + The constant closed-loop phase contours + cl_mag_labels: list of `matplotlib.text.Text` + mcontour labels; each entry corresponds to the respective entry + in ``cl_mag_lines`` + cl_phase_labels: list of `matplotlib.text.Text` + ncontour labels; each entry corresponds to the respective entry + in ``cl_phase_lines`` """ if ax is None: ax = plt.gca() @@ -163,8 +176,8 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted', ax=None, ol_mag_min = -40.0 ol_mag_max = default_ol_mag_max = 50.0 - # Find bounds of the current dataset, if there is one. if ax.has_data(): + # Find extent of intersection the current dataset or view ol_phase_min, ol_mag_min, ol_phase_max, ol_mag_max = _inner_extents(ax) # M-circle magnitudes. @@ -184,20 +197,22 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted', ax=None, ol_mag_min + cl_mag_step, cl_mag_step) cl_mags = np.concatenate((extended_cl_mags, key_cl_mags)) - phase_offset_min = 360.0*np.ceil(ol_phase_min/360.0) - phase_offset_max = 360.0*np.ceil(ol_phase_max/360.0) + 360.0 + # a minimum 360deg extent containing the phases + phase_round_max = 360.0*np.ceil(ol_phase_max/360.0) + phase_round_min = min(phase_round_max-360, + 360.0*np.floor(ol_phase_min/360.0)) # N-circle phases (should be in the range -360 to 0) if cl_phases is None: # aim for 9 lines, but always show (-360+eps, -180, -eps) # smallest spacing is 45, biggest is 180 - phase_span = phase_offset_max - phase_offset_min + phase_span = phase_round_max - phase_round_min spacing = np.clip(round(phase_span / 8 / 45) * 45, 45, 180) key_cl_phases = np.array([-0.25, -359.75]) other_cl_phases = np.arange(-spacing, -360.0, -spacing) cl_phases = np.unique(np.concatenate((key_cl_phases, other_cl_phases))) - else: - assert ((-360.0 < np.min(cl_phases)) and (np.max(cl_phases) < 0.0)) + elif not ((-360 < np.min(cl_phases)) and (np.max(cl_phases) < 0.0)): + raise ValueError('cl_phases must between -360 and 0, exclusive') # Find the M-contours m = m_circles(cl_mags, phase_min=np.min(cl_phases), @@ -216,21 +231,29 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted', ax=None, # over the range -360 < phase < 0. Given the range # the base chart is computed over, the phase offset should be 0 # for -360 < ol_phase_min < 0. - phase_offsets = np.arange(phase_offset_min, phase_offset_max, 360.0) + phase_offsets = 360 + np.arange(phase_round_min, phase_round_max, 360.0) + + cl_mag_lines = [] + cl_phase_lines = [] + cl_mag_labels = [] + cl_phase_labels = [] for phase_offset in phase_offsets: # Draw M and N contours - ax.plot(m_phase + phase_offset, m_mag, color='lightgray', - linestyle=line_style, zorder=0) - ax.plot(n_phase + phase_offset, n_mag, color='lightgray', - linestyle=line_style, zorder=0) + cl_mag_lines.extend( + ax.plot(m_phase + phase_offset, m_mag, color='lightgray', + linestyle=line_style, zorder=0)) + cl_phase_lines.extend( + ax.plot(n_phase + phase_offset, n_mag, color='lightgray', + linestyle=line_style, zorder=0)) # Add magnitude labels for x, y, m in zip(m_phase[:][-1] + phase_offset, m_mag[:][-1], cl_mags): align = 'right' if m < 0.0 else 'left' - ax.text(x, y, str(m) + ' dB', size='small', ha=align, - color='gray', clip_on=True) + cl_mag_labels.append( + ax.text(x, y, str(m) + ' dB', size='small', ha=align, + color='gray', clip_on=True)) # phase labels if label_cl_phases: @@ -243,20 +266,23 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted', ax=None, align = 'center' else: align = 'left' - ax.text(x, y, f'{round(p)}\N{DEGREE SIGN}', - size='small', - ha=align, - va='bottom', - color='gray', - clip_on=True) + cl_phase_labels.append( + ax.text(x, y, f'{round(p)}\N{DEGREE SIGN}', + size='small', + ha=align, + va='bottom', + color='gray', + clip_on=True)) # Fit axes to generated chart - ax.axis([phase_offset_min - 360.0, - phase_offset_max - 360.0, + ax.axis([phase_round_min, + phase_round_max, np.min(np.concatenate([cl_mags,[ol_mag_min]])), np.max([ol_mag_max, default_ol_mag_max])]) + return cl_mag_lines, cl_phase_lines, cl_mag_labels, cl_phase_labels + # # Utility functions # From b813e0caea6f994ec0f718043218fda1255799f6 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Wed, 27 Apr 2022 10:05:56 +0200 Subject: [PATCH 81/87] Add tests for nichols_grid --- control/tests/nichols_test.py | 74 ++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/control/tests/nichols_test.py b/control/tests/nichols_test.py index 4cdfcaa65..cd28dd9a4 100644 --- a/control/tests/nichols_test.py +++ b/control/tests/nichols_test.py @@ -3,9 +3,11 @@ RMM, 31 Mar 2011 """ +import matplotlib.pyplot as plt + import pytest -from control import StateSpace, nichols_plot, nichols +from control import StateSpace, nichols_plot, nichols, nichols_grid, pade, tf @pytest.fixture() @@ -26,3 +28,73 @@ def test_nichols(tsys, mplcleanup): def test_nichols_alias(tsys, mplcleanup): """Test the control.nichols alias and the grid=False parameter""" nichols(tsys, grid=False) + + +class TestNicholsGrid: + def test_ax(self): + # check grid is plotted into gca, or specified axis + fig, axs = plt.subplots(2,2) + plt.sca(axs[0,1]) + + cl_mag_lines = nichols_grid()[1] + assert cl_mag_lines[0].axes is axs[0, 1] + + cl_mag_lines = nichols_grid(ax=axs[1,1])[1] + assert cl_mag_lines[0].axes is axs[1, 1] + # nichols_grid didn't change what the "current axes" are + assert plt.gca() is axs[0, 1] + + + def test_cl_phase_label_control(self): + # test label_cl_phases argument + plt.clf() + cl_mag_lines, cl_phase_lines, cl_mag_labels, cl_phase_labels \ + = nichols_grid() + assert len(cl_phase_labels) > 0 + + plt.clf() + cl_mag_lines, cl_phase_lines, cl_mag_labels, cl_phase_labels \ + = nichols_grid(label_cl_phases=False) + assert len(cl_phase_labels) == 0 + + + def test_labels_clipped(self): + # regression test: check that contour labels are clipped + plt.clf() + mcontours, ncontours, mlabels, nlabels = nichols_grid() + assert all(ml.get_clip_on() for ml in mlabels) + assert all(nl.get_clip_on() for nl in nlabels) + + + def test_minimal_phase(self): + # regression test: phase extent is minimal + g = tf([1],[1,1]) * tf([1],[1/1, 2*0.1/1, 1]) + plt.clf() + nichols(g) + ax = plt.gca() + assert ax.get_xlim()[1] <= 0 + + + def test_fixed_view(self): + # respect xlim, ylim set by user + g = (tf([1],[1/1, 2*0.01/1, 1]) + * tf([1],[1/100**2, 2*0.001/100, 1]) + * tf(*pade(0.01, 5))) + + # normally a broad axis + plt.clf() + m, p = nichols(g) + + assert(plt.xlim()[0] == -1440) + assert(plt.ylim()[0] <= -240) + + plt.clf() + nichols(g, grid=False) + + # zoom in + plt.axis([-360,0,-40,50]) + + # nichols_grid doesn't expand limits + nichols_grid() + assert(plt.xlim()[0] == -360) + assert(plt.ylim()[1] >= -40) From 71f3b6134b0622d337eefa3521a3409c0de3ef9b Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Wed, 27 Apr 2022 10:11:33 +0200 Subject: [PATCH 82/87] Fix nichols_grid test --- control/tests/nichols_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/tests/nichols_test.py b/control/tests/nichols_test.py index cd28dd9a4..d81ee9d18 100644 --- a/control/tests/nichols_test.py +++ b/control/tests/nichols_test.py @@ -83,7 +83,7 @@ def test_fixed_view(self): # normally a broad axis plt.clf() - m, p = nichols(g) + nichols(g) assert(plt.xlim()[0] == -1440) assert(plt.ylim()[0] <= -240) From 59cd8728bd4835897471674b301b0cf605ac4bc3 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Wed, 27 Apr 2022 12:58:04 +0200 Subject: [PATCH 83/87] In nichols_grid tests: use mplcleanup, and remove plt.clf calls --- control/tests/nichols_test.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/control/tests/nichols_test.py b/control/tests/nichols_test.py index d81ee9d18..90ea74cf7 100644 --- a/control/tests/nichols_test.py +++ b/control/tests/nichols_test.py @@ -30,6 +30,7 @@ def test_nichols_alias(tsys, mplcleanup): nichols(tsys, grid=False) +@pytest.mark.usefixtures("mplcleanup") class TestNicholsGrid: def test_ax(self): # check grid is plotted into gca, or specified axis @@ -47,12 +48,10 @@ def test_ax(self): def test_cl_phase_label_control(self): # test label_cl_phases argument - plt.clf() cl_mag_lines, cl_phase_lines, cl_mag_labels, cl_phase_labels \ = nichols_grid() assert len(cl_phase_labels) > 0 - plt.clf() cl_mag_lines, cl_phase_lines, cl_mag_labels, cl_phase_labels \ = nichols_grid(label_cl_phases=False) assert len(cl_phase_labels) == 0 @@ -60,7 +59,6 @@ def test_cl_phase_label_control(self): def test_labels_clipped(self): # regression test: check that contour labels are clipped - plt.clf() mcontours, ncontours, mlabels, nlabels = nichols_grid() assert all(ml.get_clip_on() for ml in mlabels) assert all(nl.get_clip_on() for nl in nlabels) @@ -69,7 +67,6 @@ def test_labels_clipped(self): def test_minimal_phase(self): # regression test: phase extent is minimal g = tf([1],[1,1]) * tf([1],[1/1, 2*0.1/1, 1]) - plt.clf() nichols(g) ax = plt.gca() assert ax.get_xlim()[1] <= 0 @@ -82,13 +79,11 @@ def test_fixed_view(self): * tf(*pade(0.01, 5))) # normally a broad axis - plt.clf() nichols(g) assert(plt.xlim()[0] == -1440) assert(plt.ylim()[0] <= -240) - plt.clf() nichols(g, grid=False) # zoom in From 826f6610e3d658ed46a9c731a22d8268b94e7409 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 May 2022 10:58:44 +0200 Subject: [PATCH 84/87] add envs to gitignore --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 9f0a11c21..9f90aa467 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,12 @@ TAGS # Files created by Spyder .spyproject/ + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ \ No newline at end of file From 6a116f089a3d1b510d3bd7d79810436c027fbbbe Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 May 2022 22:12:01 +0200 Subject: [PATCH 85/87] fix gitignore format --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9f90aa467..3ac21ae97 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ env/ venv/ ENV/ env.bak/ -venv.bak/ \ No newline at end of file +venv.bak/ From e5a7d820247c8c78d0f95ff1be3fdc3bd763235d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 28 May 2022 09:18:22 -0700 Subject: [PATCH 86/87] update python-package-conda workflow to use conda instead of conda-forge --- .github/workflows/python-package-conda.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index 3f1910697..0744906a7 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -38,13 +38,12 @@ jobs: pip install coveralls # Install python-control dependencies - # use conda-forge until https://github.com/numpy/numpy/issues/20233 is resolved - conda install -c conda-forge numpy matplotlib scipy + conda install numpy matplotlib scipy if [[ '${{matrix.slycot}}' == 'conda' ]]; then conda install -c conda-forge slycot fi if [[ '${{matrix.pandas}}' == 'conda' ]]; then - conda install -c conda-forge pandas + conda install pandas fi - name: Test with pytest From ca8cd0e1103bd886bd9a10650d1efaaea0fac185 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 28 May 2022 10:04:16 -0700 Subject: [PATCH 87/87] fix README.rst for twine --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a7d5ed77d..f1feda7c5 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ operations for analysis and design of feedback control systems. Have a go now! -====== +============== Try out the examples in the examples folder using the binder service. .. image:: https://mybinder.org/badge_logo.svg