Skip to content

Commit d3e7387

Browse files
committed
additional stochsys documentation, unit tests
1 parent a211dad commit d3e7387

4 files changed

Lines changed: 168 additions & 45 deletions

File tree

control/sisotool.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,14 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r',
215215
derivative terms are given instead by Kp, Ki*dt/2*(z+1)/(z-1), and
216216
Kd/dt*(z-1)/z, respectively.
217217
218-
------> C_ff ------ d
219-
| | |
220-
r | e V V u y
221-
------->O---> C_f --->O--->O---> plant --->
222-
^- ^- |
223-
| | |
224-
| ----- C_b <-------|
225-
---------------------------------
218+
------> C_ff ------ d
219+
| | |
220+
r | e V V u y
221+
------->O---> C_f --->O--->O---> plant --->
222+
^- ^- |
223+
| | |
224+
| ----- C_b <-------|
225+
---------------------------------
226226
227227
It is also possible to move the derivative term into the feedback path
228228
`C_b` using `derivative_in_feedback_path=True`. This may be desired to
@@ -234,8 +234,8 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r',
234234
235235
Remark: It may be helpful to zoom in using the magnifying glass on the
236236
plot. Just ake sure to deactivate magnification mode when you are done by
237-
clicking the magnifying glass. Otherwise you will not be able to be able to choose
238-
a gain on the root locus plot.
237+
clicking the magnifying glass. Otherwise you will not be able to be able
238+
to choose a gain on the root locus plot.
239239
240240
Parameters
241241
----------
@@ -269,6 +269,7 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r',
269269
----------
270270
closedloop : class:`StateSpace` system
271271
The closed-loop system using initial gains.
272+
272273
"""
273274

274275
plant = _convert_to_statespace(plant)

control/stochsys.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232

3333
# contributed by Sawyer B. Fuller <minster@uw.edu>
34-
def lqe(*args, **keywords):
34+
def lqe(*args, **kwargs):
3535
"""lqe(A, G, C, QN, RN, [, NN])
3636
3737
Linear quadratic estimator design (Kalman filter) for continuous-time
@@ -126,18 +126,20 @@ def lqe(*args, **keywords):
126126
# Process the arguments and figure out what inputs we received
127127
#
128128

129+
# If we were passed a discrete time system as the first arg, use dlqe()
130+
if isinstance(args[0], LTI) and isdtime(args[0], strict=True):
131+
# Call dlqe
132+
return dlqe(*args, **kwargs)
133+
129134
# Get the method to use (if specified as a keyword)
130-
method = keywords.get('method', None)
135+
method = kwargs.pop('method', None)
136+
if kwargs:
137+
raise TypeError("unrecognized kwargs: ", str(kwargs))
131138

132139
# Get the system description
133140
if (len(args) < 3):
134141
raise ControlArgument("not enough input arguments")
135142

136-
# If we were passed a discrete time system as the first arg, use dlqe()
137-
if isinstance(args[0], LTI) and isdtime(args[0], strict=True):
138-
# Call dlqe
139-
return dlqe(*args, **keywords)
140-
141143
# If we were passed a state space system, use that to get system matrices
142144
if isinstance(args[0], StateSpace):
143145
A = np.array(args[0].A, ndmin=2, dtype=float)
@@ -179,7 +181,7 @@ def lqe(*args, **keywords):
179181

180182

