@@ -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
241298if __name__ == "__main__" :
0 commit comments