Skip to content

Commit a6b7a25

Browse files
committed
simplification of product of constraints
- products of rectangles/zeros are of the same type - .with_preconditioning now works fine - updated website docs (complete example)
1 parent cf4991b commit a6b7a25

5 files changed

Lines changed: 220 additions & 51 deletions

File tree

docs/python-ocp.md

Lines changed: 85 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ The dynamics is
7171
\begin{align}
7272
F(x, u; a) =
7373
\begin{bmatrix}
74-
ax_1 + 0.2x_1^2 - 1.2 x_2 + u \\
75-
\sin(x_2) + 0.2 x_1^3 - u
74+
0.98 \sin(x_1) + x_2 \\
75+
0.1x_1^2 - 0.5 x_1 + a x_2 + u
7676
\end{bmatrix},
7777
\end{align}
7878
\]</div>
79-
where $a$ is a constant parameter. Suppose the stage cost
79+
where $a$ is a parameter. Suppose the stage cost
8080
function is
8181
<div class="math">
8282
\[
@@ -91,69 +91,117 @@ cost function is
9191
<div class="math">
9292
\[
9393
\begin{align}
94-
V_f(x; x^{\mathrm{ref}}) = 10\|x_N-x^{\mathrm{ref}}\|^2.
94+
V_f(x; x^{\mathrm{ref}}) = 100\|x_N-x^{\mathrm{ref}}\|^2.
9595
\end{align}
9696
\]</div>
97-
Lastly, we
98-
This can be done as follows:
97+
Lastly, we have the state constraint $x_t \geq x_{\rm min}$, where $x_{\rm min}$ is a parameter,
98+
and the hard input constraints $|u_t| \leq 0.2$.
99+
100+
<br>
101+
102+
This optimal control problem can be constructed as follows:
99103

100104
```python
101-
# Check this out...
102-
ocp = og.ocp.OptimalControlProblem(nx=2, nu=1, horizon=5,
103-
shooting=og.ocp.ShootingMethod.SINGLE)
105+
optimizer_name = "ocp_alm"
106+
107+
# Construct the OCP
108+
ocp = og.ocp.OptimalControlProblem(nx=2, nu=1, horizon=20)
109+
110+
# Define the parameters
104111
ocp.add_parameter("x0", 2)
105112
ocp.add_parameter("xref", 2, default=[0.0, 0.0])
113+
ocp.add_parameter("q", 1, default=1)
114+
ocp.add_parameter("r", 1, default=0.1)
115+
ocp.add_parameter("a", 1, default=1)
116+
ocp.add_parameter("xmin", 1, default=-1)
106117

107118
# System dynamics
108-
ocp.with_dynamics(lambda x, u, param:
109-
cs.vertcat(x[0] + u[0], x[1] - u[0]))
110-
111-
# A typical stage cost function
119+
ocp.with_dynamics(lambda x, u, param:
120+
cs.vertcat(0.98 * cs.sin(x[0]) + x[1],
121+
0.1 * x[0]**2 - 0.5 * x[0] + param["a"] * x[1] + u[0]))
122+
# Stage cost
112123
ocp.with_stage_cost(
113124
lambda x, u, param, _t:
114-
cs.dot(x - param["xref"], x - param["xref"]) + 0.01 * cs.dot(u, u)
125+
param["q"] * cs.dot(x - param["xref"], x - param["xref"])
126+
+ param["r"] * cs.dot(u, u)
115127
)
116128

117-
# Terminal cost function
129+
# Terminal cost
118130
ocp.with_terminal_cost(
119-
lambda x, param:
120-
2.0 * cs.dot(x - param["xref"], x - param["xref"])
131+
lambda x, param: 100 * cs.dot(x - param["xref"], x - param["xref"])
121132
)
122133

123-
# Input constraints
124-
ocp.with_input_constraints(og.constraints.Rectangle([-0.4], [0.4]))
125-
126-
# State/input joint constraints
134+
# State constraint: x1 <= xmax, imposed with ALM
127135
ocp.with_path_constraint(
128-
lambda x, u, param, _t: cs.fmax(0.0, x[0] - 1.5)
136+
lambda x, u, param, _t: x[1] - param["xmin"],
137+
kind="alm",
138+
set_c=og.constraints.Rectangle([0.], [1000.0]),
129139
)
140+
141+
# Input constraints
142+
ocp.with_input_constraints(og.constraints.BallInf(radius=0.2))
130143
```
131144

132145
Having defined the above OCP, we can build the optimizer...
133146

147+
<!--DOCUSAURUS_CODE_TABS-->
134148

149+
<!--Direct intercafe-->
135150
```python
136-
solver_config = ...
137-
optimizer = og.ocp.OCPBuilder(
138-
ocp,
139-
metadata=og.config.OptimizerMeta().with_optimizer_name("ocp_single_tcp"),
140-
build_configuration=og.config.BuildConfiguration()
141-
.with_build_directory(".")
142-
.with_tcp_interface_config(
143-
tcp_interface_config=og.config.TcpServerConfiguration(bind_port=3391)
144-
),
145-
solver_configuration=solver_config,
146-
).build()
151+
ocp_optimizer = og.ocp.OCPBuilder(
152+
ocp,
153+
metadata=og.config.OptimizerMeta().with_optimizer_name(optimizer_name),
154+
build_configuration=og.config.BuildConfiguration()
155+
.with_build_python_bindings().with_rebuild(True),
156+
solver_configuration=og.config.SolverConfiguration()
157+
.with_tolerance(1e-5)
158+
.with_delta_tolerance(1e-5)
159+
.with_preconditioning(True)
160+
.with_penalty_weight_update_factor(1.8)
161+
.with_max_inner_iterations(2000)
162+
.with_max_outer_iterations(40),
163+
).build()
147164
```
148165

166+
<!--TCP socket interface-->
167+
```python
168+
ocp_optimizer = og.ocp.OCPBuilder(
169+
ocp,
170+
metadata=og.config.OptimizerMeta().with_optimizer_name(optimizer_name),
171+
build_configuration=og.config.BuildConfiguration()
172+
.with_tcp_interface_config(
173+
tcp_interface_config=og.config.TcpServerConfiguration(bind_port=3391)
174+
).with_rebuild(True),
175+
solver_configuration=og.config.SolverConfiguration()
176+
.with_tolerance(1e-5)
177+
.with_delta_tolerance(1e-5)
178+
.with_preconditioning(True)
179+
.with_penalty_weight_update_factor(1.8)
180+
.with_max_inner_iterations(2000)
181+
.with_max_outer_iterations(40),
182+
).build()
183+
```
184+
185+
<!--END_DOCUSAURUS_CODE_TABS-->
186+
149187
The optimizer can then be called as follows:
150188

151189
```python
152-
result = single_optimizer.solve(x0=[1,-1], xref=[0, 0])
190+
result = ocp_optimizer.solve(x0=[0.4, 0.2], q=30, r=1, a=0.8, xmin=-0.2)
153191
```
154192

155-
and note that the parameter `xref=xref` is optional; if not specified,
156-
the default value will be used (the default was set when we constructed the
157-
OCP).
193+
and note that all parameters except `x0` are optional; if not specified,
194+
their default values will be used (the defaults were set when we constructed the
195+
OCP). We can now plot the optimal sequence of inputs (`result.inputs`)
196+
197+
<img src="/optimization-engine/img/ocp-inputs.png" alt="Seq. inputs" width="60%">
198+
199+
and the corresponding sequence of states (`result.states`)
200+
201+
<img src="/optimization-engine/img/ocp-states.png" alt="Seq. states" width="60%">
202+
203+
The object `result` contains the above sequences of inputs and states and additional
204+
information about the solution, solver time, Lagrange multipliers, etc.
205+
158206

159207
## Step-by-step documentation

open-codegen/opengen/ocp/builder.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
from opengen.builder.problem import Problem
1616
from opengen.config.build_config import BuildConfiguration
1717
from opengen.config.solver_config import SolverConfiguration
18-
from opengen.constraints.cartesian import CartesianProduct
1918
from opengen.constraints.no_constraints import NoConstraints
2019
from opengen.tcp.optimizer_tcp_manager import OptimizerTcpManager
2120

21+
from .constraint_utils import make_constraint_product
2222
from .parameter import ParameterPack
2323
from .problem import ShootingMethod
2424
from .solution import OcpSolution
@@ -411,7 +411,7 @@ def __make_input_constraints(self):
411411

412412
segments = [((idx + 1) * self.__ocp.nu) - 1 for idx in range(self.__ocp.horizon)]
413413
constraints = [stage_constraints] * self.__ocp.horizon
414-
return CartesianProduct(segments, constraints)
414+
return make_constraint_product(segments, constraints)
415415

416416
def __make_multiple_shooting_constraints(self):
417417
"""Build the hard decision-variable set for multiple shooting.
@@ -447,7 +447,7 @@ def __make_multiple_shooting_constraints(self):
447447
for idx in range(self.__ocp.horizon)
448448
]
449449
constraints = [hard_stage_constraints] * self.__ocp.horizon
450-
return CartesianProduct(segments, constraints)
450+
return make_constraint_product(segments, constraints)
451451

452452
if hard_terminal_constraints is not None:
453453
segments = []
@@ -471,7 +471,7 @@ def __make_multiple_shooting_constraints(self):
471471
segments.append(offset)
472472
constraints.append(hard_terminal_constraints)
473473

474-
return CartesianProduct(segments, constraints)
474+
return make_constraint_product(segments, constraints)
475475

476476
segments = []
477477
constraints = []
@@ -486,7 +486,7 @@ def __make_multiple_shooting_constraints(self):
486486
segments.append(offset)
487487
constraints.append(NoConstraints())
488488

489-
return CartesianProduct(segments, constraints)
489+
return make_constraint_product(segments, constraints)
490490

491491
def build_problem(self, symbolic_model=None):
492492
"""Lower the OCP to a low-level :class:`opengen.builder.problem.Problem`.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Helpers for simplifying products of OCP constraint sets."""
2+
3+
from opengen.constraints.cartesian import CartesianProduct
4+
from opengen.constraints.no_constraints import NoConstraints
5+
from opengen.constraints.rectangle import Rectangle
6+
from opengen.constraints.zero import Zero
7+
8+
9+
def segment_dimensions(segments):
10+
"""Compute segment dimensions from Cartesian-product end indices."""
11+
dimensions = []
12+
previous = -1
13+
for segment_end in segments:
14+
dimensions.append(segment_end - previous)
15+
previous = segment_end
16+
return dimensions
17+
18+
19+
def rectangle_bounds(constraint, dimension):
20+
"""Return explicit box bounds when a set admits a rectangle representation."""
21+
if isinstance(constraint, NoConstraints):
22+
return [float("-inf")] * dimension, [float("inf")] * dimension
23+
24+
if isinstance(constraint, Zero):
25+
return [0.0] * dimension, [0.0] * dimension
26+
27+
if isinstance(constraint, Rectangle):
28+
if constraint.dimension() != dimension:
29+
raise ValueError("constraint dimension does not match its segment length")
30+
xmin = constraint.xmin if constraint.xmin is not None else [float("-inf")] * dimension
31+
xmax = constraint.xmax if constraint.xmax is not None else [float("inf")] * dimension
32+
return list(xmin), list(xmax)
33+
34+
return None
35+
36+
37+
def make_constraint_product(segments, constraints):
38+
"""Build the most specific set representing a block product of constraints."""
39+
if not constraints:
40+
return NoConstraints()
41+
42+
if len(constraints) == 1:
43+
return constraints[0]
44+
45+
if all(isinstance(constraint, NoConstraints) for constraint in constraints):
46+
return NoConstraints()
47+
48+
if all(isinstance(constraint, Zero) for constraint in constraints):
49+
return Zero()
50+
51+
dimensions = segment_dimensions(segments)
52+
rectangle_data = [
53+
rectangle_bounds(constraint, dimension)
54+
for constraint, dimension in zip(constraints, dimensions)
55+
]
56+
57+
if all(bounds is not None for bounds in rectangle_data):
58+
xmin = []
59+
xmax = []
60+
for current_xmin, current_xmax in rectangle_data:
61+
xmin.extend(current_xmin)
62+
xmax.extend(current_xmax)
63+
return Rectangle(xmin, xmax)
64+
65+
return CartesianProduct(segments, constraints)

open-codegen/opengen/ocp/problem.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
import casadi.casadi as cs
66

7-
from opengen.constraints.cartesian import CartesianProduct
87
from opengen.constraints.zero import Zero
98

9+
from .constraint_utils import make_constraint_product
1010
from .parameter import ParameterPack
1111

1212

@@ -258,7 +258,7 @@ def __assemble_constraint_set(blocks, key):
258258
offset += block["dimension"]
259259
segments.append(offset)
260260
constraints.append(block[key])
261-
return CartesianProduct(segments, constraints)
261+
return make_constraint_product(segments, constraints)
262262

263263
def with_path_constraint(self, constraint, kind="penalty", set_c=None, set_y=None):
264264
r"""Add a stage-wise symbolic constraint.

0 commit comments

Comments
 (0)