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 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