Skip to content

Commit 14079fb

Browse files
roryyorkemurrayrm
authored andcommitted
ENH: add bdschur, which calls Slycot mb03rd. Change modal_form to use bdschur
The bdschur interface is modelled after the Matlab function of the same name; it can also optionally sort the modal blocks. Added tests for bdschur, and modified tests for modal_form.
1 parent feb9fc9 commit 14079fb

File tree

2 files changed

+517
-137
lines changed

2 files changed

+517
-137
lines changed

control/canonical.py

Lines changed: 268 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
# canonical.py - functions for converting systems to canonical forms
22
# RMM, 10 Nov 2012
33

4-
from .exception import ControlNotImplemented
4+
from .exception import ControlNotImplemented, ControlSlycot
55
from .lti import issiso
6-
from .statesp import StateSpace
6+
from .statesp import StateSpace, _convertToStateSpace
77
from .statefbk import ctrb, obsv
88

9+
import numpy as np
10+
911
from numpy import zeros, zeros_like, shape, poly, iscomplex, vstack, hstack, dot, \
10-
transpose, empty
12+
transpose, empty, finfo, float64
1113
from numpy.linalg import solve, matrix_rank, eig
1214

15+
from scipy.linalg import schur
16+
1317
__all__ = ['canonical_form', 'reachable_form', 'observable_form', 'modal_form',
14-
'similarity_transform']
18+
'similarity_transform', 'bdschur']
1519

1620
def canonical_form(xsys, form='reachable'):
1721
"""Convert a system into canonical form
@@ -149,97 +153,294 @@ def observable_form(xsys):
149153

150154
return zsys, Tzx
151155

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.
154162
155163
Parameters
156164
----------
157165
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.
159174
160175
Returns
161176
-------
162177
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'
171179
180+
"""
172181
# Create a new system, starting with a copy of the old one
173182
zsys = StateSpace(xsys)
174183

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)))
177187

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
184254
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
188285
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))
205334

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)
210335

211-
return zsys, Tzx
336+
def bdschur(a, condmax=None, sort=None):
337+
"""Block-diagonal Schur decomposition
212338
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
213347
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
216372

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
219419
220420
Parameters
221421
----------
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.
226428
227429
Returns
228430
-------
229431
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
232435
"""
233-
# Create a new system, starting with a copy of the old one
234-
zsys = StateSpace(xsys)
235436

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
239442

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)
244445

245-
return zsys
446+
return similarity_transform(xsys, tmodal, inverse=True), tmodal

0 commit comments

Comments
 (0)