181183
# contributed by Sawyer B. Fuller <minster@uw.edu>
182-
def dlqe(*args, **keywords):
184+
def dlqe(*args, **kwargs):
183185
"""dlqe(A, G, C, QN, RN, [, N])
184186
185187
Linear quadratic estimator design (Kalman filter) for discrete-time
@@ -251,7 +253,9 @@ def dlqe(*args, **keywords):
251253
#
252254

253255
# Get the method to use (if specified as a keyword)
254-
method = keywords.get('method', None)
256+
method = kwargs.pop('method', None)
257+
if kwargs:
258+
raise TypeError("unrecognized kwargs: ", str(kwargs))
255259

256260
# Get the system description
257261
if (len(args) < 3):
@@ -340,14 +344,14 @@ def create_estimator_iosystem(
340344
If the system has all full states output, define the measured values
341345
to be used by the estimator. Otherwise, use the system output as the
342346
measured values.
343-
{state, covariance, output}_labels : str or list of str, optional
347+
{state, covariance, sensor, output}_labels : str or list of str, optional
344348
Set the name of the signals to use for the internal state, covariance,
345-
and output (state estimate). If a single string is specified, it
346-
should be a format string using the variable ``i`` as an index (or
347-
``i`` and ``j`` for covariance). Otherwise, a list of strings
348-
matching the size of the respective signal should be used. Default is
349-
``'xhat[{i}]'`` for state and output labels and ``'P[{i},{j}]'`` for
350-
covariance labels.
349+
sensors, and outputs (state estimate). If a single string is
350+
specified, it should be a format string using the variable ``i`` as an
351+
index (or ``i`` and ``j`` for covariance). Otherwise, a list of
352+
strings matching the size of the respective signal should be used.
353+
Default is ``'xhat[{i}]'`` for state and output labels, ``'y[{i}]'``
354+
for output labels and ``'P[{i},{j}]'`` for covariance labels.
351355
352356
Returns
353357
-------
@@ -478,6 +482,10 @@ def white_noise(T, Q, dt=0):
478482
sample time).
479483
480484
"""
485+
# Convert input arguments to arrays
486+
T = np.atleast_1d(T)
487+
Q = np.atleast_2d(Q)
488+
481489
# Check the shape of the input arguments
482490
if len(T.shape) != 1:
483491
raise ValueError("Time vector T must be 1D")
@@ -502,7 +510,37 @@ def white_noise(T, Q, dt=0):
502510
# Return a linear combination of the noise sources
503511
return sp.linalg.sqrtm(Q) @ W
504512

505-
def correlation(T, X, Y=None, dt=0, squeeze=True):
513+
def correlation(T, X, Y=None, squeeze=True):
514+
"""Compute the correlation of time signals.
515+
516+
For a time series X(t) (and optionally Y(t)), the correlation() function
517+
computes the correlation matrix E(X'(t+tau) X(t)) or the cross-correlation
518+
matrix E(X'(t+tau) Y(t)]:
519+
520+
tau, Rtau = correlation(T, X[, Y])
521+
522+
The signal X (and Y, if present) represent a continuous time signal
523+
sampled at times T. The return value provides the correlation Rtau
524+
between X(t+tau) and X(t) at a set of time offets tau.
525+
526+
Parameters
527+
----------
528+
T : 1D array_like
529+
Sample times for the signal(s).
530+
X : 1D or 2D array_like
531+
Values of the signal at each time in T. The signal can either be
532+
scalar or vector values.
533+
Y : 1D or 2D array_like, optional
534+
If present, the signal with which to compute the correlation.
535+
Defaults to X.
536+
squeeze : bool, optional
537+
If True, squeeze Rtau to remove extra dimensions (useful if the
538+
signals are scalars).
539+
540+
Returns
541+
-------
542+
543+
"""
506544
T = np.atleast_1d(T)
507545
X = np.atleast_2d(X)
508546
Y = np.atleast_2d(Y) if Y is not None else X
@@ -516,10 +554,7 @@ def correlation(T, X, Y=None, dt=0, squeeze=True):
516554
raise ValueError("Signals X and Y must have same length as T")
517555

518556
# Figure out the time increment
519-
if dt != 0:
520-
raise NotImplementedError("Discrete time systems not yet supported")
521-
else:
522-
dt = T[1] - T[0]
557+
dt = T[1] - T[0]
523558

524559
# Make sure data points are equally spaced
525560
if not np.allclose(np.diff(T), T[1] - T[0]):

