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
+ """
5
7
6
8
import numpy as np
7
- from control import statesp as ss
9
+ from control import statesp
10
+ from control .exception import ControlArgument , ControlDimension
8
11
9
12
try :
10
13
import cvxopt as cvx
11
- except ImportError as e :
14
+ except ImportError :
12
15
cvx = None
13
16
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.
14
25
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.
18
33
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:
21
35
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.
25
43
26
44
Parameters
27
45
----------
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
30
52
31
53
Returns
32
54
-------
33
- bool:
34
- The input system passive.
35
- '''
55
+ solution : ndarray
56
+ The LMI solution
57
+ """
36
58
if cvx is None :
37
59
raise ModuleNotFoundError ("cvxopt required for passivity module" )
38
60
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 )
40
70
41
71
A = sys .A
42
72
B = sys .B
@@ -45,43 +75,218 @@ def ispassive(sys):
45
75
46
76
# account for strictly proper systems
47
77
[n , m ] = D .shape
48
- D = D + np . nextafter ( 0 , 1 ) * np .eye (n , m )
78
+ D = D + eps * np .eye (n , m )
49
79
50
80
[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 )
76
146
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
77
164
number_of_opt_vars = int (
78
- (state_space_size ** 2 - state_space_size )/ 2 + state_space_size )
165
+ (n ** 2 - n )/ 2 + n )
79
166
c = cvx .matrix (0.0 , (number_of_opt_vars , 1 ))
80
167
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
+
81
175
# crunch feasibility solution
82
176
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.
86
206
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