Skip to content
196 changes: 196 additions & 0 deletions bioptim/examples/getting_started/custom_constraint_weights.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"""
This example is a trivial box sent upward. It is designed to investigate the different types of constraint weights one
can define in bioptim. Therefore, it shows how one can define the weight of the TRACK_CONTROL constraint.
Please note that setting the weight of a constraint plays on :
1) The tolerance for this specific constraint. This can be useful if you have a constraint that must be respected
strictly (high weight) and another that could be respected more loosely (small weight).
2) The conditioning of the problem. You will see that changing a constraint weight might change the constraint
scaling performed by IPOPT.
Therefore, to have a real impact on the optimal control problem, we recommend using power of 10 constraint weights.

All the types of interpolation are shown:
InterpolationType.CONSTANT: All the values are the same at each node
InterpolationType.CONSTANT_WITH_FIRST_AND_LAST_DIFFERENT: All the others are the same at each node, except the first and last ones
InterpolationType.LINEAR: The values are linearly interpolated between the first and last nodes
InterpolationType.EACH_FRAME: Each node values are specified
InterpolationType.SPLINE: The values are interpolated from the first to last node using a cubic spline
InterpolationType.CUSTOM: Provide a user-defined interpolation function
"""

import numpy as np
from bioptim import (
TorqueBiorbdModel,
Node,
OptimalControlProgram,
DynamicsOptions,
Objective,
ObjectiveFcn,
ConstraintList,
ConstraintFcn,
BoundsList,
InterpolationType,
PhaseDynamics,
ConstraintWeight,
)


def custom_weight(node: int, n_nodes: int) -> float:
"""
The custom function for the constraint weight (this particular one mimics linear interpolation)

Parameters
----------
node: int
The index of the current point to return the value
n_nodes: int
The number of index available

Returns
-------
The vector value of the bounds at current_shooting_point
"""

my_values = [0, 1]
# Linear interpolation created with custom function
if n_nodes == 1:
return my_values[0]
else:
return my_values[0] + (my_values[1] - my_values[0]) * node / (n_nodes - 1)


def prepare_ocp(
biorbd_model_path: str,
n_shooting: int,
final_time: float,
interpolation_type: InterpolationType = InterpolationType.CONSTANT,
node: Node = Node.ALL_SHOOTING,
phase_dynamics: PhaseDynamics = PhaseDynamics.SHARED_DURING_THE_PHASE,
expand_dynamics: bool = True,
) -> OptimalControlProgram:
"""
Prepare the ocp for the specified interpolation type

Parameters
----------
biorbd_model_path: str
The path to the biorbd model
n_shooting: int
The number of shooting point
final_time: float
The movement time
interpolation_type: InterpolationType
The requested InterpolationType
phase_dynamics: PhaseDynamics
If the dynamics equation within a phase is unique or changes at each node.
PhaseDynamics.SHARED_DURING_THE_PHASE is much faster, but lacks the capability to have changing dynamics within
a phase. PhaseDynamics.ONE_PER_NODE should also be used when multi-node penalties with more than 3 nodes or with COLLOCATION (cx_intermediate_list) are added to the OCP.
expand_dynamics: bool
If the dynamics function should be expanded. Please note, this will solve the problem faster, but will slow down
the declaration of the OCP, so it is a trade-off. Also depending on the solver, it may or may not work
(for instance IRK is not compatible with expanded dynamics)

Returns
-------
The OCP fully prepared and ready to be solved
"""

# BioModel path
bio_model = TorqueBiorbdModel(biorbd_model_path)
nq = bio_model.nb_q
nqdot = bio_model.nb_qdot
ntau = bio_model.nb_tau
tau_min, tau_max = -100, 100

if node == Node.START:
n_nodes = 1
elif node == Node.INTERMEDIATES:
n_nodes = n_shooting - 2
elif node == Node.ALL_SHOOTING:
n_nodes = n_shooting
else:
raise RuntimeError("This example is not designed to work with this node type.")

# Add objective functions
objective_functions = Objective(ObjectiveFcn.Mayer.MINIMIZE_STATE, key="qdot", node=node)

# DynamicsOptions
dynamics = DynamicsOptions(expand_dynamics=expand_dynamics, phase_dynamics=phase_dynamics)

# Constraints
constraints = ConstraintList()
constraints.add(ConstraintFcn.SUPERIMPOSE_MARKERS, node=Node.START, first_marker="m0", second_marker="m1")
constraints.add(ConstraintFcn.SUPERIMPOSE_MARKERS, node=Node.END, first_marker="m0", second_marker="m2")

