@@ -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 (
@@ -198,6 +211,7 @@ def __init__(
198211 constraint_lb , constraint_ub , eqconst_value = [], [], []
199212
200213 # Go through each time point and stack the bounds
214+ # TODO: for collocation method, keep track of linear vs nonlinear
201215 for t in self .timepts :
202216 for type , fun , lb , ub in self .trajectory_constraints :
203217 if np .all (lb == ub ):
@@ -233,6 +247,11 @@ def __init__(
233247 self .constraints .append (sp .optimize .NonlinearConstraint (
234248 self ._eqconst_function , self .eqconst_value ,
235249 self .eqconst_value ))
250+ if self .collocation :
251+ # Add the collocation constraints
252+ colloc_zeros = np .zeros ((self .timepts .size - 1 ) * sys .nstates )
253+ self .constraints .append (sp .optimize .NonlinearConstraint (
254+ self ._collocation_constraint , colloc_zeros , colloc_zeros ))
236255
237256 # Process the initial guess
238257 self .initial_guess = self ._process_initial_guess (initial_guess )
@@ -266,40 +285,8 @@ def _cost_function(self, coeffs):
266285 start_time = time .process_time ()
267286 logging .info ("_cost_function called at: %g" , start_time )
268287
269- # Retrieve the saved initial state
270- x = self .x
271-
272- # Compute inputs
273- if self .basis :
274- if self .log :
275- logging .debug ("coefficients = " + str (coeffs ))
276- inputs = self ._coeffs_to_inputs (coeffs )
277- else :
278- inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
279-
280- # See if we already have a simulation for this condition
281- if np .array_equal (coeffs , self .last_coeffs ) and \
282- np .array_equal (x , self .last_x ):
283- states = self .last_states
284- else :
285- if self .log :
286- logging .debug ("calling input_output_response from state\n "
287- + str (x ))
288- logging .debug ("initial input[0:3] =\n " + str (inputs [:, 0 :3 ]))
289-
290- # Simulate the system to get the state
291- # TODO: try calling solve_ivp directly for better speed?
292- _ , _ , states = ct .input_output_response (
293- self .system , self .timepts , inputs , x , return_x = True ,
294- solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
295- self .system_simulations += 1
296- self .last_x = x
297- self .last_coeffs = coeffs
298- self .last_states = states
299-
300- if self .log :
301- logging .debug ("input_output_response returned states\n "
302- + str (states ))
288+ # Compute the states and inputs
289+ states , inputs = self ._compute_states_inputs (coeffs )
303290
304291 # Trajectory cost
305292 if ct .isctime (self .system ):
@@ -372,12 +359,15 @@ def _cost_function(self, coeffs):
372359 # holds at a specific point in time, and implements the original
373360 # constraint.
374361 #
375- # To do this, we basically create a function that simulates the system
376- # dynamics and returns a vector of values corresponding to the value of
377- # the function at each time. The class initialization methods takes
378- # care of replicating the upper and lower bounds for each point in time
379- # so that the SciPy optimization algorithm can do the proper
380- # evaluation.
362+ # For collocation methods, we can directly evaluate the constraints at
363+ # the collocation points.
364+ #
365+ # For shooting methods, we do this by creating a function that
366+ # simulates the system dynamics and returns a vector of values
367+ # corresponding to the value of the function at each time. The
368+ # class initialization methods takes care of replicating the upper
369+ # and lower bounds for each point in time so that the SciPy
370+ # optimization algorithm can do the proper evaluation.
381371 #
382372 # In addition, since SciPy's optimization function does not allow us to
383373 # pass arguments to the constraint function, we have to store the initial
@@ -388,35 +378,12 @@ def _constraint_function(self, coeffs):
388378 start_time = time .process_time ()
389379 logging .info ("_constraint_function called at: %g" , start_time )
390380
391- # Retrieve the initial state
392- x = self .x
393-
394- # Compute input at time points
395- if self .basis :
396- inputs = self ._coeffs_to_inputs (coeffs )
397- else :
398- inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
399-
400- # See if we already have a simulation for this condition
401- if np .array_equal (coeffs , self .last_coeffs ) \
402- and np .array_equal (x , self .last_x ):
403- states = self .last_states
404- else :
405- if self .log :
406- logging .debug ("calling input_output_response from state\n "
407- + str (x ))
408- logging .debug ("initial input[0:3] =\n " + str (inputs [:, 0 :3 ]))
409-
410- # Simulate the system to get the state
411- _ , _ , states = ct .input_output_response (
412- self .system , self .timepts , inputs , x , return_x = True ,
413- solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
414- self .system_simulations += 1
415- self .last_x = x
416- self .last_coeffs = coeffs
417- self .last_states = states
381+ # Compute the states and inputs
382+ states , inputs = self ._compute_states_inputs (coeffs )
418383
384+ #
419385 # Evaluate the constraint function along the trajectory
386+ #
420387 value = []
421388 for i , t in enumerate (self .timepts ):
422389 for ctype , fun , lb , ub in self .trajectory_constraints :
@@ -469,37 +436,8 @@ def _eqconst_function(self, coeffs):
469436 start_time = time .process_time ()
470437 logging .info ("_eqconst_function called at: %g" , start_time )
471438
472- # Retrieve the initial state
473- x = self .x
474-
475- # Compute input at time points
476- if self .basis :
477- inputs = self ._coeffs_to_inputs (coeffs )
478- else :
479- inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
480-
481- # See if we already have a simulation for this condition
482- if np .array_equal (coeffs , self .last_coeffs ) and \
483- np .array_equal (x , self .last_x ):
484- states = self .last_states
485- else :
486- if self .log :
487- logging .debug ("calling input_output_response from state\n "
488- + str (x ))
489- logging .debug ("initial input[0:3] =\n " + str (inputs [:, 0 :3 ]))
490-
491- # Simulate the system to get the state
492- _ , _ , states = ct .input_output_response (
493- self .system , self .timepts , inputs , x , return_x = True ,
494- solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
495- self .system_simulations += 1
496- self .last_x = x
497- self .last_coeffs = coeffs
498- self .last_states = states
499-
500- if self .log :
501- logging .debug ("input_output_response returned states\n "
502- + str (states ))
439+ # Compute the states and inputs
440+ states , inputs = self ._compute_states_inputs (coeffs )
503441
504442 # Evaluate the constraint function along the trajectory
505443 value = []
@@ -530,6 +468,10 @@ def _eqconst_function(self, coeffs):
530468 # Checked above => we should never get here
531469 raise TypeError ("unknown constraint type {ctype}" )
532470
471+ # Add the collocation constraints
472+ if self .collocation :
473+ raise NotImplementedError ("collocation not yet implemented" )
474+
533475 # Update statistics
534476 self .eqconst_evaluations += 1
535477 if self .log :
@@ -547,6 +489,12 @@ def _eqconst_function(self, coeffs):
547489 # Return the value of the constraint function
548490 return np .hstack (value )
549491
492+ def _collocation_constraints (self , coeffs ):
493+ # Compute the states and inputs
494+ states , inputs = self ._compute_states_inputs (coeffs )
495+
496+ raise NotImplementedError ("collocation not yet implemented" )
497+
550498 #
551499 # Initial guess
552500 #
@@ -558,8 +506,10 @@ def _eqconst_function(self, coeffs):
558506 # vector. If a basis is specified, this is converted to coefficient
559507 # values (which are generally of smaller dimension).
560508 #
509+ # TODO: figure out how to modify this for collocation
510+ #
561511 def _process_initial_guess (self , initial_guess ):
562- if initial_guess is not None :
512+ if self . shooting and initial_guess is not None :
563513 # Convert to a 1D array (or higher)
564514 initial_guess = np .atleast_1d (initial_guess )
565515
@@ -584,10 +534,15 @@ def _process_initial_guess(self, initial_guess):
584534 # Reshape for use by scipy.optimize.minimize()
585535 return initial_guess .reshape (- 1 )
586536
587- # Default is zero
588- return np .zeros (
589- self .system .ninputs *
590- (self .timepts .size if self .basis is None else self .basis .N ))
537+ elif self .collocation and initial_guess is not None :
538+ raise NotImplementedError ("collocation not yet implemented" )
539+
540+ # Default is no initial guess
541+ input_size = self .system .ninputs * \
542+ (self .timepts .size if self .basis is None else self .basis .N )
543+ state_size = 0 if not self .collocation else \
544+ (self .timepts .size - 1 ) * self .sys .nstates
545+ return np .zeros (input_size + state_size )
591546
592547 #
593548 # Utility function to convert input vector to coefficient vector
@@ -671,6 +626,62 @@ def _print_statistics(self, reset=True):
671626 if reset :
672627 self ._reset_statistics (self .log )
673628
629+ #
630+ # Compute the states and inputs from the coefficient vector
631+ #
632+ # These internal functions return the states and inputs at the
633+ # collocation points given the ceofficient (optimizer state) vector.
634+ # They keep track of whether a shooting method is being used or not and
635+ # simulate the dynamis of needed.
636+ #
637+
638+ # Compute the states and inputs from the coefficients
639+ def _compute_states_inputs (self , coeffs ):
640+ #
641+ # Compute out the states and inputs
642+ #
643+ if self .collocation :
644+ # States are appended to end of (input) coefficients
645+ states = coeffs [- self .sys .nstates * self .timepts .size :0 ]
646+ coeffs = coeffs [0 :- self .sys .nstates * self .timepts .size ]
647+
648+ # Compute input at time points
649+ if self .basis :
650+ inputs = self ._coeffs_to_inputs (coeffs )
651+ else :
652+ inputs = coeffs .reshape ((self .system .ninputs , - 1 ))
653+
654+ if self .shooting :
655+ # See if we already have a simulation for this condition
656+ if np .array_equal (coeffs , self .last_coeffs ) \
657+ and np .array_equal (self .x , self .last_x ):
658+ states = self .last_states
659+ else :
660+ states = self ._simulate_states (self .x , inputs )
661+ self .last_x = self .x
662+ self .last_states = states
663+ self .last_coeffs = coeffs
664+
665+ return states , inputs
666+
667+ # Simulate the system dynamis to retrieve the state
668+ def _simulate_states (self , x0 , inputs ):
669+ if self .log :
670+ logging .debug (
671+ "calling input_output_response from state\n " + str (x0 ))
672+ logging .debug ("input =\n " + str (inputs ))
673+
674+ # Simulate the system to get the state
675+ _ , _ , states = ct .input_output_response (
676+ self .system , self .timepts , inputs , x0 , return_x = True ,
677+ solve_ivp_kwargs = self .solve_ivp_kwargs , t_eval = self .timepts )
678+ self .system_simulations += 1
679+
680+ if self .log :
681+ logging .debug (
682+ "input_output_response returned states\n " + str (states ))
683+ return states
684+
674685 #
675686 # Optimal control computations
676687 #
@@ -980,7 +991,7 @@ def solve_ocp(
980991 Notes
981992 -----
982993 Additional keyword parameters can be used to fine tune the behavior of
983- the underlying optimization and integrations functions. See
994+ the underlying optimization and integration functions. See
984995 :func:`OptimalControlProblem` for more information.
985996
986997 """
0 commit comments