Skip to content

Commit 67d6ef0

Browse files
committed
Add unit tests, change okid output for siso
1 parent 23c0a44 commit 67d6ef0

File tree

2 files changed

+142
-2
lines changed

2 files changed

+142
-2
lines changed

control/modelsimp.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,5 +764,9 @@ def okid(*args, m=None, transpose=False, dt=True, truncate=False):
764764
H[:,:,1:] = Y[:,:,:]
765765
H = H/dt # scaling
766766

767+
# for siso return a 1D array instead of a 3D array
768+
if q == 1 and p == 1:
769+
H = np.squeeze(H)
770+
767771
# Return the first m Markov parameters
768772
return H if not transpose else np.transpose(H)

control/tests/modelsimp_test.py

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import pytest
88

99

10-
from control import StateSpace, forced_response, tf, rss, c2d
10+
from control import StateSpace, impulse_response, forced_response, tf, rss, c2d
1111
from control.exception import ControlMIMONotImplemented
1212
from control.tests.conftest import slycotonly
13-
from control.modelsimp import balred, hsvd, markov, modred
13+
from control.modelsimp import balred, hsvd, markov, okid, modred
1414

1515

1616
class TestModelsimp:
@@ -111,6 +111,142 @@ def testMarkovResults(self, k, m, n):
111111
# for k=5, m=n=10: 0.015 %
112112
np.testing.assert_allclose(Mtrue, Mcomp, rtol=1e-6, atol=1e-8)
113113

114+
def testOKIDSignature(self):
115+
116+
# Example 6.1, Applied System Identification
117+
m1, k1, c1 = 1., 1., 0.01
118+
A = np.array([
119+
[0., 1.],
120+
[-k1/m1, -c1/m1],
121+
])
122+
B = np.array([[0.],[1./m1]])
123+
C = np.array([[-k1/m1, -c1/m1]])
124+
D = np.array([[1.]])
125+
sys = StateSpace(A, B, C, D)
126+
dt = 0.1
127+
sysd = sys.sample(dt, method='zoh')
128+
129+
T = np.arange(0,200,dt)
130+
U = np.random.randn(sysd.B.shape[-1], len(T))
131+
response = forced_response(sysd, U=U)
132+
Y = response.outputs
133+
134+
m = 5
135+
ir_true = impulse_response(sysd, T=T)
136+
Htrue = ir_true.outputs[:m+1]*dt
137+
H = okid(Y, U, m, dt=True)
138+
139+
np.testing.assert_allclose(Htrue, H, atol=1e-1)
140+
141+
# Test mimo example
142+
# Mechanical Vibrations: Theory and Application, SI Edition, 1st ed.
143+
# Figure 6.5 / Example 6.7
144+
m1, k1, c1 = 1., 4., 1.
145+
m2, k2, c2 = 2., 2., 1.
146+
k3, c3 = 6., 2.
147+
148+
A = np.array([
149+
[0., 0., 1., 0.],
150+
[0., 0., 0., 1.],
151+
[-(k1+k2)/m1, (k2)/m1, -(c1+c2)/m1, c2/m1],
152+
[(k2)/m2, -(k2+k3)/m2, c2/m2, -(c2+c3)/m2]
153+
])
154+
B = np.array([[0.,0.],[0.,0.],[1/m1,0.],[0.,1/m2]])
155+
C = np.array([[1.0, 0.0, 0.0, 0.0],[0.0, 1.0, 0.0, 0.0]])
156+
D = np.zeros((2,2))
157+
158+
sys = StateSpace(A, B, C, D)
159+
dt = 0.25
160+
sysd = sys.sample(dt, method='zoh')
161+
162+
T = np.arange(0,100,dt)
163+
U = np.random.randn(sysd.B.shape[-1], len(T))
164+
response = forced_response(sysd, U=U)
165+
Y = response.outputs
166+
167+
m = 100
168+
_, Htrue = impulse_response(sysd, T=dt*(m))
169+
170+
171+
# test array_like
172+
atol = 1e-1
173+
H = okid(Y, U, m, dt=dt)
174+
np.testing.assert_allclose(H, Htrue, atol=atol)
175+
176+
# test array_like, truncate
177+
H = okid(Y, U, m, dt=dt, truncate=True)
178+
np.testing.assert_allclose(H, Htrue, atol=atol)
179+
180+
# test array_like, transpose
181+
HT = okid(Y.T, U.T, m, dt=dt, transpose=True)
182+
np.testing.assert_allclose(HT, np.transpose(Htrue), atol=atol)
183+
184+
# test response data
185+
H = okid(response, m, dt=dt)
186+
np.testing.assert_allclose(H, Htrue, atol=atol)
187+
188+
# test response data
189+
H = okid(response, m, dt=dt, truncate=True)
190+
np.testing.assert_allclose(H, Htrue, atol=atol)
191+
192+
# test response data, transpose
193+
response.transpose = True
194+
HT = okid(response, m, dt=dt)
195+
np.testing.assert_allclose(HT, np.transpose(Htrue), atol=atol)
196+
197+
# Make sure markov() returns the right answer
198+
@pytest.mark.parametrize("k, m, n",
199+
[(2, 2, 2),
200+
(2, 5, 5),
201+
(5, 2, 2),
202+
(5, 5, 5),
203+
(5, 10, 10)])
204+
def testOkidResults(self, k, m, n):
205+
#
206+
# Test over a range of parameters
207+
#
208+
# k = order of the system
209+
# m = number of Markov parameters
210+
# n = size of the data vector
211+
#
212+
# Values *should* match exactly for n = m, otherewise you get a
213+
# close match but errors due to the assumption that C A^k B =
214+
# 0 for k > m-2 (see modelsimp.py).
215+
#
216+
217+
# Generate stable continuous time system
218+
Hc = rss(k, 1, 1)
219+
220+
# Choose sampling time based on fastest time constant / 10
221+
w, _ = np.linalg.eig(Hc.A)
222+
Ts = np.min(-np.real(w)) / 10.
223+
224+
# Convert to a discrete time system via sampling
225+
Hd = c2d(Hc, Ts, 'zoh')
226+
227+
# Compute the Markov parameters from state space
228+
Mtrue = np.hstack([Hd.D] + [
229+
Hd.C @ np.linalg.matrix_power(Hd.A, i) @ Hd.B
230+
for i in range(m-1)])
231+
232+
Mtrue = np.squeeze(Mtrue)
233+
234+
# Generate input/output data
235+
T = np.array(range(n)) * Ts
236+
U = np.cos(T) + np.sin(T/np.pi)
237+
_, Y = forced_response(Hd, T, U, squeeze=True)
238+
Mcomp = okid(Y, U, m-1)
239+
240+
241+
# TODO: Check tol
242+
# Compare to results from markov()
243+
# experimentally determined probability to get non matching results
244+
# with rtot=1e-6 and atol=1e-8 due to numerical errors
245+
# for k=5, m=n=10: 0.015 %
246+
np.testing.assert_allclose(Mtrue, Mcomp, atol=1e-1)
247+
248+
249+
114250
def testModredMatchDC(self):
115251
#balanced realization computed in matlab for the transfer function:
116252
# num = [1 11 45 32], den = [1 15 60 200 60]

0 commit comments

Comments
 (0)