Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New dynamic optimization method, NMPC, external force, cycling #8

Open
wants to merge 130 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
c8a45d4
feat(external forces): implementing external forces into msk model
Oct 9, 2024
63df875
feat(external forces) last node control fix and functional external F
Oct 10, 2024
5d927a6
feat(pedal): trying to implement a pedal with 2 additional dof
Oct 15, 2024
558e68c
Merge remote-tracking branch 'pyomeca/main' into external_forces
Nov 22, 2024
ee59847
feat(external_force): example with external torque for a biceps curl
Nov 22, 2024
a37b367
feat(external_force): cycling example (optimal solution found but wrong)
Nov 22, 2024
fb2b95d
feat(external_force): added example of cycling with ext_f and contact
Nov 26, 2024
1708b6e
feat (nmpc fes): some developpement for fes nmpc
Dec 3, 2024
d1f2422
feat (nmpc with fes): additionnal files to work on examples
Dec 4, 2024
211544f
feat (stimulation in model) : enabled the stimulation time in the model
Dec 5, 2024
19b1cfe
feat (stimulation in model): now available for nmpc
Dec 5, 2024
7ae30a5
feat (stimulation in model) : update stimulation time and width in nmpc
Dec 6, 2024
e02522a
temp
Dec 17, 2024
d2acab7
fix(integration): identified bug for integrators using rk4
Jan 7, 2025
248597f
working on convergence
Jan 14, 2025
205ab66
.
Jan 14, 2025
75d0f7d
example regrouping torque/muscle/fes driven
Jan 17, 2025
da85724
correct pedal inertial for model
Jan 17, 2025
7f298b0
Making truncation possible again
Jan 17, 2025
227b327
Reducing node shooting to increase optimization time
Jan 17, 2025
005f439
FIX: now all parameters sent are associated to the good muscle
Jan 31, 2025
f065f85
example cleaning
Jan 31, 2025
62974a5
truncation adjustment
Jan 31, 2025
deba936
fes_ocp cleaning
Jan 31, 2025
64c5cd6
removed the for_cycling argument
Jan 31, 2025
00546eb
Custom rk4 integrator to enable rk4 calculation w/ out calcium error
Jan 31, 2025
cf79af2
nmpc example
Feb 4, 2025
2b073e0
Cleaning cycling examples
Feb 4, 2025
c75b304
cleaning
Feb 4, 2025
f979b92
get pulse width
Feb 4, 2025
851a5d3
Passive force implementation
Feb 5, 2025
c428a69
working on nmpc
AnaisFarr Feb 5, 2025
8535d1e
Passive force
Feb 5, 2025
9631df0
Modified the automated way of determining number of node shooting
Feb 14, 2025
6b1d9d5
removed stim_time from param and removed use_sx for faster computation
Feb 14, 2025
9a1b574
Corrected parameter values to the good unit
Feb 14, 2025
1a8753a
Graphical class to plot figures
Feb 14, 2025
31a0878
New graphics and nmpc continuity fix
Feb 17, 2025
cfd63c5
feat(numerical_data_time_series): new method to gain ocp creation time
Feb 19, 2025
320daa8
Enabled bimapping and ivp with new optimization method
Feb 19, 2025
238f551
Examples and fes_ocp's update to new dynamic calculation method
Feb 21, 2025
ab339d1
get correct total cycle len
Feb 24, 2025
cff8831
black
Feb 24, 2025
3035014
Passive force
Feb 5, 2025
990274a
Modified the automated way of determining number of node shooting
Feb 14, 2025
f815a55
removed stim_time from param and removed use_sx for faster computation
Feb 14, 2025
efd5072
Corrected parameter values to the good unit
Feb 14, 2025
4106233
Graphical class to plot figures
Feb 14, 2025
2e6dc71
New graphics and nmpc continuity fix
Feb 17, 2025
ed01805
feat(numerical_data_time_series): new method to gain ocp creation time
Feb 19, 2025
d1cc15f
Enabled bimapping and ivp with new optimization method
Feb 19, 2025
68b6da6
Examples and fes_ocp's update to new dynamic calculation method
Feb 21, 2025
9a2db3e
get correct total cycle len
Feb 24, 2025
152b261
black
Feb 24, 2025
985a365
Merge remote-tracking branch 'origin/external_forces' into external_f…
Feb 24, 2025
0e8edee
rerun version enforced
Ipuch Feb 24, 2025
4fa3ffe
mamba to conda
Feb 24, 2025
f46e660
delete: image pickle and data
Ipuch Feb 24, 2025
341f8f1
renaming a model_msk
Ipuch Feb 24, 2025
07fe2f6
obsolete scripts
Ipuch Feb 24, 2025
1ce9b1b
black
Ipuch Feb 24, 2025
58a5b1f
fix control init order
Feb 24, 2025
4cdd390
ivp_test
Feb 24, 2025
09c4f44
back
Feb 24, 2025
22e1c85
refactor : example folder in root :
Ipuch Feb 24, 2025
555ed13
model test
Feb 24, 2025
e45a255
test model modification
Feb 24, 2025
33664de
todo
Feb 24, 2025
94e7fac
ready to test getting started
Ipuch Feb 24, 2025
ef89964
test model maker
Feb 24, 2025
c5c1829
reblack again !
Ipuch Feb 24, 2025
9f5582e
identification
Ipuch Feb 24, 2025
323bd44
nomore useless data
Ipuch Feb 24, 2025
e849206
delete: pickles
Ipuch Feb 24, 2025
424de62
renaming to fest_multibody
Ipuch Feb 24, 2025
8eae35f
ignore pkl
Ipuch Feb 24, 2025
4a502fa
id test
Feb 24, 2025
4b68634
submodul update
Feb 24, 2025
dc2a0c1
actions
Ipuch Feb 24, 2025
ed76246
make graph
Ipuch Feb 24, 2025
4ecfa17
frequecy
Ipuch Feb 24, 2025
2f2e7c8
tests
Feb 24, 2025
dbd23d5
refactor an example
Ipuch Feb 24, 2025
c80be5d
comment test
Feb 24, 2025
82146ad
Refactor in progess for new OcpFes class
Feb 24, 2025
a5aba1b
Removed submodule external/bioptim
Feb 26, 2025
21dd79f
example refactor to new ocpfes
Feb 26, 2025
1691dd9
fix cn error introduiced in previous commit
Feb 26, 2025
5dc1589
WIP id model
Feb 27, 2025
567d222
Refactor identification method
Feb 28, 2025
9f057be
fes multibody examples
Feb 28, 2025
33ec36e
getting started nmpc example fix
Feb 28, 2025
c8ad91b
multibody example refactor
Feb 28, 2025
3ba892e
multibody and nmpc example
Feb 28, 2025
5fe98e8
init
Feb 28, 2025
f1882d0
nmpc_cycling wip
Mar 1, 2025
b462380
changed bound to facilitate convergence
Mar 1, 2025
fc536b3
init
Mar 1, 2025
a0dd46a
torque val
Mar 1, 2025
8e76564
.
Mar 1, 2025
6a64ffa
int error difference
Mar 2, 2025
49e19f8
max iter
Mar 2, 2025
ad488a5
modification for more flexibility
Mar 3, 2025
c74bf8f
submodules
Mar 3, 2025
274bb65
Revert "Removed submodule external/bioptim"
Mar 3, 2025
c01d9c7
saved UNDO information for reset changes and discarded files
Mar 3, 2025
5c2e69c
submodule
Mar 3, 2025
fc5369e
example test
Mar 3, 2025
11da0bb
ivp test
Mar 3, 2025
198d945
Added submodule external/bioptim
Mar 3, 2025
4abc61e
ivp test
Mar 3, 2025
0c906b5
obsolete
Mar 3, 2025
0359fb4
todo
Mar 3, 2025
78d95f4
multibody tests
Mar 3, 2025
9be2d4e
python version to match bioptim
Mar 3, 2025
cd191e1
Added submodule external/bioptim
Mar 3, 2025
d59cb74
definition and pathfile name
Mar 3, 2025
cea0841
version
Mar 3, 2025
f757e69
v4
Mar 3, 2025
66f2a55
Revert "v4"
Mar 3, 2025
7b15322
v4
Mar 3, 2025
9a5d810
v4
Mar 3, 2025
da29cf5
fes cycling bound modification
Mar 4, 2025
ebc2111
fine tuned bounds and constraints for nmpc
Mar 4, 2025
a2761cc
float error
Mar 5, 2025
8182766
coverage testing
Mar 5, 2025
8e3d726
.
Mar 5, 2025
0212c6a
.
Mar 5, 2025
ee530a8
Cleaning before merge
Mar 6, 2025
9d6c437
Adressed issue #12 data_process folder
Mar 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/.gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "external/bioptim"]
path = external/bioptim
url = https://github.com/Kev1CO/bioptim
21 changes: 10 additions & 11 deletions .github/workflows/run_tests_win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,15 @@ jobs:
- name: Setup environment
uses: conda-incubator/setup-miniconda@v2
with:
miniforge-variant: Mambaforge
miniforge-version: latest
use-mamba: true
activate-environment: cocofest
environment-file: environment.yml

