1212"""
1313
1414import numpy as np
15+ import scipy as sp
1516import numpy .linalg as la
17+ import warnings
1618
1719import control as ct
1820
1921__all__ = ['norm' ]
2022
2123#------------------------------------------------------------------------------
2224
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 ):
2487 """Computes norm of system.
2588
2689 Parameters
2790 ----------
2891 system : LTI (:class:`StateSpace` or :class:`TransferFunction`)
2992 System in continuous or discrete time for which the norm should be computed.
3093 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.
3295 tol : float
33- Relative tolerance for accuracy of L_infinity norm computation. Ignored
96+ Relative tolerance for accuracy of L-infinity norm computation. Ignored
3497 unless p='inf'.
3598 print_warning : bool
3699 Print warning message in case norm value may be uncertain.
100+ use_slycot : bool
101+ Use Slycot routines if available.
37102
38103 Returns
39104 -------
@@ -42,7 +107,7 @@ def norm(system, p=2, tol=1e-6, print_warning=True):
42107
43108 Notes
44109 -----
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 .
46111
47112 Examples
48113 --------
@@ -58,123 +123,163 @@ def norm(system, p=2, tol=1e-6, print_warning=True):
58123 C = G .C
59124 D = G .D
60125
61- #
62- # H_2- norm computation
63- #
126+ # -------------------
127+ # H2 norm computation
128+ # -------------------
64129 if p == 2 :
130+ # --------------------
65131 # Continuous time case
132+ # --------------------
66133 if G .isctime ():
134+
135+ # Check for cases with infinite norm
67136 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
69138 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." )
71140 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!" )
73144 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 )
80154
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
87156 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' )
92165 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
94171
172+ # ------------------
95173 # Discrete time case
174+ # ------------------
96175 elif G .isdtime ():
176+
177+ # Check for cases with infinite norm
97178 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
99180 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." )
101182 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!" )
103186 return float ('inf' )
187+
104188 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 :
106195 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
110196
111197 # System is stable to reach this point, and P should be positive semi-definite.
112198 # 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 ):
114200 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." )
116202 return float ('inf' )
117203 else :
118204 norm_value = np .sqrt (np .trace (C @P @C .T + D @D .T )) # Argument in sqrt should be non-negative
119205 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." )
122207 else :
123- return norm_value
124- #
125- # L_infinity-norm computation
126- #
208+ return norm_value
209+
210+ # ---------------------------
211+ # L-infinity norm computation
212+ # ---------------------------
127213 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
143219 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." )
145226 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' )
163227
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 ]
170231
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+ # ----------------------
178283 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