Skip to content

Commit bb15b85

Browse files
committed
updated markov calculation + new unit tests
1 parent 7ed68ec commit bb15b85

File tree

3 files changed

+179
-44
lines changed

3 files changed

+179
-44
lines changed

control/modelsimp.py

Lines changed: 110 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
# External packages and modules
4747
import numpy as np
4848
import warnings
49-
from .exception import ControlSlycot, ControlMIMONotImplemented
49+
from .exception import ControlSlycot, ControlMIMONotImplemented, \
50+
ControlDimension
5051
from .lti import isdtime, isctime
5152
from .statesp import StateSpace
5253
from .statefbk import gram
@@ -394,10 +395,22 @@ def era(YY, m, n, nin, nout, r):
394395
raise NotImplementedError('This function is not implemented yet.')
395396

396397

397-
def markov(Y, U, m, transpose=None):
398-
"""Calculate the first `M` Markov parameters [D CB CAB ...]
398+
def markov(Y, U, m=None, transpose=None):
399+
"""Calculate the first `m` Markov parameters [D CB CAB ...]
399400
from input `U`, output `Y`.
400401
402+
This function computes the Markov parameters for a discrete time system
403+
404+
.. math::
405+
406+
x[k+1] &= A x[k] + B u[k] \\\\
407+
y[k] &= C x[k] + D u[k]
408+
409+
given data for u and y. The algorithm assumes that that C A^k B = 0 for
410+
k > m-2 (see [1]). Note that the problem is ill-posed if the length of
411+
the input data is less than the desired number of Markov parameters (a
412+
warning message is generated in this case).
413+
401414
Parameters
402415
----------
403416
Y : array_like
@@ -407,8 +420,8 @@ def markov(Y, U, m, transpose=None):
407420
time points.
408421
U : array_like
409422
Input data, arranged in the same way as `Y`.
410-
m : int
411-
Number of Markov parameters to output.
423+
m : int, optional
424+
Number of Markov parameters to output. Defaults to len(U).
412425
transpose : bool, optional
413426
Assume that input data is transposed relative to the standard
414427
:ref:`time-series-convention`. The default value is true for
@@ -417,7 +430,14 @@ def markov(Y, U, m, transpose=None):
417430
Returns
418431
-------
419432
H : ndarray
420-
First m Markov parameters
433+
First m Markov parameters, [D CB CAB ...]
434+
435+
References
436+
----------
437+
.. [1] J.-N. Juang, M. Phan, L. G. Horta, and R. W. Longman,
438+
Identification of observer/Kalman filter Markov parameters - Theory
439+
and experiments. Journal of Guidance Control and Dynamics, 16(2),
440+
320–329, 2012. http://doi.org/10.2514/3.21006
421441
422442
Notes
423443
-----
@@ -432,8 +452,8 @@ def markov(Y, U, m, transpose=None):
432452
--------
433453
>>> T = numpy.linspace(0, 10, 100)
434454
>>> U = numpy.ones((1, 100))
435-
>>> Y = forced_response(tf([1], [1, 1]), T, U)
436-
>>> H = markov(Y, U, m, transpose=False)
455+
>>> T, Y, _ = forced_response(tf([1], [1, 0.5], True), T, U)
456+
>>> H = markov(Y, U, 3, transpose=False)
437457
438458
"""
439459
# Check on the specified format of the input
@@ -449,29 +469,93 @@ def markov(Y, U, m, transpose=None):
449469
Umat = np.array(U, ndmin=2)
450470
Ymat = np.array(Y, ndmin=2)
451471

452-
# Transpose the data from our normal convention to match algorithm
453-
# TODO: rewrite the algorithm to use standard conventions
454-
if not transpose:
455-
Umat = np.transpose(Umat)
456-
Ymat = np.transpose(Ymat)
472+
# If data is in transposed format, switch it around
473+
if transpose:
474+
Umat, Ymat = np.transpose(Umat), np.transpose(Ymat)
457475

458476
# Make sure the system is a SISO system
459-
if Umat.shape[1] != 1 or Ymat.shape[1] != 1:
477+
if Umat.shape[0] != 1 or Ymat.shape[0] != 1:
460478
raise ControlMIMONotImplemented
461-
n = Umat.shape[0]
462479

463-
# Construct a matrix of control inputs to invert
480+
# Make sure the number of time points match
481+
if Umat.shape[1] != Ymat.shape[1]:
482+
raise ControlDimension(
483+
"Input and output data are of differnent lengths")
484+
n = Umat.shape[1]
485+
486+
# If number of desired parameters was not given, set to size of input data
487+
if m is None:
488+
m = Umat.shape[1]
489+
490+
# Make sure there is enough data to compute parameters
491+
if m > n:
492+
warn.warning("Not enough data for requested number of parameters")
493+
494+
#
495+
# Original algorithm (with mapping to standard order)
496+
#
497+
# RMM note, 24 Dec 2020: This algorithm sets the problem up correctly
498+
# until the final column of the UU matrix is created, at which point it
499+
# makes some modifications that I don't understand. This version of the
500+
# algorithm does not seem to return the actual Markov parameters for a
501+
# system.
502+
#
503+
# # Create the matrix of (shifted) inputs
504+
# UU = np.transpose(Umat)
505+
# for i in range(1, m-1):
506+
# # Shift previous column down and add a zero at the top
507+
# newCol = np.vstack((0, np.reshape(UU[0:n-1, i-1], (-1, 1))))
508+
# UU = np.hstack((UU, newCol))
509+
#
510+
# # Shift previous column down and add a zero at the top
511+
# Ulast = np.vstack((0, np.reshape(UU[0:n-1, m-2], (-1, 1))))
512+
#
513+
# # Replace the elements of the last column new values (?)
514+
# # Each row gets the sum of the rows above it (?)
515+
# for i in range(n-1, 0, -1):
516+
# Ulast[i] = np.sum(Ulast[0:i-1])
517+
# UU = np.hstack((UU, Ulast))
518+
#
519+
# # Solve for the Markov parameters from Y = H @ UU
520+
# # H = [[D], [CB], [CAB], ..., [C A^{m-3} B], [???]]
521+
# H = np.linalg.lstsq(UU, np.transpose(Ymat))[0]
522+
#
523+
# # Markov parameters are in rows => transpose if needed
524+
# return H if transpose else np.transpose(H)
525+
526+
#
527+
# New algorithm - Construct a matrix of control inputs to invert
528+
#
529+
# This algorithm sets up the following problem and solves it for
530+
# the Markov parameters
531+
#
532+
# [ y(0) ] [ u(0) 0 0 ] [ D ]
533+
# [ y(1) ] [ u(1) u(0) 0 ] [ C B ]
534+
# [ y(2) ] = [ u(2) u(1) u(0) ] [ C A B ]
535+
# [ : ] [ : : : : ] [ : ]
536+
# [ y(n-1) ] [ u(n-1) u(n-2) u(n-3) ... u(n-m) ] [ C A^{m-2} B ]
537+
#
538+
# Note: if the number of Markov parameters (m) is less than the size of
539+
# the input/output data (n), then this algorithm assumes C A^{j} B = 0
540+
# for j > m-2. See equation (3) in
541+
#
542+
# J.-N. Juang, M. Phan, L. G. Horta, and R. W. Longman, Identification
543+
# of observer/Kalman filter Markov parameters - Theory and
544+
# experiments. Journal of Guidance Control and Dynamics, 16(2),
545+
# 320–329, 2012. http://doi.org/10.2514/3.21006
546+
#
547+
548+
# Create matrix of (shifted) inputs
464549
UU = Umat
465-
for i in range(1, m-1):
466-
# TODO: second index on UU doesn't seem right; could be neg or pos??
467-
newCol = np.vstack((0, np.reshape(UU[0:n-1, i-2], (-1, 1))))
468-
UU = np.hstack((UU, newCol))
469-
Ulast = np.vstack((0, np.reshape(UU[0:n-1, m-2], (-1, 1))))
470-
for i in range(n-1, 0, -1):
471-
Ulast[i] = np.sum(Ulast[0:i-1])
472-
UU = np.hstack((UU, Ulast))
550+
for i in range(1, m):
551+
# Shift previous column down and add a zero at the top
552+
new_row = np.hstack((0, UU[i-1, 0:-1]))
553+
UU = np.vstack((UU, new_row))
554+
UU = np.transpose(UU)
473555

474556
# Invert and solve for Markov parameters
475-
H = np.linalg.lstsq(UU, Ymat)[0]
557+
YY = np.transpose(Ymat)
558+
H, _, _, _ = np.linalg.lstsq(UU, YY, rcond=None)
476559

477-
return H
560+
# Return the first m Markov parameters
561+
return H if transpose else np.transpose(H)

control/tests/modelsimp_array_test.py

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,41 @@ def testHSVD(self):
4949
# Go back to using the normal np.array representation
5050
control.use_numpy_matrix(False)
5151

52-
def testMarkov(self):
52+
def testMarkovSignature(self):
5353
U = np.array([[1., 1., 1., 1., 1.]])
5454
Y = U
55-
M = 3
56-
H = markov(Y, U, M, transpose=False)
57-
Htrue = np.array([[1.], [0.], [0.]])
55+
m = 3
56+
H = markov(Y, U, m, transpose=False)
57+
Htrue = np.array([[1., 0., 0.]])
5858
np.testing.assert_array_almost_equal( H, Htrue )
5959

6060
# Make sure that transposed data also works
61-
H = markov(np.transpose(Y), np.transpose(U), M, transpose=True)
61+
H = markov(np.transpose(Y), np.transpose(U), m, transpose=True)
62+
np.testing.assert_array_almost_equal( H, np.transpose(Htrue) )
63+
64+
# Default (in v0.8.4 and below) should be transpose=True (w/ warning)
65+
import warnings
66+
warnings.simplefilter('always', UserWarning) # don't supress
67+
with warnings.catch_warnings(record=True) as w:
68+
# Set up warnings filter to only show warnings in control module
69+
warnings.filterwarnings("ignore")
70+
warnings.filterwarnings("always", module="control")
71+
72+
# Generate Markov parameters without any arguments
73+
H = markov(np.transpose(Y), np.transpose(U), m)
74+
np.testing.assert_array_almost_equal( H, np.transpose(Htrue) )
75+
76+
# Make sure we got a warning
77+
self.assertEqual(len(w), 1)
78+
self.assertIn("assumed to be in rows", str(w[-1].message))
79+
self.assertIn("change in a future release", str(w[-1].message))
6280

6381
# Test example from docstring
6482
T = np.linspace(0, 10, 100)
6583
U = np.ones((1, 100))
66-
_, Y, _ = control.forced_response(control.tf([1], [1, 1]), T, U)
67-
H = markov(Y, U, M, transpose=False)
84+
T, Y, _ = control.forced_response(
85+
control.tf([1], [1, 0.5], True), T, U)
86+
H = markov(Y, U, 3, transpose=False)
6887

6988
# Test example from issue #395
7089
inp = np.array([1, 2])
@@ -73,7 +92,47 @@ def testMarkov(self):
7392

7493
# Make sure MIMO generates an error
7594
U = np.ones((2, 100)) # 2 inputs (Y unchanged, with 1 output)
76-
np.testing.assert_raises(ControlMIMONotImplemented, markov, U, Y, M)
95+
np.testing.assert_raises(ControlMIMONotImplemented, markov, Y, U, m)
96+
97+
# Make sure markov() returns the right answer
98+
def testMarkovResults(self):
99+
#
100+
# Test over a range of parameters
101+
#
102+
# k = order of the system
103+
# m = number of Markov parameters
104+
# n = size of the data vector
105+
#
106+
# Values should match exactly for n = m, otherewise you get a
107+
# close match but errors due to the assumption that C A^k B =
108+
# 0 for k > m-2 (see modelsimp.py).
109+
#
110+
for k, m, n in \
111+
((2, 2, 2), (2, 5, 5), (5, 2, 2), (5, 5, 5), (5, 10, 10)):
112+
113+
# Generate stable continuous time system
114+
Hc = control.rss(k, 1, 1)
115+
116+
# Choose sampling time based on fastest time constant / 10
117+
w, _ = np.linalg.eig(Hc.A)
118+
Ts = np.min(-np.real(w)) / 10.
119+
120+
# Convert to a discrete time system via sampling
121+
Hd = control.c2d(Hc, Ts, 'zoh')
122+
123+
# Compute the Markov parameters from state space
124+
Mtrue = np.hstack([Hd.D] + [np.dot(
125+
Hd.C, np.dot(np.linalg.matrix_power(Hd.A, i),
126+
Hd.B)) for i in range(m-1)])
127+
128+
# Generate input/output data
129+
T = np.array(range(n)) * Ts
130+
U = np.cos(T) + np.sin(T/np.pi)
131+
_, Y, _ = control.forced_response(Hd, T, U, squeeze=True)
132+
Mcomp = markov(Y, U, m, transpose=False)
133+
134+
# Compare to results from markov()
135+
np.testing.assert_array_almost_equal(Mtrue, Mcomp)
77136

78137
def testModredMatchDC(self):
79138
#balanced realization computed in matlab for the transfer function:

control/tests/modelsimp_test.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,13 @@ def testHSVD(self):
2222
np.testing.assert_array_almost_equal(hsv, hsvtrue)
2323

2424
def testMarkov(self):
25-
U = np.matrix("1., 1., 1., 1., 1.")
25+
U = np.matrix("1.; 1.; 1.; 1.; 1.")
2626
Y = U
2727
M = 3
28-
H = markov(Y, U, M, transpose=False)
28+
H = markov(Y, U, M)
2929
Htrue = np.matrix("1.; 0.; 0.")
3030
np.testing.assert_array_almost_equal( H, Htrue )
3131

32-
# Make sure that transposed data also works
33-
H = markov(np.transpose(Y), np.transpose(U), M, transpose=True)
34-
np.testing.assert_array_almost_equal( H, Htrue )
35-
36-
# Default (in v0.8.4 and below) should be transpose=True
37-
H = markov(np.transpose(Y), np.transpose(U), M)
38-
np.testing.assert_array_almost_equal( H, Htrue )
39-
4032
def testModredMatchDC(self):
4133
#balanced realization computed in matlab for the transfer function:
4234
# num = [1 11 45 32], den = [1 15 60 200 60]

0 commit comments

Comments
 (0)