Skip to content

Commit e99273a

Browse files
committed
added dynamics and output to statespace and iosystems
1 parent 92de12d commit e99273a

3 files changed

Lines changed: 215 additions & 13 deletions

File tree

control/iosys.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ def _update_params(self, params, warning=False):
358358
if (warning):
359359
warn("Parameters passed to InputOutputSystem ignored.")
360360

361-
def _rhs(self, t, x, u):
361+
def _rhs(self, t, x, u, params={}):
362362
"""Evaluate right hand side of a differential or difference equation.
363363
364364
Private function used to compute the right hand side of an
@@ -368,6 +368,39 @@ def _rhs(self, t, x, u):
368368
NotImplemented("Evaluation not implemented for system of type ",
369369
type(self))
370370

371+
def dynamics(self, t, x, u, params={}):
372+
"""Compute the dynamics of a differential or difference equation.
373+
374+
Given time `t`, input `u` and state `x`, returns the value of the
375+
right hand side of the dynamical system. If the system is continuous,
376+
returns the time derivative
377+
378+
dx/dt = f(t, x, u)
379+
380+
where `f` is the system's (possibly nonlinear) dynamics function.
381+
If the system is discrete-time, returns the next value of `x`:
382+
383+
x[t+dt] = f(t, x[t], u[t])
384+
385+
Where `t` is a scalar.
386+
387+
The inputs `x` and `u` must be of the correct length.
388+
389+
Parameters
390+
----------
391+
t : float
392+
the time at which to evaluate
393+
x : array_like
394+
current state
395+
u : array_like
396+
input
397+
398+
Returns
399+
-------
400+
dx/dt or x[t+dt] : ndarray
401+
"""
402+
return self._rhs(t, x, u, params)
403+
371404
def _out(self, t, x, u, params={}):
372405
"""Evaluate the output of a system at a given state, input, and time
373406
@@ -378,6 +411,31 @@ def _out(self, t, x, u, params={}):
378411
# If no output function was defined in subclass, return state
379412
return x
380413

414+
def output(self, t, x, u, params={}):
415+
"""Compute the output of the system
416+
417+
Given time `t`, input `u` and state `x`, returns the output of the
418+
system:
419+
420+
y = g(t, x, u)
421+
422+
The inputs `x` and `u` must be of the correct length.
423+
424+
Parameters
425+
----------
426+
t : float
427+
the time at which to evaluate
428+
x : array_like
429+
current state
430+
u : array_like
431+
input
432+
433+
Returns
434+
-------
435+
y : ndarray
436+
"""
437+
return self._out(t, x, u, params)
438+
381439
def set_inputs(self, inputs, prefix='u'):
382440
"""Set the number/names of the system inputs.
383441
@@ -694,18 +752,8 @@ def _update_params(self, params={}, warning=True):
694752
if params and warning:
695753
warn("Parameters passed to LinearIOSystems are ignored.")
696754

697-
def _rhs(self, t, x, u):
698-
# Convert input to column vector and then change output to 1D array
699-
xdot = np.dot(self.A, np.reshape(x, (-1, 1))) \
700-
+ np.dot(self.B, np.reshape(u, (-1, 1)))
701-
return np.array(xdot).reshape((-1,))
702-
703-
def _out(self, t, x, u):
704-
# Convert input to column vector and then change output to 1D array
705-
y = np.dot(self.C, np.reshape(x, (-1, 1))) \
706-
+ np.dot(self.D, np.reshape(u, (-1, 1)))
707-
return np.array(y).reshape((-1,))
708-
755+
_rhs = StateSpace.dynamics
756+
_out = StateSpace.output
709757

710758
class NonlinearIOSystem(InputOutputSystem):
711759
"""Nonlinear I/O system.

control/statesp.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,12 +1227,102 @@ def dcgain(self, warn_infinite=False):
12271227
return self(0, warn_infinite=warn_infinite) if self.isctime() \
12281228
else self(1, warn_infinite=warn_infinite)
12291229

