Skip to content

Commit 136a4ec

Browse files
author
ipa-lth
committed
add canonical form 'modal'
The respective unittest is weak, yet I did not envisioned somthing better one
1 parent 601b581 commit 136a4ec

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)
@@ -139,3 +141,69 @@ def observable_form(xsys):
139141
zsys.B = Tzx * xsys.B
140142

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

control/tests/canonical_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,33 @@ def test_unreachable_system(self):
5252

5353
# Check if an exception is raised
5454
np.testing.assert_raises(ValueError, canonical_form, sys, "reachable")
55+
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)

0 commit comments

Comments
 (0)