Skip to content

Commit 70db513

Browse files
authored
Merge pull request #201 from ipa-lth/modal_form
add canonical form 'modal'
2 parents 79d2cea + 00e4052 commit 70db513

2 files changed

Lines changed: 101 additions & 3 deletions

File tree

control/canonical.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from .statesp import StateSpace
77
from .statefbk import ctrb, obsv
88

9-
from numpy import zeros, shape, poly
10-
from numpy.linalg import solve, matrix_rank
9+
from numpy import zeros, shape, poly, iscomplex, hstack
10+
from numpy.linalg import solve, matrix_rank, eig
1111

1212
__all__ = ['canonical_form', 'reachable_form', 'observable_form']
1313

@@ -22,7 +22,7 @@ def canonical_form(xsys, form='reachable'):
2222
Canonical form for transformation. Chosen from:
2323
* 'reachable' - reachable canonical form
2424
* 'observable' - observable canonical form
25-
* 'modal' - modal canonical form [not implemented]
25+
* 'modal' - modal canonical form
2626
2727
Returns
2828
-------
@@ -37,6 +37,8 @@ def canonical_form(xsys, form='reachable'):
3737
return reachable_form(xsys)
3838
elif form == 'observable':
3939
return observable_form(xsys)
40+
elif form == 'modal':
41+
return modal_form(xsys)
4042
else:
4143
raise ControlNotImplemented(
4244
"Canonical form '%s' not yet implemented" % form)
@@ -142,3 +144,69 @@ def observable_form(xsys):
142144
zsys.B = Tzx * xsys.B
143145

144146
return zsys, Tzx
147+
148+
def modal_form(xsys):
149+
"""Convert a system into modal canonical form
150+
151+
Parameters
152+
----------
153+
xsys : StateSpace object
154+
System to be transformed, with state `x`
155+
156+
Returns
157+
-------
158+
zsys : StateSpace object
159+
System in modal canonical form, with state `z`
160+
T : matrix
161+
Coordinate transformation: z = T * x
162+
"""
163+
# Check to make sure we have a SISO system
164+
if not issiso(xsys):
165+
raise ControlNotImplemented(
166+
"Canonical forms for MIMO systems not yet supported")
167+
168+
# Create a new system, starting with a copy of the old one
169+
zsys = StateSpace(xsys)
170+
171+
# Calculate eigenvalues and matrix of eigenvectors Tzx,
172+
eigval, eigvec = eig(xsys.A)
173+
174+
# Eigenvalues and according eigenvectors are not sorted,
175+
# thus modal transformation is ambiguous
176+
# Sorting eigenvalues and respective vectors by largest to smallest eigenvalue
177+
idx = eigval.argsort()[::-1]
178+
eigval = eigval[idx]
179+
eigvec = eigvec[:,idx]
180+
181+
# If all eigenvalues are real, the matrix of eigenvectors is Tzx directly
182+
if not iscomplex(eigval).any():
183+
Tzx = eigvec
184+
else:
185+
# A is an arbitrary semisimple matrix
186+
187+
# Keep track of complex conjugates (need only one)
188+
lst_conjugates = []
189+
Tzx = None
190+
for val, vec in zip(eigval, eigvec.T):
191+
if iscomplex(val):
192+
if val not in lst_conjugates:
193+
lst_conjugates.append(val.conjugate())
194+
if Tzx is not None:
195+
Tzx = hstack((Tzx, hstack((vec.real.T, vec.imag.T))))
196+
else:
197+
Tzx = hstack((vec.real.T, vec.imag.T))
198+
else:
199+
# if conjugate has already been seen, skip this eigenvalue
200+
lst_conjugates.remove(val)
201+
else:
202+
if Tzx is not None:
203+
Tzx = hstack((Tzx, vec.real.T))
204+
else:
205+
Tzx = vec.real.T
206+
207+
# Generate the system matrices for the desired canonical form
208+
zsys.A = solve(Tzx, xsys.A).dot(Tzx)
209+
zsys.B = solve(Tzx, xsys.B)
210+
zsys.C = xsys.C.dot(Tzx)
211+
212+
return zsys, Tzx

control/tests/canonical_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,36 @@ def test_unreachable_system(self):
5353
# Check if an exception is raised
5454
np.testing.assert_raises(ValueError, canonical_form, sys, "reachable")
5555

56+
def test_modal_form(self):
57+
"""Test the modal canonical form"""
58+
59+
# Create a system in the modal canonical form
60+
A_true = np.diag([4.0, 3.0, 2.0, 1.0]) # order from the largest to the smallest
61+
B_true = np.matrix("1.1 2.2 3.3 4.4").T
62+
C_true = np.matrix("1.3 1.4 1.5 1.6")
63+
D_true = 42.0
64+
65+
# Perform a coordinate transform with a random invertible matrix
66+
T_true = np.matrix([[-0.27144004, -0.39933167, 0.75634684, 0.44135471],
67+
[-0.74855725, -0.39136285, -0.18142339, -0.50356997],
68+
[-0.40688007, 0.81416369, 0.38002113, -0.16483334],
69+
[-0.44769516, 0.15654653, -0.50060858, 0.72419146]])
70+
A = np.linalg.solve(T_true, A_true)*T_true
71+
B = np.linalg.solve(T_true, B_true)
72+
C = C_true*T_true
73+
D = D_true
74+
75+
# Create a state space system and convert it to the modal canonical form
76+
sys_check, T_check = canonical_form(ss(A, B, C, D), "modal")
77+
78+
# Check against the true values
79+
#TODO: Test in respect to ambiguous transformation (system characteristics?)
80+
np.testing.assert_array_almost_equal(sys_check.A, A_true)
81+
#np.testing.assert_array_almost_equal(sys_check.B, B_true)
82+
#np.testing.assert_array_almost_equal(sys_check.C, C_true)
83+
np.testing.assert_array_almost_equal(sys_check.D, D_true)
84+
#np.testing.assert_array_almost_equal(T_check, T_true)
85+
5686
def test_observable_form(self):
5787
"""Test the observable canonical form"""
5888

0 commit comments

Comments
 (0)