@@ -65,6 +65,9 @@ class OptimalControlProblem():
6565 inputs should either be a 2D vector of shape (ninputs, horizon)
6666 or a 1D input of shape (ninputs,) that will be broadcast by
6767 extension of the time axis.
68+ method : string, optional
69+ Method to use for carrying out the optimization. Currently only
70+ 'shooting' is supported.
6871 log : bool, optional
6972 If `True`, turn on logging messages (using Python logging module).
7073 Use ``logging.basicConfig`` to enable logging output (e.g., to a file).
@@ -79,6 +82,9 @@ class OptimalControlProblem():
7982
8083 Additional parameters
8184 ---------------------
85+ basis : BasisFamily, optional
86+ Use the given set of basis functions for the inputs instead of
87+ setting the value of the input at each point in the timepts vector.
8288 solve_ivp_method : str, optional
8389 Set the method used by :func:`scipy.integrate.solve_ivp`.
8490 solve_ivp_kwargs : str, optional
@@ -127,7 +133,7 @@ class OptimalControlProblem():
127133 def __init__ (
128134 self , sys , timepts , integral_cost , trajectory_constraints = [],
129135 terminal_cost = None , terminal_constraints = [], initial_guess = None ,
130- basis = None , log = False , ** kwargs ):
136+ method = 'shooting' , basis = None , log = False , ** kwargs ):
131137 """Set up an optimal control problem."""
132138 # Save the basic information for use later
133139 self .system = sys
@@ -137,6 +143,13 @@ def __init__(
137143 self .terminal_constraints = terminal_constraints
138144 self .basis = basis
139145
146+ # Keep track of what type of method we are using
147+ if method not in {'shooting' , 'collocation' }:
148+ raise NotImplementedError (f"Unkown method { method } " )
149+ else :
150+ self .shooting = method in {'shooting' }
151+ self .collocation = method in {'collocation' }
152+
140153 # Process keyword arguments
141154 self .solve_ivp_kwargs = {}
142155 self .solve_ivp_kwargs ['method' ] = kwargs .pop (
@@ -206,6 +219,7 @@ def __init__(
206219 constraint_lb , constraint_ub , eqconst_value = [], [], []
207220
208221 # Go through each time point and stack the bounds
222+ # TODO: for collocation method, keep track of linear vs nonlinear
209223 for t in self .timepts :
210224 for type , fun , lb , ub in self .trajectory_constraints :
211225 if np .all (lb == ub ):
@@ -241,6 +255,11 @@ def __init__(
241255 self .constraints .append (sp .optimize .NonlinearConstraint (
242256 self ._eqconst_function , self .eqconst_value ,
243257 self .eqconst_value ))
258+ if self .collocation :
259+ # Add the collocation constraints
260+ colloc_zeros = np .zeros ((self .timepts .size - 1 ) * sys .nstates )
261+ self .constraints .append (sp .optimize .NonlinearConstraint (
262+ self ._collocation_constraint , colloc_zeros , colloc_zeros ))
244263
245264 # Process the initial guess
246265 self .initial_guess = self ._process_initial_guess (initial_guess )
@@ -274,40 +293,8 @@ def _cost_function(self, coeffs):
274293 start_time = time .process_time ()
275294 logging .info ("_cost_function called at: %g" , start_time )
276295
277- # Retrieve the saved initial state
278- x = self .x
279-
280- # Compute inputs
281- if self .basis :
282- if self .log :
283- logging .debug ("coefficients = " + str (coeffs ))
284- inputs = self ._coeffs_to_inputs (coeffs )
285- else :
286- inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
287-
288- # See if we already have a simulation for this condition
289- if np .array_equal (coeffs , self .last_coeffs ) and \
290- np .array_equal (x , self .last_x ):
291- states = self .last_states
292- else :
293- if self .log :
294- logging .debug ("calling input_output_response from state\n "
295- + str (x ))
296- logging .debug ("initial input[0:3] =\n " + str (inputs [:, 0 :3 ]))
297-
298- # Simulate the system to get the state
299- # TODO: try calling solve_ivp directly for better speed?
300- _ , _ , states = ct .input_output_response (
301- self .system , self .timepts , inputs , x , return_x = True ,
302- solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
303- self .system_simulations += 1
304- self .last_x = x
305- self .last_coeffs = coeffs
306- self .last_states = states
307-
308- if self .log :
309- logging .debug ("input_output_response returned states\n "
310- + str (states ))
296+ # Compute the states and inputs
297+ states , inputs = self ._compute_states_inputs (coeffs )
311298
312299 # Trajectory cost
313300 if ct .isctime (self .system ):
@@ -380,12 +367,15 @@ def _cost_function(self, coeffs):
380367 # holds at a specific point in time, and implements the original
381368 # constraint.
382369 #
383- # To do this, we basically create a function that simulates the system
384- # dynamics and returns a vector of values corresponding to the value of
385- # the function at each time. The class initialization methods takes
386- # care of replicating the upper and lower bounds for each point in time
387- # so that the SciPy optimization algorithm can do the proper
388- # evaluation.
370+ # For collocation methods, we can directly evaluate the constraints at
371+ # the collocation points.
372+ #
373+ # For shooting methods, we do this by creating a function that
374+ # simulates the system dynamics and returns a vector of values
375+ # corresponding to the value of the function at each time. The
376+ # class initialization methods takes care of replicating the upper
377+ # and lower bounds for each point in time so that the SciPy
378+ # optimization algorithm can do the proper evaluation.
389379 #
390380 # In addition, since SciPy's optimization function does not allow us to
391381 # pass arguments to the constraint function, we have to store the initial
@@ -396,35 +386,12 @@ def _constraint_function(self, coeffs):
396386 start_time = time .process_time ()
397387 logging .info ("_constraint_function called at: %g" , start_time )
398388
399- # Retrieve the initial state
400- x = self .x
401-
402- # Compute input at time points
403- if self .basis :
404- inputs = self ._coeffs_to_inputs (coeffs )
405- else :
406- inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
407-
408- # See if we already have a simulation for this condition
409- if np .array_equal (coeffs , self .last_coeffs ) \
410- and np .array_equal (x , self .last_x ):
411- states = self .last_states
412- else :
413- if self .log :
414- logging .debug ("calling input_output_response from state\n "
415- + str (x ))
416- logging .debug ("initial input[0:3] =\n " + str (inputs [:, 0 :3 ]))
417-
418- # Simulate the system to get the state
419- _ , _ , states = ct .input_output_response (
420- self .system , self .timepts , inputs , x , return_x = True ,
421- solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
422- self .system_simulations += 1
423- self .last_x = x
424- self .last_coeffs = coeffs
425- self .last_states = states
389+ # Compute the states and inputs
390+ states , inputs = self ._compute_states_inputs (coeffs )
426391
392+ #
427393 # Evaluate the constraint function along the trajectory
394+ #
428395 value = []
429396 for i , t in enumerate (self .timepts ):
430397 for ctype , fun , lb , ub in self .trajectory_constraints :
@@ -477,37 +444,8 @@ def _eqconst_function(self, coeffs):
477444 start_time = time .process_time ()
478445 logging .info ("_eqconst_function called at: %g" , start_time )
479446
480- # Retrieve the initial state
481- x = self .x
482-
483- # Compute input at time points
484- if self .basis :
485- inputs = self ._coeffs_to_inputs (coeffs )
486- else :
487- inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
488-
489- # See if we already have a simulation for this condition
490- if np .array_equal (coeffs , self .last_coeffs ) and \
491- np .array_equal (x , self .last_x ):
492- states = self .last_states
493- else :
494- if self .log :
495- logging .debug ("calling input_output_response from state\n "
496- + str (x ))
497- logging .debug ("initial input[0:3] =\n " + str (inputs [:, 0 :3 ]))
498-
499- # Simulate the system to get the state
500- _ , _ , states = ct .input_output_response (
501- self .system , self .timepts , inputs , x , return_x = True ,
502- solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
503- self .system_simulations += 1
504- self .last_x = x
505- self .last_coeffs = coeffs
506- self .last_states = states
507-
508- if self .log :
509- logging .debug ("input_output_response returned states\n "
510- + str (states ))
447+ # Compute the states and inputs
448+ states , inputs = self ._compute_states_inputs (coeffs )
511449
512450 # Evaluate the constraint function along the trajectory
513451 value = []
@@ -538,6 +476,10 @@ def _eqconst_function(self, coeffs):
538476 # Checked above => we should never get here
539477 raise TypeError ("unknown constraint type {ctype}" )
540478
479+ # Add the collocation constraints
480+ if self .collocation :
481+ raise NotImplementedError ("collocation not yet implemented" )
482+
541483 # Update statistics
542484 self .eqconst_evaluations += 1
543485 if self .log :
@@ -555,6 +497,12 @@ def _eqconst_function(self, coeffs):
555497 # Return the value of the constraint function
556498 return np .hstack (value )
557499
500+ def _collocation_constraints (self , coeffs ):
501+ # Compute the states and inputs
502+ states , inputs = self ._compute_states_inputs (coeffs )
503+
504+ raise NotImplementedError ("collocation not yet implemented" )
505+
558506 #
559507 # Initial guess
560508 #
@@ -566,8 +514,10 @@ def _eqconst_function(self, coeffs):
566514 # vector. If a basis is specified, this is converted to coefficient
567515 # values (which are generally of smaller dimension).
568516 #
517+ # TODO: figure out how to modify this for collocation
518+ #
569519 def _process_initial_guess (self , initial_guess ):
570- if initial_guess is not None :
520+ if self . shooting and initial_guess is not None :
571521 # Convert to a 1D array (or higher)
572522 initial_guess = np .atleast_1d (initial_guess )
573523
@@ -592,10 +542,15 @@ def _process_initial_guess(self, initial_guess):
592542 # Reshape for use by scipy.optimize.minimize()
593543 return initial_guess .reshape (- 1 )
594544
595- # Default is zero
596- return np .zeros (
597- self .system .ninputs *
598- (self .timepts .size if self .basis is None else self .basis .N ))
545+ elif self .collocation and initial_guess is not None :
546+ raise NotImplementedError ("collocation not yet implemented" )
547+
548+ # Default is no initial guess
549+ input_size = self .system .ninputs * \
550+ (self .timepts .size if self .basis is None else self .basis .N )
551+ state_size = 0 if not self .collocation else \
552+ (self .timepts .size - 1 ) * self .sys .nstates
553+ return np .zeros (input_size + state_size )
599554
600555 #
601556 # Utility function to convert input vector to coefficient vector
@@ -679,6 +634,62 @@ def _print_statistics(self, reset=True):
679634 if reset :
680635 self ._reset_statistics (self .log )
681636
637+ #
638+ # Compute the states and inputs from the coefficient vector
639+ #
640+ # These internal functions return the states and inputs at the
641+ # collocation points given the ceofficient (optimizer state) vector.
642+ # They keep track of whether a shooting method is being used or not and
643+ # simulate the dynamis of needed.
644+ #
645+
646+ # Compute the states and inputs from the coefficients
647+ def _compute_states_inputs (self , coeffs ):
648+ #
649+ # Compute out the states and inputs
650+ #
651+ if self .collocation :
652+ # States are appended to end of (input) coefficients
653+ states = coeffs [- self .sys .nstates * self .timepts .size :0 ]
654+ coeffs = coeffs [0 :- self .sys .nstates * self .timepts .size ]
655+
656+ # Compute input at time points
657+ if self .basis :
658+ inputs = self ._coeffs_to_inputs (coeffs )
659+ else :
660+ inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
661+
662+ if self .shooting :
663+ # See if we already have a simulation for this condition
664+ if np .array_equal (coeffs , self .last_coeffs ) \
665+ and np .array_equal (self .x , self .last_x ):
666+ states = self .last_states
667+ else :
668+ states = self ._simulate_states (self .x , inputs )
669+ self .last_x = self .x
670+ self .last_states = states
671+ self .last_coeffs = coeffs
672+
673+ return states , inputs
674+
675+ # Simulate the system dynamis to retrieve the state
676+ def _simulate_states (self , x0 , inputs ):
677+ if self .log :
678+ logging .debug (
679+ "calling input_output_response from state\n " + str (x0 ))
680+ logging .debug ("input =\n " + str (inputs ))
681+
682+ # Simulate the system to get the state
683+ _ , _ , states = ct .input_output_response (
684+ self .system , self .timepts , inputs , x0 , return_x = True ,
685+ solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
686+ self .system_simulations += 1
687+
688+ if self .log :
689+ logging .debug (
690+ "input_output_response returned states\n " + str (states ))
691+ return states
692+
682693 #
683694 # Optimal control computations
684695 #
@@ -992,7 +1003,7 @@ def solve_ocp(
9921003 Notes
9931004 -----
9941005 Additional keyword parameters can be used to fine tune the behavior of
995- the underlying optimization and integrations functions. See
1006+ the underlying optimization and integration functions. See
9961007 :func:`OptimalControlProblem` for more information.
9971008
9981009 """
0 commit comments