Skip to content

Commit 27a200e

Browse files
committed
update documentation + allow constraints in scipy.optimize form
1 parent 614f2a7 commit 27a200e

File tree

3 files changed

+74
-45
lines changed

3 files changed

+74
-45
lines changed

control/optimal.py

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -50,44 +50,48 @@ class OptimalControlProblem():
5050
integral_cost : callable
5151
Function that returns the integral cost given the current state
5252
and input. Called as integral_cost(x, u).
53-
trajectory_constraints : list of tuples, optional
53+
trajectory_constraints : list of constraints, optional
5454
List of constraints that should hold at each point in the time
55-
vector. Each element of the list should consist of a tuple with
56-
first element given by :meth:`~scipy.optimize.LinearConstraint` or
57-
:meth:`~scipy.optimize.NonlinearConstraint` and the remaining
58-
elements of the tuple are the arguments that would be passed to
59-
those functions. The constraints will be applied at each time
60-
point along the trajectory.
55+
vector. Each element of the list should be an object of type
56+
:class:`~scipy.optimize.LinearConstraint` with arguments `(A, lb,
57+
ub)` or :class:`~scipy.optimize.NonlinearConstraint` with arguments
58+
`(fun, lb, ub)`. The constraints will be applied at each time point
59+
along the trajectory.
6160
terminal_cost : callable, optional
6261
Function that returns the terminal cost given the current state
6362
and input. Called as terminal_cost(x, u).
64-
initial_guess : 1D or 2D array_like
65-
Initial inputs to use as a guess for the optimal input. The
66-
inputs should either be a 2D vector of shape (ninputs, horizon)
67-
or a 1D input of shape (ninputs,) that will be broadcast by
68-
extension of the time axis.
6963
trajectory_method : string, optional
7064
Method to use for carrying out the optimization. Currently supported
7165
methods are 'shooting' and 'collocation' (continuous time only). The
7266
default value is 'shooting' for discrete time systems and
7367
'collocation' for continuous time systems
68+
initial_guess : (tuple of) 1D or 2D array_like
69+
Initial states and/or inputs to use as a guess for the optimal
70+
trajectory. For shooting methods, an array of inputs for each time
71+
point should be specified. For collocation methods, the initial
72+
guess is either the input vector or a tuple consisting guesses for
73+
the state and the input. Guess should either be a 2D vector of
74+
shape (ninputs, ntimepts) or a 1D input of shape (ninputs,) that
75+
will be broadcast by extension of the time axis.
7476
log : bool, optional
7577
If `True`, turn on logging messages (using Python logging module).
76-
Use ``logging.basicConfig`` to enable logging output (e.g., to a file).
77-
kwargs : dict, optional
78-
Additional parameters (passed to :func:`scipy.optimal.minimize`).
78+
Use :py:func:`logging.basicConfig` to enable logging output
79+
(e.g., to a file).
7980
8081
Returns
8182
-------
8283
ocp : OptimalControlProblem
8384
Optimal control problem object, to be used in computing optimal
8485
controllers.
8586
86-
Additional parameters
87-
---------------------
87+
Other Parameters
88+
----------------
8889
basis : BasisFamily, optional
8990
Use the given set of basis functions for the inputs instead of
9091
setting the value of the input at each point in the timepts vector.
92+
terminal_constraints : list of constraints, optional
93+
List of constraints that should hold at the terminal point in time,
94+
in the same form as `trajectory_constraints`.
9195
solve_ivp_method : str, optional
9296
Set the method used by :func:`scipy.integrate.solve_ivp`.
9397
solve_ivp_kwargs : str, optional
@@ -174,20 +178,6 @@ def __init__(
174178
if kwargs:
175179
raise TypeError("unrecognized keyword(s): ", str(kwargs))
176180

177-
# Process trajectory constraints
178-
def _process_constraints(constraint_list, name):
179-
if isinstance(constraint_list, tuple):
180-
constraint_list = [constraint_list]
181-
elif not isinstance(constraint_list, list):
182-
raise TypeError(f"{name} constraints must be a list")
183-
184-
# Make sure that we recognize all of the constraint types
185-
for ctype, fun, lb, ub in constraint_list:
186-
if not ctype in [opt.LinearConstraint, opt.NonlinearConstraint]:
187-
raise TypeError(f"unknown {name} constraint type {ctype}")
188-
189-
return constraint_list
190-
191181
self.trajectory_constraints = _process_constraints(
192182
trajectory_constraints, "trajectory")
193183
self.terminal_constraints = _process_constraints(
@@ -1005,9 +995,6 @@ def solve_ocp(
1005995
If True, assume that 2D input arrays are transposed from the standard
1006996
format. Used to convert MATLAB-style inputs to our format.
1007997
1008-
kwargs : dict, optional
1009-
Additional parameters (passed to :func:`scipy.optimal.minimize`).
1010-
1011998
Returns
1012999
-------
10131000
res : OptimalControlResult
@@ -1443,3 +1430,45 @@ def _evaluate_output_range_constraint(x, u):
14431430

14441431
# Return a nonlinear constraint object based on the polynomial
14451432
return (opt.NonlinearConstraint, _evaluate_output_range_constraint, lb, ub)
1433+
1434+
#
1435+
# Utility functions
1436+
#
1437+
1438+
#
1439+
# Process trajectory constraints
1440+
#
1441+
# Constraints were originally specified as a tuple with the type of
1442+
# constraint followed by the arguments. However, they are now specified
1443+
# directly as SciPy constraint objects.
1444+
#
1445+
# The _process_constraints() function will covert everything to a consistent
1446+
# internal representation (currently a tuple with the constraint type as the
1447+
# first element.
1448+
#
1449+
def _process_constraints(clist, name):
1450+
if isinstance(
1451+
clist, (tuple, opt.LinearConstraint, opt.NonlinearConstraint)):
1452+
clist = [clist]
1453+
elif not isinstance(clist, list):
1454+
raise TypeError(f"{name} constraints must be a list")
1455+
1456+
# Process individual list elements
1457+
constraint_list = []
1458+
for constraint in clist:
1459+
if isinstance(constraint, tuple):
1460+
# Original style of constraint
1461+
ctype, fun, lb, ub = constraint
1462+
if not ctype in [opt.LinearConstraint, opt.NonlinearConstraint]:
1463+
raise TypeError(f"unknown {name} constraint type {ctype}")
1464+
constraint_list.append(constraint)
1465+
elif isinstance(constraint, opt.LinearConstraint):
1466+
constraint_list.append(
1467+
(opt.LinearConstraint, constraint.A,
1468+
constraint.lb, constraint.ub))
1469+
elif isinstance(constraint, opt.NonlinearConstraint):
1470+
constraint_list.append(
1471+
(opt.NonlinearConstraint, constraint.fun,
1472+
constraint.lb, constraint.ub))
1473+
1474+
return constraint_list

control/tests/optimal_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def test_finite_horizon_simple(method):
6464

6565
# State and input constraints
6666
constraints = [
67-
(sp.optimize.LinearConstraint, np.eye(3), [-5, -5, -1], [5, 5, 1]),
67+
sp.optimize.LinearConstraint(np.eye(3), [-5, -5, -1], [5, 5, 1]),
6868
]
6969

7070
# Quadratic state and input penalty
@@ -148,7 +148,7 @@ def test_discrete_lqr():
148148

149149
# Add state and input constraints
150150
trajectory_constraints = [
151-
(sp.optimize.LinearConstraint, np.eye(3), [-5, -5, -.5], [5, 5, 0.5]),
151+
sp.optimize.LinearConstraint(np.eye(3), [-5, -5, -.5], [5, 5, 0.5]),
152152
]
153153

154154
# Re-solve
@@ -461,7 +461,7 @@ def test_ocp_argument_errors():
461461

462462
# State and input constraints
463463
constraints = [
464-
(sp.optimize.LinearConstraint, np.eye(3), [-5, -5, -1], [5, 5, 1]),
464+
sp.optimize.LinearConstraint(np.eye(3), [-5, -5, -1], [5, 5, 1]),
465465
]
466466

467467
# Quadratic state and input penalty
@@ -513,7 +513,7 @@ def test_optimal_basis_simple(basis):
513513

514514
# State and input constraints
515515
constraints = [
516-
(sp.optimize.LinearConstraint, np.eye(3), [-5, -5, -1], [5, 5, 1]),
516+
sp.optimize.LinearConstraint(np.eye(3), [-5, -5, -1], [5, 5, 1]),
517517
]
518518

519519
# Quadratic state and input penalty
@@ -585,7 +585,7 @@ def test_equality_constraints():
585585
def final_point_eval(x, u):
586586
return x
587587
final_point = [
588-
(sp.optimize.NonlinearConstraint, final_point_eval, [0, 0], [0, 0])]
588+
sp.optimize.NonlinearConstraint(final_point_eval, [0, 0], [0, 0])]
589589

590590
optctrl = opt.OptimalControlProblem(
591591
sys, time, cost, terminal_constraints=final_point)

doc/optimal.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,16 @@ parameter can be used to specify a cost function for the final point in the
125125
trajectory.
126126

127127
The `constraints` parameter is a list of constraints similar to that used by
128-
the :func:`scipy.optimize.minimize` function. Each constraint is a tuple of
129-
one of the following forms::
128+
the :func:`scipy.optimize.minimize` function. Each constraint is specified
129+
using one of the following forms::
130130

131-
(LinearConstraint, A, lb, ub)
132-
(NonlinearConstraint, f, lb, ub)
131+
LinearConstraint(A, lb, ub)
132+
NonlinearConstraint(f, lb, ub)
133133

134134
For a linear constraint, the 2D array `A` is multiplied by a vector
135135
consisting of the current state `x` and current input `u` stacked
136-
vertically, then compared with the upper and lower bound. This constrain is
137-
satisfied if
136+
vertically, then compared with the upper and lower bound. This constraint
137+
is satisfied if
138138

139139
.. code:: python
140140

0 commit comments

Comments
 (0)