Skip to content

Commit e4bcec6

Browse files
authored
Merge pull request #17 from Kev1CO/simulation_processing
Adding moving time horizon for cycling tasks
2 parents f3f8d50 + 8e98325 commit e4bcec6

File tree

119 files changed

+8749
-289682
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+8749
-289682
lines changed

.gitignore

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,10 @@ __pycache__/
55

66
.pytest_cache
77

8-
9-
examples/data/debug.py
10-
118
external/bioptim
129

1310
example/*.pkl
14-
example/fes_multibody_cycling/*.pkl
15-
example/reaching_task/*.pkl
16-
example/minimize_fatigue/*.pkl
17-
18-
example/minimize_fatigue/result_file/*.pkl
19-
examples/nmpc/results/*.pkl
11+
**/cycling/plot/
12+
**/cycling/result/
2013

2114
*.pkl

README.md

Lines changed: 303 additions & 100 deletions
Large diffs are not rendered by default.

cocofest/custom_objectives.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ def minimize_overall_muscle_fatigue(controller: PenaltyController) -> MX:
2424
The sum of each force scaling factor
2525
"""
2626
muscle_name_list = controller.model.bio_model.muscle_names
27+
muscle_model = controller.model.muscles_dynamics_model
2728
muscle_fatigue = vertcat(
28-
*[controller.states["A_" + muscle_name_list[x]].cx for x in range(len(muscle_name_list))]
29+
*[
30+
1 - (controller.states["A_" + muscle_name_list[x]].cx / muscle_model[x].a_scale)
31+
for x in range(len(muscle_name_list))
32+
]
2933
)
3034
return muscle_fatigue
3135

@@ -44,8 +48,12 @@ def minimize_overall_muscle_force_production(controller: PenaltyController) -> M
4448
The sum of each force
4549
"""
4650
muscle_name_list = controller.model.bio_model.muscle_names
51+
muscle_model = controller.model.muscles_dynamics_model
4752
muscle_force = vertcat(
48-
*[controller.states["F_" + muscle_name_list[x]].cx for x in range(len(muscle_name_list))]
53+
*[
54+
controller.states["F_" + muscle_name_list[x]].cx / muscle_model[x].fmax
55+
for x in range(len(muscle_name_list))
56+
]
4957
)
5058
return muscle_force
5159

@@ -69,6 +77,7 @@ def minimize_overall_stimulation_charge(controller: PenaltyController) -> MX:
6977
stim_charge = vertcat(
7078
*[
7179
controller.controls["last_pulse_width_" + muscle_name_list[x]].cx
80+
/ controller.ocp.nlp[0].u_bounds["last_pulse_width_" + muscle_name_list[x]].max[0][0]
7281
for x in range(len(muscle_name_list))
7382
]
7483
)