1230+
def dynamics(self, *args):
1231+
"""Compute the dynamics of the system
1232+
1233+
Given input `u` and state `x`, returns the dynamics of the state-space
1234+
system. If the system is continuous, returns the time derivative dx/dt
1235+
1236+
dx/dt = A x + B u
1237+
1238+
where A and B are the state-space matrices of the system. If the
1239+
system is discrete-time, returns the next value of `x`:
1240+
1241+
x[t+dt] = A x[t] + B u[t]
1242+
1243+
The inputs `x` and `u` must be of the correct length for the system.
1244+
1245+
The calling signature is ``out = sys.dynamics(t, x[, u])``
1246+
The first argument `t` is ignored because :class:`StateSpace` systems
1247+
are time-invariant. It is included so that the dynamics can be passed
1248+
to most numerical integrators, such as scipy's `integrate.solve_ivp` and
1249+
for consistency with :class:`IOSystem` systems.
1250+
1251+
Parameters
1252+
----------
1253+
t : float (ignored)
1254+
time
1255+
x : array_like
1256+
current state
1257+
u : array_like (optional)
1258+
input, zero if omitted
1259+
1260+
Returns
1261+
-------
1262+
dx/dt or x[t+dt] : ndarray
1263+
"""
1264+
if len(args) not in (2, 3):
1265+
raise ValueError("received"+len(args)+"args, expected 2 or 3")
1266+
if np.size(args[1]) != self.nstates:
1267+
raise ValueError("len(x) must be equal to number of states")
1268+
if len(args) == 2: # received t and x, ignore t
1269+
return self.A.dot(args[1]).reshape((-1,)) # return as row vector
1270+
else: # received t, x, and u, ignore t
1271+
if np.size(args[2]) != self.ninputs:
1272+
raise ValueError("len(u) must be equal to number of inputs")
1273+
return self.A.dot(args[1]).reshape((-1,)) \
1274+
+ self.B.dot(args[2]).reshape((-1,)) # return as row vector
1275+
1276+
def output(self, *args):
1277+
"""Compute the output of the system
1278+
1279+
Given input `u` and state `x`, returns the output `y` of the
1280+
state-space system:
1281+
1282+
y = C x + D u
1283+
1284+
where A and B are the state-space matrices of the system.
1285+
1286+
The calling signature is ``y = sys.output(t, x[, u])``
1287+
The first argument `t` is ignored because :class:`StateSpace` systems
1288+
are time-invariant. It is included so that the dynamics can be passed
1289+
to most numerical integrators, such as scipy's `integrate.solve_ivp` and
1290+
for consistency with :class:`IOSystem` systems.
1291+
1292+
The inputs `x` and `u` must be of the correct length for the system.
1293+
1294+
Parameters
1295+
----------
1296+
t : float (ignored)
1297+
time
1298+
x : array_like
1299+
current state
1300+
u : array_like (optional)
1301+
input (zero if omitted)
1302+
1303+
Returns
1304+
-------
1305+
y : ndarray
1306+
"""
1307+
if len(args) not in (2, 3):
1308+
raise ValueError("received"+len(args)+"args, expected 2 or 3")
1309+
if np.size(args[1]) != self.nstates:
1310+
raise ValueError("len(x) must be equal to number of states")
1311+
if len(args) == 2: # received t and x, ignore t
1312+
return self.C.dot(args[1]).reshape((-1,)) # return as row vector
1313+
else: # received t, x, and u, ignore t
1314+
if np.size(args[2]) != self.ninputs:
1315+
raise ValueError("len(u) must be equal to number of inputs")
1316+
return self.C.dot(args[1]).reshape((-1,)) \
1317+
+ self.D.dot(args[2]).reshape((-1,)) # return as row vector
1318+
12301319
def _isstatic(self):
12311320
"""True if and only if the system has no dynamics, that is,
12321321
if A and B are zero. """
12331322
return not np.any(self.A) and not np.any(self.B)
12341323

12351324

