1- '''
2- Author: Mark Yeatman
3- Date: May 15, 2022
4- '''
1+ """
2+ Functions for passive control.
3+
4+ Author: Mark Yeatman
5+ Date: July 17, 2022
6+ """
57
68import numpy as np
7- from control import statesp as ss
9+ from control import statesp
10+ from control .exception import ControlArgument , ControlDimension
811
912try :
1013 import cvxopt as cvx
11- except ImportError as e :
14+ except ImportError :
1215 cvx = None
1316
17+ eps = np .nextafter (0 , 1 )
18+
19+ __all__ = ["get_output_fb_index" , "get_input_ff_index" , "ispassive" ,
20+ "solve_passivity_LMI" ]
21+
22+
23+ def solve_passivity_LMI (sys , rho = None , nu = None ):
24+ """Compute passivity indices and/or solves feasiblity via a LMI.
1425
15- def ispassive (sys ):
16- '''
17- Indicates if a linear time invariant (LTI) system is passive
26+ Constructs a linear matrix inequality (LMI) such that if a solution exists
27+ and the last element of the solution is positive, the system `sys` is
28+ passive. Inputs of None for `rho` or `nu` indicate that the function should
29+ solve for that index (they are mutually exclusive, they can't both be
30+ None, otherwise you're trying to solve a nonconvex bilinear matrix
31+ inequality.) The last element of `solution` is either the output or input
32+ passivity index, for `rho` = None and `nu` = None respectively.
1833
19- Constructs a linear matrix inequality and a feasibility optimization
20- such that if a solution exists, the system is passive.
34+ The sources for the algorithm are:
2135
22- The source for the algorithm is:
23- McCourt, Michael J., and Panos J. Antsaklis.
24- "Demonstrating passivity and dissipativity using computational methods." ISIS 8 (2013).
36+ McCourt, Michael J., and Panos J. Antsaklis
37+ "Demonstrating passivity and dissipativity using computational
38+ methods."
39+
40+ Nicholas Kottenstette and Panos J. Antsaklis
41+ "Relationships Between Positive Real, Passive Dissipative, & Positive
42+ Systems", equation 36.
2543
2644 Parameters
2745 ----------
28- sys: A continuous LTI system
29- System to be checked.
46+ sys : LTI
47+ System to be checked
48+ rho : float or None
49+ Output feedback passivity index
50+ nu : float or None
51+ Input feedforward passivity index
3052
3153 Returns
3254 -------
33- bool:
34- The input system passive.
35- '''
55+ solution : ndarray
56+ The LMI solution
57+ """
3658 if cvx is None :
3759 raise ModuleNotFoundError ("cvxopt required for passivity module" )
3860
39- sys = ss ._convert_to_statespace (sys )
61+ if sys .ninputs != sys .noutputs :
62+ raise ControlDimension (
63+ "The number of system inputs must be the same as the number of "
64+ "system outputs." )
65+
66+ if rho is None and nu is None :
67+ raise ControlArgument ("rho or nu must be given a numerical value." )
68+
69+ sys = statesp ._convert_to_statespace (sys )
4070
4171 A = sys .A
4272 B = sys .B
@@ -45,43 +75,218 @@ def ispassive(sys):
4575
4676 # account for strictly proper systems
4777 [n , m ] = D .shape
48- D = D + np . nextafter ( 0 , 1 ) * np .eye (n , m )
78+ D = D + eps * np .eye (n , m )
4979
5080 [n , _ ] = A .shape
51- A = A - np .nextafter (0 , 1 )* np .eye (n )
52-
53- def make_LMI_matrix (P ):
54- V = np .vstack ((
55- np .hstack ((A .T @ P + P @A , P @B )),
56- np .hstack ((B .T @P , np .zeros_like (D ))))
57- )
58- return V
59-
60- matrix_list = []
61- state_space_size = sys .nstates
62- for i in range (0 , state_space_size ):
63- for j in range (0 , state_space_size ):
64- if j <= i :
65- P = np .zeros_like (A )
66- P [i , j ] = 1.0
67- P [j , i ] = 1.0
68- matrix_list .append (make_LMI_matrix (P ).flatten ())
69-
70- coefficents = np .vstack (matrix_list ).T
71-
72- constants = - np .vstack ((
73- np .hstack ((np .zeros_like (A ), - C .T )),
74- np .hstack ((- C , - D - D .T )))
75- )
81+ A = A - eps * np .eye (n )
82+
83+ def make_LMI_matrix (P , rho , nu , one ):
84+ q = sys .noutputs
85+ Q = - rho * np .eye (q , q )
86+ S = 1.0 / 2.0 * (one + rho * nu )* np .eye (q )
87+ R = - nu * np .eye (m )
88+ if sys .isctime ():
89+ off_diag = P @B - (C .T @S + C .T @Q @D )
90+ return np .vstack ((
91+ np .hstack ((A .T @ P + P @A - C .T @Q @C , off_diag )),
92+ np .hstack ((off_diag .T , - (D .T @Q @D + D .T @S + S .T @D + R )))
93+ ))
94+ else :
95+ off_diag = A .T @P @B - (C .T @S + C .T @Q @D )
96+ return np .vstack ((
97+ np .hstack ((A .T @ P @ A - P - C .T @Q @C , off_diag )),
98+ np .hstack ((off_diag .T , B .T @P @B - (D .T @Q @D + D .T @S + S .T @D + R )))
99+ ))
100+
101+ def make_P_basis_matrices (n , rho , nu ):
102+ """Make list of matrix constraints for passivity LMI.
103+
104+ Utility function to make basis matrices for a LMI from a
105+ symmetric matrix P of size n by n representing a parametrized symbolic
106+ matrix
107+ """
108+ matrix_list = []
109+ for i in range (0 , n ):
110+ for j in range (0 , n ):
111+ if j <= i :
112+ P = np .zeros ((n , n ))
113+ P [i , j ] = 1
114+ P [j , i ] = 1
115+ matrix_list .append (make_LMI_matrix (P , 0 , 0 , 0 ).flatten ())
116+ zeros = eps * np .eye (n )
117+ if rho is None :
118+ matrix_list .append (make_LMI_matrix (zeros , 1 , 0 , 0 ).flatten ())
119+ elif nu is None :
120+ matrix_list .append (make_LMI_matrix (zeros , 0 , 1 , 0 ).flatten ())
121+ return matrix_list
122+
123+
124+ def P_pos_def_constraint (n ):
125+ """Make a list of matrix constraints for P >= 0.
126+
127+ Utility function to make basis matrices for a LMI that ensures
128+ parametrized symbolic matrix of size n by n is positive definite
129+ """
130+ matrix_list = []
131+ for i in range (0 , n ):
132+ for j in range (0 , n ):
133+ if j <= i :
134+ P = np .zeros ((n , n ))
135+ P [i , j ] = - 1
136+ P [j , i ] = - 1
137+ matrix_list .append (P .flatten ())
138+ if rho is None or nu is None :
139+ matrix_list .append (np .zeros ((n , n )).flatten ())
140+ return matrix_list
141+
142+ n = sys .nstates
143+
144+ # coefficents for passivity indices and feasibility matrix
145+ sys_matrix_list = make_P_basis_matrices (n , rho , nu )
76146
147+ # get constants for numerical values of rho and nu
148+ sys_constants = list ()
149+ if rho is not None and nu is not None :
150+ sys_constants = - make_LMI_matrix (np .zeros_like (A ), rho , nu , 1.0 )
151+ elif rho is not None :
152+ sys_constants = - make_LMI_matrix (np .zeros_like (A ), rho , eps , 1.0 )
153+ elif nu is not None :
154+ sys_constants = - make_LMI_matrix (np .zeros_like (A ), eps , nu , 1.0 )
155+
156+ sys_coefficents = np .vstack (sys_matrix_list ).T
157+
158+ # LMI to ensure P is positive definite
159+ P_matrix_list = P_pos_def_constraint (n )
160+ P_coefficents = np .vstack (P_matrix_list ).T
161+ P_constants = np .zeros ((n , n ))
162+
163+ # cost function
77164 number_of_opt_vars = int (
78- (state_space_size ** 2 - state_space_size )/ 2 + state_space_size )
165+ (n ** 2 - n )/ 2 + n )
79166 c = cvx .matrix (0.0 , (number_of_opt_vars , 1 ))
80167
168+ #we're maximizing a passivity index, include it in the cost function
169+ if rho is None or nu is None :
170+ c = cvx .matrix (np .append (np .array (c ), - 1.0 ))
171+
172+ Gs = [cvx .matrix (sys_coefficents )] + [cvx .matrix (P_coefficents )]
173+ hs = [cvx .matrix (sys_constants )] + [cvx .matrix (P_constants )]
174+
81175 # crunch feasibility solution
82176 cvx .solvers .options ['show_progress' ] = False
83- sol = cvx .solvers .sdp (c ,
84- Gs = [cvx .matrix (coefficents )],
85- hs = [cvx .matrix (constants )])
177+ sol = cvx .solvers .sdp (c , Gs = Gs , hs = hs )
178+ return sol ["x" ]
179+
180+
181+ def get_output_fb_index (sys ):
182+ """Return the output feedback passivity (OFP) index for the system.
183+
184+ The OFP is the largest gain that can be placed in positive feedback
185+ with a system such that the new interconnected system is passive.
186+
187+ Parameters
188+ ----------
189+ sys : LTI
190+ System to be checked
191+
192+ Returns
193+ -------
194+ float
195+ The OFP index
196+ """
197+ sol = solve_passivity_LMI (sys , nu = eps )
198+ if sol is None :
199+ raise RuntimeError ("LMI passivity problem is infeasible" )
200+ else :
201+ return sol [- 1 ]
202+
203+
204+ def get_input_ff_index (sys ):
205+ """Return the input feedforward passivity (IFP) index for the system.
86206
87- return (sol ["x" ] is not None )
207+ The IFP is the largest gain that can be placed in negative parallel
208+ interconnection with a system such that the new interconnected system is
209+ passive.
210+
211+ Parameters
212+ ----------
213+ sys : LTI
214+ System to be checked.
215+
216+ Returns
217+ -------
218+ float
219+ The IFP index
220+ """
221+ sol = solve_passivity_LMI (sys , rho = eps )
222+ if sol is None :
223+ raise RuntimeError ("LMI passivity problem is infeasible" )
224+ else :
225+ return sol [- 1 ]
226+
227+
228+ def get_relative_index (sys ):
229+ """Return the relative passivity index for the system.
230+
231+ (not implemented yet)
232+ """
233+ raise NotImplementedError ("Relative passivity index not implemented" )
234+
235+
236+ def get_combined_io_index (sys ):
237+ """Return the combined I/O passivity index for the system.
238+
239+ (not implemented yet)
240+ """
241+ raise NotImplementedError ("Combined I/O passivity index not implemented" )
242+
243+
244+ def get_directional_index (sys ):
245+ """Return the directional passivity index for the system.
246+
247+ (not implemented yet)
248+ """
249+ raise NotImplementedError ("Directional passivity index not implemented" )
250+
251+
252+ def ispassive (sys , ofp_index = 0 , ifp_index = 0 ):
253+ r"""Indicate if a linear time invariant (LTI) system is passive.
254+
255+ Checks if system is passive with the given output feedback (OFP) and input
256+ feedforward (IFP) passivity indices.
257+
258+ Parameters
259+ ----------
260+ sys : LTI
261+ System to be checked
262+ ofp_index : float
263+ Output feedback passivity index
264+ ifp_index : float
265+ Input feedforward passivity index
266+
267+ Returns
268+ -------
269+ bool
270+ The system is passive.
271+
272+ Notes
273+ -----
274+ Querying if the system is passive in the sense of
275+
276+ .. math:: V(x) >= 0 \land \dot{V}(x) <= y^T u
277+
278+ is equivalent to the default case of `ofp_index` = 0 and `ifp_index` = 0.
279+ Note that computing the `ofp_index` and `ifp_index` for a system, then
280+ using both values simultaneously as inputs to this function is not
281+ guaranteed to have an output of True (the system might not be passive with
282+ both indices at the same time).
283+
284+ For more details, see [1].
285+
286+ References
287+ ----------
288+ .. [1] McCourt, Michael J., and Panos J. Antsaklis
289+ "Demonstrating passivity and dissipativity using computational
290+ methods."
291+ """
292+ return solve_passivity_LMI (sys , rho = ofp_index , nu = ifp_index ) is not None
0 commit comments