@@ -103,9 +103,8 @@ def __init__(
103
103
# Initial guess
104
104
#
105
105
# We store an initial guess (zero input) in case it is not specified
106
- # later.
107
- #
108
- # TODO: add the ability to overwride this when calling the optimizer.
106
+ # later. Note that create_mpc_iosystem() will reset the initial guess
107
+ # based on the current state of the MPC controller.
109
108
#
110
109
self .initial_guess = np .zeros (
111
110
self .system .ninputs * self .time_vector .size )
@@ -128,24 +127,25 @@ def cost_function(self, inputs):
128
127
x = self .x
129
128
inputs = inputs .reshape (
130
129
(self .system .ninputs , self .time_vector .size ))
131
-
130
+
132
131
# Simulate the system to get the state
133
132
_ , _ , states = ct .input_output_response (
134
133
self .system , self .time_vector , inputs , x , return_x = True )
135
-
134
+
136
135
# Trajectory cost
137
136
# TODO: vectorize
138
137
cost = 0
139
138
for i , time in enumerate (self .time_vector ):
140
- cost += self .integral_cost (states [:,i ], inputs [:,i ])
141
-
139
+ cost += self .integral_cost (states [:,i ], inputs [:,i ])
140
+
142
141
# Terminal cost
143
142
if self .terminal_cost is not None :
144
143
cost += self .terminal_cost (states [:,- 1 ], inputs [:,- 1 ])
145
-
144
+
146
145
# Return the total cost for this input sequence
147
146
return cost
148
147
148
+
149
149
#
150
150
# Constraints
151
151
#
@@ -188,7 +188,7 @@ def constraint_function(self, inputs):
188
188
x = self .x
189
189
inputs = inputs .reshape (
190
190
(self .system .ninputs , self .time_vector .size ))
191
-
191
+
192
192
# Simulate the system to get the state
193
193
_ , _ , states = ct .input_output_response (
194
194
self .system , self .time_vector , inputs , x , return_x = True )
@@ -234,15 +234,15 @@ def __call__(self, x, squeeze=None):
234
234
def mpc (self , x , squeeze = None ):
235
235
"""Compute the optimal input at state x"""
236
236
_ , inputs = self .compute_trajectory (x , squeeze = squeeze )
237
- return None if inputs is None else inputs . transpose ()[ 0 ]
237
+ return None if inputs is None else inputs [:, 0 ]
238
238
239
239
# Compute the optimal trajectory from the current state
240
240
def compute_trajectory (
241
241
self , x , squeeze = None , transpose = None , return_x = None ):
242
242
"""Compute the optimal input at state x"""
243
243
# Store the initial state (for use in constraint_function)
244
244
self .x = x
245
-
245
+
246
246
# Call ScipPy optimizer
247
247
res = sp .optimize .minimize (
248
248
self .cost_function , self .initial_guess ,
@@ -263,14 +263,33 @@ def compute_trajectory(
263
263
self .system , self .time_vector , inputs , x , return_x = True )
264
264
else :
265
265
states = None
266
-
266
+
267
267
return _process_time_response (
268
268
self .system , self .time_vector , inputs , states ,
269
269
transpose = transpose , return_x = return_x , squeeze = squeeze )
270
270
271
+ # Create an input/output system implementing an MPC controller
272
+ def create_mpc_iosystem (self , dt = True ):
273
+ def _update (t , x , u , params = {}):
274
+ inputs = x .reshape ((self .system .ninputs , self .time_vector .size ))
275
+ self .initial_guess = np .hstack (
276
+ [inputs [:,1 :], inputs [:,- 1 :]]).reshape (- 1 )
277
+ _ , inputs = self .compute_trajectory (u )
278
+ return inputs .reshape (- 1 )
279
+
280
+ def _output (t , x , u , params = {}):
281
+ inputs = x .reshape ((self .system .ninputs , self .time_vector .size ))
282
+ return inputs [:,0 ]
283
+
284
+ return ct .NonlinearIOSystem (
285
+ _update , _output , dt = dt ,
286
+ inputs = self .system .nstates ,
287
+ outputs = self .system .ninputs ,
288
+ states = self .system .ninputs * self .time_vector .size )
289
+
271
290
272
291
#
273
- # Create a polytope constraint on the system state
292
+ # Create a polytope constraint on the system state: A x <= b
274
293
#
275
294
# As in the cost function evaluation, the main "trick" in creating a constrain
276
295
# on the state or input is to properly evaluate the constraint on the stacked
@@ -283,26 +302,48 @@ def compute_trajectory(
283
302
# optimization methods LinearConstraint and NonlinearConstraint as "types" to
284
303
# keep things consistent with the terminology in scipy.optimize.
285
304
#
286
- def state_poly_constraint (sys , polytope ):
305
+ def state_poly_constraint (sys , A , b ):
306
+ """Create state constraint from polytope"""
307
+ # TODO: make sure the system and constraints are compatible
308
+
309
+ # Return a linear constraint object based on the polynomial
310
+ return (opt .LinearConstraint ,
311
+ np .hstack ([A , np .zeros ((A .shape [0 ], sys .ninputs ))]),
312
+ np .full (A .shape [0 ], - np .inf ), polytope .b )
313
+
314
+
315
+ def state_range_constraint (sys , lb , ub ):
287
316
"""Create state constraint from polytope"""
288
317
# TODO: make sure the system and constraints are compatible
289
318
290
319
# Return a linear constraint object based on the polynomial
291
320
return (opt .LinearConstraint ,
292
321
np .hstack (
293
- [polytope .A , np .zeros ((polytope .A .shape [0 ], sys .ninputs ))]),
294
- np .full (polytope .A .shape [0 ], - np .inf ), polytope .b )
322
+ [np .eye (sys .nstates ), np .zeros ((sys .nstates , sys .ninputs ))]),
323
+ np .array (lb ), np .array (ub ))
324
+
295
325
296
326
# Create a constraint polytope on the system input
297
- def input_poly_constraint (sys , polytope ):
327
+ def input_poly_constraint (sys , A , b ):
328
+ """Create input constraint from polytope"""
329
+ # TODO: make sure the system and constraints are compatible
330
+
331
+ # Return a linear constraint object based on the polynomial
332
+ return (opt .LinearConstraint ,
333
+ np .hstack (
334
+ [np .zeros ((A .shape [0 ], sys .nstates )), A ]),
335
+ np .full (A .shape [0 ], - np .inf ), b )
336
+
337
+
338
+ def input_range_constraint (sys , lb , ub ):
298
339
"""Create input constraint from polytope"""
299
340
# TODO: make sure the system and constraints are compatible
300
341
301
342
# Return a linear constraint object based on the polynomial
302
343
return (opt .LinearConstraint ,
303
344
np .hstack (
304
- [np .zeros ((polytope . A . shape [ 0 ] , sys .nstates )), polytope . A ]),
305
- np .full ( polytope . A . shape [ 0 ], - np .inf ), polytope . b )
345
+ [np .zeros ((sys . ninputs , sys .nstates )), np . eye ( sys . ninputs ) ]),
346
+ np .array ( lb ), np .array ( ub ) )
306
347
307
348
308
349
#
@@ -316,9 +357,9 @@ def input_poly_constraint(sys, polytope):
316
357
# imposing a linear constraint:
317
358
#
318
359
# np.hstack(
319
- # [polytope. A @ sys.C, np.zeros((polytope. A.shape[0], sys.ninputs))])
360
+ # [A @ sys.C, np.zeros((A.shape[0], sys.ninputs))])
320
361
#
321
- def output_poly_constraint (sys , polytope ):
362
+ def output_poly_constraint (sys , A , b ):
322
363
"""Create output constraint from polytope"""
323
364
# TODO: make sure the system and constraints are compatible
324
365
@@ -329,12 +370,12 @@ def _evaluate_output_constraint(x):
329
370
states = x [:sys .nstates ]
330
371
inputs = x [sys .nstates :]
331
372
outputs = sys ._out (0 , states , inputs )
332
- return polytope . A @ outputs
373
+ return A @ outputs
333
374
334
375
# Return a nonlinear constraint object based on the polynomial
335
376
return (opt .NonlinearConstraint ,
336
377
_evaluate_output_constraint ,
337
- np .full (polytope . A .shape [0 ], - np .inf ), polytope . b )
378
+ np .full (A .shape [0 ], - np .inf ), b )
338
379
339
380
340
381
#
@@ -345,8 +386,8 @@ def _evaluate_output_constraint(x):
345
386
# evaluates to associted quadratic cost. This is compatible with the way that
346
387
# the `cost_function` evaluates the cost at each point in the trajectory.
347
388
#
348
- def quadratic_cost (sys , Q , R ):
389
+ def quadratic_cost (sys , Q , R , x0 = 0 , u0 = 0 ):
349
390
"""Create quadratic cost function"""
350
391
Q = np .atleast_2d (Q )
351
392
R = np .atleast_2d (R )
352
- return lambda x , u : x @ Q @ x + u @ R @ u
393
+ return lambda x , u : (( x - x0 ) @ Q @ ( x - x0 ) + ( u - u0 ) @ R @ ( u - u0 )). item ()
0 commit comments