# ConstraintWeight
if interpolation_type == InterpolationType.CONSTANT:
weight = [1]
weight = ConstraintWeight(weight, interpolation=InterpolationType.CONSTANT)
elif interpolation_type == InterpolationType.LINEAR:
weight = [0, 1]
weight = ConstraintWeight(weight, interpolation=InterpolationType.LINEAR)
elif interpolation_type == InterpolationType.EACH_FRAME:
weight = np.linspace(0, 1, n_nodes)
weight = ConstraintWeight(weight, interpolation=InterpolationType.EACH_FRAME)
elif interpolation_type == InterpolationType.SPLINE:
spline_time = np.hstack((0, np.sort(np.random.random((3,)) * final_time), final_time))
spline_points = np.random.random((5,)) * (-10) - 5
weight = ConstraintWeight(spline_points, interpolation=InterpolationType.SPLINE, t=spline_time)
elif interpolation_type == InterpolationType.CUSTOM:
# The custom functions refer to the one at the beginning of the file.
# For this particular instance, they emulate a Linear interpolation
extra_params = {"n_nodes": n_nodes}
weight = ConstraintWeight(custom_weight, interpolation=InterpolationType.CUSTOM, **extra_params)
else:
raise NotImplementedError("Not implemented yet")

constraints.add(ConstraintFcn.TRACK_CONTROL, key="tau", target=np.ones((3, 1)), node=node, weight=weight)

# Path condition
x_bounds = BoundsList()
x_bounds.add("q", min_bound=[-100] * nq, max_bound=[100] * nq, interpolation=InterpolationType.CONSTANT)
x_bounds.add("qdot", min_bound=[-100] * nqdot, max_bound=[100] * nqdot, interpolation=InterpolationType.CONSTANT)
u_bounds = BoundsList()
u_bounds.add(
"tau", min_bound=[tau_min] * ntau, max_bound=[tau_max] * ntau, interpolation=InterpolationType.CONSTANT
)

return OptimalControlProgram(
bio_model,
n_shooting,
final_time,
dynamics=dynamics,
x_bounds=x_bounds,
u_bounds=u_bounds,
objective_functions=objective_functions,
constraints=constraints,
)


def main():
"""
Show all the InterpolationType implemented in bioptim
"""

nodes_to_test = [Node.START, Node.INTERMEDIATES, Node.ALL_SHOOTING]

for interpolation_type in InterpolationType:
for node in nodes_to_test:
if (
interpolation_type == InterpolationType.ALL_POINTS
or interpolation_type == InterpolationType.CONSTANT_WITH_FIRST_AND_LAST_DIFFERENT
):
continue

print(f"Solving problem using {interpolation_type} weight applied at {node} nodes.")
ocp = prepare_ocp(
"models/cube.bioMod", n_shooting=30, final_time=2, interpolation_type=interpolation_type, node=node
)
sol = ocp.solve()
print("\n")

# Print the last solution
sol.graphs(show_bounds=True)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

