23
23
# Define module default parameter values
24
24
_optimal_trajectory_methods = {'shooting' ,'collocation' }
25
25
_optimal_defaults = {
26
- 'optimal.trajectory_method' :'shooting' ,
27
26
'optimal.minimize_method' :None ,
28
27
'optimal.minimize_options' : {},
29
28
'optimal.minimize_kwargs' : {},
@@ -147,9 +146,8 @@ def __init__(
147
146
self .terminal_constraints = terminal_constraints
148
147
self .basis = basis
149
148
150
- # Keep track of what type oftrajector method we are using
149
+ # Keep track of what type oftrajectory method we are using
151
150
if trajectory_method is None :
152
- # TODO: change default
153
151
trajectory_method = 'collocation' if sys .isctime ()else 'shooting'
154
152
elif trajectory_method not in _optimal_trajectory_methods :
155
153
raise NotImplementedError (f"Unkown method{ method } " )
@@ -185,30 +183,23 @@ def __init__(
185
183
raise TypeError ("unrecognized keyword(s): " ,str (kwargs ))
186
184
187
185
# Process trajectory constraints
188
- if isinstance (trajectory_constraints ,tuple ):
189
- self .trajectory_constraints = [trajectory_constraints ]
190
- elif not isinstance (trajectory_constraints ,list ):
191
- raise TypeError ("trajectory constraints must be a list" )
192
- else :
193
- self .trajectory_constraints = trajectory_constraints
186
+ def _process_constraints (constraint_list ,name ):
187
+ if isinstance (constraint_list ,tuple ):
188
+ constraint_list = [constraint_list ]
189
+ elif not isinstance (constraint_list ,list ):
190
+ raise TypeError (f"{ name } constraints must be a list" )
194
191
195
- # Make sure that we recognize all of the constraint types
196
- for ctype ,fun ,lb ,ub in self . trajectory_constraints :
197
- if not ctype in [opt .LinearConstraint ,opt .NonlinearConstraint ]:
198
- raise TypeError (f"unknown constraint type{ ctype } " )
192
+ # Make sure that we recognize all of the constraint types
193
+ for ctype ,fun ,lb ,ub in constraint_list :
194
+ if not ctype in [opt .LinearConstraint ,opt .NonlinearConstraint ]:
195
+ raise TypeError (f"unknown{ name } constraint type{ ctype } " )
199
196
200
- # Process terminal constraints
201
- if isinstance (terminal_constraints ,tuple ):
202
- self .terminal_constraints = [terminal_constraints ]
203
- elif not isinstance (terminal_constraints ,list ):
204
- raise TypeError ("terminal constraints must be a list" )
205
- else :
206
- self .terminal_constraints = terminal_constraints
197
+ return constraint_list
207
198
208
- # Make sure that we recognize all of the constraint types
209
- for ctype , fun , lb , ub in self . terminal_constraints :
210
- if not ctype in [ opt . LinearConstraint , opt . NonlinearConstraint ]:
211
- raise TypeError ( f"unknown constraint type { ctype } " )
199
+ self . trajectory_constraints = _process_constraints (
200
+ trajectory_constraints , "trajectory" )
201
+ self . terminal_constraints = _process_constraints (
202
+ terminal_constraints , "terminal " )
212
203
213
204
#
214
205
# Compute and store constraints
@@ -223,10 +214,13 @@ def __init__(
223
214
# is consistent with the `_constraint_function` that is used at
224
215
# evaluation time.
225
216
#
217
+ # TODO: when using the collocation method, linear constraints on the
218
+ # states and inputs can potentially maintain their linear structure
219
+ # rather than being converted to nonlinear constraints.
220
+ #
226
221
constraint_lb ,constraint_ub ,eqconst_value = [], [], []
227
222
228
223
# Go through each time point and stack the bounds
229
- # TODO: for collocation method, keep track of linear vs nonlinear
230
224
for t in self .timepts :
231
225
for type ,fun ,lb ,ub in self .trajectory_constraints :
232
226
if np .all (lb == ub ):
@@ -253,15 +247,19 @@ def __init__(
253
247
self .eqconst_value = np .hstack (eqconst_value )if eqconst_value else []
254
248
255
249
# Create the constraints (inequality and equality)
250
+ # TODO: for collocation method, keep track of linear vs nonlinear
256
251
self .constraints = []
252
+
257
253
if len (self .constraint_lb )!= 0 :
258
254
self .constraints .append (sp .optimize .NonlinearConstraint (
259
255
self ._constraint_function ,self .constraint_lb ,
260
256
self .constraint_ub ))
257
+
261
258
if len (self .eqconst_value )!= 0 :
262
259
self .constraints .append (sp .optimize .NonlinearConstraint (
263
260
self ._eqconst_function ,self .eqconst_value ,
264
261
self .eqconst_value ))
262
+
265
263
if self .collocation :
266
264
# Add the collocation constraints
267
265
colloc_zeros = np .zeros (sys .nstates * self .timepts .size )
@@ -275,7 +273,7 @@ def __init__(
275
273
# Process the initial guess
276
274
self .initial_guess = self ._process_initial_guess (initial_guess )
277
275
278
- # Store states, input, used later to minimize re-computation
276
+ # Store states, input ( used later to minimize re-computation)
279
277
self .last_x = np .full (self .system .nstates ,np .nan )
280
278
self .last_coeffs = np .full (self .initial_guess .shape ,np .nan )
281
279
@@ -286,10 +284,14 @@ def __init__(
286
284
#
287
285
# Cost function
288
286
#
289
- # Given the input U = [u[0], ... u[N]], we need to compute the cost of
290
- # the trajectory generated by that input. This means we have to
291
- # simulate the system to get the state trajectory X = [x[0], ...,
292
- # x[N]] and then compute the cost at each point:
287
+ # For collocation methods we are given the states and inputs at each
288
+ # time point and we use a trapezoidal approximation to compute the
289
+ # integral cost, then add on the terminal cost.
290
+ #
291
+ # For shooting methods, given the input U = [u[0], ... u[N]] we need to
292
+ # compute the cost of the trajectory generated by that input. This
293
+ # means we have to simulate the system to get the state trajectory X =
294
+ # [x[0], ..., x[N]] and then compute the cost at each point:
293
295
#
294
296
# cost = sum_k integral_cost(x[k], u[k]) + terminal_cost(x[N], u[N])
295
297
#
@@ -362,11 +364,12 @@ def _cost_function(self, coeffs):
362
364
# is called at each point along the trajectory and compared against the
363
365
# upper and lower bounds.
364
366
#
365
- # * If the upper and lower bound for the constraint are identical, then we
366
- # separate out the evaluation into two different constraints, which
367
- # allows the SciPy optimizers to be more efficient (and stops them from
368
- # generating a warning about mixed constraints). This is handled
369
- # through the use of the `_eqconst_function` and `eqconst_value` members.
367
+ # * If the upper and lower bound for the constraint are identical, then
368
+ # we separate out the evaluation into two different constraints, which
369
+ # allows the SciPy optimizers to be more efficient (and stops them
370
+ # from generating a warning about mixed constraints). This is handled
371
+ # through the use of the `_eqconst_function` and `eqconst_value`
372
+ # members.
370
373
#
371
374
# In both cases, the constraint is specified at a single point, but we
372
375
# extend this to apply to each point in the trajectory. This means
@@ -378,16 +381,12 @@ def _cost_function(self, coeffs):
378
381
# For collocation methods, we can directly evaluate the constraints at
379
382
# the collocation points.
380
383
#
381
- # For shooting methods, we do this by creating a function that
382
- # simulates the system dynamics and returns a vector of values
383
- # corresponding to the value of the function at each time. The
384
- # class initialization methods takes care of replicating the upper
385
- # and lower bounds for each point in time so that the SciPy
386
- # optimization algorithm can do the proper evaluation.
387
- #
388
- # In addition, since SciPy's optimization function does not allow us to
389
- # pass arguments to the constraint function, we have to store the initial
390
- # state prior to optimization and retrieve it here.
384
+ # For shooting methods, we do this by creating a function that simulates
385
+ # the system dynamics and returns a vector of values corresponding to
386
+ # the value of the function at each time. The class initialization
387
+ # methods takes care of replicating the upper and lower bounds for each
388
+ # point in time so that the SciPy optimization algorithm can do the
389
+ # proper evaluation.
391
390
#
392
391
def _constraint_function (self ,coeffs ):
393
392
if self .log :
@@ -437,8 +436,7 @@ def _constraint_function(self, coeffs):
437
436
"_constraint_function elapsed time: %g" ,
438
437
stop_time - start_time )
439
438
440
- # Debugging information
441
- if self .log :
439
+ # Debugging information
442
440
logging .debug (
443
441
"constraint values\n " + str (value )+ "\n " +
444
442
"lb, ub =\n " + str (self .constraint_lb )+ "\n " +
@@ -492,8 +490,7 @@ def _eqconst_function(self, coeffs):
492
490
logging .info (
493
491
"_eqconst_function elapsed time: %g" ,stop_time - start_time )
494
492
495
- # Debugging information
496
- if self .log :
493
+ # Debugging information
497
494
logging .debug (
498
495
"eqconst values\n " + str (value )+ "\n " +
499
496
"desired =\n " + str (self .eqconst_value ))
@@ -515,7 +512,7 @@ def _collocation_constraint(self, coeffs):
515
512
self .timepts [0 ],states [:,0 ],inputs [:,0 ])
516
513
continue
517
514
518
- # M. Kelly, SIAM Review (2017), equation (3.2), i = k+1
515
+ #From M. Kelly, SIAM Review (2017), equation (3.2), i = k+1
519
516
# x[k+1] - x[k] = 0.5 hk (f(x[k+1], u[k+1] + f(x[k], u[k]))
520
517
fkp1 = self .system ._rhs (t ,states [:,i ],inputs [:,i ])
521
518
self .colloc_vals [:,i ]= states [:,i ]- states [:,i - 1 ]- \
@@ -529,18 +526,20 @@ def _collocation_constraint(self, coeffs):
529
526
return self .colloc_vals .reshape (- 1 )
530
527
531
528
#
532
- # Initial guess
529
+ # Initial guess processing
533
530
#
534
531
# We store an initial guess in case it is not specified later. Note
535
532
# that create_mpc_iosystem() will reset the initial guess based on
536
533
# the current state of the MPC controller.
537
534
#
538
- # Note: the initial guess is passed as the inputs at the given time
535
+ # The functions below are used to process the initial guess, which can
536
+ # either consist of an input only (for shooting methods) or an input
537
+ # and/or state trajectory (for collocaiton methods).
538
+ #
539
+ # Note: The initial input guess is passed as the inputs at the given time
539
540
# vector. If a basis is specified, this is converted to coefficient
540
541
# values (which are generally of smaller dimension).
541
542
#
542
- # TODO: figure out how to modify this for collocation
543
- #
544
543
def _process_initial_guess (self ,initial_guess ):
545
544
# Sort out the input guess and the state guess
546
545
if self .collocation and initial_guess is not None and \
@@ -583,6 +582,7 @@ def _process_initial_guess(self, initial_guess):
583
582
# Reshape for use by scipy.optimize.minimize()
584
583
return input_guess .reshape (- 1 )
585
584
585
+ # Utility function to broadcast an initial guess to the right shape
586
586
def _broadcast_initial_guess (self ,initial_guess ,shape ):
587
587
# Convert to a 1D array (or higher)
588
588
initial_guess = np .atleast_1d (initial_guess )
@@ -604,11 +604,11 @@ def _broadcast_initial_guess(self, initial_guess, shape):
604
604
#
605
605
# Utility function to convert input vector to coefficient vector
606
606
#
607
- #Initially guesses from the user are passed as input vectors as a
607
+ #Initial guesses from the user are passed as input vectors as a
608
608
# function of time, but internally we store the guess in terms of the
609
609
# basis coefficients. We do this by solving a least squares problem to
610
- # find coefficients that match the input functions at the time points (as
611
- # much as possible, if the problem is under-determined).
610
+ # find coefficients that match the input functions at the time points
611
+ #(as much as possible, if the problem is under-determined).
612
612
#
613
613
def _inputs_to_coeffs (self ,inputs ):
614
614
# If there is no basis function, just return inputs as coeffs
@@ -638,6 +638,7 @@ def _inputs_to_coeffs(self, inputs):
638
638
# Utility function to convert coefficient vector to input vector
639
639
def _coeffs_to_inputs (self ,coeffs ):
640
640
# TODO: vectorize
641
+ # TODO: use BasisFamily eval() method (if more efficient)?
641
642
inputs = np .zeros ((self .system .ninputs ,self .timepts .size ))
642
643
offset = 0
643
644
for i in range (self .system .ninputs ):
@@ -689,7 +690,7 @@ def _print_statistics(self, reset=True):
689
690
# These internal functions return the states and inputs at the
690
691
# collocation points given the ceofficient (optimizer state) vector.
691
692
# They keep track of whether a shooting method is being used or not and
692
- # simulate thedynamis of needed.
693
+ # simulate thedynamics if needed.
693
694
#
694
695
695
696
# Compute the states and inputs from the coefficients
@@ -738,6 +739,7 @@ def _simulate_states(self, x0, inputs):
738
739
if self .log :
739
740
logging .debug (
740
741
"input_output_response returned states\n " + str (states ))
742
+
741
743
return states
742
744
743
745
#
@@ -930,16 +932,6 @@ def __init__(
930
932
ocp ._print_statistics ()
931
933
print ("* Final cost:" ,self .cost )
932
934
933
- if return_states and inputs .shape [1 ]== ocp .timepts .shape [0 ]:
934
- # Simulate the system if we need the state back
935
- # TODO: this is probably not needed due to compute_states_inputs()
936
- _ ,_ ,states = ct .input_output_response (
937
- ocp .system ,ocp .timepts ,inputs ,ocp .x ,return_x = True ,
938
- solve_ivp_kwargs = ocp .solve_ivp_kwargs ,t_eval = ocp .timepts )
939
- ocp .system_simulations += 1
940
- else :
941
- states = None
942
-
943
935
# Process data as a time response (with "outputs" = inputs)
944
936
response = TimeResponseData (
945
937
ocp .timepts ,inputs ,states ,issiso = ocp .system .issiso (),
@@ -1065,10 +1057,22 @@ def solve_ocp(
1065
1057
'optimal' ,'return_x' ,kwargs ,return_states ,pop = True )
1066
1058
1067
1059
# Process (legacy) method keyword
1068
- if kwargs .get ('method' )and method not in optimal_methods :
1069
- if kwargs .get ('minimize_method' ):
1070
- raise ValueError ("'minimize_method' specified more than once" )
1071
- kwargs ['minimize_method' ]= kwargs .pop ('method' )
1060
+ if kwargs .get ('method' ):
1061
+ method = kwargs .pop ('method' )
1062
+ if method not in optimal_methods :
1063
+ if kwargs .get ('minimize_method' ):
1064
+ raise ValueError ("'minimize_method' specified more than once" )
1065
+ warnings .warn (
1066
+ "'method' parameter is deprecated; assuming minimize_method" ,
1067
+ DeprecationWarning )
1068
+ kwargs ['minimize_method' ]= method
1069
+ else :
1070
+ if kwargs .get ('trajectory_method' ):
1071
+ raise ValueError ("'trajectory_method' specified more than once" )
1072
+ warnings .warn (
1073
+ "'method' parameter is deprecated; assuming trajectory_method" ,
1074
+ DeprecationWarning )
1075
+ kwargs ['trajectory_method' ]= method
1072
1076
1073
1077
# Set up the optimal control problem
1074
1078
ocp = OptimalControlProblem (