Skip to content

Commit 2074619

Browse files
committed
fix issues + API wip
- changes in API (tentative) - fix issues with double-printing log msg - retry warnings suppressed - updated unit tests - new unit test in CI
1 parent f8c8181 commit 2074619

File tree

6 files changed

+199
-54
lines changed

6 files changed

+199
-54
lines changed

ci/script.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ regular_test() {
5252
export PYTHONPATH=.
5353
python -W ignore test/test_constraints.py -v
5454
python -W ignore test/test.py -v
55+
python -W ignore test/test_ocp.py -v
5556
python -W ignore test/test_raspberry_pi.py -v
5657

5758

open-codegen/opengen/builder/optimizer_builder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ def with_verbosity_level(self, verbosity_level):
8686
self.__logger.setLevel(verbosity_level)
8787
self.__logger.handlers.clear()
8888
self.__logger.addHandler(stream_handler)
89+
# Keep logs on this named logger only; otherwise callers that configure
90+
# the root logger can end up seeing every builder message twice.
91+
self.__logger.propagate = False
8992

9093
return self
9194

open-codegen/opengen/ocp/builder.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,55 @@ def __make_multiple_shooting_constraints(self):
161161
if stage_constraints is None:
162162
stage_constraints = NoConstraints()
163163

164+
hard_stage_constraints = self.__ocp.hard_stage_state_input_constraints
165+
hard_terminal_constraints = self.__ocp.hard_terminal_state_constraints
166+
167+
if hard_stage_constraints is not None and self.__ocp.input_constraints is not None:
168+
raise ValueError(
169+
"cannot combine with_input_constraints with "
170+
"with_hard_stage_state_input_constraints automatically; "
171+
"encode the input bounds directly in the stage state-input set"
172+
)
173+
174+
if hard_stage_constraints is not None and hard_terminal_constraints is not None:
175+
raise ValueError(
176+
"cannot combine with_hard_stage_state_input_constraints with "
177+
"with_hard_terminal_state_constraints automatically; "
178+
"their intersection on the terminal stage is not supported"
179+
)
180+
181+
if hard_stage_constraints is not None:
182+
segments = [
183+
((idx + 1) * (self.__ocp.nu + self.__ocp.nx)) - 1
184+
for idx in range(self.__ocp.horizon)
185+
]
186+
constraints = [hard_stage_constraints] * self.__ocp.horizon
187+
return CartesianProduct(segments, constraints)
188+
189+
if hard_terminal_constraints is not None:
190+
segments = []
191+
constraints = []
192+
offset = -1
193+
194+
for _ in range(self.__ocp.horizon - 1):
195+
offset += self.__ocp.nu
196+
segments.append(offset)
197+
constraints.append(stage_constraints)
198+
199+
offset += self.__ocp.nx
200+
segments.append(offset)
201+
constraints.append(NoConstraints())
202+
203+
offset += self.__ocp.nu
204+
segments.append(offset)
205+
constraints.append(stage_constraints)
206+
207+
offset += self.__ocp.nx
208+
segments.append(offset)
209+
constraints.append(hard_terminal_constraints)
210+
211+
return CartesianProduct(segments, constraints)
212+
164213
segments = []
165214
constraints = []
166215
offset = -1

open-codegen/opengen/ocp/problem.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def __init__(self, nx, nu, horizon, shooting=ShootingMethod.SINGLE):
3939
self.__stage_cost = None
4040
self.__terminal_cost = None
4141
self.__input_constraints = None
42+
self.__hard_stage_state_input_constraints = None
43+
self.__hard_terminal_state_constraints = None
4244
self.__path_constraints = []
4345
self.__terminal_constraints = []
4446

@@ -74,6 +76,14 @@ def dynamics(self):
7476
def input_constraints(self):
7577
return self.__input_constraints
7678

79+
@property
80+
def hard_stage_state_input_constraints(self):
81+
return self.__hard_stage_state_input_constraints
82+
83+
@property
84+
def hard_terminal_state_constraints(self):
85+
return self.__hard_terminal_state_constraints
86+
7787
@property
7888
def path_constraints(self):
7989
return list(self.__path_constraints)
@@ -108,6 +118,14 @@ def with_input_constraints(self, constraints):
108118
self.__input_constraints = constraints
109119
return self
110120

121+
def with_hard_stage_state_input_constraints(self, constraints):
122+
self.__hard_stage_state_input_constraints = constraints
123+
return self
124+
125+
def with_hard_terminal_state_constraints(self, constraints):
126+
self.__hard_terminal_state_constraints = constraints
127+
return self
128+
111129
@staticmethod
112130
def __constraint_dimension(mapping):
113131
return mapping.size1() * mapping.size2()
@@ -166,6 +184,15 @@ def validate(self):
166184
raise ValueError("dynamics must be specified")
167185
if self.__stage_cost is None:
168186
raise ValueError("stage cost must be specified")
187+
if self.__shooting == ShootingMethod.SINGLE:
188+
if self.__hard_stage_state_input_constraints is not None:
189+
raise ValueError(
190+
"with_hard_stage_state_input_constraints is only available in multiple shooting"
191+
)
192+
if self.__hard_terminal_state_constraints is not None:
193+
raise ValueError(
194+
"with_hard_terminal_state_constraints is only available in multiple shooting"
195+
)
169196
return self
170197

171198
def __build_single_shooting_model(self):
@@ -264,6 +291,7 @@ def __build_multiple_shooting_model(self):
264291
cost = 0
265292
penalty_terms = []
266293
alm_blocks = []
294+
dynamics_alm_terms = []
267295

268296
x_current = x0
269297
offset = 0
@@ -296,19 +324,23 @@ def __build_multiple_shooting_model(self):
296324
if self.__dynamics_constraint_kind == "penalty":
297325
penalty_terms.append(dynamics_defect)
298326
else:
299-
alm_blocks.append({
300-
"mapping": dynamics_defect,
301-
"dimension": self.__constraint_dimension(dynamics_defect),
302-
"set_c": Zero(),
303-
"set_y": None,
304-
})
327+
dynamics_alm_terms.append(dynamics_defect)
305328

306329
x_current = x_next
307330
states.append(x_current)
308331

309332
if self.__terminal_cost is not None:
310333
cost += self.__terminal_cost(x_current, param)
311334

335+
if dynamics_alm_terms:
336+
dynamics_mapping = cs.vertcat(*dynamics_alm_terms)
337+
alm_blocks.append({
338+
"mapping": dynamics_mapping,
339+
"dimension": self.__constraint_dimension(dynamics_mapping),
340+
"set_c": Zero(),
341+
"set_y": None,
342+
})
343+
312344
for definition in self.__terminal_constraints:
313345
mapping = definition["constraint"](x_current, param)
314346
if definition["kind"] == "penalty":

open-codegen/opengen/tcp/optimizer_tcp_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from .solver_response import SolverResponse
1212
from importlib.metadata import version
1313

14+
# A bit of warning suppressing for the retry module
15+
logging.getLogger("retry").setLevel(logging.ERROR)
16+
1417
class OptimizerTcpManager:
1518
"""Client for TCP interface of parametric optimizers
1619

open-codegen/test/test_ocp.py

Lines changed: 105 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ def test_parameter_defaults_are_packed(self):
6464
packed = ocp.parameters.pack({"x0": [0.5, -0.25]})
6565
self.assertEqual(packed, [0.5, -0.25, 1.0, -1.0])
6666

67+
def test_single_shooting_rejects_hard_state_based_constraints(self):
68+
ocp_stage = self.make_ocp()
69+
ocp_stage.with_hard_stage_state_input_constraints(
70+
og.constraints.Rectangle([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0])
71+
)
72+
with self.assertRaises(ValueError):
73+
ocp_stage.build_symbolic_model()
74+
75+
ocp_terminal = self.make_ocp()
76+
ocp_terminal.with_hard_terminal_state_constraints(
77+
og.constraints.Rectangle([-1.0, -1.0], [1.0, 1.0])
78+
)
79+
with self.assertRaises(ValueError):
80+
ocp_terminal.build_symbolic_model()
81+
6782
def test_symbolic_lowering_builds_penalty_mapping_and_cartesian_constraints(self):
6883
ocp = self.make_ocp()
6984
builder = og.ocp.OCPBuilder(
@@ -123,12 +138,13 @@ def test_generated_optimizer_uses_named_parameters_and_defaults(self):
123138
)
124139

125140
result = optimizer.solve(x0=[0.0, 0.0])
126-
print(result)
141+
# print(result)
127142

128143
self.assertEqual(backend.last_call["p"], [0.0, 0.0, 1.0, -1.0])
129144
self.assertEqual(result.inputs, [[0.1], [0.2], [0.3]])
130145
self.assertEqual(result.states[0], [0.0, 0.0])
131-
self.assertEqual(result.states[-1], [0.6000000000000001, -0.6000000000000001])
146+
self.assertAlmostEqual(result.states[-1][0], 0.6)
147+
self.assertAlmostEqual(result.states[-1][1], -0.6)
132148

133149
def test_multiple_shooting_builds_decision_vector_and_defect_constraints(self):
134150
ocp = self.make_ocp(shooting=og.ocp.ShootingMethod.MULTIPLE)
@@ -156,8 +172,23 @@ def test_multiple_shooting_can_impose_dynamics_with_alm(self):
156172
self.assertEqual(low_level.dim_decision_variables(), 9)
157173
self.assertEqual(low_level.dim_constraints_penalty(), 3)
158174
self.assertEqual(low_level.dim_constraints_aug_lagrangian(), 6)
159-
self.assertIsInstance(low_level.alm_set_c, og.constraints.CartesianProduct)
160-
self.assertEqual(low_level.alm_set_c.segments, [1, 3, 5])
175+
self.assertIsInstance(low_level.alm_set_c, og.constraints.Zero)
176+
self.assertIsInstance(low_level.alm_set_y, og.constraints.BallInf)
177+
178+
def test_multiple_shooting_supports_hard_terminal_state_constraints(self):
179+
ocp = self.make_ocp(shooting=og.ocp.ShootingMethod.MULTIPLE)
180+
ocp.with_hard_terminal_state_constraints(
181+
og.constraints.Rectangle([-10.0, -10.0], [10.0, 10.0])
182+
)
183+
builder = og.ocp.OCPBuilder(
184+
ocp,
185+
metadata=og.config.OptimizerMeta().with_optimizer_name("ocp_ms_terminal_hard"),
186+
)
187+
low_level = builder.build_problem()
188+
189+
self.assertIsInstance(low_level.constraints, og.constraints.CartesianProduct)
190+
self.assertEqual(low_level.constraints.segments, [0, 2, 3, 5, 6, 8])
191+
self.assertIsInstance(low_level.constraints.constraints[-1], og.constraints.Rectangle)
161192

162193
def test_multiple_shooting_solution_extracts_states_from_decision_vector(self):
163194
ocp = self.make_ocp(shooting=og.ocp.ShootingMethod.MULTIPLE)
@@ -179,38 +210,28 @@ def test_multiple_shooting_solution_extracts_states_from_decision_vector(self):
179210
self.assertEqual(result.inputs, [[0.1], [0.2], [0.4]])
180211
self.assertEqual(result.states, [[0.0, 0.0], [0.1, -0.1], [0.3, -0.3], [0.7, -0.7]])
181212

182-
def test_ocp_generates_rust_solver_and_calls_tcp_interface(self):
183-
optimizer_name = "ocp_tcp_smoke"
184-
port = 3391
185-
186-
ocp = og.ocp.OptimalControlProblem(nx=2, nu=1, horizon=5,
187-
shooting=og.ocp.ShootingMethod.MULTIPLE)
188-
ocp.add_parameter("x0", 2)
189-
ocp.add_parameter("xref", 2, default=[0.0, 0.0])
190-
ocp.with_dynamics(lambda x, u, param: cs.vertcat(x[0] + u[0],
191-
x[1] - u[0]))
192-
ocp.with_dynamics_constraints("alm")
193-
ocp.with_stage_cost(
194-
lambda x, u, param, _t:
195-
cs.exp( cs.dot(x - param["xref"], x - param["xref"]) + 0.01 * cs.dot(u, u))
196-
)
197-
ocp.with_terminal_cost(
198-
lambda x, param:
199-
2.0 * cs.dot(x - param["xref"], x - param["xref"])
200-
)
201-
ocp.with_input_constraints(og.constraints.Rectangle([-0.4], [0.4]))
202-
ocp.with_path_constraint(
203-
lambda x, u, param, _t: cs.fmax(0.0, x[0] - 1.5)
204-
)
205-
206-
meta = og.config.OptimizerMeta().with_optimizer_name(optimizer_name)
207-
build_config = og.config.BuildConfiguration() \
208-
.with_open_version(local_path=OcpTestCase.get_open_local_absolute_path()) \
209-
.with_build_directory(OcpTestCase.TEST_DIR) \
210-
.with_build_mode(og.config.BuildConfiguration.DEBUG_MODE) \
211-
.with_tcp_interface_config(
212-
tcp_interface_config=og.config.TcpServerConfiguration(bind_port=port)
213+
def test_single_and_multiple_shooting_give_approximately_equal_results(self):
214+
def make_problem(shooting):
215+
ocp = og.ocp.OptimalControlProblem(nx=2, nu=1, horizon=5, shooting=shooting)
216+
ocp.add_parameter("x0", 2)
217+
ocp.add_parameter("xref", 2, default=[0.0, 0.0])
218+
ocp.with_dynamics(lambda x, u, param: cs.vertcat(x[0] + u[0], x[1] - u[0]))
219+
ocp.with_stage_cost(
220+
lambda x, u, param, _t:
221+
cs.dot(x - param["xref"], x - param["xref"]) + 0.01 * cs.dot(u, u)
213222
)
223+
ocp.with_terminal_cost(
224+
lambda x, param:
225+
2.0 * cs.dot(x - param["xref"], x - param["xref"])
226+
)
227+
ocp.with_input_constraints(og.constraints.Rectangle([-0.4], [0.4]))
228+
ocp.with_path_constraint(
229+
lambda x, u, param, _t: cs.fmax(0.0, x[0] - 1.5)
230+
)
231+
if shooting == og.ocp.ShootingMethod.MULTIPLE:
232+
ocp.with_dynamics_constraints("alm")
233+
return ocp
234+
214235
solver_config = og.config.SolverConfiguration() \
215236
.with_tolerance(1e-5) \
216237
.with_delta_tolerance(1e-5) \
@@ -219,23 +240,59 @@ def test_ocp_generates_rust_solver_and_calls_tcp_interface(self):
219240
.with_max_inner_iterations(5000) \
220241
.with_max_outer_iterations(20)
221242

222-
builder = og.ocp.OCPBuilder(
223-
ocp,
224-
metadata=meta,
225-
build_configuration=build_config,
243+
single_optimizer = og.ocp.OCPBuilder(
244+
make_problem(og.ocp.ShootingMethod.SINGLE),
245+
metadata=og.config.OptimizerMeta().with_optimizer_name("ocp_single_tcp"),
246+
build_configuration=og.config.BuildConfiguration()
247+
.with_open_version(local_path=OcpTestCase.get_open_local_absolute_path())
248+
.with_build_directory(OcpTestCase.TEST_DIR)
249+
.with_build_mode(og.config.BuildConfiguration.DEBUG_MODE)
250+
.with_tcp_interface_config(
251+
tcp_interface_config=og.config.TcpServerConfiguration(bind_port=3391)
252+
),
226253
solver_configuration=solver_config,
227-
)
228-
optimizer = builder.build()
254+
).build()
255+
multiple_optimizer = og.ocp.OCPBuilder(
256+
make_problem(og.ocp.ShootingMethod.MULTIPLE),
257+
metadata=og.config.OptimizerMeta().with_optimizer_name("ocp_multiple_tcp"),
258+
build_configuration=og.config.BuildConfiguration()
259+
.with_open_version(local_path=OcpTestCase.get_open_local_absolute_path())
260+
.with_build_directory(OcpTestCase.TEST_DIR)
261+
.with_build_mode(og.config.BuildConfiguration.DEBUG_MODE)
262+
.with_tcp_interface_config(
263+
tcp_interface_config=og.config.TcpServerConfiguration(bind_port=3392)
264+
),
265+
solver_configuration=solver_config,
266+
).build()
229267

230268
try:
231-
result = optimizer.solve(x0=[1.0, -1.0], xref=[0.0, 0.0])
232-
print(result)
233-
self.assertEqual("Converged", result.exit_status)
234-
self.assertEqual(15, len(result.solution))
235-
self.assertEqual(5, len(result.inputs))
236-
self.assertEqual(6, len(result.states))
269+
x0 = [1.0, -1.0]
270+
xref = [0.0, 0.0]
271+
single_result = single_optimizer.solve(x0=x0, xref=xref)
272+
multiple_result = multiple_optimizer.solve(x0=x0, xref=xref)
273+
# print("SINGLE\n------------")
274+
# print(single_result)
275+
# print("MULTIPLE\n------------")
276+
# print(multiple_result)
277+
278+
self.assertEqual("Converged", single_result.exit_status)
279+
self.assertEqual("Converged", multiple_result.exit_status)
280+
self.assertEqual(5, len(single_result.inputs))
281+
self.assertEqual(5, len(multiple_result.inputs))
282+
self.assertEqual(6, len(single_result.states))
283+
self.assertEqual(6, len(multiple_result.states))
284+
285+
for u_single, u_multiple in zip(single_result.inputs, multiple_result.inputs):
286+
self.assertAlmostEqual(u_single[0], u_multiple[0], delta=1e-3)
287+
288+
for x_single, x_multiple in zip(single_result.states, multiple_result.states):
289+
self.assertAlmostEqual(x_single[0], x_multiple[0], delta=1e-3)
290+
self.assertAlmostEqual(x_single[1], x_multiple[1], delta=1e-3)
291+
292+
self.assertAlmostEqual(single_result.cost, multiple_result.cost, delta=1e-3)
237293
finally:
238-
optimizer.kill()
294+
single_optimizer.kill()
295+
multiple_optimizer.kill()
239296

240297

241298
if __name__ == "__main__":

0 commit comments

Comments
 (0)