def custom_weight(node: int, n_nodes: int) -> float:
"""
The custom function for the objective wright (this particular one mimics linear interpolation)
The custom function for the objective weight (this particular one mimics linear interpolation)

Parameters
----------
Expand Down
18 changes: 14 additions & 4 deletions bioptim/limits/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(
quadratic: Bool = False,
phase: Int = -1,
is_stochastic: Bool = False,
weight: Int | Float | ConstraintWeight = ConstraintWeight(),
weight: Int | Float | ConstraintWeight = None,
**extra_parameters: Any,
):
"""
Expand All @@ -66,6 +66,9 @@ def __init__(
extra_parameters:
Generic parameters for options
"""
if weight is None:
weight = ConstraintWeight()

custom_function = None
if not isinstance(constraint, ConstraintFcn):
custom_function = constraint
Expand Down Expand Up @@ -174,7 +177,7 @@ class ConstraintList(OptionList):
def add(
self,
constraint: Callable | Constraint | Any,
weight: Int | Float | ConstraintWeight = ConstraintWeight(),
weight: Int | Float | ConstraintWeight = None,
**extra_arguments: Any,
):
"""
Expand All @@ -189,6 +192,8 @@ def add(
extra_arguments: dict
Any parameters to pass to Constraint
"""
if weight is None:
weight = ConstraintWeight()

if isinstance(constraint, Constraint):
self.copy(constraint)
Expand Down Expand Up @@ -916,7 +921,7 @@ def __init__(
min_bound: NpArrayorFloatOptional = None,
max_bound: NpArrayorFloatOptional = None,
quadratic: Bool = False,
weight: Int | Float | ConstraintWeight = ConstraintWeight(),
weight: Int | Float | ConstraintWeight = None,
**extra_parameters: Any,
):
"""
Expand All @@ -935,6 +940,9 @@ def __init__(
extra_parameters:
Generic parameters for options
"""
if weight is None:
weight = ConstraintWeight()

custom_function = None
if not isinstance(parameter_constraint, ConstraintFcn):
custom_function = parameter_constraint
Expand Down Expand Up @@ -1042,7 +1050,7 @@ class ParameterConstraintList(OptionList):
def add(
self,
parameter_constraint: Callable | ParameterConstraint | Any,
weight: Int | Float | ConstraintWeight = ConstraintWeight(),
weight: Int | Float | ConstraintWeight = None,
**extra_arguments: Any,
):
"""
Expand All @@ -1057,6 +1065,8 @@ def add(
extra_arguments: dict
Any parameters to pass to Constraint
"""
if weight is None:
weight = ConstraintWeight()

if isinstance(parameter_constraint, Constraint):
self.copy(parameter_constraint)
Expand Down
5 changes: 4 additions & 1 deletion bioptim/limits/multinode_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class MultinodeConstraintList(MultinodePenaltyList):
def add(
self,
multinode_constraint: Any,
weight: Int | Float | ConstraintWeight = ConstraintWeight(),
weight: Int | Float | ConstraintWeight = None,
**extra_arguments: Any,
):
"""
Expand All @@ -112,6 +112,9 @@ def add(
Any parameters to pass to Constraint
"""

if weight is None:
weight = ConstraintWeight()

if not isinstance(weight, ConstraintWeight):
if isinstance(weight, (int, float)):
weight = ConstraintWeight(weight)
Expand Down
4 changes: 3 additions & 1 deletion bioptim/limits/multinode_objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class MultinodeObjectiveList(MultinodePenaltyList):
def add(
self,
multinode_objective: Any,
weight: Int | Float | ObjectiveWeight = ObjectiveWeight(),
weight: Int | Float | ObjectiveWeight = None,
**extra_arguments: Any,
):
"""
Expand All @@ -78,6 +78,8 @@ def add(
extra_arguments: dict
Any parameters to pass to Objective
"""
if weight is None:
weight = ObjectiveWeight()

if not isinstance(weight, ObjectiveWeight):
if isinstance(weight, (int, float)):
Expand Down
18 changes: 14 additions & 4 deletions bioptim/limits/objective_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(
custom_type: Any = None,
phase: Int = -1,
is_stochastic: Bool = False,
weight: Int | Float | ObjectiveWeight = ObjectiveWeight(),
weight: Int | Float | ObjectiveWeight = None,
**extra_parameters: Any,
):
"""
Expand All @@ -48,6 +48,8 @@ def __init__(
extra_parameters: dict
Generic parameters for options
"""
if weight is None:
weight = ObjectiveWeight()

custom_function = None
if not isinstance(objective, ObjectiveFcn.Lagrange) and not isinstance(objective, ObjectiveFcn.Mayer):
Expand Down Expand Up @@ -194,7 +196,7 @@ class ObjectiveList(OptionList):
def add(
self,
objective: Callable | Objective | Any,
weight: Int | Float | ObjectiveWeight = ObjectiveWeight(),
weight: Int | Float | ObjectiveWeight = None,
**extra_arguments: Any,
):
"""
Expand All @@ -209,6 +211,9 @@ def add(
extra_arguments: dict
Any parameters to pass to ObjectiveFcn
"""
if weight is None:
weight = ObjectiveWeight()

if isinstance(objective, Objective):
self.copy(objective)
else:
Expand Down Expand Up @@ -524,7 +529,7 @@ def __init__(
self,
parameter_objective: Any,
custom_type: Any = None,
weight: Int | Float | ObjectiveWeight = ObjectiveWeight(),
weight: Int | Float | ObjectiveWeight = None,
**extra_parameters: Any,
):
"""
Expand All @@ -539,6 +544,9 @@ def __init__(
extra_parameters: dict
Generic parameters for options
"""
if weight is None:
weight = ObjectiveWeight()

custom_function = None
if not isinstance(parameter_objective, ObjectiveFcn.Parameter):
custom_function = parameter_objective
Expand Down Expand Up @@ -635,7 +643,7 @@ class ParameterObjectiveList(OptionList):
def add(
self,
parameter_objective: Callable | ParameterObjective | Any,
weight: Int | Float | ObjectiveWeight = ObjectiveWeight(),
weight: Int | Float | ObjectiveWeight = None,
**extra_arguments: Any,
):
"""
Expand All @@ -650,6 +658,8 @@ def add(
extra_arguments: dict
Any parameters to pass to ParameterObjectiveFcn
"""
if weight is None:
weight = ObjectiveWeight()

if isinstance(parameter_objective, ParameterObjective):
self.copy(parameter_objective)
Expand Down
Loading
Loading