Skip to content

Commit 22b9953

Browse files
authored
Merge pull request #558 from sawyerbfuller/dynamics2
new dynamics and output functions for state space and I/O systems
2 parents 39b874a + 23c4b09 commit 22b9953

File tree

3 files changed

+230
-3
lines changed

3 files changed

+230
-3
lines changed

control/iosys.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,26 +358,88 @@ 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
365-
input/output system model.
365+
input/output system model. Intended for fast
366+
evaluation; for a more user-friendly interface
367+
you may want to use :meth:`dynamics`.
366368
367369
"""
368370
NotImplemented("Evaluation not implemented for system of type ",
369371
type(self))
370372

373+
def dynamics(self, t, x, u):
374+
"""Compute the dynamics of a differential or difference equation.
375+
376+
Given time `t`, input `u` and state `x`, returns the value of the
377+
right hand side of the dynamical system. If the system is continuous,
378+
returns the time derivative
379+
380+
dx/dt = f(t, x, u)
381+
382+
where `f` is the system's (possibly nonlinear) dynamics function.
383+
If the system is discrete-time, returns the next value of `x`:
384+
385+
x[t+dt] = f(t, x[t], u[t])
386+
387+
Where `t` is a scalar.
388+
389+
The inputs `x` and `u` must be of the correct length.
390+
391+
Parameters
392+
----------
393+
t : float
394+
the time at which to evaluate
395+
x : array_like
396+
current state
397+
u : array_like
398+
input
399+
400+
Returns
401+
-------
402+
dx/dt or x[t+dt] : ndarray
403+
"""
404+
return self._rhs(t, x, u)
405+
371406
def _out(self, t, x, u, params={}):
372407
"""Evaluate the output of a system at a given state, input, and time
373408
374409
Private function used to compute the output of of an input/output
375-
system model given the state, input, parameters, and time.
410+
system model given the state, input, parameters. Intended for fast
411+
evaluation; for a more user-friendly interface you may want to use
412+
:meth:`output`.
376413
377414
"""
378415
# If no output function was defined in subclass, return state
379416
return x
380417

418+
def output(self, t, x, u):
419+
"""Compute the output of the system
420+
421+
Given time `t`, input `u` and state `x`, returns the output of the
422+
system:
423+
424+
y = g(t, x, u)
425+
426+
The inputs `x` and `u` must be of the correct length.
427+
428+
Parameters
429+
----------
430+
t : float
431+
the time at which to evaluate
432+
x : array_like
433+
current state
434+
u : array_like
435+
input
436+
437+
Returns
438+
-------
439+
y : ndarray
440+
"""
441+
return self._out(t, x, u)
442+
381443
def set_inputs(self, inputs, prefix='u'):
382444
"""Set the number/names of the system inputs.
383445

control/statesp.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,12 +1227,101 @@ 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, t, x, u=0):
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 first argument `t` is ignored because :class:`StateSpace` systems
1246+
are time-invariant. It is included so that the dynamics can be passed
1247+
to most numerical integrators, such as :func:`scipy.integrate.solve_ivp`
1248+
and for consistency with :class:`IOSystem` systems.
1249+
1250+
Parameters
1251+
----------
1252+
t : float (ignored)
1253+
time
1254+
x : array_like
1255+
current state
1256+
u : array_like (optional)
1257+
input, zero if omitted
1258+
1259+
Returns
1260+
-------
1261+
dx/dt or x[t+dt] : ndarray
1262+
"""
1263+
x = np.reshape(x, (-1, 1)) # force to a column in case matrix
1264+
if np.size(x) != self.nstates:
1265+
raise ValueError("len(x) must be equal to number of states")
1266+
if u is 0:
1267+
return self.A.dot(x).reshape((-1,)) # return as row vector
1268+
else: # received t, x, and u, ignore t
1269+
u = np.reshape(u, (-1, 1)) # force to a column in case matrix
1270+
if np.size(u) != self.ninputs:
1271+
raise ValueError("len(u) must be equal to number of inputs")
1272+
return self.A.dot(x).reshape((-1,)) \
1273+
+ self.B.dot(u).reshape((-1,)) # return as row vector
1274+
1275+
def output(self, t, x, u=0):
1276+
"""Compute the output of the system
1277+
1278+
Given input `u` and state `x`, returns the output `y` of the
1279+
state-space system:
1280+
1281+
y = C x + D u
1282+
1283+
where A and B are the state-space matrices of the system.
1284+
1285+
The first argument `t` is ignored because :class:`StateSpace` systems
1286+
are time-invariant. It is included so that the dynamics can be passed
1287+
to most numerical integrators, such as scipy's `integrate.solve_ivp` and
1288+
for consistency with :class:`IOSystem` systems.
1289+
1290+
The inputs `x` and `u` must be of the correct length for the system.
1291+
1292+
Parameters
1293+
----------
1294+
t : float (ignored)
1295+
time
1296+
x : array_like
1297+
current state
1298+
u : array_like (optional)
1299+
input (zero if omitted)
1300+
1301+
Returns
1302+
-------
1303+
y : ndarray
1304+
"""
1305+
x = np.reshape(x, (-1, 1)) # force to a column in case matrix
1306+
if np.size(x) != self.nstates:
1307+
raise ValueError("len(x) must be equal to number of states")
1308+
1309+
if u is 0:
1310+
return self.C.dot(x).reshape((-1,)) # return as row vector
1311+
else: # received t, x, and u, ignore t
1312+
u = np.reshape(u, (-1, 1)) # force to a column in case matrix
1313+
if np.size(u) != self.ninputs:
1314+
raise ValueError("len(u) must be equal to number of inputs")
1315+
return self.C.dot(x).reshape((-1,)) \
1316+
+ self.D.dot(u).reshape((-1,)) # return as row vector
1317+
12301318
def _isstatic(self):
12311319
"""True if and only if the system has no dynamics, that is,
12321320
if A and B are zero. """
12331321
return not np.any(self.A) and not np.any(self.B)
12341322

12351323

1324+
12361325
# TODO: add discrete time check
12371326
def _convert_to_statespace(sys, **kw):
12381327
"""Convert a system to state space form (if needed).

