Skip to content

Commit

Permalink
Building examples and features for the cycling motion
Browse files Browse the repository at this point in the history
  • Loading branch information
Kevin CO committed May 24, 2024
1 parent b90bb9f commit 1988b48
Show file tree
Hide file tree
Showing 12 changed files with 881 additions and 318 deletions.
1 change: 1 addition & 0 deletions cocofest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
from .identification.hmed2018_force_parameter_identification import (
DingModelPulseIntensityFrequencyForceParameterIdentification,
)
from .dynamics.inverse_kinematics_and_dynamics import get_circle_coord, inverse_kinematics_cycling, inverse_dynamics_cycling
Empty file added cocofest/dynamics/__init__.py
Empty file.
133 changes: 133 additions & 0 deletions cocofest/dynamics/inverse_kinematics_and_dynamics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import math
import numpy as np

import biorbd


# This function gets the x, y, z circle coordinates based on the angle theta
def get_circle_coord(theta: int|float, x_center: int|float, y_center: int|float, radius: int|float, z: int|float=None)-> list:
"""
Get the x, y, z coordinates of a circle based on the angle theta and the center of the circle
Parameters
----------
theta: int | float
The angle of the circle in radians
x_center: int | float
The x coordinate of the center of the circle
y_center: int | float
The y coordinate of the center of the circle
radius: int | float
The radius of the circle
z: int | float
The z coordinate of the center of the circle. If None, the z coordinate of the circle will be 0
Returns
----------
x, y, z : list
The x, y, z coordinates of the circle
"""
x = radius * math.cos(theta) + x_center
y = radius * math.sin(theta) + y_center
if z is None:
return [x, y, 0]

Check warning on line 34 in cocofest/dynamics/inverse_kinematics_and_dynamics.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/inverse_kinematics_and_dynamics.py#L31-L34

Added lines #L31 - L34 were not covered by tests
else:
return [x, y, z]

Check warning on line 36 in cocofest/dynamics/inverse_kinematics_and_dynamics.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/inverse_kinematics_and_dynamics.py#L36

Added line #L36 was not covered by tests


# This function gives the inverse kinematics q of a cycling movement for a given model
def inverse_kinematics_cycling(
model_path: str,
n_shooting: int,
x_center: int|float,
y_center: int|float,
radius: int|float,
ik_method: str = "trf",
) -> tuple:
"""
Perform the inverse kinematics of a cycling movement
Parameters
----------
model_path: str
The path to the model
n_shooting: int
The number of shooting points
x_center: int | float
The x coordinate of the center of the circle
y_center: int | float
The y coordinate of the center of the circle
radius: int | float
The radius of the circle
ik_method: str
The method to solve the inverse kinematics problem
If ik_method = 'lm', the 'trf' method will be used for the first frame, in order to respect the bounds of the model.
Then, the 'lm' method will be used for the following frames.
If ik_method = 'trf', the 'trf' method will be used for all the frames.
If ik_method = 'only_lm', the 'lm' method will be used for all the frames.
In least_square:
-‘trf’ : Trust Region Reflective algorithm, particularly suitable for large sparse problems
with bounds.
Generally robust method.
-‘lm’ : Levenberg-Marquardt algorithm as implemented in MINPACK.
Does not handle bounds and sparse Jacobians.
Usually the most efficient method for small unconstrained problems.
Returns
----------
q : np.array
joints angles
"""

model = biorbd.Model(model_path)
if 0 <= model.nbMarkers() > 1:
raise ValueError("The model must have only one markers to perform the inverse kinematics")

Check warning on line 86 in cocofest/dynamics/inverse_kinematics_and_dynamics.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/inverse_kinematics_and_dynamics.py#L84-L86

Added lines #L84 - L86 were not covered by tests

z = model.markers(np.array([0, 0]))[0].to_array()[2]
if z != model.markers(np.array([np.pi/2, np.pi/2]))[0].to_array()[2]:
print("The model not strictly 2d. Warm start not optimal.")

Check warning on line 90 in cocofest/dynamics/inverse_kinematics_and_dynamics.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/inverse_kinematics_and_dynamics.py#L88-L90

Added lines #L88 - L90 were not covered by tests