cocofest/dynamics/inverse_kinematics_and_dynamics.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ def inverse_kinematics_cycling(
9494
model = biorbd.Model(model_path)
9595

9696
z = model.markers(np.array([0] * model.nbQ()))[0].to_array()[2]
97-
if z != model.markers(np.array([np.pi / 2] * model.nbQ()))[0].to_array()[2]:
97+
if not np.isclose(
98+
z, model.markers(np.array([np.pi / 2] * model.nbQ()))[0].to_array()[2], rtol=1e-05, atol=1e-08, equal_nan=False
99+
):
98100
print("The model not strictly 2d. Warm start not optimal.")
99101

100102
f = interp1d(

cocofest/models/ding2007/ding2007_with_fatigue.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(
4343
)
4444
self._with_fatigue = True
4545
self.stim_time = stim_time
46-
self.fmax = 315
46+
self.fmax = 248
4747

4848
# --- Default values --- #
4949
ALPHA_A_DEFAULT = -4.0 * 10e-2 # Value from Ding's experimentation [1] (s^-2)

cocofest/models/dynamical_model.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
DynamicsEvaluation,
1313
ParameterList,
1414
ContactType,
15+
OdeSolver,
1516
)
1617

1718
from ..models.fes_model import FesModel
@@ -199,9 +200,16 @@ def muscle_dynamic(
199200
ddq = nlp.model.forward_dynamics(with_contact=with_contact)(
200201
q, qdot, total_torque, external_forces, parameters
201202
) # q, qdot, tau, external_forces, parameters
203+
202204
dxdt = vertcat(dxdt_muscle_list, dq, ddq)
203205

204-
return DynamicsEvaluation(dxdt=dxdt, defects=None)
206+
defects = None
207+
if isinstance(nlp.dynamics_type.ode_solver, OdeSolver.COLLOCATION):
208+
slope_q = DynamicsFunctions.get(nlp.states_dot["q"], nlp.states_dot.scaled.cx)
209+
slope_qdot = DynamicsFunctions.get(nlp.states_dot["qdot"], nlp.states_dot.scaled.cx)
210+
defects = vertcat(slope_q, slope_qdot) * nlp.dt - vertcat(dq, ddq) * nlp.dt
211+
212+
return DynamicsEvaluation(dxdt=dxdt, defects=defects)
205213

206214
@staticmethod
207215
def muscles_joint_torque(

cocofest/models/fes_model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class FesModel(ABC):
99
def __init__(self):
1010
self.stim_time = None
11+
self.previous_stim = None
1112

1213
@abstractmethod
1314
def set_a_rest(self, model, a_rest: MX | float):

cocofest/optimization/fes_nmpc.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
from bioptim import (
55
MultiCyclicNonlinearModelPredictiveControl,
66
MultiCyclicCycleSolutions,
7-
OdeSolver,
87
OptimalControlProgram,
98
InitialGuessList,
10-
InterpolationType,
119
ParameterList,
1210
VariableScaling,
1311
Solution,
@@ -37,8 +35,8 @@ def advance_window_initial_guess_states(self, sol, n_cycles_simultaneous=None):
3735
return True
3836

3937
def advance_window_bounds_controls(self, sol, n_cycles_simultaneous=None, **extra):
40-
super(FesNmpc, self).advance_window_bounds_controls(sol)
41-
return True
38+
bound_have_changed = super(FesNmpc, self).advance_window_bounds_controls(sol)
39+
return bound_have_changed
4240

4341
@staticmethod
4442
def build_new_model(model, previous_stim):

cocofest/optimization/fes_nmpc_multibody.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
from copy import deepcopy
23
from casadi import SX
34
from bioptim import (
45
BiorbdModel,
@@ -11,7 +12,7 @@
1112
Solver,
1213
MultiCyclicCycleSolutions,
1314
ExternalForceSetTimeSeries,
14-
OdeSolver,
15+
ControlType,
1516
)
1617
from .fes_nmpc import FesNmpc
1718
from ..models.dynamical_model import FesMskModel
@@ -99,7 +100,9 @@ def _initialize_solution(self, dt: float, states: list, controls: list, paramete
99100
dynamics=self.nlp[0].dynamics_type,
100101
n_shooting=self.total_optimization_run * self.cycle_len,
101102
phase_time=self.total_optimization_run * self.cycle_len * dt,
103+
x_bounds=self.nlp[0].x_bounds,
102104
x_init=x_init,
105+
u_bounds=self.nlp[0].u_bounds,
103106
u_init=u_init,
104107
use_sx=self.cx == SX,
105108
parameters=parameters,
@@ -108,6 +111,65 @@ def _initialize_solution(self, dt: float, states: list, controls: list, paramete
108111
a_init = InitialGuessList()
109112
return Solution.from_initial_guess(solution_ocp, [np.array([dt]), x_init, u_init, p_init, a_init])
110113

114+
def _initialize_one_cycle(self, dt: float, states: np.ndarray, controls: np.ndarray, parameters: np.ndarray):
115+
"""return a solution for a single window kept of the MHE"""
116+
x_init = InitialGuessList()
117+
for key in self.nlp[0].states.keys():
118+
x_init.add(
119+
key,
120+
states[key],
121+
interpolation=self.nlp[0].x_init.type,
122+
phase=0,
123+
)
124+
125+
u_init = InitialGuessList()
126+
u_init_for_solution = InitialGuessList()
127+
for key in self.nlp[0].controls.keys():
128+
controls_tp = controls[key]
129+
u_init_for_solution.add(key, controls_tp, interpolation=InterpolationType.EACH_FRAME, phase=0)
130+
if self.nlp[0].control_type == ControlType.CONSTANT:
131+
controls_tp = controls_tp[:, :-1]
132+
u_init.add(key, controls_tp, interpolation=InterpolationType.EACH_FRAME, phase=0)
133+
134+
model_serialized = self.nlp[0].model.serialize()
135+
model_class = model_serialized[0]
136+
model_initializer = model_serialized[1]
137+
138+
param_list = ParameterList(use_sx=self.cx == SX)
139+
p_init = InitialGuessList()
140+
for key in self.nlp[0].parameters.keys():
141+
parameters_tp = parameters[key]
142+
param_list.add(
143+
name=key,
144+
function=self.nlp[0].parameters[key].function,
145+
size=self.nlp[0].parameters[key].shape,
146+
scaling=self.nlp[0].parameters[key].scaling,
147+
)
148+
p_init.add(
149+
key,
150+
parameters_tp,
151+
interpolation=InterpolationType.EACH_FRAME,
152+
phase=0,
153+
)
154+
155+
solution_ocp = OptimalControlProgram(
156+
bio_model=model_class(**model_initializer),
157+
dynamics=self.nlp[0].dynamics_type,
158+
objective_functions=deepcopy(self.common_objective_functions),
159+
n_shooting=self.cycle_len,
160+
phase_time=self.cycle_len * dt,
161+
x_bounds=self.nlp[0].x_bounds,
162+
x_init=x_init,
163+
u_bounds=self.nlp[0].u_bounds,
164+
u_init=u_init,
165+
use_sx=self.cx == SX,
166+
parameters=param_list,
167+
parameter_init=p_init,
168+
parameter_bounds=self.parameter_bounds,
169+
)
170+
a_init = InitialGuessList()
171+
return Solution.from_initial_guess(solution_ocp, [np.array([dt]), x_init, u_init_for_solution, p_init, a_init])
172+
111173
def create_model_from_list(self, models: list):
112174
if isinstance(models[0], BiorbdModel):
113175
return models[0]

cocofest/optimization/fes_ocp.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,22 @@ def set_x_bounds(model):
127127
interpolation=InterpolationType.CONSTANT_WITH_FIRST_AND_LAST_DIFFERENT,
128128
)
129129

130+
return x_bounds
131+
132+
@staticmethod
133+
def set_x_init(model):
134+
variable_bound_list = model.name_dof
130135
x_init = InitialGuessList()
131136
for j in range(len(variable_bound_list)):
132137
x_init.add(variable_bound_list[j], model.standard_rest_values()[j])
133138

134-
return x_bounds, x_init
139+
return x_init
135140

136141
@staticmethod
137142
def set_u_bounds(model, max_bound: int | float):
138143
u_bounds = BoundsList() # Controls bounds
139-
u_init = InitialGuessList() # Controls initial guess
140144

141145
if isinstance(model, DingModelPulseWidthFrequency):
142-
u_init.add(key="last_pulse_width", initial_guess=[0], phase=0)
143146
min_pulse_width = model.pd0 if isinstance(model.pd0, int | float) else 0
144147
u_bounds.add(
145148
"last_pulse_width",
@@ -149,7 +152,6 @@ def set_u_bounds(model, max_bound: int | float):
149152
)
150153

151154
if isinstance(model, DingModelPulseIntensityFrequency):
152-
u_init.add(key="pulse_intensity", initial_guess=[0] * model.sum_stim_truncation, phase=0)
153155
min_pulse_intensity = (
154156
model.min_pulse_intensity() if isinstance(model.min_pulse_intensity(), int | float) else 0
155157
)
@@ -160,7 +162,19 @@ def set_u_bounds(model, max_bound: int | float):
160162
interpolation=InterpolationType.CONSTANT,
161163
)
162164

163-
return u_bounds, u_init
165+
return u_bounds
166+
167+
@staticmethod
168+
def set_u_init(model):
169+
u_init = InitialGuessList() # Controls initial guess
170+
171+
if isinstance(model, DingModelPulseWidthFrequency):
172+
u_init.add(key="last_pulse_width", initial_guess=[0], phase=0)
173+
174+
if isinstance(model, DingModelPulseIntensityFrequency):
175+
u_init.add(key="pulse_intensity", initial_guess=[0] * model.sum_stim_truncation, phase=0)
176+
177+
return u_init
164178

165179
# TODO: Remove this method
166180
@staticmethod

0 commit comments

Comments
 (0)