From 98e6d18b61819694fb127c70ddd42d5949ac57e0 Mon Sep 17 00:00:00 2001 From: mickaelbegon Date: Wed, 22 Jul 2026 11:15:42 -0400 Subject: [PATCH] Use physical targets for parameter objectives --- bioptim/limits/penalty.py | 15 ++++++++++++--- tests/shard6/test_penalty.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/bioptim/limits/penalty.py b/bioptim/limits/penalty.py index a1a8b7114..ed221e5fa 100644 --- a/bioptim/limits/penalty.py +++ b/bioptim/limits/penalty.py @@ -82,7 +82,7 @@ def minimize_states(penalty: PenaltyOption, controller: PenaltyController, key: penalty.add_target_to_plot(controller=controller, combine_to=f"{key}_states") penalty.multi_thread = True if penalty.multi_thread is None else penalty.multi_thread - # TODO: We should scale the target here! + # States exposed by the controller are already unscaled, so targets are expressed in physical units. return controller.states[key].cx_start @staticmethod @@ -109,7 +109,7 @@ def minimize_controls(penalty: PenaltyOption, controller: PenaltyController, key penalty.add_target_to_plot(controller=controller, combine_to=f"{key}_controls") penalty.multi_thread = True if penalty.multi_thread is None else penalty.multi_thread - # TODO: We should scale the target here! + # Controls exposed by the controller are already unscaled, so targets are expressed in physical units. return controller.controls[key].cx_start @staticmethod @@ -1355,7 +1355,16 @@ def minimize_parameter(penalty: PenaltyOption, controller: PenaltyController, ke penalty.quadratic = True if penalty.quadratic is None else penalty.quadratic penalty.multi_thread = True if penalty.multi_thread is None else penalty.multi_thread - return controller.parameters.cx if key is None or key == "all" else controller.parameters[key].cx + if key is None or key == "all": + return vertcat( + *( + controller.parameters[parameter_key].cx + * controller.parameters[parameter_key].scaling.scaling + for parameter_key in controller.parameters.keys() + ) + ) + + return controller.parameters[key].cx * controller.parameters[key].scaling.scaling @staticmethod def add(ocp, nlp): diff --git a/tests/shard6/test_penalty.py b/tests/shard6/test_penalty.py index acf759476..eea909542 100644 --- a/tests/shard6/test_penalty.py +++ b/tests/shard6/test_penalty.py @@ -26,6 +26,8 @@ TorqueActivationBiorbdModel, DynamicsOptions, ObjectiveWeight, + ParameterList, + VariableScaling, ) from bioptim.limits.penalty import PenaltyOption from bioptim.limits.penalty_controller import PenaltyController @@ -249,6 +251,22 @@ def test_penalty_minimize_state(penalty_origin, value, phase_dynamics): npt.assert_almost_equal(res, np.array([[value]] * 4)) +@pytest.mark.parametrize("key", ["gravity", "all", None]) +def test_penalty_minimize_parameter_returns_physical_values(key): + parameters = ParameterList(use_sx=False) + parameters.add("gravity", lambda *_: None, size=2, scaling=VariableScaling("gravity", [10, 100])) + parameters.add("mass", lambda *_: None, size=1, scaling=VariableScaling("mass", [5])) + + controller = type("Controller", (), {"parameters": parameters})() + penalty = type("Penalty", (), {"quadratic": None, "multi_thread": None})() + value = ObjectiveFcn.Parameter.MINIMIZE_PARAMETER(penalty, controller, key=key) + function = Function("physical_parameter", [parameters.cx], [value]) + + result = np.array(function([2, 3, 4])).squeeze() + expected = np.array([20, 300]) if key == "gravity" else np.array([20, 300, 20]) + npt.assert_equal(result, expected) + + @pytest.mark.parametrize("phase_dynamics", [PhaseDynamics.SHARED_DURING_THE_PHASE, PhaseDynamics.ONE_PER_NODE]) @pytest.mark.parametrize("penalty_origin", [ObjectiveFcn.Lagrange, ObjectiveFcn.Mayer]) @pytest.mark.parametrize("value", [0.1, -10])