12
12
"""
13
13
14
14
import numpy as np
15
+ import scipy as sp
15
16
import numpy .linalg as la
17
+ import warnings
16
18
17
19
import control as ct
18
20
19
21
__all__ = ['norm' ]
20
22
21
23
#------------------------------------------------------------------------------
22
24
23
- def norm (system , p = 2 , tol = 1e-6 , print_warning = True ):
25
+ def _h2norm_slycot (sys , print_warning = True ):
26
+ """H2 norm of a linear system. For internal use. Requires Slycot.
27
+
28
+ See also
29
+ --------
30
+ slycot.ab13bd : the Slycot routine that does the calculation
31
+ https://github.com/python-control/Slycot/issues/199 : Post on issue with ab13bf
32
+ """
33
+
34
+ try :
35
+ from slycot import ab13bd
36
+ except ImportError :
37
+ ct .ControlSlycot ("Can't find slycot module 'ab13bd'!" )
38
+
39
+ try :
40
+ from slycot .exceptions import SlycotArithmeticError
41
+ except ImportError :
42
+ raise ct .ControlSlycot ("Can't find slycot class 'SlycotArithmeticError'!" )
43
+
44
+ A , B , C , D = ct .ssdata (ct .ss (sys ))
45
+
46
+ n = A .shape [0 ]
47
+ m = B .shape [1 ]
48
+ p = C .shape [0 ]
49
+
50
+ dico = 'C' if sys .isctime () else 'D' # Continuous or discrete time
51
+ jobn = 'H' # H2 (and not L2 norm)
52
+
53
+ if n == 0 :
54
+ # ab13bd does not accept empty A, B, C
55
+ if dico == 'C' :
56
+ if any (D .flat != 0 ):
57
+ if print_warning :
58
+ warnings .warn ("System has a direct feedthrough term!" )
59
+ return float ("inf" )
60
+ else :
61
+ return 0.0
62
+ elif dico == 'D' :
63
+ return np .sqrt (D @D .T )
64
+
65
+ try :
66
+ norm = ab13bd (dico , jobn , n , m , p , A , B , C , D )
67
+ except SlycotArithmeticError as e :
68
+ if e .info == 3 :
69
+ if print_warning :
70
+ warnings .warn ("System has pole(s) on the stability boundary!" )
71
+ return float ("inf" )
72
+ elif e .info == 5 :
73
+ if print_warning :
74
+ warnings .warn ("System has a direct feedthrough term!" )
75
+ return float ("inf" )
76
+ elif e .info == 6 :
77
+ if print_warning :
78
+ warnings .warn ("System is unstable!" )
79
+ return float ("inf" )
80
+ else :
81
+ raise e
82
+ return norm
83
+
84
+ #------------------------------------------------------------------------------
85
+
86
+ def norm (system , p = 2 , tol = 1e-10 , print_warning = True , use_slycot = True ):
24
87
"""Computes norm of system.
25
88
26
89
Parameters
27
90
----------
28
91
system : LTI (:class:`StateSpace` or :class:`TransferFunction`)
29
92
System in continuous or discrete time for which the norm should be computed.
30
93
p : int or str
31
- Type of norm to be computed. p=2 gives the H_2 norm, and p='inf' gives the L_infinity norm.
94
+ Type of norm to be computed. p=2 gives the H2 norm, and p='inf' gives the L-infinity norm.
32
95
tol : float
33
- Relative tolerance for accuracy of L_infinity norm computation. Ignored
96
+ Relative tolerance for accuracy of L-infinity norm computation. Ignored
34
97
unless p='inf'.
35
98
print_warning : bool
36
99
Print warning message in case norm value may be uncertain.
100
+ use_slycot : bool
101
+ Use Slycot routines if available.
37
102
38
103
Returns
39
104
-------
@@ -42,7 +107,7 @@ def norm(system, p=2, tol=1e-6, print_warning=True):
42
107
43
108
Notes
44
109
-----
45
- Does not yet compute the L_infinity norm for discrete time systems with pole(s) in z=0.
110
+ Does not yet compute the L-infinity norm for discrete time systems with pole(s) in z=0 unless Slycot is used .
46
111
47
112
Examples
48
113
--------
@@ -58,123 +123,163 @@ def norm(system, p=2, tol=1e-6, print_warning=True):
58
123
C = G .C
59
124
D = G .D
60
125
61
- #
62
- # H_2- norm computation
63
- #
126
+ # -------------------
127
+ # H2 norm computation
128
+ # -------------------
64
129
if p == 2 :
130
+ # --------------------
65
131
# Continuous time case
132
+ # --------------------
66
133
if G .isctime ():
134
+
135
+ # Check for cases with infinite norm
67
136
poles_real_part = G .poles ().real
68
- if any (np .isclose (poles_real_part , 0.0 )):
137
+ if any (np .isclose (poles_real_part , 0.0 )): # Poles on imaginary axis
69
138
if print_warning :
70
- print ( "Warning: Poles close to, or on, the imaginary axis. Norm value may be uncertain." )
139
+ warnings . warn ( " Poles close to, or on, the imaginary axis. Norm value may be uncertain." )
71
140
return float ('inf' )
72
- elif (D != 0 ).any () or any (poles_real_part > 0.0 ): # System unstable or has direct feedthrough?
141
+ elif any (poles_real_part > 0.0 ): # System unstable
142
+ if print_warning :
143
+ warnings .warn ("System is unstable!" )
73
144
return float ('inf' )
74
- else :
75
- try :
76
- P = ct .lyap (A , B @B .T )
77
- except Exception as e :
78
- print (f"An error occurred solving the continuous time Lyapunov equation: { e } " )
79
- return None
145
+ elif any (D .flat != 0 ): # System has direct feedthrough
146
+ if print_warning :
147
+ warnings .warn ("System has a direct feedthrough term!" )
148
+ return float ('inf' )
149
+
150
+ else :
151
+ # Use slycot, if available, to compute (finite) norm
152
+ if ct .slycot_check () and use_slycot :
153
+ return _h2norm_slycot (G , print_warning )
80
154
81
- # System is stable to reach this point, and P should be positive semi-definite.
82
- # Test next is a precaution in case the Lyapunov equation is ill conditioned.
83
- if any (la .eigvals (P ) < 0.0 ):
84
- if print_warning :
85
- print ("Warning: There appears to be poles close to the imaginary axis. Norm value may be uncertain." )
86
- return float ('inf' )
155
+ # Else use scipy
87
156
else :
88
- norm_value = np .sqrt (np .trace (C @P @C .T )) # Argument in sqrt should be non-negative
89
- if np .isnan (norm_value ):
90
- print ("Unknown error. Norm computation resulted in NaN." )
91
- return None
157
+ P = ct .lyap (A , B @B .T ) # Solve for controllability Gramian
158
+
159
+ # System is stable to reach this point, and P should be positive semi-definite.
160
+ # Test next is a precaution in case the Lyapunov equation is ill conditioned.
161
+ if any (la .eigvals (P ).real < 0.0 ):
162
+ if print_warning :
163
+ warnings .warn ("There appears to be poles close to the imaginary axis. Norm value may be uncertain." )
164
+ return float ('inf' )
92
165
else :
93
- return norm_value
166
+ norm_value = np .sqrt (np .trace (C @P @C .T )) # Argument in sqrt should be non-negative
167
+ if np .isnan (norm_value ):
168
+ raise ct .ControlArgument ("Norm computation resulted in NaN." )
169
+ else :
170
+ return norm_value
94
171
172
+ # ------------------
95
173
# Discrete time case
174
+ # ------------------
96
175
elif G .isdtime ():
176
+
177
+ # Check for cases with infinite norm
97
178
poles_abs = abs (G .poles ())
98
- if any (np .isclose (poles_abs , 1.0 )):
179
+ if any (np .isclose (poles_abs , 1.0 )): # Poles on imaginary axis
99
180
if print_warning :
100
- print ( "Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain." )
181
+ warnings . warn ( " Poles close to, or on, the complex unit circle. Norm value may be uncertain." )
101
182
return float ('inf' )
102
- elif any (poles_abs > 1.0 ): # System unstable?
183
+ elif any (poles_abs > 1.0 ): # System unstable
184
+ if print_warning :
185
+ warnings .warn ("System is unstable!" )
103
186
return float ('inf' )
187
+
104
188
else :
105
- try :
189
+ # Use slycot, if available, to compute (finite) norm
190
+ if ct .slycot_check () and use_slycot :
191
+ return _h2norm_slycot (G , print_warning )
192
+
193
+ # Else use scipy
194
+ else :
106
195
P = ct .dlyap (A , B @B .T )
107
- except Exception as e :
108
- print (f"An error occurred solving the discrete time Lyapunov equation: { e } " )
109
- return None
110
196
111
197
# System is stable to reach this point, and P should be positive semi-definite.
112
198
# Test next is a precaution in case the Lyapunov equation is ill conditioned.
113
- if any (la .eigvals (P ) < 0.0 ):
199
+ if any (la .eigvals (P ). real < 0.0 ):
114
200
if print_warning :
115
- print ("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain." )
201
+ warnings . warn ("Warning: There appears to be poles close to the complex unit circle. Norm value may be uncertain." )
116
202
return float ('inf' )
117
203
else :
118
204
norm_value = np .sqrt (np .trace (C @P @C .T + D @D .T )) # Argument in sqrt should be non-negative
119
205
if np .isnan (norm_value ):
120
- print ("Unknown error. Norm computation resulted in NaN." )
121
- return None
206
+ raise ct .ControlArgument ("Norm computation resulted in NaN." )
122
207
else :
123
- return norm_value
124
- #
125
- # L_infinity-norm computation
126
- #
208
+ return norm_value
209
+
210
+ # ---------------------------
211
+ # L-infinity norm computation
212
+ # ---------------------------
127
213
elif p == "inf" :
128
- def _Hamilton_matrix (gamma ):
129
- """Constructs Hamiltonian matrix. For internal use."""
130
- R = Ip * gamma ** 2 - D .T @D
131
- invR = la .inv (R )
132
- return np .block ([[A + B @invR @D .T @C , B @invR @B .T ], [- C .T @(Ip + D @invR @D .T )@C , - (A + B @invR @D .T @C ).T ]])
133
-
134
- # Discrete time case
135
- # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0.
136
- # Allows us to use test for continuous time systems next.
137
- if G .isdtime ():
138
- Ad = A
139
- Bd = B
140
- Cd = C
141
- Dd = D
142
- if any (np .isclose (abs (la .eigvals (Ad )), 1.0 )):
214
+
215
+ # Check for cases with infinite norm
216
+ poles = G .poles ()
217
+ if G .isdtime (): # Discrete time
218
+ if any (np .isclose (abs (poles ), 1.0 )): # Poles on unit circle
143
219
if print_warning :
144
- print ("Warning: Poles close to, or on, the complex unit circle. Norm value may be uncertain." )
220
+ warnings .warn ("Poles close to, or on, the complex unit circle. Norm value may be uncertain." )
221
+ return float ('inf' )
222
+ else : # Continuous time
223
+ if any (np .isclose (poles .real , 0.0 )): # Poles on imaginary axis
224
+ if print_warning :
225
+ warnings .warn ("Poles close to, or on, the imaginary axis. Norm value may be uncertain." )
145
226
return float ('inf' )
146
- elif any (np .isclose (la .eigvals (Ad ), 0.0 )):
147
- print ("L_infinity norm computation for discrete time system with pole(s) at z=0 currently not supported." )
148
- return None
149
-
150
- # Inverse bilinear transformation
151
- In = np .eye (len (Ad ))
152
- Adinv = la .inv (Ad + In )
153
- A = 2 * (Ad - In )@Adinv
154
- B = 2 * Adinv @Bd
155
- C = 2 * Cd @Adinv
156
- D = Dd - Cd @Adinv @Bd
157
-
158
- # Continus time case
159
- if any (np .isclose (la .eigvals (A ).real , 0.0 )):
160
- if print_warning :
161
- print ("Warning: Poles close to, or on, imaginary axis. Norm value may be uncertain." )
162
- return float ('inf' )
163
227
164
- gaml = la .norm (D ,ord = 2 ) # Lower bound
165
- gamu = max (1.0 , 2.0 * gaml ) # Candidate upper bound
166
- Ip = np .eye (len (D ))
167
-
168
- while any (np .isclose (la .eigvals (_Hamilton_matrix (gamu )).real , 0.0 )): # Find actual upper bound
169
- gamu *= 2.0
228
+ # Use slycot, if available, to compute (finite) norm
229
+ if ct .slycot_check () and use_slycot :
230
+ return ct .linfnorm (G , tol )[0 ]
170
231
171
- while (gamu - gaml )/ gamu > tol :
172
- gam = (gamu + gaml )/ 2.0
173
- if any (np .isclose (la .eigvals (_Hamilton_matrix (gam )).real , 0.0 )):
174
- gaml = gam
175
- else :
176
- gamu = gam
177
- return gam
232
+ # Else use scipy
233
+ else :
234
+
235
+ # ------------------
236
+ # Discrete time case
237
+ # ------------------
238
+ # Use inverse bilinear transformation of discrete time system to s-plane if no poles on |z|=1 or z=0.
239
+ # Allows us to use test for continuous time systems next.
240
+ if G .isdtime ():
241
+ Ad = A
242
+ Bd = B
243
+ Cd = C
244
+ Dd = D
245
+ if any (np .isclose (la .eigvals (Ad ), 0.0 )):
246
+ raise ct .ControlArgument ("L-infinity norm computation for discrete time system with pole(s) in z=0 currently not supported unless Slycot installed." )
247
+
248
+ # Inverse bilinear transformation
249
+ In = np .eye (len (Ad ))
250
+ Adinv = la .inv (Ad + In )
251
+ A = 2 * (Ad - In )@Adinv
252
+ B = 2 * Adinv @Bd
253
+ C = 2 * Cd @Adinv
254
+ D = Dd - Cd @Adinv @Bd
255
+
256
+ # --------------------
257
+ # Continuous time case
258
+ # --------------------
259
+ def _Hamilton_matrix (gamma ):
260
+ """Constructs Hamiltonian matrix. For internal use."""
261
+ R = Ip * gamma ** 2 - D .T @D
262
+ invR = la .inv (R )
263
+ return np .block ([[A + B @invR @D .T @C , B @invR @B .T ], [- C .T @(Ip + D @invR @D .T )@C , - (A + B @invR @D .T @C ).T ]])
264
+
265
+ gaml = la .norm (D ,ord = 2 ) # Lower bound
266
+ gamu = max (1.0 , 2.0 * gaml ) # Candidate upper bound
267
+ Ip = np .eye (len (D ))
268
+
269
+ while any (np .isclose (la .eigvals (_Hamilton_matrix (gamu )).real , 0.0 )): # Find actual upper bound
270
+ gamu *= 2.0
271
+
272
+ while (gamu - gaml )/ gamu > tol :
273
+ gam = (gamu + gaml )/ 2.0
274
+ if any (np .isclose (la .eigvals (_Hamilton_matrix (gam )).real , 0.0 )):
275
+ gaml = gam
276
+ else :
277
+ gamu = gam
278
+ return gam
279
+
280
+ # ----------------------
281
+ # Other norm computation
282
+ # ----------------------
178
283
else :
179
- print (f"Norm computation for p={ p } currently not supported." )
180
- return None
284
+ raise ct . ControlArgument (f"Norm computation for p={ p } currently not supported." )
285
+
0 commit comments