Skip to content

Commit 0862b5a

Browse files
committed
add trajectory_method='shooting'
1 parent 832527d commit 0862b5a

1 file changed

Lines changed: 116 additions & 105 deletions

File tree

control/optimal.py

Lines changed: 116 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)