control/tests/statesp_test.py

Lines changed: 76 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,70 @@ 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+
assert_array_almost_equal(
777+
sys121.dynamics(0, x),
778+
sys121.A.dot(x).reshape((-1,)))
779+
assert_array_almost_equal(
780+
sys121.output(0, x),
781+
sys121.C.dot(x).reshape((-1,)))
782+
783+
# too few and too many states and inputs
784+
@pytest.mark.parametrize('x', [0, 1, [], [1, 2, 3], np.atleast_1d(2)])
785+
def test_error_x_dynamics_and_output_siso(self, x, sys121):
786+
with pytest.raises(ValueError):
787+
sys121.dynamics(0, x)
788+
with pytest.raises(ValueError):
789+
sys121.output(0, x)
790+
@pytest.mark.parametrize('u', [[1, 1], np.atleast_1d((2, 2))])
791+
def test_error_u_dynamics_output_siso(self, u, sys121):
792+
with pytest.raises(ValueError):
793+
sys121.dynamics(0, 1, u)
794+
with pytest.raises(ValueError):
795+
sys121.output(0, 1, u)
796+
797+
@pytest.mark.parametrize('x',
798+
[[1, 1], [[1], [1]], np.atleast_2d([1,1]).T])
799+
@pytest.mark.parametrize('u',
800+
[[1, 1], [[1], [1]], np.atleast_2d([1,1]).T])
801+
def test_dynamics_and_output_mimo(self, x, u, sys222):
802+
assert_array_almost_equal(
803+
sys222.dynamics(0, x, u),
804+
sys222.A.dot(x).reshape((-1,)) + sys222.B.dot(u).reshape((-1,)))
805+
assert_array_almost_equal(
806+
sys222.output(0, x, u),
807+
sys222.C.dot(x).reshape((-1,)) + sys222.D.dot(u).reshape((-1,)))
808+
assert_array_almost_equal(
809+
sys222.dynamics(0, x),
810+
sys222.A.dot(x).reshape((-1,)))
811+
assert_array_almost_equal(
812+
sys222.output(0, x),
813+
sys222.C.dot(x).reshape((-1,)))
814+
815+
# too few and too many states and inputs
816+
@pytest.mark.parametrize('x', [0, 1, [1, 1, 1]])
817+
def test_error_x_dynamics_mimo(self, x, sys222):
818+
with pytest.raises(ValueError):
819+
sys222.dynamics(0, x)
820+
with pytest.raises(ValueError):
821+
sys222.output(0, x)
822+
@pytest.mark.parametrize('u', [1, [1, 1, 1]])
823+
def test_error_u_dynamics_mimo(self, u, sys222):
824+
with pytest.raises(ValueError):
825+
sys222.dynamics(0, (1, 1), u)
826+
with pytest.raises(ValueError):
827+
sys222.output(0, (1, 1), u)
828+
829+
754830
class TestRss:
755831
"""These are tests for the proper functionality of statesp.rss."""
756832

0 commit comments

Comments
 (0)