From aad5502925d408665969bcd4ae98ee13e601976d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 29 Nov 2024 08:23:48 -0800 Subject: [PATCH 1/8] allow state elimination on unstable systems with warning --- control/canonical.py | 2 +- control/modelsimp.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/control/canonical.py b/control/canonical.py index a62044322..7be7f88ad 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -205,7 +205,7 @@ def similarity_transform(xsys, T, timescale=1, inverse=False): timescale : float, optional If present, also rescale the time unit to tau = timescale * t inverse : bool, optional - If True (default), transform so z = T x. If False, transform + If False (default), transform so z = T x. If True, transform so x = T z. Returns diff --git a/control/modelsimp.py b/control/modelsimp.py index 966da1ce7..10cd4f3e5 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -108,7 +108,7 @@ def hankel_singular_values(sys): return hsv[::-1] -def model_reduction(sys, ELIM, method='matchdc'): +def model_reduction(sys, ELIM, method='matchdc', warn_unstable=True): """Model reduction by state elimination. Model reduction of `sys` by eliminating the states in `ELIM` using a given @@ -122,7 +122,9 @@ def model_reduction(sys, ELIM, method='matchdc'): Vector of states to eliminate. method : string Method of removing states in `ELIM`: either 'truncate' or - 'matchdc'. + 'matchdc' (default). + warn_unstable: bool, option + If `False`, don't warn if system is unstable. Returns ------- @@ -132,12 +134,14 @@ def model_reduction(sys, ELIM, method='matchdc'): Raises ------ ValueError - Raised under the following conditions: + If `method` is not either ``'matchdc'`` or ``'truncate'``. + NotImplementedError + If `sys` is a discrete time system. - * if `method` is not either ``'matchdc'`` or ``'truncate'`` - - * if eigenvalues of `sys.A` are not all in left half plane - (`sys` must be stable) + Warns + ----- + UserWarning + If eigenvalues of `sys.A` are not all stable. Examples -------- @@ -162,8 +166,8 @@ def model_reduction(sys, ELIM, method='matchdc'): raise NotImplementedError("Function not implemented in discrete time") # Check system is stable - if np.any(np.linalg.eigvals(sys.A).real >= 0.0): - raise ValueError("Oops, the system is unstable!") + if np.any(np.linalg.eigvals(sys.A).real >= 0.0) and warn_unstable: + warnings.warn("System is unstable; reduction may be meaningless") ELIM = np.sort(ELIM) # Create list of elements not to eliminate (NELIM) From e6824d79f1d2f5a7acf6a552bfb88892f1096d45 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 29 Nov 2024 08:35:04 -0800 Subject: [PATCH 2/8] update unit tests --- control/tests/modelsimp_test.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 616ef5f09..51a4b167b 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -3,11 +3,14 @@ RMM, 30 Mar 2011 (based on TestModelSimp from v0.4a) """ +import warnings + import numpy as np import pytest +import control as ct from control import StateSpace, TimeResponseData, c2d, forced_response, \ - impulse_response, step_response, rss, tf + impulse_response, rss, step_response, tf from control.exception import ControlArgument, ControlDimension from control.modelsimp import balred, eigensys_realization, hsvd, markov, \ modred @@ -343,7 +346,7 @@ def testModredMatchDC(self): np.testing.assert_array_almost_equal(rsys.D, Drtrue, decimal=2) def testModredUnstable(self): - """Check if an error is thrown when an unstable system is given""" + """Check if warning is issued when an unstable system is given""" A = np.array( [[4.5418, 3.3999, 5.0342, 4.3808], [0.3890, 0.3599, 0.4195, 0.1760], @@ -353,7 +356,16 @@ def testModredUnstable(self): C = np.array([[1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]]) D = np.array([[0.0, 0.0], [0.0, 0.0]]) sys = StateSpace(A, B, C, D) - np.testing.assert_raises(ValueError, modred, sys, [2, 3]) + + # Make sure we get a warning message + with pytest.warns(UserWarning, match="System is unstable"): + newsys1 = modred(sys, [2, 3]) + + # Make sure we can turn the warning off + with warnings.catch_warnings(): + warnings.simplefilter('error') + newsys2 = ct.model_reduction(sys, [2, 3], warn_unstable=False) + np.testing.assert_equal(newsys1.A, newsys2.A) def testModredTruncate(self): #balanced realization computed in matlab for the transfer function: From 77b25d58fd0cbe1644303f5a7d7e0774f65bf614 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 2 Dec 2024 15:58:02 -0800 Subject: [PATCH 3/8] initial refactoring of model_reduction (no changes in functionality) --- control/modelsimp.py | 190 ++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 102 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index 10cd4f3e5..fbb2cad2e 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -1,4 +1,3 @@ -#! TODO: add module docstring # modelsimp.py - tools for model simplification # # Author: Steve Brunton, Kevin Chen, Lauren Padilla @@ -6,47 +5,16 @@ # # This file contains routines for obtaining reduced order models # -# Copyright (c) 2010 by California Institute of Technology -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# 3. Neither the name of the California Institute of Technology nor -# the names of its contributors may be used to endorse or promote -# products derived from this software without specific prior -# written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH -# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# -# $Id$ + +import warnings # External packages and modules import numpy as np -import warnings -from .exception import ControlSlycot, ControlArgument, ControlDimension -from .iosys import isdtime, isctime -from .statesp import StateSpace + +from .exception import ControlArgument, ControlDimension, ControlSlycot +from .iosys import isctime, isdtime from .statefbk import gram +from .statesp import StateSpace from .timeresp import TimeResponseData __all__ = ['hankel_singular_values', 'balanced_reduction', 'model_reduction', @@ -108,20 +76,35 @@ def hankel_singular_values(sys): return hsv[::-1] -def model_reduction(sys, ELIM, method='matchdc', warn_unstable=True): - """Model reduction by state elimination. - - Model reduction of `sys` by eliminating the states in `ELIM` using a given - method. +def model_reduction( + sys, elim_states=None, method='matchdc', elim_inputs=None, + elim_outputs=None, keep_states=None, keep_inputs=None, + keep_outputs=None, remove_hidden_states='unobs', warn_unstable=True): + """Model reduction by input, output, or state elimination. + + This function produces a reduced-order model of a system by eliminating + specified inputs,outputs, or states from the original system. The + specific states, inputs, or outputs that are eliminated can be + specified by listing either the states, inputs, or outputs to be + eliminated or those to be kept. In addition, unused states (those that + do not affect the inputs or outputs of the reduced system) can also be + eliminated. Parameters ---------- sys : StateSpace Original system to reduce. - ELIM : array - Vector of states to eliminate. + elim_inputs, elim_outputs, elim_states : array of int or str, optional + Vector of inputs, outputs, or states to eliminate. Can be specified + either as an offset into the appropriate vector or as a signal name. + keep_inputs, keep_outputs, keep_states : array, optional + Vector of inputs, outputs, or states to keep. Can be specified + either as an offset into the appropriate vector or as a signal name. + remove_hidden_states : str, optional + If `unobs` (default), then eliminate any states that are unobservable + via the reduced-order system outputs. method : string - Method of removing states in `ELIM`: either 'truncate' or + Method of removing states in `elim`: either 'truncate' or 'matchdc' (default). warn_unstable: bool, option If `False`, don't warn if system is unstable. @@ -129,14 +112,12 @@ def model_reduction(sys, ELIM, method='matchdc', warn_unstable=True): Returns ------- rsys : StateSpace - A reduced order model. + Reduced order model. Raises ------ ValueError If `method` is not either ``'matchdc'`` or ``'truncate'``. - NotImplementedError - If `sys` is a discrete time system. Warns ----- @@ -146,55 +127,53 @@ def model_reduction(sys, ELIM, method='matchdc', warn_unstable=True): Examples -------- >>> G = ct.rss(4) - >>> Gr = ct.modred(G, [0, 2], method='matchdc') + >>> Gr = ct.model_reduction(G, [0, 2], method='matchdc') >>> Gr.nstates 2 """ + if not isinstance(sys, StateSpace): + raise TypeError("system must be a a StateSpace system") - # Check for ss system object, need a utility for this? + # Check system is stable + if warn_unstable: + if isctime(sys) and np.any(np.linalg.eigvals(sys.A).real >= 0.0) or \ + np.any(np.abs(np.linalg.eigvals(sys.A)) >= 1): + warnings.warn("System is unstable; reduction may be meaningless") - # TODO: Check for continous or discrete, only continuous supported for now - # if isCont(): - # dico = 'C' - # elif isDisc(): - # dico = 'D' - # else: - if (isctime(sys)): - dico = 'C' - else: - raise NotImplementedError("Function not implemented in discrete time") + # Utility function to process keep/elim keywords + def _process_elim_or_keep(elim, keep, labels): + elim = np.sort(elim).tolist() + return elim, [i for i in range(len(labels)) if i not in elim] - # Check system is stable - if np.any(np.linalg.eigvals(sys.A).real >= 0.0) and warn_unstable: - warnings.warn("System is unstable; reduction may be meaningless") - - ELIM = np.sort(ELIM) - # Create list of elements not to eliminate (NELIM) - NELIM = [i for i in range(len(sys.A)) if i not in ELIM] - # A1 is a matrix of all columns of sys.A not to eliminate - A1 = sys.A[:, NELIM[0]].reshape(-1, 1) - for i in NELIM[1:]: - A1 = np.hstack((A1, sys.A[:, i].reshape(-1, 1))) - A11 = A1[NELIM, :] - A21 = A1[ELIM, :] - # A2 is a matrix of all columns of sys.A to eliminate - A2 = sys.A[:, ELIM[0]].reshape(-1, 1) - for i in ELIM[1:]: - A2 = np.hstack((A2, sys.A[:, i].reshape(-1, 1))) - A12 = A2[NELIM, :] - A22 = A2[ELIM, :] - - C1 = sys.C[:, NELIM] - C2 = sys.C[:, ELIM] - B1 = sys.B[NELIM, :] - B2 = sys.B[ELIM, :] + # Determine which states to keep + elim_states, keep_states = _process_elim_or_keep( + elim_states, keep_states, sys.state_labels) + + keep_inputs = slice(None, None) + keep_outputs = slice(None, None) + + # Create submatrix of states we are keeping + A11 = sys.A[:, keep_states][keep_states, :] # states we are keeping + A12 = sys.A[:, elim_states][keep_states, :] # needed for 'matchdc' + A21 = sys.A[:, keep_states][elim_states, :] + A22 = sys.A[:, elim_states][elim_states, :] + B1 = sys.B[keep_states, :] + B2 = sys.B[elim_states, :] + + C1 = sys.C[:, keep_states] + C2 = sys.C[:, elim_states] + + # Figure out the new state space system if method == 'matchdc': - # if matchdc, residualize + if sys.isdtime(strict=True): + raise NotImplementedError( + "'matchdc' not (yet) supported for discrete time systems") + # if matchdc, residualize # Check if the matrix A22 is invertible - if np.linalg.matrix_rank(A22) != len(ELIM): + if np.linalg.matrix_rank(A22) != len(elim_states): raise ValueError("Matrix A22 is singular to working precision.") # Now precompute A22\A21 and A22\B2 (A22I = inv(A22)) @@ -210,22 +189,29 @@ def model_reduction(sys, ELIM, method='matchdc', warn_unstable=True): Br = B1 - A12 @ A22I_B2 Cr = C1 - C2 @ A22I_A21 Dr = sys.D - C2 @ A22I_B2 + elif method == 'truncate': # if truncate, simply discard state x2 Ar = A11 Br = B1 Cr = C1 Dr = sys.D + else: raise ValueError("Oops, method is not supported!") + # Get rid of additional inputs and outputs + Br = Br[:, keep_inputs] + Cr = Cr[keep_outputs, :] + Dr = Dr[keep_outputs, :][:, keep_inputs] + rsys = StateSpace(Ar, Br, Cr, Dr) return rsys def balanced_reduction(sys, orders, method='truncate', alpha=None): """Balanced reduced order model of sys of a given order. - + States are eliminated based on Hankel singular value. If sys has unstable modes, they are removed, the balanced realization is done on the stable part, then @@ -280,7 +266,7 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): raise ValueError("supported methods are 'truncate' or 'matchdc'") elif method == 'truncate': try: - from slycot import ab09md, ab09ad + from slycot import ab09ad, ab09md except ImportError: raise ControlSlycot( "can't find slycot subroutine ab09md or ab09ad") @@ -350,7 +336,7 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): def minimal_realization(sys, tol=None, verbose=True): """ Eliminate uncontrollable or unobservable states. - + Eliminates uncontrollable or unobservable states in state-space models or cancelling pole-zero pairs in transfer functions. The output sysr has minimal order and the same response @@ -382,14 +368,14 @@ def _block_hankel(Y, m, n): """Create a block Hankel matrix from impulse response.""" q, p, _ = Y.shape YY = Y.transpose(0, 2, 1) # transpose for reshape - + H = np.zeros((q*m, p*n)) - + for r in range(m): # shift and add row to Hankel matrix new_row = YY[:, r:r+n, :] H[q*r:q*(r+1), :] = new_row.reshape((q, p*n)) - + return H @@ -435,7 +421,7 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): unit-area impulse response of python-control. Default is True. transpose : bool, optional Assume that input data is transposed relative to the standard - :ref:`time-series-convention`. For TimeResponseData this parameter + :ref:`time-series-convention`. For TimeResponseData this parameter is ignored. Default is False. Returns @@ -470,7 +456,7 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): YY = np.array(arg, ndmin=3) if transpose: YY = np.transpose(YY) - + q, p, l = YY.shape if m is None: @@ -480,14 +466,14 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): if m*q < r or n*p < r: raise ValueError("Hankel parameters are to small") - + if (l-1) < m+n: raise ValueError("not enough data for requested number of parameters") - + H = _block_hankel(YY[:, :, 1:], m, n+1) # Hankel matrix (q*m, p*(n+1)) Hf = H[:, :-p] # first p*n columns of H Hl = H[:, p:] # last p*n columns of H - + U,S,Vh = np.linalg.svd(Hf, True) Ur =U[:, 0:r] Vhr =Vh[0:r, :] @@ -504,7 +490,7 @@ def eigensys_realization(arg, r, m=None, n=None, dt=True, transpose=False): def markov(*args, m=None, transpose=False, dt=None, truncate=False): """markov(Y, U, [, m]) - + Calculate the first `m` Markov parameters [D CB CAB ...] from data. This function computes the Markov parameters for a discrete time @@ -583,7 +569,7 @@ def markov(*args, m=None, transpose=False, dt=None, truncate=False): # Get the system description if len(args) < 1: raise ControlArgument("not enough input arguments") - + if isinstance(args[0], TimeResponseData): data = args[0] Umat = np.array(data.inputs, ndmin=2) @@ -643,7 +629,7 @@ def markov(*args, m=None, transpose=False, dt=None, truncate=False): # This algorithm sets up the following problem and solves it for # the Markov parameters # - # (l,q) = (l,p*m) @ (p*m,q) + # (l,q) = (l,p*m) @ (p*m,q) # YY = UU @ H.T # # [ y(0) ] [ u(0) 0 0 ] [ D ] @@ -679,7 +665,7 @@ def markov(*args, m=None, transpose=False, dt=None, truncate=False): # Truncate first t=0 or t=m time steps, transpose the problem for lsq YY = Ymat[:, t:].T UU = UUT[:, t:].T - + # Solve for the Markov parameters from YY = UU @ H.T HT, _, _, _ = np.linalg.lstsq(UU, YY, rcond=None) H = HT.T/dt # scaling From ae43212b8f7e60c6bcf51f113c85edf2f4f62ad4 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 2 Dec 2024 21:25:03 -0800 Subject: [PATCH 4/8] add input/output removal + unit tests --- control/modelsimp.py | 35 +++++++++++----- control/tests/docstrings_test.py | 2 +- control/tests/modelsimp_test.py | 69 ++++++++++++++++++++++++-------- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index fbb2cad2e..2d6a6cf85 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -138,20 +138,33 @@ def model_reduction( # Check system is stable if warn_unstable: if isctime(sys) and np.any(np.linalg.eigvals(sys.A).real >= 0.0) or \ - np.any(np.abs(np.linalg.eigvals(sys.A)) >= 1): + isdtime(sys) and np.any(np.abs(np.linalg.eigvals(sys.A)) >= 1): warnings.warn("System is unstable; reduction may be meaningless") # Utility function to process keep/elim keywords - def _process_elim_or_keep(elim, keep, labels): - elim = np.sort(elim).tolist() - return elim, [i for i in range(len(labels)) if i not in elim] + def _process_elim_or_keep(elim, keep, labels, allow_both=False): + elim = [] if elim is None else np.atleast_1d(elim) + keep = [] if keep is None else np.atleast_1d(keep) + + if len(elim) > 0 and len(keep) > 0: + if not allow_both: + raise ValueError( + "can't provide both 'keep' and 'elim' for same variables") + elif len(keep) > 0: + keep = np.sort(keep).tolist() + elim = [i for i in range(len(labels)) if i not in keep] + else: + elim = [] if elim is None else np.sort(elim).tolist() + keep = [i for i in range(len(labels)) if i not in elim] + return elim, keep # Determine which states to keep elim_states, keep_states = _process_elim_or_keep( elim_states, keep_states, sys.state_labels) - - keep_inputs = slice(None, None) - keep_outputs = slice(None, None) + elim_inputs, keep_inputs = _process_elim_or_keep( + elim_inputs, keep_inputs, sys.input_labels) + elim_outputs, keep_outputs = _process_elim_or_keep( + elim_outputs, keep_outputs, sys.output_labels) # Create submatrix of states we are keeping A11 = sys.A[:, keep_states][keep_states, :] # states we are keeping @@ -166,11 +179,11 @@ def _process_elim_or_keep(elim, keep, labels): C2 = sys.C[:, elim_states] # Figure out the new state space system - if method == 'matchdc': + if method == 'matchdc' and A22.size > 0: if sys.isdtime(strict=True): raise NotImplementedError( "'matchdc' not (yet) supported for discrete time systems") - + # if matchdc, residualize # Check if the matrix A22 is invertible if np.linalg.matrix_rank(A22) != len(elim_states): @@ -190,8 +203,8 @@ def _process_elim_or_keep(elim, keep, labels): Cr = C1 - C2 @ A22I_A21 Dr = sys.D - C2 @ A22I_B2 - elif method == 'truncate': - # if truncate, simply discard state x2 + elif method == 'truncate' or A22.size == 0: + # Get rid of unwanted states Ar = A11 Br = B1 Cr = C1 diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py index 27ced105f..991ead3e5 100644 --- a/control/tests/docstrings_test.py +++ b/control/tests/docstrings_test.py @@ -39,7 +39,7 @@ control.ss2tf: '48ff25d22d28e7b396e686dd5eb58831', control.tf: '53a13f4a7f75a31c81800e10c88730ef', control.tf2ss: '086a3692659b7321c2af126f79f4bc11', - control.markov: 'eda7c4635bbb863ae6659e574285d356', + control.markov: 'a4199c54cb50f07c0163d3790739eafe', control.gangof4: '0e52eb6cf7ce024f9a41f3ae3ebf04f7', } diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 51a4b167b..91891a772 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -3,6 +3,7 @@ RMM, 30 Mar 2011 (based on TestModelSimp from v0.4a) """ +import math import warnings import numpy as np @@ -45,7 +46,7 @@ def testMarkovSignature(self): inputs=U, input_labels='u', ) - + # setup m = 3 Htrue = np.array([1., 0., 0.]) @@ -104,7 +105,6 @@ def testMarkovSignature(self): HT = markov(response, m) np.testing.assert_array_almost_equal(HT, np.transpose(Htrue)) response.transpose=False - # Test example from docstring # TODO: There is a problem here, last markov parameter does not fit @@ -215,7 +215,7 @@ def testMarkovResults(self, k, m, n): Mtrue = np.hstack([Hd.D] + [ Hd.C @ np.linalg.matrix_power(Hd.A, i) @ Hd.B for i in range(m-1)]) - + Mtrue = np.squeeze(Mtrue) # Generate input/output data @@ -241,8 +241,8 @@ def testMarkovResults(self, k, m, n): Mcomp_scaled = markov(response, m, dt=Ts) np.testing.assert_allclose(Mtrue, Mcomp, rtol=1e-6, atol=1e-8) - np.testing.assert_allclose(Mtrue_scaled, Mcomp_scaled, rtol=1e-6, atol=1e-8) - + np.testing.assert_allclose( + Mtrue_scaled, Mcomp_scaled, rtol=1e-6, atol=1e-8) def testERASignature(self): @@ -256,7 +256,7 @@ def testERASignature(self): B = np.array([[1.],[1.,]]) C = np.array([[1., 0.,]]) D = np.array([[0.,]]) - + T = np.arange(0,10,1) sysd_true = StateSpace(A,B,C,D,True) ir_true = impulse_response(sysd_true,T=T) @@ -265,7 +265,7 @@ def testERASignature(self): sysd_est, _ = eigensys_realization(ir_true,r=2) ir_est = impulse_response(sysd_est, T=T) _, H_est = ir_est - + np.testing.assert_allclose(H_true, H_est, rtol=1e-6, atol=1e-8) # test ndarray @@ -273,7 +273,7 @@ def testERASignature(self): sysd_est, _ = eigensys_realization(YY_true,r=2) ir_est = impulse_response(sysd_est, T=T) _, H_est = ir_est - + np.testing.assert_allclose(H_true, H_est, rtol=1e-6, atol=1e-8) # test mimo @@ -307,10 +307,10 @@ def testERASignature(self): step_true = step_response(sysd_true) step_est = step_response(sysd_est) - np.testing.assert_allclose(step_true.outputs, + np.testing.assert_allclose(step_true.outputs, step_est.outputs, rtol=1e-6, atol=1e-8) - + # test ndarray _, YY_true = ir_true sysd_est, _ = eigensys_realization(YY_true,r=4,dt=dt) @@ -318,7 +318,7 @@ def testERASignature(self): step_true = step_response(sysd_true, T=T) step_est = step_response(sysd_est, T=T) - np.testing.assert_allclose(step_true.outputs, + np.testing.assert_allclose(step_true.outputs, step_est.outputs, rtol=1e-6, atol=1e-8) @@ -403,7 +403,7 @@ def testBalredTruncate(self): B = np.array([[2.], [0.], [0.], [0.]]) C = np.array([[0.5, 0.6875, 0.7031, 0.5]]) D = np.array([[0.]]) - + sys = StateSpace(A, B, C, D) orders = 2 rsys = balred(sys, orders, method='truncate') @@ -424,7 +424,7 @@ def testBalredTruncate(self): # Apply a similarity transformation Ar, Br, Cr = T @ Ar @ T, T @ Br, Cr @ T break - + # Make sure we got the correct answer np.testing.assert_array_almost_equal(Ar, Artrue, decimal=2) np.testing.assert_array_almost_equal(Br, Brtrue, decimal=4) @@ -444,12 +444,12 @@ def testBalredMatchDC(self): B = np.array([[2.], [0.], [0.], [0.]]) C = np.array([[0.5, 0.6875, 0.7031, 0.5]]) D = np.array([[0.]]) - + sys = StateSpace(A, B, C, D) orders = 2 rsys = balred(sys,orders,method='matchdc') Ar, Br, Cr, Dr = rsys.A, rsys.B, rsys.C, rsys.D - + # Result from MATLAB Artrue = np.array( [[-4.43094773, -4.55232904], @@ -457,7 +457,7 @@ def testBalredMatchDC(self): Brtrue = np.array([[1.36235673], [1.03114388]]) Crtrue = np.array([[1.36235673, 1.03114388]]) Drtrue = np.array([[-0.08383902]]) - + # Look for possible changes in state in slycot T1 = np.array([[1, 0], [0, -1]]) T2 = np.array([[-1, 0], [0, 1]]) @@ -467,9 +467,44 @@ def testBalredMatchDC(self): # Apply a similarity transformation Ar, Br, Cr = T @ Ar @ T, T @ Br, Cr @ T break - + # Make sure we got the correct answer np.testing.assert_array_almost_equal(Ar, Artrue, decimal=2) np.testing.assert_array_almost_equal(Br, Brtrue, decimal=4) np.testing.assert_array_almost_equal(Cr, Crtrue, decimal=4) np.testing.assert_array_almost_equal(Dr, Drtrue, decimal=4) + + +@pytest.mark.parametrize("kwargs, nstates, noutputs, ninputs", [ + ({'elim_states': [1, 3]}, 3, 3, 3), + ({'elim_inputs': [1, 2], 'keep_states': [1, 3]}, 2, 3, 1), + ({'elim_outputs': [1, 2], 'keep_inputs': [0, 1],}, 5, 1, 2), + ({'keep_states': [2, 0], 'keep_outputs': [0, 1]}, 2, 2, 3), + ({'elim_inputs': [0, 1, 2]}, 5, 3, 0), # no inputs + ({'elim_outputs': [0, 1, 2]}, 5, 0, 3), # no outputs + ({'elim_states': [0, 1, 2, 3, 4]}, 0, 3, 3), # no states + ({'elim_states': [0, 1], 'keep_states': [1, 2]}, None, None, None) +]) +@pytest.mark.parametrize("method", ['truncate', 'matchdc']) +def test_model_reduction(method, kwargs, nstates, noutputs, ninputs): + sys = ct.rss(5, 3, 3) + + if nstates is None: + # Arguments should generate an error + with pytest.raises(ValueError, match="can't provide both"): + red = ct.model_reduction(sys, **kwargs, method=method) + return + else: + red = ct.model_reduction(sys, **kwargs, method=method) + + assert red.nstates == nstates + assert red.ninputs == ninputs + assert red.noutputs == noutputs + + if method == 'matchdc': + # Define a new system with truncated inputs and outputs + # (assumes we always keep the initial inputs and outputs) + chk = ct.ss( + sys.A, sys.B[:, :ninputs], sys.C[:noutputs, :], + sys.D[:noutputs, :][:, :ninputs]) + np.testing.assert_allclose(red(0), chk(0)) From 310798e63c80d0121d490430135601925fcf6960 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 2 Dec 2024 21:54:41 -0800 Subject: [PATCH 5/8] update documentation for basic functionality --- control/modelsimp.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index 2d6a6cf85..b9ec0ea15 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -83,12 +83,15 @@ def model_reduction( """Model reduction by input, output, or state elimination. This function produces a reduced-order model of a system by eliminating - specified inputs,outputs, or states from the original system. The + specified inputs, outputs, and/or states from the original system. The specific states, inputs, or outputs that are eliminated can be - specified by listing either the states, inputs, or outputs to be - eliminated or those to be kept. In addition, unused states (those that - do not affect the inputs or outputs of the reduced system) can also be - eliminated. + specified by either listing the states, inputs, or outputs to be + eliminated or those to be kept. + + Two methods of state reduction are possible: 'truncate' removes the + states marked for elimination, while 'matchdc' replaces the the + eliminated states with their equilibrium values (thereby keeping the + input/output gain unchanged at zero frequency ["DC"]). Parameters ---------- @@ -100,9 +103,6 @@ def model_reduction( keep_inputs, keep_outputs, keep_states : array, optional Vector of inputs, outputs, or states to keep. Can be specified either as an offset into the appropriate vector or as a signal name. - remove_hidden_states : str, optional - If `unobs` (default), then eliminate any states that are unobservable - via the reduced-order system outputs. method : string Method of removing states in `elim`: either 'truncate' or 'matchdc' (default). @@ -117,7 +117,9 @@ def model_reduction( Raises ------ ValueError - If `method` is not either ``'matchdc'`` or ``'truncate'``. + If `method` is not either 'matchdc' or 'truncate'. + NotImplementedError + If the 'matchdc' method is used for a discrete time system. Warns ----- @@ -131,6 +133,19 @@ def model_reduction( >>> Gr.nstates 2 + See Also + -------- + balanced_reduction : Eliminate states using Hankel singular values. + minimal_realization : Eliminate unreachable or unobseravble states. + + Notes + ----- + The model_reduction function issues a warning if the system has + unstable eigenvalues, since in those situations the stability reduced + order model may be different that the stability of the full model. No + other checking is done, so users to be careful not to render a system + unobservable or unreachable. + """ if not isinstance(sys, StateSpace): raise TypeError("system must be a a StateSpace system") From b96a0392d6d8a21425aad9ff736cb0ba58cf3ad1 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 2 Dec 2024 22:17:49 -0800 Subject: [PATCH 6/8] add slice processing --- control/modelsimp.py | 15 +++++++++++++-- control/tests/modelsimp_test.py | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index b9ec0ea15..c520d9a93 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -146,6 +146,9 @@ def model_reduction( other checking is done, so users to be careful not to render a system unobservable or unreachable. + States, inputs, and outputs can be specified using integer offers. + Slices can also be specified, but must use the Python ``slice()`` function. + """ if not isinstance(sys, StateSpace): raise TypeError("system must be a a StateSpace system") @@ -158,8 +161,16 @@ def model_reduction( # Utility function to process keep/elim keywords def _process_elim_or_keep(elim, keep, labels, allow_both=False): - elim = [] if elim is None else np.atleast_1d(elim) - keep = [] if keep is None else np.atleast_1d(keep) + def _expand_key(key): + if key is None: + return [] + elif isinstance(key, slice): + return range(len(labels))[key] + else: + return np.atleast_1d(key) + + elim = _expand_key(elim) + keep = _expand_key(keep) if len(elim) > 0 and len(keep) > 0: if not allow_both: diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 91891a772..8441f8d10 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -480,6 +480,7 @@ def testBalredMatchDC(self): ({'elim_inputs': [1, 2], 'keep_states': [1, 3]}, 2, 3, 1), ({'elim_outputs': [1, 2], 'keep_inputs': [0, 1],}, 5, 1, 2), ({'keep_states': [2, 0], 'keep_outputs': [0, 1]}, 2, 2, 3), + ({'keep_states': slice(0, 4, 2), 'keep_outputs': slice(None, 2)}, 2, 2, 3), ({'elim_inputs': [0, 1, 2]}, 5, 3, 0), # no inputs ({'elim_outputs': [0, 1, 2]}, 5, 0, 3), # no outputs ({'elim_states': [0, 1, 2, 3, 4]}, 0, 3, 3), # no states From 412ef9754d033b4fb75589c47b9411475663604f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 2 Dec 2024 22:45:09 -0800 Subject: [PATCH 7/8] add signal name processing --- control/modelsimp.py | 17 +++++++++++------ control/tests/modelsimp_test.py | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index c520d9a93..9171a9559 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -79,7 +79,7 @@ def hankel_singular_values(sys): def model_reduction( sys, elim_states=None, method='matchdc', elim_inputs=None, elim_outputs=None, keep_states=None, keep_inputs=None, - keep_outputs=None, remove_hidden_states='unobs', warn_unstable=True): + keep_outputs=None, warn_unstable=True): """Model reduction by input, output, or state elimination. This function produces a reduced-order model of a system by eliminating @@ -146,8 +146,9 @@ def model_reduction( other checking is done, so users to be careful not to render a system unobservable or unreachable. - States, inputs, and outputs can be specified using integer offers. - Slices can also be specified, but must use the Python ``slice()`` function. + States, inputs, and outputs can be specified using integer offers or + using signal names. Slices can also be specified, but must use the + Python ``slice()`` function. """ if not isinstance(sys, StateSpace): @@ -164,13 +165,17 @@ def _process_elim_or_keep(elim, keep, labels, allow_both=False): def _expand_key(key): if key is None: return [] + elif isinstance(key, str): + return labels.index(key) + elif isinstance(key, list): + return [_expand_key(k) for k in key] elif isinstance(key, slice): return range(len(labels))[key] else: - return np.atleast_1d(key) + return key - elim = _expand_key(elim) - keep = _expand_key(keep) + elim = np.atleast_1d(_expand_key(elim)) + keep = np.atleast_1d(_expand_key(keep)) if len(elim) > 0 and len(keep) > 0: if not allow_both: diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 8441f8d10..65f74aee5 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -481,6 +481,7 @@ def testBalredMatchDC(self): ({'elim_outputs': [1, 2], 'keep_inputs': [0, 1],}, 5, 1, 2), ({'keep_states': [2, 0], 'keep_outputs': [0, 1]}, 2, 2, 3), ({'keep_states': slice(0, 4, 2), 'keep_outputs': slice(None, 2)}, 2, 2, 3), + ({'keep_states': ['x[0]', 'x[3]'], 'keep_inputs': 'u[0]'}, 2, 3, 1), ({'elim_inputs': [0, 1, 2]}, 5, 3, 0), # no inputs ({'elim_outputs': [0, 1, 2]}, 5, 0, 3), # no outputs ({'elim_states': [0, 1, 2, 3, 4]}, 0, 3, 3), # no states From eff6c40d46b6909314b3280c65a164cbea36e7f4 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 6 Dec 2024 21:58:19 -0800 Subject: [PATCH 8/8] updates to address @slivingston review comments --- control/modelsimp.py | 32 +++++++++++++++----------------- control/tests/modelsimp_test.py | 2 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/control/modelsimp.py b/control/modelsimp.py index 9171a9559..fe519b82d 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -89,9 +89,9 @@ def model_reduction( eliminated or those to be kept. Two methods of state reduction are possible: 'truncate' removes the - states marked for elimination, while 'matchdc' replaces the the - eliminated states with their equilibrium values (thereby keeping the - input/output gain unchanged at zero frequency ["DC"]). + states marked for elimination, while 'matchdc' replaces the eliminated + states with their equilibrium values (thereby keeping the input/output + gain unchanged at zero frequency ["DC"]). Parameters ---------- @@ -104,9 +104,8 @@ def model_reduction( Vector of inputs, outputs, or states to keep. Can be specified either as an offset into the appropriate vector or as a signal name. method : string - Method of removing states in `elim`: either 'truncate' or - 'matchdc' (default). - warn_unstable: bool, option + Method of removing states: either 'truncate' or 'matchdc' (default). + warn_unstable : bool, option If `False`, don't warn if system is unstable. Returns @@ -136,23 +135,23 @@ def model_reduction( See Also -------- balanced_reduction : Eliminate states using Hankel singular values. - minimal_realization : Eliminate unreachable or unobseravble states. + minimal_realization : Eliminate unreachable or unobservable states. Notes ----- The model_reduction function issues a warning if the system has - unstable eigenvalues, since in those situations the stability reduced - order model may be different that the stability of the full model. No - other checking is done, so users to be careful not to render a system - unobservable or unreachable. + unstable eigenvalues, since in those situations the stability of the + reduced order model may be different than the stability of the full + model. No other checking is done, so users must to be careful not to + render a system unobservable or unreachable. - States, inputs, and outputs can be specified using integer offers or + States, inputs, and outputs can be specified using integer offsets or using signal names. Slices can also be specified, but must use the Python ``slice()`` function. """ if not isinstance(sys, StateSpace): - raise TypeError("system must be a a StateSpace system") + raise TypeError("system must be a StateSpace system") # Check system is stable if warn_unstable: @@ -161,7 +160,7 @@ def model_reduction( warnings.warn("System is unstable; reduction may be meaningless") # Utility function to process keep/elim keywords - def _process_elim_or_keep(elim, keep, labels, allow_both=False): + def _process_elim_or_keep(elim, keep, labels): def _expand_key(key): if key is None: return [] @@ -178,9 +177,8 @@ def _expand_key(key): keep = np.atleast_1d(_expand_key(keep)) if len(elim) > 0 and len(keep) > 0: - if not allow_both: - raise ValueError( - "can't provide both 'keep' and 'elim' for same variables") + raise ValueError( + "can't provide both 'keep' and 'elim' for same variables") elif len(keep) > 0: keep = np.sort(keep).tolist() elim = [i for i in range(len(labels)) if i not in keep] diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index 65f74aee5..dead4eb75 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -485,7 +485,7 @@ def testBalredMatchDC(self): ({'elim_inputs': [0, 1, 2]}, 5, 3, 0), # no inputs ({'elim_outputs': [0, 1, 2]}, 5, 0, 3), # no outputs ({'elim_states': [0, 1, 2, 3, 4]}, 0, 3, 3), # no states - ({'elim_states': [0, 1], 'keep_states': [1, 2]}, None, None, None) + ({'elim_states': [0, 1], 'keep_states': [1, 2]}, None, None, None), ]) @pytest.mark.parametrize("method", ['truncate', 'matchdc']) def test_model_reduction(method, kwargs, nstates, noutputs, ninputs): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy