Skip to content

Commit f2bdde4

Browse files
committed
balred() now has 'matchdc' option and may handle unstable systems and will accept a list of 'orders'. gram() now does Cholesky factored gramians if desired. Requires slycot routines AB09MD, AB09ND, SB03OD.
1 parent cc4b5c7 commit f2bdde4

File tree

4 files changed

+90
-109
lines changed

4 files changed

+90
-109
lines changed

control/modelsimp.py

Lines changed: 27 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from .statesp import StateSpace
5151
from .statefbk import gram
5252

53-
__all__ = ['hsvd', 'balred', 'modred', 'era', 'markov', 'minreal']
53+
__all__ = ['hsvd', 'balred', 'modred', 'era', 'markov', 'minreal',]
5454

5555
# Hankel Singular Value Decomposition
5656
# The following returns the Hankel singular values, which are singular values
@@ -197,38 +197,7 @@ def modred(sys, ELIM, method='matchdc'):
197197
rsys = StateSpace(Ar,Br,Cr,Dr)
198198
return rsys
199199

200-
def stabsep(T_schur, Z_schur, sys, ldim, no_in, no_out):
201-
"""
202-
Performs stable/unstabe decomposition of sys after Schur forms have been computed for system matrix.
203-
204-
Reference: Hsu,C.S., and Hou,D., 1991,
205-
Reducing unstable linear control systems via real Schur transformation.
206-
Electronics Letters, 27, 984-986.
207-
208-
"""
209-
#Author: M. Clement (mdclemen@eng.ucsd.edu) 2016
210-
As = np.asmatrix(T_schur)
211-
Bs = Z_schur.T*sys.B
212-
Cs = sys.C*Z_schur
213-
#from ref 1 eq(1) As = [A_ Ac], Bs = [B_], and Cs = [C_ C+]; _ denotes stable subsystem
214-
# [0 A+] [B+]
215-
A_ = As[0:ldim,0:ldim]
216-
Ac = As[0:ldim,ldim::]
217-
Ap = As[ldim::,ldim::]
218-
219-
B_ = Bs[0:ldim,:]
220-
Bp = Bs[ldim::,:]
221-
222-
C_ = Cs[:,0:ldim]
223-
Cp = Cs[:,ldim::]
224-
#do some more tricky math IAW ref 1 eq(3)
225-
B_tilde = np.bmat([[B_, Ac]])
226-
D_tilde = np.bmat([[np.zeros((no_out, no_in)), Cp]])
227-
228-
return A_, B_tilde, C_, D_tilde, Ap, Bp, Cp
229-
230-
231-
def balred(sys, orders, method='truncate'):
200+
def balred(sys, orders, method='truncate', alpha=None):
232201
"""
233202
Balanced reduced order model of sys of a given order.
234203
States are eliminated based on Hankel singular value.
@@ -249,6 +218,13 @@ def balred(sys, orders, method='truncate'):
249218
of systems)
250219
method: string
251220
Method of removing states, either ``'truncate'`` or ``'matchdc'``.
221+
alpha: float
222+
Specifies the alpha-stability boundary for the eigenvalues
223+
of the state dynamics matrix A. For a continuous-time
224+
system, alpha <= 0 is the boundary value for the real parts
225+
of eigenvalues, while for a discrete-time system, 0 <= alpha <= 1
226+
represents the boundary value for the moduli of eigenvalues.
227+
The alpha-stability domain does not include the boundary.
252228
253229
Returns
254230
-------
@@ -260,7 +236,7 @@ def balred(sys, orders, method='truncate'):
260236
ValueError
261237
* if `method` is not ``'truncate'`` or ``'matchdc'``
262238
ImportError
263-
if slycot routine ab09ad or ab09bd is not found
239+
if slycot routine ab09md or ab09nd is not found
264240
265241
ValueError
266242
if there are more unstable modes than any value in orders
@@ -274,18 +250,15 @@ def balred(sys, orders, method='truncate'):
274250
raise ValueError("supported methods are 'truncate' or 'matchdc'")
275251
elif method=='truncate':
276252
try:
277-
from slycot import ab09ad
253+
from slycot import ab09md
278254
except ImportError:
279-
raise ControlSlycot("can't find slycot subroutine ab09ad")
255+
raise ControlSlycot("can't find slycot subroutine ab09md")
280256
elif method=='matchdc':
281257
try:
282-
from slycot import ab09bd
258+
from slycot import ab09nd
283259
except ImportError:
284-
raise ControlSlycot("can't find slycot subroutine ab09bd")
285-
260+
raise ControlSlycot("can't find slycot subroutine ab09nd")
286261

287-
from scipy.linalg import schur#, cholesky, svd
288-
from numpy.linalg import cholesky, svd
289262
#Check for ss system object, need a utility for this?
290263

291264
#TODO: Check for continous or discrete, only continuous supported right now
@@ -297,6 +270,11 @@ def balred(sys, orders, method='truncate'):
297270
dico = 'C'
298271
job = 'B' # balanced (B) or not (N)
299272
equil = 'N' # scale (S) or not (N)
273+
if alpha is None:
274+
if dico == 'C':
275+
alpha = 0.
276+
elif dico == 'D':
277+
alpha = 1.
300278

301279
rsys = [] #empty list for reduced systems
302280

@@ -305,64 +283,18 @@ def balred(sys, orders, method='truncate'):
305283
order = iter(orders)
306284
except TypeError: #if orders is a scalar
307285
orders = [orders]
308-
309-
#first get original system order
310-
nn = sys.A.shape[0] #no. of states
311-
mm = sys.B.shape[1] #no. of inputs
312-
rr = sys.C.shape[0] #no. of outputs
313-
#first do the schur decomposition
314-
T, V, l = schur(sys.A, sort = 'lhp') #l will contain the number of eigenvalues in the open left half plane, i.e. no. of stable eigenvalues
315286

316287
for i in orders:
317-
rorder = i - (nn - l)
318-
if rorder <= 0:
319-
raise ValueError("System has %i unstable states which is more than ORDER(%i)" % (nn-l, i))
320-
321-
for i in orders:
322-
if (nn - l) > 0: #handles the stable/unstable decomposition if unstable eigenvalues are found, nn - l is the number of ustable eigenvalues
323-
#Author: M. Clement (mdclemen@eng.ucsd.edu) 2016
324-
print("Unstable eigenvalues found, performing stable/unstable decomposition")
325-
326-
rorder = i - (nn - l)
327-
A_, B_tilde, C_, D_tilde, Ap, Bp, Cp = stabsep(T, V, sys, l, mm, rr)
328-
329-
subSys = StateSpace(A_, B_tilde, C_, D_tilde)
330-
n = np.size(subSys.A,0)
331-
m = np.size(subSys.B,1)
332-
p = np.size(subSys.C,0)
333-
334-
if method == 'truncate':
335-
Nr, Ar, Br, Cr, hsv = ab09ad(dico,job,equil,n,m,p,subSys.A,subSys.B,subSys.C,nr=rorder,tol=0.0)
336-
rsubSys = StateSpace(Ar, Br, Cr, np.zeros((p,m)))
337-
338-
elif method == 'matchdc':
339-
Nr, Ar, Br, Cr, Dr, hsv = ab09bd(dico,job,equil,n,m,p,subSys.A,subSys.B,subSys.C,subSys.D,nr=rorder,tol1=0.0,tol2=0.0)
340-
rsubSys = StateSpace(Ar, Br, Cr, Dr)
341-
342-
A_r = rsubSys.A
343-
#IAW ref 1 eq(4) B^{tilde}_r = [B_r, Acr]
344-
B_r = rsubSys.B[:,0:mm]
345-
Acr = rsubSys.B[:,mm:mm+(nn-l)]
346-
C_r = rsubSys.C
347-
348-
#now put the unstable subsystem back in
349-
Ar = np.bmat([[A_r, Acr], [np.zeros((nn-l,rorder)), Ap]])
350-
Br = np.bmat([[B_r], [Bp]])
351-
Cr = np.bmat([[C_r, Cp]])
352-
288+
n = np.size(sys.A,0)
289+
m = np.size(sys.B,1)
290+
p = np.size(sys.C,0)
291+
if method == 'truncate':
292+
Nr, Ar, Br, Cr, Ns, hsv = ab09md(dico,job,equil,n,m,p,sys.A,sys.B,sys.C,alpha=alpha,nr=i,tol=0.0)
353293
rsys.append(StateSpace(Ar, Br, Cr, sys.D))
354294

355-
else: #stable system branch
356-
n = np.size(sys.A,0)
357-
m = np.size(sys.B,1)
358-
p = np.size(sys.C,0)
359-
if method == 'truncate':
360-
Nr, Ar, Br, Cr, hsv = ab09ad(dico,job,equil,n,m,p,sys.A,sys.B,sys.C,nr=i,tol=0.0)
361-
rsys.append(StateSpace(Ar, Br, Cr, sys.D))
362-
363-
elif method == 'matchdc':
364-
Nr, Ar, Br, Cr, Dr, hsv = ab09bd(dico,job,equil,n,m,p,sys.A,sys.B,sys.C,sys.D,nr=rorder,tol1=0.0,tol2=0.0)
365-
rsys.append(StateSpace(Ar, Br, Cr, Dr))
295+
elif method == 'matchdc':
296+
Nr, Ar, Br, Cr, Dr, Ns, hsv = ab09nd(dico,job,equil,n,m,p,sys.A,sys.B,sys.C,sys.D,alpha=alpha,nr=i,tol1=0.0,tol2=0.0)
297+
rsys.append(StateSpace(Ar, Br, Cr, Dr))
366298

367299
#if orders was a scalar, just return the single reduced model, not a list
368300
if len(orders) == 1:

control/statefbk.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ def gram(sys,type):
347347
#Check for ss system object
348348
if not isinstance(sys,statesp.StateSpace):
349349
raise ValueError("System must be StateSpace!")
350+
gramtypes = ['c', 'o', 'cf', 'of']
351+
if type not in gramtypes:
352+
raise ValueError("That type is not supported!")
350353

351354
#TODO: Check for continous or discrete, only continuous supported right now
352355
# if isCont():
@@ -362,16 +365,6 @@ def gram(sys,type):
362365
for e in D:
363366
if e.real >= 0:
364367
raise ValueError("Oops, the system is unstable!")
365-
if type=='c' or type=='cf':
366-
tra = 'T'
367-
if type=='c':
368-
C = -np.dot(sys.B,sys.B.transpose())
369-
elif type=='o' or type=='of':
370-
tra = 'N'
371-
if type=='o':
372-
C = -np.dot(sys.C.transpose(),sys.C)
373-
else:
374-
raise ValueError("Oops, neither observable, nor controllable!")
375368

376369
#Compute Gramian by the Slycot routine sb03md
377370
#make sure Slycot is installed
@@ -380,24 +373,38 @@ def gram(sys,type):
380373
from slycot import sb03md
381374
except ImportError:
382375
raise ControlSlycot("can't find slycot module 'sb03md'")
376+
if type=='c':
377+
tra = 'T'
378+
C = -np.dot(sys.B,sys.B.transpose())
379+
elif type=='o':
380+
tra = 'N'
381+
C = -np.dot(sys.C.transpose(),sys.C)
383382
n = sys.states
384383
U = np.zeros((n,n))
385384
A = np.array(sys.A) # convert to NumPy array for slycot
386385
X,scale,sep,ferr,w = sb03md(n, C, A, U, dico, job='X', fact='N', trana=tra)
387386
gram = X
388387
return gram
388+
#Comupte cholesky factored gramian from slycot routine sb03od
389389
elif type=='cf' or type=='of':
390390
try:
391391
from slycot import sb03od
392392
except ImportError:
393393
raise ControlSlycot("can't find slycot module 'sb03od'")
394+
tra='N'
394395
n = sys.states
395396
Q = np.zeros((n,n))
396397
A = np.array(sys.A) # convert to NumPy array for slycot
397-
B = np.zeros_like(A)
398-
B[0:sys.B.shape[0],0:sys.B.shape[1]] = sys.B
399-
m = sys.B.shape[0]
400-
X,scale,w = sb03od(n, m, A, Q, B, dico, fact='N', trans=tra)
398+
if type=='cf':
399+
m = sys.B.shape[1]
400+
B = np.zeros_like(A)
401+
B[0:m,0:n] = sys.B.transpose()
402+
X,scale,w = sb03od(n, m, A.transpose(), Q, B, dico, fact='N', trans=tra)
403+
elif type=='of':
404+
m = sys.C.shape[0]
405+
C = np.zeros_like(A)
406+
C[0:n,0:m] = sys.C.transpose()
407+
X,scale,w = sb03od(n, m, A, Q, C.transpose(), dico, fact='N', trans=tra)
401408
gram = X
402409
return gram
403410

control/tests/modelsimp_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,28 @@ def testBalredTruncate(self):
9595
np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4)
9696
np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4)
9797

98+
def testBalredMatchDC(self):
99+
#controlable canonical realization computed in matlab for the transfer function:
100+
# num = [1 11 45 32], den = [1 15 60 200 60]
101+
A = np.matrix('-15., -7.5, -6.25, -1.875; \
102+
8., 0., 0., 0.; \
103+
0., 4., 0., 0.; \
104+
0., 0., 1., 0.')
105+
B = np.matrix('2.; 0.; 0.; 0.')
106+
C = np.matrix('0.5, 0.6875, 0.7031, 0.5')
107+
D = np.matrix('0.')
108+
sys = ss(A,B,C,D)
109+
orders = 2
110+
rsys = balred(sys,orders,method='matchdc')
111+
Artrue = np.matrix('-4.43094773, -4.55232904; -4.55232904, -5.36195206')
112+
Brtrue = np.matrix('1.36235673; 1.03114388')
113+
Crtrue = np.matrix('1.36235673, 1.03114388')
114+
Drtrue = np.matrix('-0.08383902')
115+
np.testing.assert_array_almost_equal(rsys.A, Artrue,decimal=2)
116+
np.testing.assert_array_almost_equal(rsys.B, Brtrue,decimal=4)
117+
np.testing.assert_array_almost_equal(rsys.C, Crtrue,decimal=4)
118+
np.testing.assert_array_almost_equal(rsys.D, Drtrue,decimal=4)
119+
98120
def suite():
99121
return unittest.TestLoader().loadTestsFromTestCase(TestModelsimp)
100122

control/tests/statefbk_test.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ def testGramWc(self):
7171
Wc = gram(sys,'c')
7272
np.testing.assert_array_almost_equal(Wc, Wctrue)
7373

74+
def testGramRc(self):
75+
A = np.matrix("1. -2.; 3. -4.")
76+
B = np.matrix("5. 6.; 7. 8.")
77+
C = np.matrix("4. 5.; 6. 7.")
78+
D = np.matrix("13. 14.; 15. 16.")
79+
sys = ss(A, B, C, D)
80+
Rctrue = np.matrix("4.30116263 5.6961343; 0. 0.23249528")
81+
Rc = gram(sys,'cf')
82+
np.testing.assert_array_almost_equal(Rc, Rctrue)
83+
7484
@unittest.skipIf(not slycot_check(), "slycot not installed")
7585
def testGramWo(self):
7686
A = np.matrix("1. -2.; 3. -4.")
@@ -93,6 +103,16 @@ def testGramWo2(self):
93103
Wo = gram(sys,'o')
94104
np.testing.assert_array_almost_equal(Wo, Wotrue)
95105

106+
def testGramRo(self):
107+
A = np.matrix("1. -2.; 3. -4.")
108+
B = np.matrix("5. 6.; 7. 8.")
109+
C = np.matrix("4. 5.; 6. 7.")
110+
D = np.matrix("13. 14.; 15. 16.")
111+
sys = ss(A, B, C, D)
112+
Rotrue = np.matrix("16.04680654 -5.8890222; 0. 4.67112593")
113+
Ro = gram(sys,'of')
114+
np.testing.assert_array_almost_equal(Ro, Rotrue)
115+
96116
def testGramsys(self):
97117
num =[1.]
98118
den = [1., 1., 1.]

0 commit comments

Comments
 (0)