|
1 | 1 | # canonical.py - functions for converting systems to canonical forms
|
2 | 2 | # RMM, 10 Nov 2012
|
3 | 3 |
|
4 |
| -from .exception import ControlNotImplemented |
| 4 | +from .exception import ControlNotImplemented, ControlSlycot |
5 | 5 | from .lti import issiso
|
6 |
| -from .statesp import StateSpace |
| 6 | +from .statesp import StateSpace, _convertToStateSpace |
7 | 7 | from .statefbk import ctrb, obsv
|
8 | 8 |
|
| 9 | +import numpy as np |
| 10 | + |
9 | 11 | from numpy import zeros, zeros_like, shape, poly, iscomplex, vstack, hstack, dot, \
|
10 |
| - transpose, empty |
| 12 | + transpose, empty, finfo, float64 |
11 | 13 | from numpy.linalg import solve, matrix_rank, eig
|
12 | 14 |
|
| 15 | +from scipy.linalg import schur |
| 16 | + |
13 | 17 | __all__ = ['canonical_form', 'reachable_form', 'observable_form', 'modal_form',
|
14 |
| - 'similarity_transform'] |
| 18 | + 'similarity_transform', 'bdschur'] |
15 | 19 |
|
16 | 20 | def canonical_form(xsys, form='reachable'):
|
17 | 21 | """Convert a system into canonical form
|
@@ -149,97 +153,294 @@ def observable_form(xsys):
|
149 | 153 |
|
150 | 154 | return zsys, Tzx
|
151 | 155 |
|
152 |
| -def modal_form(xsys): |
153 |
| - """Convert a system into modal canonical form |
| 156 | + |
| 157 | +def similarity_transform(xsys, T, timescale=1, inverse=False): |
| 158 | + """Perform a similarity transformation, with option time rescaling. |
| 159 | +
|
| 160 | + Transform a linear state space system to a new state space representation |
| 161 | + z = T x, or x = T z, where T is an invertible matrix. |
154 | 162 |
|
155 | 163 | Parameters
|
156 | 164 | ----------
|
157 | 165 | xsys : StateSpace object
|
158 |
| - System to be transformed, with state `x` |
| 166 | + System to transform |
| 167 | + T : 2D invertible array |
| 168 | + The matrix `T` defines the new set of coordinates z = T x. |
| 169 | + timescale : float |
| 170 | + If present, also rescale the time unit to tau = timescale * t |
| 171 | + inverse: boolean |
| 172 | + If True (default), transform so z = T x. If False, transform |
| 173 | + so x = T z. |
159 | 174 |
|
160 | 175 | Returns
|
161 | 176 | -------
|
162 | 177 | zsys : StateSpace object
|
163 |
| - System in modal canonical form, with state `z` |
164 |
| - T : matrix |
165 |
| - Coordinate transformation: z = T * x |
166 |
| - """ |
167 |
| - # Check to make sure we have a SISO system |
168 |
| - if not issiso(xsys): |
169 |
| - raise ControlNotImplemented( |
170 |
| - "Canonical forms for MIMO systems not yet supported") |
| 178 | + System in transformed coordinates, with state 'z' |
171 | 179 |
|
| 180 | + """ |
172 | 181 | # Create a new system, starting with a copy of the old one
|
173 | 182 | zsys = StateSpace(xsys)
|
174 | 183 |
|
175 |
| - # Calculate eigenvalues and matrix of eigenvectors Tzx, |
176 |
| - eigval, eigvec = eig(xsys.A) |
| 184 | + # Define a function to compute the right inverse (solve x M = y) |
| 185 | + def rsolve(M, y): |
| 186 | + return transpose(solve(transpose(M), transpose(y))) |
177 | 187 |
|
178 |
| - # Eigenvalues and corresponding eigenvectors are not sorted, |
179 |
| - # thus modal transformation is ambiguous |
180 |
| - # Sort eigenvalues and vectors from largest to smallest eigenvalue |
181 |
| - idx = eigval.argsort()[::-1] |
182 |
| - eigval = eigval[idx] |
183 |
| - eigvec = eigvec[:,idx] |
| 188 | + # Update the system matrices |
| 189 | + if not inverse: |
| 190 | + zsys.A = rsolve(T, dot(T, zsys.A)) / timescale |
| 191 | + zsys.B = dot(T, zsys.B) / timescale |
| 192 | + zsys.C = rsolve(T, zsys.C) |
| 193 | + else: |
| 194 | + zsys.A = solve(T, zsys.A).dot(T) / timescale |
| 195 | + zsys.B = solve(T, zsys.B) / timescale |
| 196 | + zsys.C = zsys.C.dot(T) |
| 197 | + |
| 198 | + return zsys |
| 199 | + |
| 200 | + |
| 201 | +_IM_ZERO_TOL = np.finfo(np.float64).eps ** 0.5 |
| 202 | +_PMAX_SEARCH_TOL = 1.001 |
| 203 | + |
| 204 | + |
| 205 | +def _bdschur_defective(blksizes, eigvals): |
| 206 | + """Check for defective modal decomposition |
| 207 | +
|
| 208 | + Parameters |
| 209 | + ---------- |
| 210 | + blksizes: size of Schur blocks |
| 211 | + eigvals: eigenvalues |
| 212 | +
|
| 213 | + Returns |
| 214 | + ------- |
| 215 | + True iff Schur blocks are defective |
| 216 | +
|
| 217 | + blksizes, eigvals are 3rd and 4th results returned by mb03rd. |
| 218 | + """ |
| 219 | + if any(blksizes > 2): |
| 220 | + return True |
| 221 | + |
| 222 | + if all(blksizes == 1): |
| 223 | + return False |
| 224 | + |
| 225 | + # check eigenvalues associated with blocks of size 2 |
| 226 | + init_idxs = np.cumsum(np.hstack([0, blksizes[:-1]])) |
| 227 | + blk_idx2 = blksizes == 2 |
| 228 | + |
| 229 | + im = eigvals[init_idxs[blk_idx2]].imag |
| 230 | + re = eigvals[init_idxs[blk_idx2]].real |
| 231 | + |
| 232 | + if any(abs(im) < _IM_ZERO_TOL * abs(re)): |
| 233 | + return True |
| 234 | + |
| 235 | + return False |
| 236 | + |
| 237 | + |
| 238 | +def _bdschur_condmax_search(aschur, tschur, condmax): |
| 239 | + """Block-diagonal Schur decomposition search up to condmax |
| 240 | +
|
| 241 | + Iterates mb03rd with different pmax values until: |
| 242 | + - result is non-defective; |
| 243 | + - or condition number of similarity transform is unchanging despite large pmax; |
| 244 | + - or condition number of similarity transform is close to condmax. |
| 245 | +
|
| 246 | + Parameters |
| 247 | + ---------- |
| 248 | + aschur: (n, n) array |
| 249 | + real Schur-form matrix |
| 250 | + tschur: (n, n) array |
| 251 | + orthogonal transformation giving aschur from some initial matrix a |
| 252 | + condmax: positive scalar >= 1 |
| 253 | + maximum condition number of final transformation |
184 | 254 |
|
185 |
| - # If all eigenvalues are real, the matrix of eigenvectors is Tzx directly |
186 |
| - if not iscomplex(eigval).any(): |
187 |
| - Tzx = eigvec |
| 255 | + Returns |
| 256 | + ------- |
| 257 | + amodal: n, n array |
| 258 | + block diagonal Schur form |
| 259 | + tmodal: |
| 260 | + similarity transformation give amodal from aschur |
| 261 | + blksizes: |
| 262 | + Array of Schur block sizes |
| 263 | + eigvals: |
| 264 | + Eigenvalues of amodal (and a, etc.) |
| 265 | +
|
| 266 | + Notes |
| 267 | + ----- |
| 268 | + Outputs as for slycot.mb03rd |
| 269 | +
|
| 270 | + aschur, tschur are as returned by scipy.linalg.schur. |
| 271 | + """ |
| 272 | + try: |
| 273 | + from slycot import mb03rd |
| 274 | + except ImportError: |
| 275 | + raise ControlSlycot("can't find slycot module 'mb03rd'") |
| 276 | + |
| 277 | + # see notes on RuntimeError below |
| 278 | + pmaxlower = None |
| 279 | + |
| 280 | + # get lower bound; try condmax ** 0.5 first |
| 281 | + pmaxlower = condmax ** 0.5 |
| 282 | + amodal, tmodal, blksizes, eigvals = mb03rd(aschur.shape[0], aschur, tschur, pmax=pmaxlower) |
| 283 | + if np.linalg.cond(tmodal) <= condmax: |
| 284 | + reslower = amodal, tmodal, blksizes, eigvals |
188 | 285 | else:
|
189 |
| - # A is an arbitrary semisimple matrix |
190 |
| - |
191 |
| - # Keep track of complex conjugates (need only one) |
192 |
| - lst_conjugates = [] |
193 |
| - Tzx = empty((0, xsys.A.shape[0])) # empty zero-height row matrix |
194 |
| - for val, vec in zip(eigval, eigvec.T): |
195 |
| - if iscomplex(val): |
196 |
| - if val not in lst_conjugates: |
197 |
| - lst_conjugates.append(val.conjugate()) |
198 |
| - Tzx = vstack((Tzx, vec.real, vec.imag)) |
199 |
| - else: |
200 |
| - # if conjugate has already been seen, skip this eigenvalue |
201 |
| - lst_conjugates.remove(val) |
202 |
| - else: |
203 |
| - Tzx = vstack((Tzx, vec.real)) |
204 |
| - Tzx = Tzx.T |
| 286 | + pmaxlower = 1.0 |
| 287 | + amodal, tmodal, blksizes, eigvals = mb03rd(aschur.shape[0], aschur, tschur, pmax=pmaxlower) |
| 288 | + cond = np.linalg.cond(tmodal) |
| 289 | + if cond > condmax: |
| 290 | + msg = 'minimum cond={} > condmax={}; try increasing condmax'.format(cond, condmax) |
| 291 | + raise RuntimeError(msg) |
| 292 | + |
| 293 | + pmax = pmaxlower |
| 294 | + |
| 295 | + # phase 1: search for upper bound on pmax |
| 296 | + for i in range(50): |
| 297 | + amodal, tmodal, blksizes, eigvals = mb03rd(aschur.shape[0], aschur, tschur, pmax=pmax) |
| 298 | + cond = np.linalg.cond(tmodal) |
| 299 | + if cond < condmax: |
| 300 | + pmaxlower = pmax |
| 301 | + reslower = amodal, tmodal, blksizes, eigvals |
| 302 | + else: |
| 303 | + # upper bound found; go to phase 2 |
| 304 | + pmaxupper = pmax |
| 305 | + break |
| 306 | + |
| 307 | + if _bdschur_defective(blksizes, eigvals): |
| 308 | + pmax *= 2 |
| 309 | + else: |
| 310 | + return amodal, tmodal, blksizes, eigvals |
| 311 | + else: |
| 312 | + # no upper bound found; return current result |
| 313 | + return reslower |
| 314 | + |
| 315 | + # phase 2: bisection search |
| 316 | + for i in range(50): |
| 317 | + pmax = (pmaxlower * pmaxupper) ** 0.5 |
| 318 | + amodal, tmodal, blksizes, eigvals = mb03rd(aschur.shape[0], aschur, tschur, pmax=pmax) |
| 319 | + cond = np.linalg.cond(tmodal) |
| 320 | + |
| 321 | + if cond < condmax: |
| 322 | + if not _bdschur_defective(blksizes, eigvals): |
| 323 | + return amodal, tmodal, blksizes, eigvals |
| 324 | + pmaxlower = pmax |
| 325 | + reslower = amodal, tmodal, blksizes, eigvals |
| 326 | + else: |
| 327 | + pmaxupper = pmax |
| 328 | + |
| 329 | + if pmaxupper / pmaxlower < _PMAX_SEARCH_TOL: |
| 330 | + # hit search limit |
| 331 | + return reslower |
| 332 | + else: |
| 333 | + raise ValueError('bisection failed to converge; pmaxlower={}, pmaxupper={}'.format(pmaxlower, pmaxupper)) |
205 | 334 |
|
206 |
| - # Generate the system matrices for the desired canonical form |
207 |
| - zsys.A = solve(Tzx, xsys.A).dot(Tzx) |
208 |
| - zsys.B = solve(Tzx, xsys.B) |
209 |
| - zsys.C = xsys.C.dot(Tzx) |
210 | 335 |
|
211 |
| - return zsys, Tzx |
| 336 | +def bdschur(a, condmax=None, sort=None): |
| 337 | + """Block-diagonal Schur decomposition |
212 | 338 |
|
| 339 | + Parameters |
| 340 | + ---------- |
| 341 | + a: real (n, n) array |
| 342 | + Matrix to decompose |
| 343 | + condmax: real scalar >= 1 |
| 344 | + If None (default), use `1/sqrt(eps)`, which is approximately 2e-8 |
| 345 | + sort: None, 'continuous', or 'discrete' |
| 346 | + See below |
213 | 347 |
|
214 |
| -def similarity_transform(xsys, T, timescale=1): |
215 |
| - """Perform a similarity transformation, with option time rescaling. |
| 348 | + Returns |
| 349 | + ------- |
| 350 | + amodal: (n, n) array, dtype `np.double` |
| 351 | + Block-diagonal Schur decomposition of `a` |
| 352 | + tmodal: (n, n) array |
| 353 | + similarity transform relating `a` and `amodal` |
| 354 | + blksizes: |
| 355 | + Array of Schur block sizes |
| 356 | +
|
| 357 | + Notes |
| 358 | + ----- |
| 359 | + If `sort` is None, the blocks are not sorted. |
| 360 | +
|
| 361 | + If `sort` is 'continuous', the blocks are sorted according to |
| 362 | + associated eigenvalues. The ordering is first by real part of |
| 363 | + eigenvalue, in descending order, then by absolute value of |
| 364 | + imaginary part of eigenvalue, also in decreasing order. |
| 365 | +
|
| 366 | + If `sort` is 'discrete', the blocks are sorted as for |
| 367 | + 'continuous', but applied to log of eigenvalues |
| 368 | + (continuous-equivalent). |
| 369 | + """ |
| 370 | + if condmax is None: |
| 371 | + condmax = np.finfo(np.float64).eps ** -0.5 |
216 | 372 |
|
217 |
| - Transform a linear state space system to a new state space representation |
218 |
| - z = T x, where T is an invertible matrix. |
| 373 | + if not (np.isscalar(condmax) and condmax >= 1.0): |
| 374 | + raise ValueError('condmax="{}" must be a scalar >= 1.0'.format(condmax)) |
| 375 | + |
| 376 | + a = np.atleast_2d(a) |
| 377 | + if a.shape[0] == 0 or a.shape[1] == 0: |
| 378 | + return a.copy(), np.eye(a.shape[1], a.shape[0]), np.array([]) |
| 379 | + |
| 380 | + aschur, tschur = schur(a) |
| 381 | + amodal, tmodal, blksizes, eigvals = _bdschur_condmax_search(aschur, tschur, condmax) |
| 382 | + |
| 383 | + if sort in ('continuous', 'discrete'): |
| 384 | + |
| 385 | + idxs = np.cumsum(np.hstack([0, blksizes[:-1]])) |
| 386 | + |
| 387 | + ev_per_blk = [complex(eigvals[i].real, abs(eigvals[i].imag)) |
| 388 | + for i in idxs] |
| 389 | + |
| 390 | + if sort == 'discrete': |
| 391 | + ev_per_blk = np.log(ev_per_blk) |
| 392 | + |
| 393 | + # put most unstable first |
| 394 | + sortidx = np.argsort(ev_per_blk)[::-1] |
| 395 | + |
| 396 | + # block indices |
| 397 | + blkidxs = [np.arange(i0, i0+ilen) |
| 398 | + for i0, ilen in zip(idxs, blksizes)] |
| 399 | + |
| 400 | + # reordered |
| 401 | + permidx = np.hstack([blkidxs[i] for i in sortidx]) |
| 402 | + rperm = np.eye(amodal.shape[0])[permidx] |
| 403 | + |
| 404 | + tmodal = tmodal.dot(rperm) |
| 405 | + amodal = rperm.dot(amodal).dot(rperm.T) |
| 406 | + blksizes = blksizes[sortidx] |
| 407 | + |
| 408 | + elif sort is None: |
| 409 | + pass |
| 410 | + |
| 411 | + else: |
| 412 | + raise ValueError('unknown sort value "{}"'.format(sort)) |
| 413 | + |
| 414 | + return amodal, tmodal, blksizes |
| 415 | + |
| 416 | + |
| 417 | +def modal_form(xsys, condmax=None, sort=False): |
| 418 | + """Convert a system into modal canonical form |
219 | 419 |
|
220 | 420 | Parameters
|
221 | 421 | ----------
|
222 |
| - T : 2D invertible array |
223 |
| - The matrix `T` defines the new set of coordinates z = T x. |
224 |
| - timescale : float |
225 |
| - If present, also rescale the time unit to tau = timescale * t |
| 422 | + xsys : StateSpace object |
| 423 | + System to be transformed, with state `x` |
| 424 | + condmax: real scalar >= 1 |
| 425 | + An upper bound on individual transformations. If None, use `bdschur` default. |
| 426 | + sort: False (default) |
| 427 | + If true, Schur blocks will be sorted. See `bdschur` for sort order. |
226 | 428 |
|
227 | 429 | Returns
|
228 | 430 | -------
|
229 | 431 | zsys : StateSpace object
|
230 |
| - System in transformed coordinates, with state 'z' |
231 |
| -
|
| 432 | + System in modal canonical form, with state `z` |
| 433 | + T : matrix |
| 434 | + Coordinate transformation: z = T * x |
232 | 435 | """
|
233 |
| - # Create a new system, starting with a copy of the old one |
234 |
| - zsys = StateSpace(xsys) |
235 | 436 |
|
236 |
| - # Define a function to compute the right inverse (solve x M = y) |
237 |
| - def rsolve(M, y): |
238 |
| - return transpose(solve(transpose(M), transpose(y))) |
| 437 | + if sort: |
| 438 | + discrete = xsys.dt is not None and xsys.dt > 0 |
| 439 | + bd_sort = 'discrete' if discrete else 'continuous' |
| 440 | + else: |
| 441 | + bd_sort = None |
239 | 442 |
|
240 |
| - # Update the system matrices |
241 |
| - zsys.A = rsolve(T, dot(T, zsys.A)) / timescale |
242 |
| - zsys.B = dot(T, zsys.B) / timescale |
243 |
| - zsys.C = rsolve(T, zsys.C) |
| 443 | + xsys = _convertToStateSpace(xsys) |
| 444 | + amodal, tmodal, _ = bdschur(xsys.A, condmax=condmax, sort=bd_sort) |
244 | 445 |
|
245 |
| - return zsys |
| 446 | + return similarity_transform(xsys, tmodal, inverse=True), tmodal |
0 commit comments