control/tests/stochsys_test.py

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ def check_LQE(L, P, poles, G, QN, RN):
1313
P_expected = asmatarrayout(np.sqrt(G @ QN @ G @ RN))
1414
L_expected = asmatarrayout(P_expected / RN)
1515
poles_expected = -np.squeeze(np.asarray(L_expected))
16-
np.testing.assert_array_almost_equal(P, P_expected)
17-
np.testing.assert_array_almost_equal(L, L_expected)
18-
np.testing.assert_array_almost_equal(poles, poles_expected)
16+
np.testing.assert_almost_equal(P, P_expected)
17+
np.testing.assert_almost_equal(L, L_expected)
18+
np.testing.assert_almost_equal(poles, poles_expected)
1919

2020
# Utility function to check discrete LQE solutions
2121
def check_DLQE(L, P, poles, G, QN, RN):
2222
P_expected = asmatarrayout(G.dot(QN).dot(G))
2323
L_expected = asmatarrayout(0)
2424
poles_expected = -np.squeeze(np.asarray(L_expected))
25-
np.testing.assert_array_almost_equal(P, P_expected)
26-
np.testing.assert_array_almost_equal(L, L_expected)
27-
np.testing.assert_array_almost_equal(poles, poles_expected)
25+
np.testing.assert_almost_equal(P, P_expected)
26+
np.testing.assert_almost_equal(L, L_expected)
27+
np.testing.assert_almost_equal(poles, poles_expected)
2828

2929
@pytest.mark.parametrize("method", [None, 'slycot', 'scipy'])
3030
def test_LQE(matarrayin, method):
@@ -51,9 +51,9 @@ def test_lqe_call_format(cdlqe):
5151

5252
# Call with system instead of matricees
5353
L, P, E = cdlqe(sys, Q, R)
54-
np.testing.assert_array_almost_equal(Lref, L)
55-
np.testing.assert_array_almost_equal(Pref, P)
56-
np.testing.assert_array_almost_equal(Eref, E)
54+
np.testing.assert_almost_equal(Lref, L)
55+
np.testing.assert_almost_equal(Pref, P)
56+
np.testing.assert_almost_equal(Eref, E)
5757

5858
# Make sure we get an error if we specify N
5959
with pytest.raises(ct.ControlNotImplemented):
@@ -156,10 +156,10 @@ def test_estimator_iosys():
156156
# Check to make sure everything matches
157157
cls = clsys.linearize(0, 0)
158158
nstates = sys.nstates
159-
np.testing.assert_array_almost_equal(cls.A[:2*nstates, :2*nstates], A_clchk)
160-
np.testing.assert_array_almost_equal(cls.B[:2*nstates, :], B_clchk)
161-
np.testing.assert_array_almost_equal(cls.C[:, :2*nstates], C_clchk)
162-
np.testing.assert_array_almost_equal(cls.D, D_clchk)
159+
np.testing.assert_almost_equal(cls.A[:2*nstates, :2*nstates], A_clchk)
160+
np.testing.assert_almost_equal(cls.B[:2*nstates, :], B_clchk)
161+
np.testing.assert_almost_equal(cls.C[:, :2*nstates], C_clchk)
162+
np.testing.assert_almost_equal(cls.D, D_clchk)
163163

164164

