Skip to content

Commit d9ec3ec

Browse files
committed
add trajectory_method='shooting'
1 parent 4d89991 commit d9ec3ec

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(
@@ -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

Comments
 (0)