From c94828a615fbf79dce321048b679c604b16d55ca Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Sun, 6 Nov 2022 20:52:20 -0800 Subject: [PATCH 1/7] add support for params more consistently in _rhs and _out methods --- control/iosys.py | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 19f527c22..e016acbaf 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -114,12 +114,12 @@ class for a set of subclasses that are used to implement specific The :class:`~control.InputOuputSystem` class (and its subclasses) makes use of two special methods for implementing much of the work of the class: - * _rhs(t, x, u): compute the right hand side of the differential or - difference equation for the system. This must be specified by the + * _rhs(t, x, u, params={}): compute the right hand side of the differential + or difference equation for the system. This must be specified by the subclass for the system. - * _out(t, x, u): compute the output for the current state of the system. - The default is to return the entire system state. + * _out(t, x, u, params={}): compute the output for the current state of the + system. The default is to return the entire system state. """ @@ -369,7 +369,7 @@ def _rhs(self, t, x, u, params={}): NotImplemented("Evaluation not implemented for system of type ", type(self)) - def dynamics(self, t, x, u): + def dynamics(self, t, x, u, params={}): """Compute the dynamics of a differential or difference equation. Given time `t`, input `u` and state `x`, returns the value of the @@ -395,12 +395,15 @@ def dynamics(self, t, x, u): current state u : array_like input + params : dict (optional) + system parameters + Returns ------- dx/dt or x[t+dt] : ndarray """ - return self._rhs(t, x, u) + return self._rhs(t, x, u, params=params) def _out(self, t, x, u, params={}): """Evaluate the output of a system at a given state, input, and time @@ -414,7 +417,7 @@ def _out(self, t, x, u, params={}): # If no output function was defined in subclass, return state return x - def output(self, t, x, u): + def output(self, t, x, u, params={}): """Compute the output of the system Given time `t`, input `u` and state `x`, returns the output of the @@ -432,12 +435,14 @@ def output(self, t, x, u): current state u : array_like input + params : dict (optional) + system parameters Returns ------- y : ndarray """ - return self._out(t, x, u) + return self._out(t, x, u, params=params) def feedback(self, other=1, sign=-1, params={}): """Feedback interconnection between two input/output systems @@ -673,13 +678,13 @@ def _update_params(self, params={}, warning=True): if params and warning: warn("Parameters passed to LinearIOSystems are ignored.") - def _rhs(self, t, x, u): + def _rhs(self, t, x, u, params={}): # Convert input to column vector and then change output to 1D array xdot = self.A @ np.reshape(x, (-1, 1)) \ + self.B @ np.reshape(u, (-1, 1)) return np.array(xdot).reshape((-1,)) - def _out(self, t, x, u): + def _out(self, t, x, u, params={}): # Convert input to column vector and then change output to 1D array y = self.C @ np.reshape(x, (-1, 1)) \ + self.D @ np.reshape(u, (-1, 1)) @@ -840,13 +845,17 @@ def _update_params(self, params, warning=False): self._current_params = self.params.copy() self._current_params.update(params) - def _rhs(self, t, x, u): - xdot = self.updfcn(t, x, u, self._current_params) \ + def _rhs(self, t, x, u, params={}): + current_params = self._current_params.copy() + current_params.update(params) + xdot = self.updfcn(t, x, u, current_params) \ if self.updfcn is not None else [] return np.array(xdot).reshape((-1,)) - def _out(self, t, x, u): - y = self.outfcn(t, x, u, self._current_params) \ + def _out(self, t, x, u, params={}): + current_params = self._current_params.copy() + current_params.update(params) + y = self.outfcn(t, x, u, current_params) \ if self.outfcn is not None else x return np.array(y).reshape((-1,)) @@ -1018,7 +1027,7 @@ def _update_params(self, params, warning=False): local.update(params) # update with locally passed parameters sys._update_params(local, warning=warning) - def _rhs(self, t, x, u): + def _rhs(self, t, x, u, params={}): # Make sure state and input are vectors x = np.array(x, ndmin=1) u = np.array(u, ndmin=1) @@ -1031,10 +1040,12 @@ def _rhs(self, t, x, u): state_index, input_index = 0, 0 # Start at the beginning for sys in self.syslist: # Update the right hand side for this subsystem + sys_params = sys._current_params.copy() + sys_params.update(params) if sys.nstates != 0: xdot[state_index:state_index + sys.nstates] = sys._rhs( t, x[state_index:state_index + sys.nstates], - ulist[input_index:input_index + sys.ninputs]) + ulist[input_index:input_index + sys.ninputs], sys_params) # Update the state and input index counters state_index += sys.nstates @@ -1042,7 +1053,7 @@ def _rhs(self, t, x, u): return xdot - def _out(self, t, x, u): + def _out(self, t, x, u, params={}): # Make sure state and input are vectors x = np.array(x, ndmin=1) u = np.array(u, ndmin=1) @@ -2838,7 +2849,7 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], params={}, 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]): + if all([isinstance(sys, (LinearIOSystem, StateSpace)) for sys in syslist]): return LinearICSystem(newsys, None) return newsys From 826916d35d2061bd4c6ee8eb6ddfe107342a215b Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Sun, 6 Nov 2022 21:21:20 -0800 Subject: [PATCH 2/7] added standard style of parameter handling for iosystem.__call__, added _current_params to iosys class --- control/iosys.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index e016acbaf..f23624159 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -149,6 +149,7 @@ def __init__(self, params={}, **kwargs): # default parameters self.params = params.copy() + self._current_params = self.params.copy() def __mul__(sys2, sys1): """Multiply two input/output systems (series interconnection)""" @@ -804,7 +805,7 @@ def __str__(self): f"Output: {self.outfcn}" # Return the value of a static nonlinear system - def __call__(sys, u, params=None, squeeze=None): + def __call__(sys, u, params={}, squeeze=None): """Evaluate a (static) nonlinearity at a given input value If a nonlinear I/O system has no internal state, then evaluating the @@ -830,12 +831,8 @@ def __call__(sys, u, params=None, squeeze=None): "function evaluation is only supported for static " "input/output systems") - # If we received any parameters, update them before calling _out() - if params is not None: - sys._update_params(params) - # Evaluate the function on the argument - out = sys._out(0, np.array((0,)), np.asarray(u)) + out = sys._out(0, np.array((0,)), np.asarray(u), params) _, out = _process_time_response( None, out, issiso=sys.issiso(), squeeze=squeeze) return out From bdfb6b5ad647b9bf91f5cb6feb40577257a0b7fd Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 7 Nov 2022 09:44:58 -0800 Subject: [PATCH 3/7] _rhs, _out, and other additions to StateSpace Systems to be compatible with LinearIO systems --- control/iosys.py | 10 ++++---- control/statesp.py | 63 +++++++++++++++++++++++++++++++++------------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index f23624159..fdf4c8564 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -680,16 +680,16 @@ def _update_params(self, params={}, warning=True): warn("Parameters passed to LinearIOSystems are ignored.") def _rhs(self, t, x, u, params={}): - # Convert input to column vector and then change output to 1D array - xdot = self.A @ np.reshape(x, (-1, 1)) \ + # Convert input to column vector in case A, B are numpy matrix + output = self.A @ np.reshape(x, (-1, 1)) \ + self.B @ np.reshape(u, (-1, 1)) - return np.array(xdot).reshape((-1,)) + return np.array(output).reshape((-1,)) # change output to 1D array def _out(self, t, x, u, params={}): - # Convert input to column vector and then change output to 1D array + # Convert input to column vector in case A, B are numpy matrix y = self.C @ np.reshape(x, (-1, 1)) \ + self.D @ np.reshape(u, (-1, 1)) - return np.array(y).reshape((-1,)) + return np.array(y).reshape((-1,)) # change output to 1D array def __repr__(self): # Need to define so that I/O system gets used instead of StateSpace diff --git a/control/statesp.py b/control/statesp.py index 374b036ca..962449573 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -386,6 +386,9 @@ def __init__(self, *args, init_namedio=True, **kwargs): # Check for states that don't do anything, and remove them if remove_useless_states: self._remove_useless_states() + # params for compatibility with LinearICSystems + self.params = {} + self._current_params = self.params.copy() # # Class attributes @@ -1388,8 +1391,22 @@ def dcgain(self, warn_infinite=False): scalar is returned. """ return self._dcgain(warn_infinite) + + def _rhs(self, t, x, u=None, params={}): + """Compute the right hand side of the differential or difference + equation for the system. Please :meth:`dynamics` for a more user- + friendly interface. """ - def dynamics(self, t, x, u=None): + x = np.reshape(x, (-1, 1)) # force to a column in case matrix + if u is None: # received t and x, but ignore t + output = self.A @ x + else: # received t, x, and u, ignore t + u = np.reshape(u, (-1, 1)) # force to column in case matrix + output = self.A @ x + self.B @ u + + return output.reshape((-1,)) # return as row vector + + def dynamics(self, t, x, u=None, params={}): """Compute the dynamics of the system Given input `u` and state `x`, returns the dynamics of the state-space @@ -1417,25 +1434,35 @@ def dynamics(self, t, x, u=None): current state u : array_like (optional) input, zero if omitted + params : dict (optional) + included for compatibility but ignored for :class:`StateSpace` systems Returns ------- dx/dt or x[t+dt] : ndarray """ - x = np.reshape(x, (-1, 1)) # force to a column in case matrix + # chechs if np.size(x) != self.nstates: raise ValueError("len(x) must be equal to number of states") + if u is not None: + if np.size(u) != self.ninputs: + raise ValueError("len(u) must be equal to number of inputs") + return self._rhs(t, x, u, params) + + def _out(self, t, x, u=None, params={}): + """Compute the output of the system system. Please :meth:`dynamics` + for a more user-friendly interface. """ + + x = np.reshape(x, (-1, 1)) # force to a column in case matrix if u is None: - return (self.A @ x).reshape((-1,)) # return as row vector + y = self.C @ x else: # received t, x, and u, ignore t - u = np.reshape(u, (-1, 1)) # force to column in case matrix - if np.size(u) != self.ninputs: - raise ValueError("len(u) must be equal to number of inputs") - return (self.A @ x).reshape((-1,)) \ - + (self.B @ u).reshape((-1,)) # return as row vector + u = np.reshape(u, (-1, 1)) # force to a column in case matrix + y = self.C @ x + self.D @ u + return y.reshape((-1,)) # return as row vector - def output(self, t, x, u=None): + def output(self, t, x, u=None, params={}): """Compute the output of the system Given input `u` and state `x`, returns the output `y` of the @@ -1465,19 +1492,19 @@ def output(self, t, x, u=None): ------- y : ndarray """ - x = np.reshape(x, (-1, 1)) # force to a column in case matrix if np.size(x) != self.nstates: raise ValueError("len(x) must be equal to number of states") - - if u is None: - return (self.C @ x).reshape((-1,)) # return as row vector - else: # received t, x, and u, ignore t - u = np.reshape(u, (-1, 1)) # force to a column in case matrix + if u is not None: if np.size(u) != self.ninputs: raise ValueError("len(u) must be equal to number of inputs") - return (self.C @ x).reshape((-1,)) \ - + (self.D @ u).reshape((-1,)) # return as row vector - + return self._out(t, x, u, params) + + def _update_params(self, params={}, warning=False): + # Parameters not supported; issue a warning + if params and warning: + warn("Parameters passed to StateSpace system are ignored.") + + def _isstatic(self): """True if and only if the system has no dynamics, that is, if A and B are zero. """ From 3df05676dbadfb2e176d1737e803ce0d6c13d3e5 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 7 Nov 2022 10:12:31 -0800 Subject: [PATCH 4/7] docstring fix --- control/statesp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/statesp.py b/control/statesp.py index 962449573..03708b174 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -386,7 +386,7 @@ def __init__(self, *args, init_namedio=True, **kwargs): # Check for states that don't do anything, and remove them if remove_useless_states: self._remove_useless_states() - # params for compatibility with LinearICSystems + # params for compatibility with LinearIOSystems self.params = {} self._current_params = self.params.copy() From dc633d08c4995c7fdf243a8363a6a108f4c2e462 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 7 Nov 2022 10:29:59 -0800 Subject: [PATCH 5/7] added unit test --- control/tests/iosys_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 693be979e..b080cd860 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -66,6 +66,10 @@ def test_linear_iosys(self, tsys): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.) + # Make sure that a combination of a LinearIOSystem and a StateSpace + # system results in a LinearIOSystem + assert isinstance(linsys*iosys, ios.LinearIOSystem) + def test_tf2io(self, tsys): # Create a transfer function from the state space system linsys = tsys.siso_linsys From 98fba6e852567c70b6e11424c369b2eafc6517a1 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 7 Nov 2022 20:15:34 -0800 Subject: [PATCH 6/7] use standard method to assess whether a system is a static gain, so that it has dt=None by default, when constructing a LinearIOSystem --- control/statesp.py | 3 +-- control/tests/iosys_test.py | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 03708b174..b39bd90a0 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -346,9 +346,8 @@ def __init__(self, *args, init_namedio=True, **kwargs): 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) + kwargs, defaults, static=self._isstatic(), end=True) # Initialize LTI (NamedIOSystem) object super().__init__( diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index b080cd860..cb38205a1 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -41,6 +41,9 @@ class TSys: [[-1, 1], [0, -2]], [[0, 1], [1, 0]], [[1, 0], [0, 1]], np.zeros((2, 2))) + # Create a static gain linear system + T.staticgain = ct.StateSpace(0, 0, 0, 1) + # Create simulation parameters T.T = np.linspace(0, 10, 100) T.U = np.sin(T.T) @@ -70,6 +73,12 @@ def test_linear_iosys(self, tsys): # system results in a LinearIOSystem assert isinstance(linsys*iosys, ios.LinearIOSystem) + # Make sure that a static linear system has dt=None + # and otherwise dt is as specified + assert ios.LinearIOSystem(tsys.staticgain).dt is None + assert ios.LinearIOSystem(tsys.staticgain, dt=.1).dt == .1 + + def test_tf2io(self, tsys): # Create a transfer function from the state space system linsys = tsys.siso_linsys From 9baafd5b759a5ee20722479b76f941e81510bf5d Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 8 Nov 2022 12:23:26 -0800 Subject: [PATCH 7/7] inherit _rhs and _out methods rather than duplicate them in LinearIOSystem --- control/iosys.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index fdf4c8564..9eb136cfb 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -679,17 +679,9 @@ def _update_params(self, params={}, warning=True): if params and warning: warn("Parameters passed to LinearIOSystems are ignored.") - def _rhs(self, t, x, u, params={}): - # Convert input to column vector in case A, B are numpy matrix - output = self.A @ np.reshape(x, (-1, 1)) \ - + self.B @ np.reshape(u, (-1, 1)) - return np.array(output).reshape((-1,)) # change output to 1D array - - def _out(self, t, x, u, params={}): - # Convert input to column vector in case A, B are numpy matrix - y = self.C @ np.reshape(x, (-1, 1)) \ - + self.D @ np.reshape(u, (-1, 1)) - return np.array(y).reshape((-1,)) # change output to 1D array + # inherit methods as necessary + _rhs = StateSpace._rhs + _out = StateSpace._out def __repr__(self): # Need to define so that I/O system gets used instead of StateSpace