From f701b758151af2379276c2282752ec99d79b82bb Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 16 Feb 2021 12:24:33 -0800 Subject: [PATCH 1/5] added methods dynamics and output to StateSpace --- control/statesp.py | 70 ++++++++++++++++++++++++++++++++++- control/tests/statesp_test.py | 56 +++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index abd55ad15..79f6a6291 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1215,9 +1215,77 @@ def dcgain(self): def _isstatic(self): """True if and only if the system has no dynamics, that is, - if A and B are zero. """ + if `self.A` and `self.B` are zero. + """ return not np.any(self.A) and not np.any(self.B) + def dynamics(self, x, u=None): + """Compute the dynamics of the system + + Given input `u` and state `x`, returns the dynamics of the state-space + system. If the system is continuous, returns the time derivative dx/dt + + dx/dt = A x + B u + + where A and B are the state-space matrices of the system. If the + system is discrete-time, returns the next value of `x`: + + x[k+1] = A x[k] + B u[k] + + The inputs `x` and `u` must be of the correct length. + + Parameters + ---------- + x : array_like + current state + u : array_like + input (optional) + + Returns + ------- + dx/dt or x[k+1] : ndarray + """ + + if len(np.atleast_1d(x)) != self.nstates: + raise ValueError("len(x) must be equal to number of states") + if u is not None: + if len(np.atleast_1d(u)) != self.ninputs: + raise ValueError("len(u) must be equal to number of inputs") + + return self.A.dot(x) if u is None else self.A.dot(x) + self.B.dot(u) + + def output(self, x, u=None): + """Compute the output of the system + + Given input `u` and state `x`, returns the output `y` of the + state-space system: + + y = C x + D u + + where A and B are the state-space matrices of the system. The inputs + `x` and `u` must be of the correct length. + + Parameters + ---------- + x : array_like + current state + u : array_like + input (optional) + + Returns + ------- + y : ndarray + """ + if len(np.atleast_1d(x)) != self.nstates: + raise ValueError("len(x) must be equal to number of states") + if u is not None: + if len(np.atleast_1d(u)) != self.ninputs: + raise ValueError("len(u) must be equal to number of inputs") + + return self.C.dot(x) if u is None else self.C.dot(x) + self.D.dot(u) + + + # TODO: add discrete time check def _convert_to_statespace(sys, **kw): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 1c76efbc0..2579c1aff 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -8,6 +8,7 @@ """ import numpy as np +from numpy.testing import assert_allclose, assert_array_almost_equal import pytest import operator from numpy.linalg import solve @@ -42,14 +43,26 @@ def sys322ABCD(self): [0., 1.]] return (A322, B322, C322, D322) + + @pytest.fixture + def sys121(self): + """2 state, 1 input, 1 output (siso) system""" + A121 = [[4., 1.], + [2., -3]] + B121 = [[5.], + [-3.]] + C121 = [[2., -4]] + D121 = [[3.]] + return StateSpace(A121, B121, C121, D121) + @pytest.fixture def sys322(self, sys322ABCD): - """3-states square system (2 inputs x 2 outputs)""" + """3-state square system (2 inputs x 2 outputs)""" return StateSpace(*sys322ABCD) @pytest.fixture def sys222(self): - """2-states square system (2 inputs x 2 outputs)""" + """2-state square system (2 inputs x 2 outputs)""" A222 = [[4., 1.], [2., -3]] B222 = [[5., 2.], @@ -744,6 +757,45 @@ def test_horner(self, sys322): np.squeeze(sys322.horner(1.j)), mag[:, :, 0] * np.exp(1.j * phase[:, :, 0])) + @pytest.mark.parametrize('x', [[1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) + @pytest.mark.parametrize('u', [None, 0, 1, np.atleast_1d(2)]) + def test_dynamics_output_siso(self, x, u, sys121): + assert_array_almost_equal( + sys121.dynamics(x, u), + sys121.A.dot(x) + (0 if u is None else sys121.B.dot(u))) + assert_array_almost_equal( + sys121.output(x, u), + sys121.C.dot(x) + (0 if u is None else sys121.D.dot(u))) + + # too few and too many states and inputs + @pytest.mark.parametrize('x', [0, 1, [], [1, 2, 3], np.atleast_1d(2)]) + @pytest.mark.parametrize('u', [None, [1, 1], np.atleast_1d((2, 2))]) + def test_dynamics_output_siso_fails(self, x, u, sys121): + with pytest.raises(ValueError): + sys121.dynamics(x, u) + with pytest.raises(ValueError): + sys121.output(x, u) + + @pytest.mark.parametrize('x',[[1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) + @pytest.mark.parametrize('u', + [None, [1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) + def test_dynamics_output_mimo(self, x, u, sys222): + assert_array_almost_equal( + sys222.dynamics(x, u), + sys222.A.dot(x) + (0 if u is None else sys222.B.dot(u))) + assert_array_almost_equal( + sys222.output(x, u), + sys222.C.dot(x) + (0 if u is None else sys222.D.dot(u))) + + # too few and too many states and inputs + @pytest.mark.parametrize('x', [0, 1, [1, 1, 1]]) + @pytest.mark.parametrize('u', [None, 0, 1, [1, 1, 1]]) + def test_dynamics_mimo_fails(self, x, u, sys222): + with pytest.raises(ValueError): + sys222.dynamics(x, u) + with pytest.raises(ValueError): + sys222.output(x, u) + class TestRss: """These are tests for the proper functionality of statesp.rss.""" From 033b236c4df611987e19de550b433af282708af2 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 16 Feb 2021 13:11:46 -0800 Subject: [PATCH 2/5] rename method _rhs in iosys to dynamics --- control/iosys.py | 91 +++++++++++++++++++-------- control/sisotool.py | 2 +- control/tests/iosys_test.py | 20 +++--- control/tests/type_conversion_test.py | 18 +++--- doc/iosys.rst | 22 +++---- 5 files changed, 97 insertions(+), 56 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 16ef633b7..a8280a558 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -111,7 +111,7 @@ 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 + * dynamics(t, x, u): compute the right hand side of the differential or difference equation for the system. This must be specified by the subclass for the system. @@ -353,28 +353,69 @@ def _process_signal_list(self, signals, prefix='s'): # 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) + # Update parameters used for dynamics, _out (used by subclasses) def _update_params(self, params, warning=False): if (warning): warn("Parameters passed to InputOutputSystem ignored.") - def _rhs(self, t, x, u): - """Evaluate right hand side of a differential or difference equation. + def dynamics(self, t, x, u): + """Compute the dynamics of a differential or difference equation. - Private function used to compute the right hand side of an - input/output system model. + Given time `t`, input `u` and state `x`, returns the dynamics of the + system. If the system is continuous, returns the time derivative + ``dx/dt = f(t, x, u)`` + + where `f` is the system's (possibly nonlinear) dynamics function. + If the system is discrete-time, returns the next value of `x`: + + ``x[t+dt] = f(t, x[t], u[t])`` + + Where `t` is a scalar. + + The inputs `x` and `u` must be of the correct length. + + Parameters + ---------- + t : float + the time at which to evaluate + x : array_like + current state + u : array_like + input + + Returns + ------- + `dx/dt` or `x[t+dt]` : ndarray """ - NotImplemented("Evaluation not implemented for system of type ", + + NotImplemented("Dynamics not implemented for system of type ", type(self)) def _out(self, t, x, u, params={}): - """Evaluate the output of a system at a given state, input, and time + """Compute the output of the system + + Given time `t`, input `u` and state `x`, returns the output of the + system: + + ``y = g(t, x, u)`` - Private function used to compute the output of of an input/output - system model given the state, input, parameters, and time. + The inputs `x` and `u` must be of the correct length. + Parameters + ---------- + t : float + the time at which to evaluate + x : array_like + current state + u : array_like + input + + Returns + ------- + y : ndarray """ + # If no output function was defined in subclass, return state return x @@ -533,7 +574,7 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, """ # # If the linearization is not defined by the subclass, perform a - # numerical linearization use the `_rhs()` and `_out()` member + # numerical linearization use the `dynamics()` and `_out()` member # functions. # @@ -554,7 +595,7 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, self._update_params(params) # Compute the nominal value of the update law and output - F0 = self._rhs(t, x0, u0) + F0 = self.dynamics(t, x0, u0) H0 = self._out(t, x0, u0) # Create empty matrices that we can fill up with linearizations @@ -567,14 +608,14 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, for i in range(nstates): dx = np.zeros((nstates,)) dx[i] = eps - A[:, i] = (self._rhs(t, x0 + dx, u0) - F0) / eps + A[:, i] = (self.dynamics(t, x0 + dx, u0) - F0) / eps C[:, i] = (self._out(t, x0 + dx, u0) - H0) / eps # Perturb each of the input variables and compute linearization for i in range(ninputs): du = np.zeros((ninputs,)) du[i] = eps - B[:, i] = (self._rhs(t, x0, u0 + du) - F0) / eps + B[:, i] = (self.dynamics(t, x0, u0 + du) - F0) / eps D[:, i] = (self._out(t, x0, u0 + du) - H0) / eps # Create the state space system @@ -694,7 +735,7 @@ 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 dynamics(self, t, x, u): # Convert input to column vector and then change output to 1D array xdot = np.dot(self.A, np.reshape(x, (-1, 1))) \ + np.dot(self.B, np.reshape(u, (-1, 1))) @@ -863,7 +904,7 @@ def _update_params(self, params, warning=False): self._current_params = self.params.copy() self._current_params.update(params) - def _rhs(self, t, x, u): + def dynamics(self, t, x, u): xdot = self.updfcn(t, x, u, self._current_params) \ if self.updfcn is not None else [] return np.array(xdot).reshape((-1,)) @@ -1033,7 +1074,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 dynamics(self, t, x, u): # Make sure state and input are vectors x = np.array(x, ndmin=1) u = np.array(u, ndmin=1) @@ -1047,7 +1088,7 @@ def _rhs(self, t, x, u): for sys in self.syslist: # Update the right hand side for this subsystem if sys.nstates != 0: - xdot[state_index:state_index + sys.nstates] = sys._rhs( + xdot[state_index:state_index + sys.nstates] = sys.dynamics( t, x[state_index:state_index + sys.nstates], ulist[input_index:input_index + sys.ninputs]) @@ -1497,14 +1538,14 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45', # Create a lambda function for the right hand side u = sp.interpolate.interp1d(T, U, fill_value="extrapolate") - def ivp_rhs(t, x): return sys._rhs(t, x, u(t)) + def ivp_dynamics(t, x): return sys.dynamics(t, x, u(t)) # Perform the simulation if isctime(sys): if not hasattr(sp.integrate, 'solve_ivp'): 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, + soln = sp.integrate.solve_ivp(ivp_dynamics, (T0, Tf), X0, t_eval=T, method=method, vectorized=False) # Compute the output associated with the state (and use sys.out to @@ -1549,7 +1590,7 @@ def ivp_rhs(t, x): return sys._rhs(t, x, u(t)) y.append(sys._out(T[i], x, u(T[i]))) # Update the state for the next iteration - x = sys._rhs(T[i], x, u(T[i])) + x = sys.dynamics(T[i], x, u(T[i])) # Convert output to numpy arrays soln.y = np.transpose(np.array(soln.y)) @@ -1670,8 +1711,8 @@ def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={}, if y0 is None: # 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) + def ode_dynamics(z): return sys.dynamics(t, z, u0) + result = root(ode_dynamics, x0, **kw) z = (result.x, u0, sys._out(t, result.x, u0)) else: # Take y0 as fixed and minimize over x and u @@ -1680,7 +1721,7 @@ def rootfun(z): x, u = np.split(z, [nstates]) # TODO: update to allow discrete time systems return np.concatenate( - (sys._rhs(t, x, u), sys._out(t, x, u) - y0), axis=0) + (sys.dynamics(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 x, u = np.split(result.x, [nstates]) # Split result back in two @@ -1782,7 +1823,7 @@ def rootfun(z): u[input_vars] = z[nstate_vars:] # Compute the update and output maps - dx = sys._rhs(t, x, u) - dx0 + dx = sys.dynamics(t, x, u) - dx0 if dtime: dx -= x # TODO: check dy = sys._out(t, x, u) - y0 diff --git a/control/sisotool.py b/control/sisotool.py index bfd93736e..38c7148d4 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -101,7 +101,7 @@ def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, # First time call to setup the bode and step response plots _SisotoolUpdate(sys, fig, - 1 if kvect is None else kvect[0], bode_plot_params) + 1 if kvect is None else kvect[0], bode_plot_params, tvect=tvect) # Setup the root-locus plot window root_locus(sys, kvect=kvect, xlim=xlim_rlocus, diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 9a15e83f4..3c48a77c0 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -56,7 +56,7 @@ def test_linear_iosys(self, tsys): # Make sure that the right hand side matches linear system for x, u in (([0, 0], 0), ([1, 0], 0), ([0, 1], 0), ([0, 0], 1)): np.testing.assert_array_almost_equal( - np.reshape(iosys._rhs(0, x, u), (-1, 1)), + np.reshape(iosys.dynamics(0, x, u), (-1, 1)), np.dot(linsys.A, np.reshape(x, (-1, 1))) + np.dot(linsys.B, u)) # Make sure that simulations also line up @@ -687,7 +687,7 @@ def test_find_eqpts(self, tsys): assert result.success np.testing.assert_array_almost_equal(xeq, [1.64705879, 1.17923874]) np.testing.assert_array_almost_equal( - nlsys._rhs(0, xeq, ueq), np.zeros((2,))) + nlsys.dynamics(0, xeq, ueq), np.zeros((2,))) # Ducted fan dynamics with output = velocity nlsys = ios.NonlinearIOSystem(pvtol, lambda t, x, u, params: x[0:2]) @@ -697,7 +697,7 @@ def test_find_eqpts(self, tsys): nlsys, [0, 0, 0, 0], [0, 4*9.8], return_result=True) assert result.success np.testing.assert_array_almost_equal( - nlsys._rhs(0, xeq, ueq), np.zeros((4,))) + nlsys.dynamics(0, xeq, ueq), np.zeros((4,))) np.testing.assert_array_almost_equal(xeq, [0, 0, 0, 0]) # Use a small lateral force to cause motion @@ -705,7 +705,7 @@ def test_find_eqpts(self, tsys): nlsys, [0, 0, 0, 0], [0.01, 4*9.8], return_result=True) assert result.success np.testing.assert_array_almost_equal( - nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + nlsys.dynamics(0, xeq, ueq), np.zeros((4,)), decimal=5) # Equilibrium point with fixed output xeq, ueq, result = ios.find_eqpt( @@ -715,7 +715,7 @@ def test_find_eqpts(self, tsys): np.testing.assert_array_almost_equal( nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( - nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + nlsys.dynamics(0, xeq, ueq), np.zeros((4,)), decimal=5) # Specify outputs to constrain (replicate previous) xeq, ueq, result = ios.find_eqpt( @@ -725,7 +725,7 @@ def test_find_eqpts(self, tsys): np.testing.assert_array_almost_equal( nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( - nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + nlsys.dynamics(0, xeq, ueq), np.zeros((4,)), decimal=5) # Specify inputs to constrain (replicate previous), w/ no result xeq, ueq = ios.find_eqpt( @@ -733,7 +733,7 @@ def test_find_eqpts(self, tsys): np.testing.assert_array_almost_equal( nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( - nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5) + nlsys.dynamics(0, xeq, ueq), np.zeros((4,)), decimal=5) # Now solve the problem with the original PVTOL variables # Constrain the output angle and x velocity @@ -746,7 +746,7 @@ def test_find_eqpts(self, tsys): np.testing.assert_array_almost_equal( nlsys_full._out(0, xeq, ueq)[[2, 3]], [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( - nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) + nlsys_full.dynamics(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) # Fix one input and vary the other nlsys_full = ios.NonlinearIOSystem(pvtol_full, None) @@ -759,7 +759,7 @@ def test_find_eqpts(self, tsys): np.testing.assert_array_almost_equal( nlsys_full._out(0, xeq, ueq)[[3]], [0.1], decimal=5) np.testing.assert_array_almost_equal( - nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) + nlsys_full.dynamics(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) # PVTOL with output = y velocity xeq, ueq, result = ios.find_eqpt( @@ -771,7 +771,7 @@ def test_find_eqpts(self, tsys): np.testing.assert_array_almost_equal( nlsys_full._out(0, xeq, ueq)[-3:], [0.1, 0, 0], decimal=5) np.testing.assert_array_almost_equal( - nlsys_full._rhs(0, xeq, ueq)[-5:], np.zeros((5,)), decimal=5) + nlsys_full.dynamics(0, xeq, ueq)[-5:], np.zeros((5,)), decimal=5) # Unobservable system linsys = ct.StateSpace( diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index 3f51c2bbc..95b075ba9 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -19,7 +19,7 @@ def sys_dict(): sdict['frd'] = ct.frd([10+0j, 9 + 1j, 8 + 2j], [1,2,3]) 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'].dynamics, sdict['lio']._out, 1, 1, 1) sdict['arr'] = np.array([[2.0]]) sdict['flt'] = 3. return sdict @@ -66,7 +66,7 @@ def sys_dict(): ('add', 'ios', ['xos', 'xos', 'E', 'ios', 'ios', 'xos', 'xos']), ('add', 'arr', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), ('add', 'flt', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'flt']), - + # op left ss tf frd lio ios arr flt ('sub', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), ('sub', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), @@ -75,7 +75,7 @@ def sys_dict(): ('sub', 'ios', ['xos', 'xio', 'E', 'ios', 'xos' 'xos', 'xos']), ('sub', 'arr', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), ('sub', 'flt', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'flt']), - + # op left ss tf frd lio ios arr flt ('mul', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), ('mul', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), @@ -84,7 +84,7 @@ def sys_dict(): ('mul', 'ios', ['xos', 'xos', 'E', 'ios', 'ios', 'xos', 'xos']), ('mul', 'arr', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), ('mul', 'flt', ['ss', 'tf', 'frd', 'xio', 'xos', 'arr', 'flt']), - + # op left ss tf frd lio ios arr flt ('truediv', 'ss', ['xs', 'tf', 'xrd', 'xio', 'xos', 'xs', 'xs' ]), ('truediv', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), @@ -100,7 +100,7 @@ def sys_dict(): for rtype, expected in zip(rtype_list, expected_list): # Add this to the list of tests to run test_matrix.append([opname, ltype, rtype, expected]) - + @pytest.mark.parametrize("opname, ltype, rtype, expected", test_matrix) def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): op = getattr(operator, opname) @@ -110,7 +110,7 @@ def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): # Get rid of warnings for InputOutputSystem objects by making a copy if isinstance(leftsys, ct.InputOutputSystem) and leftsys == rightsys: rightsys = leftsys.copy() - + # Make sure we get the right result if expected == 'E' or expected[0] == 'x': # Exception expected @@ -119,7 +119,7 @@ def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): else: # Operation should work and return the given type result = op(leftsys, rightsys) - + # Print out what we are testing in case something goes wrong assert isinstance(result, type_dict[expected]) @@ -138,7 +138,7 @@ def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): # # * For IOS/LTI, convert to IOS. In the case of a linear I/O system (LIO), # this will preserve the linear structure since the LTI system will -# be converted to state space. +# be converted to state space. # # * When combining state space or transfer with linear I/O systems, the # * output should be of type Linear IO system, since that maintains the @@ -149,7 +149,7 @@ def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): type_list = ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] conversion_table = [ - # L \ R ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] + # 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' ]), diff --git a/doc/iosys.rst b/doc/iosys.rst index 1b160bad1..58d0f849f 100644 --- a/doc/iosys.rst +++ b/doc/iosys.rst @@ -29,8 +29,8 @@ Input/output systems can be created from state space LTI systems by using the 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 +:class:`~control.NonlinearIOSystem` class, which requires the definition of a +dynamics function (the right hand side of the differential or difference equation) and and output function (computes the outputs from the state):: io_sys = NonlinearIOSystem(updfcn, outfcn, inputs=M, outputs=P, states=N) @@ -68,7 +68,7 @@ We begin by defining the dynamics of the system import numpy as np import matplotlib.pyplot as plt - def predprey_rhs(t, x, u, params): + def predprey_dynamics(t, x, u, params): # Parameter setup a = params.get('a', 3.2) b = params.get('b', 0.6) @@ -76,18 +76,18 @@ We begin by defining the dynamics of the system d = params.get('d', 0.56) k = params.get('k', 125) r = params.get('r', 1.6) - + # Map the states into local variable names H = x[0] L = x[1] # Compute the control action (only allow addition of food) u_0 = u if u > 0 else 0 - + # Compute the discrete updates dH = (r + u_0) * H * (1 - H/k) - (a * H * L)/(c + H) dL = b * (a * H * L)/(c + H) - d * L - + return [dH, dL] We now create an input/output system using these dynamics: @@ -95,7 +95,7 @@ We now create an input/output system using these dynamics: .. code-block:: python io_predprey = control.NonlinearIOSystem( - predprey_rhs, None, inputs=('u'), outputs=('H', 'L'), + predprey_dynamics, None, inputs=('u'), outputs=('H', 'L'), states=('H', 'L'), name='predprey') Note that since we have not specified an output function, the entire state @@ -108,10 +108,10 @@ of the system: X0 = [25, 20] # Initial H, L 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) - + # Plot the response plt.figure(1) plt.plot(t, y[0]) @@ -171,14 +171,14 @@ function: inplist=['control.Ld'], outlist=['predprey.H', 'predprey.L', 'control.y[0]'] ) - + 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]) - + # Plot the response plt.figure(2) plt.subplot(2, 1, 1) From dd681e2851d13bbc8376ac5777659fe4a175be7a Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 16 Feb 2021 13:36:56 -0800 Subject: [PATCH 3/5] rename _out methods in iosys to output --- control/iosys.py | 46 +++++++++++++-------------- control/tests/iosys_test.py | 12 +++---- control/tests/type_conversion_test.py | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index a8280a558..7a7f8074b 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -115,7 +115,7 @@ class for a set of subclasses that are used to implement specific 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. + * output(t, x, u): compute the output for the current state of the system. The default is to return the entire system state. """ @@ -392,7 +392,7 @@ def dynamics(self, t, x, u): NotImplemented("Dynamics not implemented for system of type ", type(self)) - def _out(self, t, x, u, params={}): + 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 @@ -574,7 +574,7 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, """ # # If the linearization is not defined by the subclass, perform a - # numerical linearization use the `dynamics()` and `_out()` member + # numerical linearization use the `dynamics()` and `output()` member # functions. # @@ -589,14 +589,14 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, u0 = np.ones((ninputs,)) * u0 # Compute number of outputs by evaluating the output function - noutputs = _find_size(self.noutputs, self._out(t, x0, u0)) + noutputs = _find_size(self.noutputs, self.output(t, x0, u0)) # Update the current parameters self._update_params(params) # Compute the nominal value of the update law and output F0 = self.dynamics(t, x0, u0) - H0 = self._out(t, x0, u0) + H0 = self.output(t, x0, u0) # Create empty matrices that we can fill up with linearizations A = np.zeros((nstates, nstates)) # Dynamics matrix @@ -609,14 +609,14 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6, dx = np.zeros((nstates,)) dx[i] = eps A[:, i] = (self.dynamics(t, x0 + dx, u0) - F0) / eps - C[:, i] = (self._out(t, x0 + dx, u0) - H0) / eps + C[:, i] = (self.output(t, x0 + dx, u0) - H0) / eps # Perturb each of the input variables and compute linearization for i in range(ninputs): du = np.zeros((ninputs,)) du[i] = eps B[:, i] = (self.dynamics(t, x0, u0 + du) - F0) / eps - D[:, i] = (self._out(t, x0, u0 + du) - H0) / eps + D[:, i] = (self.output(t, x0, u0 + du) - H0) / eps # Create the state space system linsys = LinearIOSystem( @@ -741,7 +741,7 @@ def dynamics(self, t, x, u): + np.dot(self.B, np.reshape(u, (-1, 1))) return np.array(xdot).reshape((-1,)) - def _out(self, t, x, u): + def output(self, t, x, u): # Convert input to column vector and then change output to 1D array y = np.dot(self.C, np.reshape(x, (-1, 1))) \ + np.dot(self.D, np.reshape(u, (-1, 1))) @@ -890,12 +890,12 @@ 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 we received any parameters, update them before calling output() 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.output(0, np.array((0,)), np.asarray(u)) _, out = _process_time_response(sys, None, out, None, squeeze=squeeze) return out @@ -909,7 +909,7 @@ def dynamics(self, t, x, u): if self.updfcn is not None else [] return np.array(xdot).reshape((-1,)) - def _out(self, t, x, u): + def output(self, t, x, u): y = self.outfcn(t, x, u, self._current_params) \ if self.outfcn is not None else x return np.array(y).reshape((-1,)) @@ -1098,7 +1098,7 @@ def dynamics(self, t, x, u): return xdot - def _out(self, t, x, u): + def output(self, t, x, u): # Make sure state and input are vectors x = np.array(x, ndmin=1) u = np.array(u, ndmin=1) @@ -1130,7 +1130,7 @@ def _compute_static_io(self, t, x, u): state_index, input_index, output_index = 0, 0, 0 for sys in self.syslist: # Compute outputs for each system from current state - ysys = sys._out( + ysys = sys.output( t, x[state_index:state_index + sys.nstates], ulist[input_index:input_index + sys.ninputs]) @@ -1521,10 +1521,10 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45', 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))) + y = np.zeros((np.shape(sys.output(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) + y[:, i] = sys.output(T[i], [], u) return _process_time_response( sys, T, y, np.array((0, 0, np.asarray(T).size)), transpose=transpose, return_x=return_x, squeeze=squeeze) @@ -1551,10 +1551,10 @@ def ivp_dynamics(t, x): return sys.dynamics(t, x, u(t)) # 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))) + y = np.zeros((np.shape(sys.output(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) + y[:, i] = sys.output(T[i], soln.y[:, i], u) elif isdtime(sys): # Make sure the time vector is uniformly spaced @@ -1587,7 +1587,7 @@ def ivp_dynamics(t, x): return sys.dynamics(t, x, u(t)) for i in range(len(T)): # Store the current state and output soln.y.append(x) - y.append(sys._out(T[i], x, u(T[i]))) + y.append(sys.output(T[i], x, u(T[i]))) # Update the state for the next iteration x = sys.dynamics(T[i], x, u(T[i])) @@ -1713,7 +1713,7 @@ def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={}, # TODO: update to allow discrete time systems def ode_dynamics(z): return sys.dynamics(t, z, u0) result = root(ode_dynamics, x0, **kw) - z = (result.x, u0, sys._out(t, result.x, u0)) + z = (result.x, u0, sys.output(t, result.x, u0)) else: # Take y0 as fixed and minimize over x and u def rootfun(z): @@ -1721,11 +1721,11 @@ def rootfun(z): x, u = np.split(z, [nstates]) # TODO: update to allow discrete time systems return np.concatenate( - (sys.dynamics(t, x, u), sys._out(t, x, u) - y0), axis=0) + (sys.dynamics(t, x, u), sys.output(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 x, u = np.split(result.x, [nstates]) # Split result back in two - z = (x, u, sys._out(t, x, u)) + z = (x, u, sys.output(t, x, u)) else: # General case: figure out what variables to constrain @@ -1826,7 +1826,7 @@ def rootfun(z): dx = sys.dynamics(t, x, u) - dx0 if dtime: dx -= x # TODO: check - dy = sys._out(t, x, u) - y0 + dy = sys.output(t, x, u) - y0 # Map the results into the constrained variables return np.concatenate((dx[deriv_vars], dy[output_vars]), axis=0) @@ -1840,7 +1840,7 @@ def rootfun(z): # Extract out the results and insert into x and u x[state_vars] = result.x[:nstate_vars] u[input_vars] = result.x[nstate_vars:] - z = (x, u, sys._out(t, x, u)) + z = (x, u, sys.output(t, x, u)) # Return the result based on what the user wants and what we found if not return_y: diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 3c48a77c0..2ecb14559 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -713,7 +713,7 @@ def test_find_eqpts(self, tsys): y0=[0.1, 0.1], return_result=True) assert result.success np.testing.assert_array_almost_equal( - nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) + nlsys.output(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( nlsys.dynamics(0, xeq, ueq), np.zeros((4,)), decimal=5) @@ -723,7 +723,7 @@ def test_find_eqpts(self, tsys): iy = [0, 1], return_result=True) assert result.success np.testing.assert_array_almost_equal( - nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) + nlsys.output(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( nlsys.dynamics(0, xeq, ueq), np.zeros((4,)), decimal=5) @@ -731,7 +731,7 @@ def test_find_eqpts(self, tsys): xeq, ueq = ios.find_eqpt( nlsys, [0, 0, 0, 0], [0.01, 4*9.8], y0=[0.1, 0.1], iu = []) np.testing.assert_array_almost_equal( - nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5) + nlsys.output(0, xeq, ueq), [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( nlsys.dynamics(0, xeq, ueq), np.zeros((4,)), decimal=5) @@ -744,7 +744,7 @@ def test_find_eqpts(self, tsys): idx=[2, 3, 4, 5], ix=[0, 1], return_result=True) assert result.success np.testing.assert_array_almost_equal( - nlsys_full._out(0, xeq, ueq)[[2, 3]], [0.1, 0.1], decimal=5) + nlsys_full.output(0, xeq, ueq)[[2, 3]], [0.1, 0.1], decimal=5) np.testing.assert_array_almost_equal( nlsys_full.dynamics(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) @@ -757,7 +757,7 @@ def test_find_eqpts(self, tsys): assert result.success np.testing.assert_almost_equal(ueq[1], 4*9.8, decimal=5) np.testing.assert_array_almost_equal( - nlsys_full._out(0, xeq, ueq)[[3]], [0.1], decimal=5) + nlsys_full.output(0, xeq, ueq)[[3]], [0.1], decimal=5) np.testing.assert_array_almost_equal( nlsys_full.dynamics(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5) @@ -769,7 +769,7 @@ def test_find_eqpts(self, tsys): ix=[0, 1], return_result=True) assert result.success np.testing.assert_array_almost_equal( - nlsys_full._out(0, xeq, ueq)[-3:], [0.1, 0, 0], decimal=5) + nlsys_full.output(0, xeq, ueq)[-3:], [0.1, 0, 0], decimal=5) np.testing.assert_array_almost_equal( nlsys_full.dynamics(0, xeq, ueq)[-5:], np.zeros((5,)), decimal=5) diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index 95b075ba9..ffb8bb9ed 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -19,7 +19,7 @@ def sys_dict(): sdict['frd'] = ct.frd([10+0j, 9 + 1j, 8 + 2j], [1,2,3]) sdict['lio'] = ct.LinearIOSystem(ct.ss([[-1]], [[5]], [[5]], [[0]])) sdict['ios'] = ct.NonlinearIOSystem( - sdict['lio'].dynamics, sdict['lio']._out, 1, 1, 1) + sdict['lio'].dynamics, sdict['lio'].output, 1, 1, 1) sdict['arr'] = np.array([[2.0]]) sdict['flt'] = 3. return sdict From b010b9945bb4f7aba0691a0ed913752ae219423a Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 17 Feb 2021 09:35:57 -0800 Subject: [PATCH 4/5] updated statesp.dynamics and output to put optoinal time argument t first; more coverage in unit tests --- control/iosys.py | 9 +++-- control/statesp.py | 69 ++++++++++++++++++++++------------- control/tests/statesp_test.py | 57 ++++++++++++++++++++--------- 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 7a7f8074b..2ce56e639 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -736,10 +736,11 @@ def _update_params(self, params={}, warning=True): warn("Parameters passed to LinearIOSystems are ignored.") def dynamics(self, t, x, u): - # Convert input to column vector and then change output to 1D array - xdot = np.dot(self.A, np.reshape(x, (-1, 1))) \ - + np.dot(self.B, np.reshape(u, (-1, 1))) - return np.array(xdot).reshape((-1,)) + return StateSpace.dynamics(self, t, x, u) +# # Convert input to column vector and then change output to 1D array +# xdot = np.dot(self.A, np.reshape(x, (-1, 1))) \ +# + np.dot(self.B, np.reshape(u, (-1, 1))) +# return np.array(xdot).reshape((-1,)) def output(self, t, x, u): # Convert input to column vector and then change output to 1D array diff --git a/control/statesp.py b/control/statesp.py index 79f6a6291..6103053fe 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1219,7 +1219,7 @@ def _isstatic(self): """ return not np.any(self.A) and not np.any(self.B) - def dynamics(self, x, u=None): + def dynamics(self, t=None, x=None, u=None): """Compute the dynamics of the system Given input `u` and state `x`, returns the dynamics of the state-space @@ -1230,31 +1230,40 @@ def dynamics(self, x, u=None): where A and B are the state-space matrices of the system. If the system is discrete-time, returns the next value of `x`: - x[k+1] = A x[k] + B u[k] + x[t+dt] = A x[t] + B u[t] - The inputs `x` and `u` must be of the correct length. + The inputs `x` and `u` must be of the correct length. The argument + `t` is included for consistency with :class:`IOSystem` systems, but + is ignored in :class:`StateSpace` systems, which are time-invariant. Parameters ---------- - x : array_like - current state - u : array_like - input (optional) + t : float (ignored) + time + x : array_like (optional) + current state, zero if omitted + u : array_like (optional) + input, zero if omitted Returns ------- - dx/dt or x[k+1] : ndarray + dx/dt or x[t+dt] : ndarray of shape (self.nstates,) """ - if len(np.atleast_1d(x)) != self.nstates: - raise ValueError("len(x) must be equal to number of states") + out = np.zeros((self.nstates, 1)) + if x is not None: + if np.size(x) != self.nstates: + raise ValueError("len(x) must be equal to number of states") + x = np.reshape(x, (-1, 1)) # x must be column vector + out = out + self.A.dot(x) if u is not None: - if len(np.atleast_1d(u)) != self.ninputs: + if np.size(u) != self.ninputs: raise ValueError("len(u) must be equal to number of inputs") + u = np.reshape(u, (-1, 1)) # u as column vector + out = out + self.B.dot(u) + return out.reshape((-1,)) # return as row vector - return self.A.dot(x) if u is None else self.A.dot(x) + self.B.dot(u) - - def output(self, x, u=None): + def output(self, t=None, x=None, u=None): """Compute the output of the system Given input `u` and state `x`, returns the output `y` of the @@ -1263,27 +1272,35 @@ def output(self, x, u=None): y = C x + D u where A and B are the state-space matrices of the system. The inputs - `x` and `u` must be of the correct length. + `x` and `u` must be of the correct length. The argument `t` is + included for consistency with :class:`IOSystem` systems, but is + ignored in :class:`StateSpace` systems, which are time-invariant. Parameters ---------- - x : array_like - current state - u : array_like - input (optional) + t : float (ignored) + time + x : array_like (optional) + current state (zero if omitted) + u : array_like (optional) + input (zero if omitted) Returns ------- - y : ndarray + y : ndarray of shape (self.noutputs,) """ - if len(np.atleast_1d(x)) != self.nstates: - raise ValueError("len(x) must be equal to number of states") + y = np.zeros((self.noutputs,1)) + if x is not None: + if np.size(x) != self.nstates: + raise ValueError("len(x) must be equal to number of states") + x = np.reshape(x, (-1, 1)) # force x to be column vector + y = y + self.C.dot(x) if u is not None: - if len(np.atleast_1d(u)) != self.ninputs: + if np.size(u) != self.ninputs: raise ValueError("len(u) must be equal to number of inputs") - - return self.C.dot(x) if u is None else self.C.dot(x) + self.D.dot(u) - + u = np.reshape(u, (-1, 1)) # force u to be column vector + y = y + self.D.dot(u) + return y.reshape((-1,)) # return as row vector diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 2579c1aff..52ad5a50f 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -757,44 +757,65 @@ def test_horner(self, sys322): np.squeeze(sys322.horner(1.j)), mag[:, :, 0] * np.exp(1.j * phase[:, :, 0])) - @pytest.mark.parametrize('x', [[1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) + @pytest.mark.parametrize('x', + [None, [1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) @pytest.mark.parametrize('u', [None, 0, 1, np.atleast_1d(2)]) def test_dynamics_output_siso(self, x, u, sys121): assert_array_almost_equal( - sys121.dynamics(x, u), - sys121.A.dot(x) + (0 if u is None else sys121.B.dot(u))) + sys121.dynamics(0, x, u), + np.zeros(2) + \ + (0 if x is None else sys121.A.dot(x).reshape((-1,))) + \ + (0 if u is None else sys121.B.dot(u).reshape((-1,)))) assert_array_almost_equal( - sys121.output(x, u), - sys121.C.dot(x) + (0 if u is None else sys121.D.dot(u))) + sys121.output(0, x, u), + np.zeros(1) + \ + (0 if x is None else sys121.C.dot(x).reshape((-1,))) + \ + (0 if u is None else sys121.D.dot(u).reshape((-1,)))) # too few and too many states and inputs @pytest.mark.parametrize('x', [0, 1, [], [1, 2, 3], np.atleast_1d(2)]) - @pytest.mark.parametrize('u', [None, [1, 1], np.atleast_1d((2, 2))]) - def test_dynamics_output_siso_fails(self, x, u, sys121): + def test_error_x_dynamics_output_siso(self, x, sys121): + with pytest.raises(ValueError): + sys121.dynamics(0, x, None) + with pytest.raises(ValueError): + sys121.output(0, x, None) + @pytest.mark.parametrize('u', [[1, 1], np.atleast_1d((2, 2))]) + def test_error_u_dynamics_output_siso(self, u, sys121): with pytest.raises(ValueError): - sys121.dynamics(x, u) + sys121.dynamics(0, None, u) with pytest.raises(ValueError): - sys121.output(x, u) + sys121.output(0, None, u) - @pytest.mark.parametrize('x',[[1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) + @pytest.mark.parametrize('x', + [None, [1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) @pytest.mark.parametrize('u', [None, [1, 1], [[1], [1]], np.atleast_2d([1,1]).T]) def test_dynamics_output_mimo(self, x, u, sys222): assert_array_almost_equal( - sys222.dynamics(x, u), - sys222.A.dot(x) + (0 if u is None else sys222.B.dot(u))) + sys222.dynamics(0, x, u), + np.zeros(2) + \ + (0 if x is None else sys222.A.dot(x).reshape((-1,))) + \ + (0 if u is None else sys222.B.dot(u).reshape((-1,)))) assert_array_almost_equal( - sys222.output(x, u), - sys222.C.dot(x) + (0 if u is None else sys222.D.dot(u))) + sys222.output(0, x, u), + np.zeros(2) + \ + (0 if x is None else sys222.C.dot(x).reshape((-1,))) + \ + (0 if u is None else sys222.D.dot(u).reshape((-1,)))) + #sys222.C.dot(x) + (0 if u is None else sys222.D.dot(u))) # too few and too many states and inputs @pytest.mark.parametrize('x', [0, 1, [1, 1, 1]]) - @pytest.mark.parametrize('u', [None, 0, 1, [1, 1, 1]]) - def test_dynamics_mimo_fails(self, x, u, sys222): + def test_error_x_dynamics_mimo(self, x, sys222): + with pytest.raises(ValueError): + sys222.dynamics(0, x) + with pytest.raises(ValueError): + sys222.output(0, x) + @pytest.mark.parametrize('u', [0, 1, [1, 1, 1]]) + def test_error_u_dynamics_mimo(self, u, sys222): with pytest.raises(ValueError): - sys222.dynamics(x, u) + sys222.dynamics(0, None, u) with pytest.raises(ValueError): - sys222.output(x, u) + sys222.output(0, None, u) class TestRss: """These are tests for the proper functionality of statesp.rss.""" From a2eafc0c3066c4aa7a5eadf1e8f4b3c647c05cc8 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 17 Feb 2021 10:15:52 -0800 Subject: [PATCH 5/5] LinearIOSys methods dynamics and output set to be from StateSpace.dynamics and output directly --- control/iosys.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 2ce56e639..ac150a1bd 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -735,18 +735,8 @@ def _update_params(self, params={}, warning=True): if params and warning: warn("Parameters passed to LinearIOSystems are ignored.") - def dynamics(self, t, x, u): - return StateSpace.dynamics(self, t, x, u) -# # Convert input to column vector and then change output to 1D array -# xdot = np.dot(self.A, np.reshape(x, (-1, 1))) \ -# + np.dot(self.B, np.reshape(u, (-1, 1))) -# return np.array(xdot).reshape((-1,)) - - def output(self, t, x, u): - # Convert input to column vector and then change output to 1D array - y = np.dot(self.C, np.reshape(x, (-1, 1))) \ - + np.dot(self.D, np.reshape(u, (-1, 1))) - return np.array(y).reshape((-1,)) + dynamics = StateSpace.dynamics + output = StateSpace.output class NonlinearIOSystem(InputOutputSystem):