x_y_z_coord = np.array([get_circle_coord(theta, x_center, y_center, radius, z=z) for theta in np.linspace(0, -2 * np.pi + (-2 * np.pi / n_shooting), n_shooting+1)]).T
target_q = x_y_z_coord.reshape((3, 1, n_shooting+1))
ik = biorbd.InverseKinematics(model, target_q)
ik_q = ik.solve(method=ik_method)
ik_qdot = np.array([np.gradient(ik_q[i], (1/n_shooting)) for i in range(ik_q.shape[0])])
ik_qddot = np.array([np.gradient(ik_qdot[i], (1 / n_shooting)) for i in range(ik_qdot.shape[0])])
return ik_q, ik_qdot, ik_qddot

Check warning on line 98 in cocofest/dynamics/inverse_kinematics_and_dynamics.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/inverse_kinematics_and_dynamics.py#L92-L98

Added lines #L92 - L98 were not covered by tests


# This function gives the inverse dynamics Tau of a cycling motion for a given model and q, qdot, qddot
def inverse_dynamics_cycling(
model_path: str,
q: np.array,
qdot: np.array,
qddot: np.array,
) -> np.array:
"""
Perform the inverse dynamics of a cycling movement
Parameters
----------
model_path: str
The path to the used model
q: np.array
joints angles
qdot: np.array
joints velocities
qddot: np.array
joints accelerations
Returns
----------
Tau : np.array
joints torques
"""
model = biorbd.Model(model_path)
tau_shape = (model.nbQ(), q.shape[1]-1)
tau = np.zeros(tau_shape)
for i in range(tau.shape[1]):
tau_i = model.InverseDynamics(q[:, i], qdot[:, i], qddot[:, i])
tau[:, i] = tau_i.to_array()
return tau

Check warning on line 133 in cocofest/dynamics/inverse_kinematics_and_dynamics.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/inverse_kinematics_and_dynamics.py#L127-L133

Added lines #L127 - L133 were not covered by tests
168 changes: 168 additions & 0 deletions cocofest/dynamics/warm_start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import numpy as np

import biorbd

from bioptim import (
Axis,
BiorbdModel,
BoundsList,
DynamicsFcn,
DynamicsList,
InitialGuessList,
InterpolationType,
ObjectiveFcn,
ObjectiveList,
OdeSolver,
OptimalControlProgram,
PhaseDynamics,
SolutionMerge,
Solver
)

from ..dynamics.inverse_kinematics_and_dynamics import get_circle_coord, inverse_kinematics_cycling


def get_initial_guess(biorbd_model_path: str, final_time: int, n_stim: int, n_shooting: int, objective: dict) -> dict:
"""
Get the initial guess for the ocp
Parameters
----------
biorbd_model_path: str
The path to the model
final_time: int
The ocp final time
n_stim: int
The number of stimulation
n_shooting: list
The shooting points number
objective: dict
The ocp objective
Returns
-------
dict
The initial guess for the ocp
"""
# Checking if the objective is a cycling objective
if objective["cycling"] is None:
raise ValueError("Only a cycling objective is implemented for the warm start")

Check warning on line 50 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L49-L50

Added lines #L49 - L50 were not covered by tests

# Getting q and qdot from the inverse kinematics
ocp, q, qdot = prepare_muscle_driven_ocp(biorbd_model_path, n_shooting[0] * n_stim, final_time, objective)

Check warning on line 53 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L53

Added line #L53 was not covered by tests

# Solving the ocp to get muscle controls
sol = ocp.solve(Solver.IPOPT(_tol=1e-4))
muscles_control = sol.decision_controls(to_merge=[SolutionMerge.PHASES, SolutionMerge.NODES])
model = biorbd.Model(biorbd_model_path)

Check warning on line 58 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L56-L58

Added lines #L56 - L58 were not covered by tests

# Reorganizing the q and qdot shape for the warm start
q_init = [q[:, n_shooting[0] * i:n_shooting[0] * (i + 1) + 1] for i in range(n_stim)]
qdot_init = [qdot[:, n_shooting[0] * i:n_shooting[0] * (i + 1) + 1] for i in range(n_stim)]

Check warning on line 62 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L61-L62

Added lines #L61 - L62 were not covered by tests

# Building the initial guess dictionary
states_init = {"q": q_init, "qdot": qdot_init}

Check warning on line 65 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L65

Added line #L65 was not covered by tests