- name: Print mamba info
run: |
mamba info
mamba list
conda info
conda list

- name: Install bioptim on Windows
run: |
Expand All @@ -49,19 +48,21 @@ jobs:
cd ..

- name: Install extra dependencies
run: mamba install pytest-cov black pytest pytest-cov codecov packaging -cconda-forge
run: conda install pytest-cov black pytest pytest-cov codecov packaging -cconda-forge

- name: Run tests with code coverage
run: pytest -v --color=yes --cov-report term-missing --cov=cocofest --cov-report=xml:coverage.xml tests/shard${{ matrix.shard }}

- name: Archive coverage report
id: archive
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: coverage${{ matrix.shard }}
path: |
coverage.xml
.coverage
if-no-files-found: error
include-hidden-files: true

merge-coverage:
needs: build
Expand All @@ -81,15 +82,15 @@ jobs:
- name: Setup environment
uses: conda-incubator/setup-miniconda@v2
with:
miniforge-variant: Mambaforge
miniforge-version: latest
use-mamba: true
activate-environment: cocofest
environment-file: environment.yml

- name: Print mamba info
run: |
mamba info
conda info
conda list

- name: Install bioptim on Windows
run: |
Expand All @@ -103,7 +104,7 @@ jobs:

- name: Download all workflow run artifacts
id: download
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4

