From db42f074fcb7d62483594ee01cf66fe14fe6ddbe Mon Sep 17 00:00:00 2001 From: mickaelbegon Date: Wed, 22 Jul 2026 09:58:44 -0400 Subject: [PATCH] feat: expose supported advanced Acados options --- bioptim/interfaces/acados_options.py | 121 ++++++++++++++++--- tests/shard4/test_acados_advanced_options.py | 46 +++++++ 2 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 tests/shard4/test_acados_advanced_options.py diff --git a/bioptim/interfaces/acados_options.py b/bioptim/interfaces/acados_options.py index bc284b0ca..3fae1d0f8 100644 --- a/bioptim/interfaces/acados_options.py +++ b/bioptim/interfaces/acados_options.py @@ -10,6 +10,8 @@ Int, IntorFloat, Float, + FloatOptional, + IntOptional, StrOptional, StrList, ) @@ -86,6 +88,18 @@ class ACADOS(GenericSolver): _sim_method_newton_iter: Int = 5 _sim_method_num_stages: Int = 4 _sim_method_num_steps: Int = 1 + _collocation_type: Str = "GAUSS_LEGENDRE" + _sim_method_newton_tol: FloatOptional = None + _sim_method_jac_reuse: Int = 0 + _qp_solver_cond_N: IntOptional = None + _qp_solver_iter_max: Int = 50 + _qp_solver_tol_stat: FloatOptional = None + _qp_solver_tol_eq: FloatOptional = None + _qp_solver_tol_ineq: FloatOptional = None + _qp_solver_tol_comp: FloatOptional = None + _regularize_method: Str = "NO_REGULARIZE" + _levenberg_marquardt: Float = 0.0 + _globalization: Str = "FIXED_STEP" _print_level: Int = 1 _cost_type: Str = "NONLINEAR_LS" _constr_type: Str = "BGH" @@ -109,14 +123,7 @@ def set_option_unsafe(self, val: IntorFloat | Str, name: Str) -> None: This function is unsafe because we did not check if the option exist in the solver option list. If it's not it just will be ignored. Please make sure that the option you're asking for exist. """ - if not hasattr(self, "__annotations__"): - if hasattr(self, "__annotations_cache__"): - self.__annotations__ = self.__annotations_cache__ - else: - raise AttributeError("No annotations found for the class.") - - if f"_{name}" not in self.__annotations__.keys(): - self.__annotations__[f"_{name}"] = val + if f"_{name}" not in self.__dict__: self.__setattr__(f"_{name}", val) self.set_only_first_options_has_changed(True) @@ -168,6 +175,93 @@ def set_sim_method_num_steps(self, val: Int) -> None: self._sim_method_num_steps = val self.set_only_first_options_has_changed(True) + @property + def collocation_type(self) -> Str: + return self._collocation_type + + def set_collocation_type(self, val: Str) -> None: + self._collocation_type = val + self.set_only_first_options_has_changed(True) + + @property + def sim_method_newton_tol(self) -> FloatOptional: + return self._sim_method_newton_tol + + def set_sim_method_newton_tol(self, val: Float) -> None: + self._sim_method_newton_tol = val + self.set_only_first_options_has_changed(True) + + @property + def sim_method_jac_reuse(self) -> Int: + return self._sim_method_jac_reuse + + def set_sim_method_jac_reuse(self, val: Int) -> None: + self._sim_method_jac_reuse = val + self.set_only_first_options_has_changed(True) + + @property + def qp_solver_cond_N(self) -> IntOptional: + return self._qp_solver_cond_N + + def set_qp_solver_cond_N(self, val: Int) -> None: + self._qp_solver_cond_N = val + self.set_only_first_options_has_changed(True) + + @property + def qp_solver_iter_max(self) -> Int: + return self._qp_solver_iter_max + + def set_qp_solver_iter_max(self, val: Int) -> None: + self._qp_solver_iter_max = val + self.set_only_first_options_has_changed(True) + + @property + def regularize_method(self) -> Str: + return self._regularize_method + + def set_regularize_method(self, val: Str) -> None: + self._regularize_method = val + self.set_only_first_options_has_changed(True) + + @property + def levenberg_marquardt(self) -> Float: + return self._levenberg_marquardt + + def set_levenberg_marquardt(self, val: Float) -> None: + self._levenberg_marquardt = val + self.set_only_first_options_has_changed(True) + + @property + def globalization(self) -> Str: + return self._globalization + + def set_globalization(self, val: Str) -> None: + self._globalization = val + self.set_only_first_options_has_changed(True) + + def set_qp_solver_tolerances(self, val: Float) -> None: + self._qp_solver_tol_stat = val + self._qp_solver_tol_eq = val + self._qp_solver_tol_ineq = val + self._qp_solver_tol_comp = val + self.set_only_first_options_has_changed(True) + + @property + def qp_solver_tol_stat(self) -> FloatOptional: + return self._qp_solver_tol_stat + + @property + def qp_solver_tol_eq(self) -> FloatOptional: + return self._qp_solver_tol_eq + + @property + def qp_solver_tol_ineq(self) -> FloatOptional: + return self._qp_solver_tol_ineq + + @property + def qp_solver_tol_comp(self) -> FloatOptional: + return self._qp_solver_tol_comp + @property def cost_type(self) -> Str: return self._cost_type @@ -270,17 +364,14 @@ def as_dict(self, solver): } # Select the set of relevant keys before entering the loop - if not hasattr(self, "__annotations__"): - if hasattr(self, "__annotations_cache__"): - self.__annotations__ = self.__annotations_cache__ - else: - raise AttributeError("No annotations found for the class.") - relevant_keys = set(self.__annotations__.keys()) - keys_to_skip + relevant_keys = set(self.__dict__) - keys_to_skip # Iterate only over relevant keys for key in relevant_keys: option_key = key[1:] if key[0] == "_" else key - options[option_key] = getattr(self, key) + value = getattr(self, key) + if value is not None: + options[option_key] = value return options diff --git a/tests/shard4/test_acados_advanced_options.py b/tests/shard4/test_acados_advanced_options.py new file mode 100644 index 000000000..440038706 --- /dev/null +++ b/tests/shard4/test_acados_advanced_options.py @@ -0,0 +1,46 @@ +from bioptim import Solver + + +def test_acados_advanced_options_are_explicitly_exposed(): + solver = Solver.ACADOS() + solver.set_integrator_type("ERK") + solver.set_collocation_type("GAUSS_RADAU_IIA") + solver.set_sim_method_num_stages(3) + solver.set_sim_method_num_steps(2) + solver.set_sim_method_newton_iter(7) + solver.set_sim_method_newton_tol(1e-9) + solver.set_sim_method_jac_reuse(1) + solver.set_qp_solver("FULL_CONDENSING_HPIPM") + solver.set_qp_solver_cond_N(5) + solver.set_qp_solver_iter_max(75) + solver.set_qp_solver_tolerances(1e-7) + solver.set_regularize_method("CONVEXIFY") + solver.set_levenberg_marquardt(1e-4) + solver.set_globalization("MERIT_BACKTRACKING") + + options = solver.as_dict(None) + assert options["integrator_type"] == "ERK" + assert options["collocation_type"] == "GAUSS_RADAU_IIA" + assert options["sim_method_num_stages"] == 3 + assert options["sim_method_num_steps"] == 2 + assert options["sim_method_newton_iter"] == 7 + assert options["sim_method_newton_tol"] == 1e-9 + assert options["sim_method_jac_reuse"] == 1 + assert options["qp_solver"] == "FULL_CONDENSING_HPIPM" + assert options["qp_solver_cond_N"] == 5 + assert options["qp_solver_iter_max"] == 75 + assert options["qp_solver_tol_stat"] == 1e-7 + assert options["qp_solver_tol_eq"] == 1e-7 + assert options["qp_solver_tol_ineq"] == 1e-7 + assert options["qp_solver_tol_comp"] == 1e-7 + assert options["regularize_method"] == "CONVEXIFY" + assert options["levenberg_marquardt"] == 1e-4 + assert options["globalization"] == "MERIT_BACKTRACKING" + assert solver.only_first_options_has_changed + + +def test_acados_optional_options_are_not_forwarded_until_configured(): + options = Solver.ACADOS().as_dict(None) + assert "sim_method_newton_tol" not in options + assert "qp_solver_cond_N" not in options + assert "qp_solver_tol_stat" not in options