|
7 | 7 | import pytest
|
8 | 8 |
|
9 | 9 |
|
10 |
| -from control import StateSpace, forced_response, tf, rss, c2d |
| 10 | +from control import StateSpace, impulse_response, forced_response, tf, rss, c2d |
11 | 11 | from control.exception import ControlMIMONotImplemented
|
12 | 12 | 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 |
14 | 14 |
|
15 | 15 |
|
16 | 16 | class TestModelsimp:
|
@@ -111,6 +111,142 @@ def testMarkovResults(self, k, m, n):
|
111 | 111 | # for k=5, m=n=10: 0.015 %
|
112 | 112 | np.testing.assert_allclose(Mtrue, Mcomp, rtol=1e-6, atol=1e-8)
|
113 | 113 |
|
| 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 | + |
114 | 250 | def testModredMatchDC(self):
|
115 | 251 | #balanced realization computed in matlab for the transfer function:
|
116 | 252 | # num = [1 11 45 32], den = [1 15 60 200 60]
|
|
0 commit comments