# Creating initial muscle forces guess from the muscle controls and the muscles max iso force characteristics
for i in range(muscles_control['muscles'].shape[0]):
fmax = model.muscle(i).characteristics().forceIsoMax() # Get the max iso force of the muscle
states_init[model.muscle(i).name().to_string()] = [

Check warning on line 70 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L68-L70

Added lines #L68 - L70 were not covered by tests
np.array([muscles_control["muscles"][i][n_shooting[0] * j:n_shooting[0] * (j + 1) + 1]]) * fmax for j in
range(n_stim)] # Multiply the muscle control by the max iso force to get the muscle force for each shooting point
states_init[model.muscle(i).name().to_string()][-1] = np.array([np.append(

Check warning on line 73 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L73

Added line #L73 was not covered by tests
states_init[model.muscle(i).name().to_string()][-1], states_init[model.muscle(i).name().to_string()][-1][0][
-1])]) # Adding a last value to the end for each interpolation frames

return states_init

Check warning on line 77 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L77

Added line #L77 was not covered by tests


def prepare_muscle_driven_ocp(
biorbd_model_path: str,
n_shooting: int,
final_time: int,
objective: dict,
) -> tuple:
"""
Prepare the muscle driven ocp with a cycling objective
Parameters
----------
biorbd_model_path: str
The path to the model
n_shooting: int
The number of shooting points
final_time: int
The ocp final time
objective: dict
The ocp objective
Returns
-------
OptimalControlProgram
The muscle driven ocp
np.array
The joints angles
np.array
The joints velocities
"""

# Adding the models to the same phase
bio_model = BiorbdModel(biorbd_model_path, )

Check warning on line 111 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L111

Added line #L111 was not covered by tests

# Add objective functions
x_center = objective["cycling"]["x_center"]
y_center = objective["cycling"]["y_center"]
radius = objective["cycling"]["radius"]
get_circle_coord_list = np.array([get_circle_coord(theta, x_center, y_center, radius)[:-1] for theta in

Check warning on line 117 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L114-L117

Added lines #L114 - L117 were not covered by tests
np.linspace(0, -2 * np.pi, n_shooting)])
objective_functions = ObjectiveList()
for i in range(n_shooting):
objective_functions.add(

Check warning on line 121 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L119-L121

Added lines #L119 - L121 were not covered by tests
ObjectiveFcn.Mayer.TRACK_MARKERS,
weight=100,
axes=[Axis.X, Axis.Y],
marker_index=0,
target=np.array(get_circle_coord_list[i]),
node=i,
phase=0,
quadratic=True,
)

# Dynamics
dynamics = DynamicsList()
dynamics.add(DynamicsFcn.MUSCLE_DRIVEN, expand_dynamics=True,

Check warning on line 134 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L133-L134

Added lines #L133 - L134 were not covered by tests
phase_dynamics=PhaseDynamics.SHARED_DURING_THE_PHASE)

# Path constraint
x_bounds = BoundsList()
q_x_bounds = bio_model.bounds_from_ranges("q")
qdot_x_bounds = bio_model.bounds_from_ranges("qdot")
x_bounds.add(key="q", bounds=q_x_bounds, phase=0)
x_bounds.add(key="qdot", bounds=qdot_x_bounds, phase=0)

Check warning on line 142 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L138-L142

Added lines #L138 - L142 were not covered by tests

# Define control path constraint
u_bounds = BoundsList()
u_bounds["muscles"] = [0] * bio_model.nb_muscles, [1] * bio_model.nb_muscles

Check warning on line 146 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L145-L146

Added lines #L145 - L146 were not covered by tests

# Initial q guess
x_init = InitialGuessList()
u_init = InitialGuessList()

Check warning on line 150 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L149-L150

Added lines #L149 - L150 were not covered by tests

q_guess, qdot_guess, qddotguess = inverse_kinematics_cycling(biorbd_model_path, n_shooting, x_center,

Check warning on line 152 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L152

Added line #L152 was not covered by tests
y_center, radius, ik_method="trf")
x_init.add("q", q_guess, interpolation=InterpolationType.EACH_FRAME)
x_init.add("qdot", qdot_guess, interpolation=InterpolationType.EACH_FRAME)

Check warning on line 155 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L154-L155

Added lines #L154 - L155 were not covered by tests

return OptimalControlProgram(

Check warning on line 157 in cocofest/dynamics/warm_start.py

View check run for this annotation

Codecov / codecov/patch

cocofest/dynamics/warm_start.py#L157

Added line #L157 was not covered by tests
bio_model,
dynamics,
n_shooting,
final_time,
x_bounds=x_bounds,
u_bounds=u_bounds,
x_init=x_init,
u_init=u_init,
objective_functions=objective_functions,
ode_solver=OdeSolver.RK4(),
), q_guess, qdot_guess
Loading

0 comments on commit 1988b48

Please sign in to comment.