@@ -103,9 +103,8 @@ def __init__(
103103 # Initial guess
104104 #
105105 # 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.
109108 #
110109 self .initial_guess = np .zeros (
111110 self .system .ninputs * self .time_vector .size )
@@ -128,24 +127,25 @@ def cost_function(self, inputs):
128127 x = self .x
129128 inputs = inputs .reshape (
130129 (self .system .ninputs , self .time_vector .size ))
131-
130+
132131 # Simulate the system to get the state
133132 _ , _ , states = ct .input_output_response (
134133 self .system , self .time_vector , inputs , x , return_x = True )
135-
134+
136135 # Trajectory cost
137136 # TODO: vectorize
138137 cost = 0
139138 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+
142141 # Terminal cost
143142 if self .terminal_cost is not None :
144143 cost += self .terminal_cost (states [:,- 1 ], inputs [:,- 1 ])
145-
144+
146145 # Return the total cost for this input sequence
147146 return cost
148147
148+
149149 #
150150 # Constraints
151151 #
@@ -188,7 +188,7 @@ def constraint_function(self, inputs):
188188 x = self .x
189189 inputs = inputs .reshape (
190190 (self .system .ninputs , self .time_vector .size ))
191-
191+
192192 # Simulate the system to get the state
193193 _ , _ , states = ct .input_output_response (
194194 self .system , self .time_vector , inputs , x , return_x = True )
@@ -234,15 +234,15 @@ def __call__(self, x, squeeze=None):
234234 def mpc (self , x , squeeze = None ):
235235 """Compute the optimal input at state x"""
236236 _ , 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 ]
238238
239239 # Compute the optimal trajectory from the current state
240240 def compute_trajectory (
241241 self , x , squeeze = None , transpose = None , return_x = None ):
242242 """Compute the optimal input at state x"""
243243 # Store the initial state (for use in constraint_function)
244244 self .x = x
245-
245+
246246 # Call ScipPy optimizer
247247 res = sp .optimize .minimize (
248248 self .cost_function , self .initial_guess ,
@@ -263,14 +263,33 @@ def compute_trajectory(
263263 self .system , self .time_vector , inputs , x , return_x = True )
264264 else :
265265 states = None
266-
266+
267267 return _process_time_response (
268268 self .system , self .time_vector , inputs , states ,
269269 transpose = transpose , return_x = return_x , squeeze = squeeze )
270270
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+
271290
272291#
273- # Create a polytope constraint on the system state
292+ # Create a polytope constraint on the system state: A x <= b
274293#
275294# As in the cost function evaluation, the main "trick" in creating a constrain
276295# on the state or input is to properly evaluate the constraint on the stacked
@@ -283,26 +302,48 @@ def compute_trajectory(
283302# optimization methods LinearConstraint and NonlinearConstraint as "types" to
284303# keep things consistent with the terminology in scipy.optimize.
285304#
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 ):
287316 """Create state constraint from polytope"""
288317 # TODO: make sure the system and constraints are compatible
289318
290319 # Return a linear constraint object based on the polynomial
291320 return (opt .LinearConstraint ,
292321 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+
295325
296326# 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 ):
298339 """Create input constraint from polytope"""
299340 # TODO: make sure the system and constraints are compatible
300341
301342 # Return a linear constraint object based on the polynomial
302343 return (opt .LinearConstraint ,
303344 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 ) )
306347
307348
308349#
@@ -316,9 +357,9 @@ def input_poly_constraint(sys, polytope):
316357# imposing a linear constraint:
317358#
318359# 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))])
320361#
321- def output_poly_constraint (sys , polytope ):
362+ def output_poly_constraint (sys , A , b ):
322363 """Create output constraint from polytope"""
323364 # TODO: make sure the system and constraints are compatible
324365
@@ -329,12 +370,12 @@ def _evaluate_output_constraint(x):
329370 states = x [:sys .nstates ]
330371 inputs = x [sys .nstates :]
331372 outputs = sys ._out (0 , states , inputs )
332- return polytope . A @ outputs
373+ return A @ outputs
333374
334375 # Return a nonlinear constraint object based on the polynomial
335376 return (opt .NonlinearConstraint ,
336377 _evaluate_output_constraint ,
337- np .full (polytope . A .shape [0 ], - np .inf ), polytope . b )
378+ np .full (A .shape [0 ], - np .inf ), b )
338379
339380
340381#
@@ -345,8 +386,8 @@ def _evaluate_output_constraint(x):
345386# evaluates to associted quadratic cost. This is compatible with the way that
346387# the `cost_function` evaluates the cost at each point in the trajectory.
347388#
348- def quadratic_cost (sys , Q , R ):
389+ def quadratic_cost (sys , Q , R , x0 = 0 , u0 = 0 ):
349390 """Create quadratic cost function"""
350391 Q = np .atleast_2d (Q )
351392 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