1325+
12361326
# TODO: add discrete time check
12371327
def _convert_to_statespace(sys, **kw):
12381328
"""Convert a system to state space form (if needed).

control/tests/statesp_test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import numpy as np
11+
from numpy.testing import assert_array_almost_equal
1112
import pytest
1213
import operator
1314
from numpy.linalg import solve
@@ -47,6 +48,17 @@ def sys322(self, sys322ABCD):
4748
"""3-states square system (2 inputs x 2 outputs)"""
4849
return StateSpace(*sys322ABCD)
4950

51+
@pytest.fixture
52+
def sys121(self):
53+
"""2 state, 1 input, 1 output (siso) system"""
54+
A121 = [[4., 1.],
55+
[2., -3]]
56+
B121 = [[5.],
57+
[-3.]]
58+
C121 = [[2., -4]]
59+
D121 = [[3.]]
60+
return StateSpace(A121, B121, C121, D121)
61+
5062
@pytest.fixture
5163
def sys222(self):
5264
"""2-states square system (2 inputs x 2 outputs)"""
@@ -751,6 +763,58 @@ def test_horner(self, sys322):
751763
np.squeeze(sys322.horner(1.j)),
752764
mag[:, :, 0] * np.exp(1.j * phase[:, :, 0]))
753765

766+
@pytest.mark.parametrize('x',
767+
[[1, 1], [[1], [1]], np.atleast_2d([1,1]).T])
768+
@pytest.mark.parametrize('u', [0, 1, np.atleast_1d(2)])
769+
def test_dynamics_and_output_siso(self, x, u, sys121):
770+
assert_array_almost_equal(
771+
sys121.dynamics(0, x, u),
772+
sys121.A.dot(x).reshape((-1,)) + sys121.B.dot(u).reshape((-1,)))
773+
assert_array_almost_equal(
774+
sys121.output(0, x, u),
775+
sys121.C.dot(x).reshape((-1,)) + sys121.D.dot(u).reshape((-1,)))
776+
777+
# too few and too many states and inputs
778+
@pytest.mark.parametrize('x', [0, 1, [], [1, 2, 3], np.atleast_1d(2)])
779+
def test_error_x_dynamics_and_output_siso(self, x, sys121):
780+
with pytest.raises(ValueError):
781+
sys121.dynamics(0, x)
782+
with pytest.raises(ValueError):
783+
sys121.output(0, x)
784+
@pytest.mark.parametrize('u', [[1, 1], np.atleast_1d((2, 2))])
785+
def test_error_u_dynamics_output_siso(self, u, sys121):
786+
with pytest.raises(ValueError):
787+
sys121.dynamics(0, 1, u)
788+
with pytest.raises(ValueError):
789+
sys121.output(0, 1, u)
790+
791+
@pytest.mark.parametrize('x',
792+
[[1, 1], [[1], [1]], np.atleast_2d([1,1]).T])
793+
@pytest.mark.parametrize('u',
794+
[[1, 1], [[1], [1]], np.atleast_2d([1,1]).T])
795+
def test_dynamics_and_output_mimo(self, x, u, sys222):
796+
assert_array_almost_equal(
797+
sys222.dynamics(0, x, u),
798+
sys222.A.dot(x).reshape((-1,)) + sys222.B.dot(u).reshape((-1,)))
799+
assert_array_almost_equal(
800+
sys222.output(0, x, u),
801+
sys222.C.dot(x).reshape((-1,)) + sys222.D.dot(u).reshape((-1,)))
802+
803+
# too few and too many states and inputs
804+
@pytest.mark.parametrize('x', [0, 1, [1, 1, 1]])
805+
def test_error_x_dynamics_mimo(self, x, sys222):
806+
with pytest.raises(ValueError):
807+
sys222.dynamics(0, x)
808+
with pytest.raises(ValueError):
809+
sys222.output(0, x)
810+
@pytest.mark.parametrize('u', [0, 1, [1, 1, 1]])
811+
def test_error_u_dynamics_mimo(self, u, sys222):
812+
with pytest.raises(ValueError):
813+
sys222.dynamics(0, (1, 1), u)
814+
with pytest.raises(ValueError):
815+
sys222.output(0, (1, 1), u)
816+
817+
754818
class TestRss:
755819
"""These are tests for the proper functionality of statesp.rss."""
756820

0 commit comments

Comments
 (0)