- name: Rename coverage files
run: |
Expand All @@ -126,11 +127,9 @@ jobs:
coverage xml
coverage report -m

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
- uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
flags: unittests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ __pycache__/
.pytest_cache


cocofest/examples/data/debug.py
examples/data/debug.py

external/bioptim

example/*.pkl
example/fes_multibody_cycling/*.pkl
example/reaching_task/*.pkl
example/minimize_fatigue/*.pkl

example/minimize_fatigue/result_file/*.pkl
examples/nmpc/results/*.pkl

*.pkl
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "external/bioptim"]
path = external/bioptim
path = external/bioptim
url = https://github.com/Kev1CO/bioptim
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Follows the steps to install everything you need to use `Cocofest`.
</br>
First, you need to create a new conda environment
```bash
conda create -n YOUR_ENV_NAME python=3.10
conda create -n YOUR_ENV_NAME python=3.11
```

Then, activate the environment
Expand Down
18 changes: 5 additions & 13 deletions cocofest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,12 @@
from .models.dynamical_model import FesMskModel
from .models.model_maker import ModelMaker
from .optimization.fes_ocp import OcpFes
from .optimization.fes_identification_ocp import OcpFesId
from .optimization.fes_ocp_dynamics import OcpFesMsk
from .optimization.fes_ocp_nmpc_cyclic import NmpcFes
from .optimization.fes_ocp_dynamics_nmpc_cyclic import NmpcFesMsk
from .optimization.fes_id_ocp import OcpFesId
from .optimization.fes_ocp_multibody import OcpFesMsk
from .optimization.fes_nmpc import FesNmpc
from .optimization.fes_nmpc_multibody import FesNmpcMsk
from .integration.ivp_fes import IvpFes
from .fourier_approx import FourierSeries
from .identification.ding2003_force_parameter_identification import (
DingModelFrequencyForceParameterIdentification,
)
from .identification.ding2007_force_parameter_identification import (
DingModelPulseWidthFrequencyForceParameterIdentification,
)
from .identification.hmed2018_force_parameter_identification import (
DingModelPulseIntensityFrequencyForceParameterIdentification,
)
from .dynamics.inverse_kinematics_and_dynamics import (
get_circle_coord,
inverse_kinematics_cycling,
Expand All @@ -33,3 +24,4 @@
from .result.plot import PlotCyclingResult
from .result.pickle import SolutionToPickle
from .result.animate import PickleAnimate
from .result.graphics import FES_plot
106 changes: 19 additions & 87 deletions cocofest/custom_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,96 +3,28 @@
"""

from casadi import MX, SX, vertcat

from bioptim import PenaltyController

from .models.hmed2018 import DingModelPulseIntensityFrequency
from .models.dynamical_model import FesMskModel


class CustomConstraint:
@staticmethod
def cn_sum(controller: PenaltyController, stim_time: list, model_idx: int = None) -> MX | SX:
model = controller.model.muscles_dynamics_model[model_idx] if isinstance(model_idx, int) else controller.model
cn_sum_key = model.cn_sum_name
km_key = model.km_name
intensity_in_model = True if isinstance(model, DingModelPulseIntensityFrequency) else False
pulse_intensity_key = model.pulse_intensity_name if intensity_in_model else None
pulse_intensity = controller.parameters[pulse_intensity_key].cx if intensity_in_model else None
lambda_i = model.get_lambda_i(nb_stim=len(stim_time), pulse_intensity=pulse_intensity)
km = controller.states[km_key].cx if model._with_fatigue else model.km_rest
r0 = model.get_r0(km=km)

return controller.controls[cn_sum_key].cx - model.cn_sum_fun(
r0=r0, t=controller.time.cx, t_stim_prev=stim_time, lambda_i=lambda_i
)

@staticmethod
def cn_sum_identification(controller: PenaltyController, stim_time: list, stim_index: list) -> MX | SX:
intensity_in_model = True if isinstance(controller.model, DingModelPulseIntensityFrequency) else False
ar, bs, Is, cr = None, None, None, None
if intensity_in_model:
ar = controller.parameters["ar"].cx if "ar" in controller.parameters.keys() else controller.model.ar
bs = controller.parameters["bs"].cx if "bs" in controller.parameters.keys() else controller.model.bs
Is = controller.parameters["Is"].cx if "Is" in controller.parameters.keys() else controller.model.Is
cr = controller.parameters["cr"].cx if "cr" in controller.parameters.keys() else controller.model.cr
lambda_i = (
[
controller.model.lambda_i_calculation_identification(
controller.parameters["pulse_intensity"].cx[i], ar, bs, Is, cr
)
for i in stim_index
]
if intensity_in_model
else [1 for _ in range(len(stim_time))]
)
km = (
controller.parameters["km_rest"].cx
if "km_rest" in controller.parameters.keys()
else controller.model.km_rest
)
r0 = km + controller.model.r0_km_relationship
return controller.controls["Cn_sum"].cx - controller.model.cn_sum_fun(
r0=r0, t=controller.time.cx, t_stim_prev=stim_time, lambda_i=lambda_i
)

@staticmethod
def a_calculation(controller: PenaltyController, last_stim_index: int) -> MX | SX:
a = controller.states["A"].cx if controller.model.with_fatigue else controller.model.a_scale
last_stim_index = 0 if controller.parameters["pulse_width"].cx.shape == (1, 1) else last_stim_index
a_calculation = controller.model.a_calculation(
a_scale=a,
pulse_width=controller.parameters["pulse_width"].cx[last_stim_index],
)
return controller.controls["A_calculation"].cx - a_calculation

@staticmethod
def a_calculation_msk(controller: PenaltyController, last_stim_index: int, model_idx: int) -> MX | SX:
model = controller.model.muscles_dynamics_model[model_idx]
muscle_name = model.muscle_name
a = controller.states["A_" + muscle_name].cx if model.with_fatigue else model.a_scale
last_stim_index = (
0 if controller.parameters["pulse_width_" + muscle_name].cx.shape == (1, 1) else last_stim_index
)
a_calculation = model.a_calculation(
a_scale=a,
pulse_width=controller.parameters["pulse_width_" + muscle_name].cx[last_stim_index],
)
return controller.controls["A_calculation_" + muscle_name].cx - a_calculation

@staticmethod
def a_calculation_identification(controller: PenaltyController, last_stim_index: int) -> MX | SX:
a = (
controller.parameters["a_scale"].cx
if "a_scale" in controller.parameters.keys()
else controller.model.a_scale
)
pd0 = controller.parameters["pd0"].cx if "pd0" in controller.parameters.keys() else controller.model.pd0
pdt = controller.parameters["pdt"].cx if "pdt" in controller.parameters.keys() else controller.model.pdt
last_stim_index = 0 if controller.parameters["pulse_width"].cx.shape == (1, 1) else last_stim_index
a_calculation = controller.model.a_calculation_identification(
a_scale=a,
pulse_width=controller.parameters["pulse_width"].cx[last_stim_index],
pd0=pd0,
pdt=pdt,
)
return controller.controls["A_calculation"].cx - a_calculation
def pulse_intensity_sliding_window_constraint(
controller: PenaltyController, last_stim_idx: int, muscle_name: str = ""
) -> MX | SX:
key = "pulse_intensity" + "_" + str(muscle_name) if muscle_name else "pulse_intensity"
parameters = [controller.parameters[key].cx[i] for i in range(last_stim_idx + 1)]
if isinstance(controller.model, FesMskModel):
model = controller.model.muscles_dynamics_model[0]
else:
model = controller.model

while len(parameters) < controller.controls[key].cx.shape[0]:
min_intensity = model.min_pulse_intensity() if isinstance(model.min_pulse_intensity(), int | float) else 0
parameters.insert(0, min_intensity)
if len(parameters) > controller.controls[key].cx.shape[0]:
size_diff = len(parameters) - controller.controls[key].cx.shape[0]
parameters = parameters[size_diff:]

return controller.controls[key].cx - vertcat(*parameters)
81 changes: 6 additions & 75 deletions cocofest/custom_objectives.py
Original file line number Diff line number Diff line change
@@ -1,78 +1,12 @@
"""
This custom objective is to enable the tracking of a curve by a state at all node. Used for sample data control problems
such as functional electro stimulation
This custom objective class regroups all the custom objectives that are used in the optimization problem.
"""

import numpy as np
from casadi import MX, SX, sum1, horzcat

from casadi import MX, vertcat
from bioptim import PenaltyController
from .fourier_approx import FourierSeries


class CustomObjective:
@staticmethod
def track_state_from_time(controller: PenaltyController, fourier_coeff: np.ndarray, key: str) -> MX | SX:
"""
Minimize the states variables.
By default, this function is quadratic, meaning that it minimizes towards the target.
Targets (default=np.zeros()) and indices (default=all_idx) can be specified.

Parameters
----------
controller: PenaltyController
The penalty node elements
fourier_coeff: np.ndarray
The values to aim for
key: str
The name of the state to minimize

Returns
-------
The difference between the two keys
"""
# get the approximated force value from the fourier series at the node time
value_from_fourier = FourierSeries().fit_func_by_fourier_series_with_real_coeffs(
controller.ocp.node_time(phase_idx=controller.phase_idx, node_idx=controller.t[0]),
fourier_coeff,
mode="casadi",
)
return value_from_fourier - controller.states[key].cx

@staticmethod
def track_state_from_time_interpolate(
controller: PenaltyController,
force: np.ndarray,
key: str,
minimization_type: str = "least square",
) -> MX:
"""
Minimize the states variables.
This function least square.
Targets (default=np.zeros()) and indices (default=all_idx) can be specified.

Parameters
----------
controller: PenaltyController
The penalty node elements
force: np.ndarray
The force vector
key: str
The name of the state to minimize
minimization_type: str
The type of minimization to perform. Either "least square" or "best fit"

Returns
-------
The difference between the two keys
"""
if minimization_type == "least square":
return force - controller.states[key].cx
elif minimization_type == "best fit":
return 1 - (force / controller.states[key].cx)
else:
raise RuntimeError(f"Minimization type {minimization_type} not implemented")

@staticmethod
def minimize_overall_muscle_fatigue(controller: PenaltyController) -> MX:
"""
Expand All @@ -88,13 +22,10 @@
The sum of each force scaling factor
"""
muscle_name_list = controller.model.bio_model.muscle_names
muscle_fatigue_rest = horzcat(
*[controller.model.bio_stim_model[x].a_rest for x in range(1, len(muscle_name_list) + 1)]
)
muscle_fatigue = horzcat(
muscle_fatigue = vertcat(

Check warning on line 25 in cocofest/custom_objectives.py

View check run for this annotation

Codecov / codecov/patch

cocofest/custom_objectives.py#L25

Added line #L25 was not covered by tests
*[controller.states["A_" + muscle_name_list[x]].cx for x in range(len(muscle_name_list))]
)
return sum1(muscle_fatigue_rest) / sum1(muscle_fatigue)
return muscle_fatigue

Check warning on line 28 in cocofest/custom_objectives.py

View check run for this annotation

Codecov / codecov/patch

cocofest/custom_objectives.py#L28

Added line #L28 was not covered by tests

@staticmethod
def minimize_overall_muscle_force_production(controller: PenaltyController) -> MX:
Expand All @@ -111,7 +42,7 @@
The sum of each force
"""
muscle_name_list = controller.model.bio_model.muscle_names
muscle_force = horzcat(
muscle_force = vertcat(
*[controller.states["F_" + muscle_name_list[x]].cx for x in range(len(muscle_name_list))]
)
return sum1(muscle_force)
return muscle_force
Loading
Loading