165165
def test_estimator_errors():
@@ -185,3 +185,78 @@ def test_estimator_errors():
185185
sys_fs.C = np.eye(4)
186186
C = np.eye(1, 4)
187187
estim = ct.create_estimator_iosystem(sys_fs, QN, RN, C=C)
188+
189+
190+
def test_white_noise():
191+
# Scalar white noise signal
192+
T = np.linspace(0, 1000, 1000)
193+
R = 0.5
194+
V = ct.white_noise(T, R)
195+
assert abs(np.mean(V)) < 0.1 # can occassionally fail
196+
assert abs(np.cov(V) - 0.5) < 0.1 # can occassionally fail
197+
198+
# Vector white noise signal
199+
R = [[0.5, 0], [0, 0.1]]
200+
V = ct.white_noise(T, R)
201+
assert abs(np.mean(V)) < 0.1 # can occassionally fail
202+
assert np.all(abs(np.cov(V) - R) < 0.1) # can occassionally fail
203+
204+
# Make sure time scaling works properly
205+
T = T / 10
206+
V = ct.white_noise(T, R)
207+
assert abs(np.mean(V)) < np.sqrt(10) # can occassionally fail
208+
assert np.all(abs(np.cov(V) - R) < 10) # can occassionally fail
209+
210+
# Make sure discrete time works properly
211+
V = ct.white_noise(T, R, dt=T[1] - T[0])
212+
assert abs(np.mean(V)) < 0.1 # can occassionally fail
213+
assert np.all(abs(np.cov(V) - R) < 0.1) # can occassionally fail
214+
215+
# Test error conditions
216+
with pytest.raises(ValueError, match="T must be 1D"):
217+
V = ct.white_noise(R, R)
218+
219+
with pytest.raises(ValueError, match="Q must be square"):
220+
R = np.outer(np.eye(2, 3), np.ones_like(T))
221+
V = ct.white_noise(T, R)
222+
223+
with pytest.raises(ValueError, match="Time values must be equally"):
224+
T = np.logspace(0, 2, 100)
225+
R = [[0.5, 0], [0, 0.1]]
226+
V = ct.white_noise(T, R)
227+
228+
229+
def test_correlation():
230+
# Create an uncorrelated random sigmal
231+
T = np.linspace(0, 1000, 1000)
232+
R = 0.5
233+
V = ct.white_noise(T, R)
234+
235+
# Compute the correlation
236+
tau, Rtau = ct.correlation(T, V)
237+
238+
# Make sure the correlation makes sense
239+
zero_index = np.where(tau == 0)
240+
np.testing.assert_almost_equal(Rtau[zero_index], np.cov(V), decimal=2)
241+
for i, t in enumerate(tau):
242+
if i == zero_index:
243+
continue
244+
assert abs(Rtau[i]) < 0.01
245+
246+
# Try passing a second argument
247+
tau, Rneg = ct.correlation(T, V, -V)
248+
np.testing.assert_equal(Rtau, -Rneg)
249+
250+
# Test error conditions
251+
with pytest.raises(ValueError, match="Time vector T must be 1D"):
252+
tau, Rtau = ct.correlation(V, V)
253+
254+
with pytest.raises(ValueError, match="X and Y must be 2D"):
255+
tau, Rtau = ct.correlation(T, np.zeros((3, T.size, 2)))
256+
257+
with pytest.raises(ValueError, match="X and Y must have same length as T"):
258+
tau, Rtau = ct.correlation(T, V[:, 0:-1])
259+
260+
with pytest.raises(ValueError, match="Time values must be equally"):
261+
T = np.logspace(0, 2, T.size)
262+
tau, Rtau = ct.correlation(T, V)

doc/control.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,11 @@ Control system synthesis
108108
:toctree: generated/
109109

110110
acker
111+
create_statefbk_iosystem
112+
dlqr
111113
h2syn
112114
hinfsyn
113115
lqr
114-
lqe
115116
mixsyn
116117
place
117118
rootlocus_pid_designer
@@ -143,6 +144,17 @@ Nonlinear system support
143144
tf2io
144145
flatsys.point_to_point
145146

147+
Stochastic system support
148+
=========================
149+
.. autosummary::
150+
:toctree: generated/
151+
152+
correlation
153+
create_estimator_iosystem
154+
dlqe
155+
lqe
156+
white_noise
157+
146158
.. _utility-and-conversions:
147159

148160
Utility functions and conversions

0 commit comments

Comments
 (0)