An Open-Source Python Package for Functional Electrical Stimulation (FES) Optimization in Optimal Control.
Supports predictive musculoskeletal simulation driven by FES, moving time horizon, and model identification.
"Prototype todayβs FES to power tomorrowβs rehab."
Table of contents
Functional electrical stimulation (FES) is a neurorehabilitation technique that promotes motor recovery after neurological injury. By delivering coordinated electrical pulses to targeted muscles, FES elicits functional movements such as walking, reaching, and grasping. Because responses to stimulation vary across individuals and muscle groups, most FES protocols still rely on empirically tuned parameters. These settings can cause over stimulation, early muscle fatigue on-set, and reduce therapeutic gains.
Advanced control approaches like optimal control-driven FES can improve FES rehabilitation efficiency by personalizing stimulation parameters to a specific task and patient.
Therefore, we designed Cocofest (Custom Optimal COntrol for Functional Electrical STimulation) an open-source Python package for optimal control-driven FES.
Cocofest relies on bioptim, an optimal control program framework for biomechanics.
bioptim uses biorbd a biomechanics library, benefits from powerful algorithmic differentiation provided by CasADi
and robust solver like Ipopt.
Important
Cocofest has no clinical clearance and should not be used for rehabilitation purposes.
Don't forget to 
Important
Cocofest is currently not available on Anaconda/PyPI. The installation must be done from the sources,
so cloning the repository is the required first step before running any command below:
git clone https://github.com/pyomeca/cocofest.git
cd cocofestThe repository contains an environment.yml file with all of Cocofest's dependencies and a
pyproject.toml file for the package itself. The setup requires the following 3 commands from the
repository root:
conda env create -f environment.yml
conda activate cocofest
pip install -e .conda env create reads environment.yml and creates an environment named cocofest with every
dependency in one go, and pip install -e . registers Cocofest as a regular importable package, so
import cocofest works from anywhere (not just from inside the examples folder), without any relative-pathing
issue.
If you would rather pick your own environment name or dependency versions, follow these steps instead.
First, create a new conda environment
conda create -n YOUR_ENV_NAME python=3.11Then, activate the environment
conda activate YOUR_ENV_NAMEAfter, install the dependencies
conda install numpy matplotlib casadi biorbd pyorerun scikit-optimize bioptim==3.4.0 -c conda-forgeFinally, from the root of the cloned repository, install Cocofest itself in editable mode
pip install -e .You are now ready to use Cocofest!
Note
pytest is not part of environment.yml. If you want to run the tests or contribute to Cocofest, install the
dev tools with pip install -e ".[dev]" or conda install -c conda-forge pytest black codecov.
Every script in the examples folder is directly runnable. For instance, the pulse width optimization
script under getting_started can be run with:
python examples/getting_started/optimization/pulse_width_optimization.pySee examples/README.md for an index of every example folder and what each script demonstrates.
All models are implemented at the muscle actuator level, making them applicable to a wide range of problems regardless of the specific optimal control problem.
| Model Name | Citation | Description / Focus |
|---|---|---|
| Veltink1992 | Veltink, P. H., Chizeck, H. J., Crago, P. E., & El-Bialy, A. (1992). Nonlinear joint angle control for artificially stimulated muscle. IEEE Transactions on Biomedical Engineering, 39(4), 368β380. | Nonlinear control of joint angles via electrical stimulation. |
| Veltink1992 + Riener1996 | Veltink et al. (1992), combined with the fatigue prediction from Riener, R., Quintern, J., & Schmidt, G. (1996). Biomechanical model of the human knee evaluated by neuromuscular stimulation. Journal of Biomechanics, 29(9), 1157β1167. | Veltink1992 joint angle control, extended with Riener1996 fatigue prediction. |
| Ding2003 | Ding, J., Wexler, A. S., & Binder-Macleod, S. A. (2003). Mathematical models for fatigue minimization during functional electrical stimulation. Journal of Electromyography and Kinesiology, 13(6), 575β588. | Focus on mathematical models for minimising fatigue. |
| Ding2007 | Ding, J., Chou, L. W., Kesar, T. M., et al. (2007). Mathematical model that predicts the forceβintensity and forceβfrequency relationships after spinal cord injuries. Muscle & Nerve, 36(2), 214β222. | Predicts forceβintensity and forceβfrequency responses post-SCI. |
| Marion2009 | Marion, M. S., Wexler, A. S., Hull, M. L., & Binder-Macleod, S. A. (2009). Predicting the effect of muscle length on fatigue during electrical stimulation. Muscle & Nerve, 40(4), 573β581. | Examines muscle length impact on fatigue under stimulation. |
| Marion2013 | Marion, M. S., Wexler, A. S., & Hull, M. L. (2013). Predicting non-isometric fatigue induced by electrical stimulation pulse trains as a function of pulse duration. Journal of NeuroEngineering and Rehabilitation, 10, 1β16. | Predicts non-isometric fatigue based on pulse duration. |
| Hmed2018 | Hmed, A. B., Bakir, T., Garnier, Y. M., Sakly, A., Lepers, R., & Binczak, S. (2018). An approach to a muscle force model with force-pulse amplitude relationship of human quadriceps muscles. Computers in Biology and Medicine, 101, 218-228. | Models the relationship between pulse amplitude and force. |
Note
Riener1996 alone is not implemented as a standalone model: only its fatigue prediction is available, combined with Veltink1992 (see VeltinkRienerModelPulseIntensityWithFatigue).
Cocofest gives access to every model above through the ModelMaker factory, using the following string keys. Each base key also has a _with_fatigue variant (adds fatigue dynamics) and, for Marion2009/Marion2013, a _modified variant (pulse width instead of frequency as control):
| Key | Class | Control(s) |
|---|---|---|
ding2003 / ding2003_with_fatigue |
DingModelFrequency(WithFatigue) |
Frequency |
ding2007 / ding2007_with_fatigue |
DingModelPulseWidthFrequency(WithFatigue) |
Frequency, pulse width |
hmed2018 / hmed2018_with_fatigue |
DingModelPulseIntensityFrequency(WithFatigue) |
Frequency, pulse intensity |
marion2009 / marion2009_with_fatigue |
Marion2009ModelFrequency(WithFatigue) |
Frequency |
marion2009_modified / marion2009_modified_with_fatigue |
Marion2009ModelPulseWidthFrequency(WithFatigue) |
Frequency, pulse width |
marion2013 / marion2013_with_fatigue |
Marion2013ModelFrequency(WithFatigue) |
Frequency |
marion2013_modified / marion2013_modified_with_fatigue |
Marion2013ModelPulseWidthFrequency(WithFatigue) |
Frequency, pulse width |
veltink1992 |
VeltinkModelPulseIntensity |
Pulse intensity |
veltink_and_riener1998 |
VeltinkRienerModelPulseIntensityWithFatigue |
Pulse intensity |
from cocofest import ModelMaker
model = ModelMaker.create_model("ding2007_with_fatigue", stim_time=[0, 0.1, 0.2])Note
It is possible to implement more FES models into Cocofest. Adventurous enough to code it by yourself, we are looking forward to read your pull request. Feel free to reach out on discord or submit an issue if you need help.
In conventional Hill-type muscle model, muscle force (
Cocofest replaces
Note
Used force-length (activate_force_length_relationship, activate_force_velocity_relationship and activate_passive_force_relationship arguments of the FesMskModel constructor (all default to False). Modification to the following file can be done to have more/different relationships.
The following example displays a reaching task using the Arm26 model driven by the Ding2007 FES model.
Figure 1: Motion performed for the reaching task and associated muscle force production.
Note
Solved in 6.7 second, computer with an AMD Ryzen Threadripper PRO 7965WXs x 48 processor.
Additional information: frequency = 30Hz, n_shooting = 30, step = 0.033s, final time = 1s, integration = Collocation radau method, polynomial_order = 3, solver = IPOPT.
You can find more examples of musculoskeletal model driven by FES in the following file.
For longer time span simulation and apprehend muscle fatigue apparition, Cocofest implements moving time horizons (MHE)
through the FesMhe (single muscle) and FesMheMsk
(musculoskeletal) classes. Each window is solved, then the horizon slides forward by re-using the previous stimulation
history to keep fatigue state continuous across windows.
from cocofest import DingModelPulseWidthFrequencyWithFatigue, OcpFes, FesMhe
model = DingModelPulseWidthFrequencyWithFatigue(stim_time=[...], sum_stim_truncation=10)
dynamics_options = OcpFes.declare_dynamics_options(...) # see full example for every argument
mhe = FesMhe(
bio_model=model,
dynamics=dynamics_options,
cycle_len=cycle_len,
cycle_duration=cycle_duration,
n_cycles_simultaneous=n_cycles_simultaneous,
n_cycles_to_advance=1,
...,
)
def update_functions(_mhe, cycle_idx, _sol):
return cycle_idx < n_cycles # keep sliding the window until n_cycles is reached
sol = mhe.solve_fes_mhe(update_functions, solver=..., total_cycles=n_cycles, cycle_solutions=...)Note
See the full, runnable version in examples/getting_started/optimization/pulse_width_optimization_mhe.py
(single muscle) or examples/fes_multibody/cycling/cycling_pulse_width_mhe.py (musculoskeletal hand-cycling, below).
Figure 2: Motion performed for the cycling task, muscle force contribution per section inspired by
Quittmann et al. (2025)
and muscle force production above 10% of the maximal force.
Note
Solved in 1.02 second, computer with an AMD Ryzen Threadripper PRO 7965WXs x 48 processor.
Additional information: frequency = 30Hz, n_shooting = 60, step = 0.033s, final time = 2s, integration = Collocation radau method, polynomial_order = 3, solver = IPOPT, simultaneous turn per optimization = 2.
The initial value problem feature enables forward nonlinear dynamic integration to simulate the modelβs behavior from given initial state and controls (i.e., series of pulse trains). This also permits comparison between FES models without using optimal control methods.
For that, the IvpFes class is used to build the problem.
from cocofest import IvpFes, DingModelFrequencyWithFatigue
stim_time = list(range(10)) # 10 stimulations, one every second
fes_parameters = {"model": DingModelFrequencyWithFatigue(stim_time=stim_time)}
ivp_parameters = {"final_time": 10}
ivp = IvpFes(fes_parameters, ivp_parameters)
result, time = ivp.integrate()To personalize FES models to simulated or experimental force, Cocofest supports model identification using optimal
control, through the OcpFesId class. Model parameters are treated as
optimization parameters and identified by minimizing the difference between a tracked force (simulated with
IvpFes, or your own experimental data) and the model's predicted force.
from cocofest import ModelMaker, OcpFesId, OcpFes
from cocofest.identification.identification_method import DataExtraction
model = ModelMaker.create_model("hmed2018", stim_time=stim_time, sum_stim_truncation=10)
# force_tracking: simulated (via IvpFes) or experimental (time, force) data to fit
force_at_node = DataExtraction.force_at_node_in_ocp(time, force, n_shooting, final_time)
x_bounds, x_init = OcpFesId.set_x_bounds(model=model, force_tracking=force_at_node)
u_bounds, u_init = OcpFesId.set_u_bounds(model=model, control_value=pulse_intensity_values, ...)
additional_key_settings = OcpFesId.set_default_values(model)
parameters, parameters_bounds, parameters_init = OcpFesId.set_parameters(
parameter_to_identify=["a_rest", "km_rest", "tau1_rest", "tau2"],
parameter_setting=additional_key_settings,
use_sx=True,
)
OcpFesId.update_model_param(model, parameters)
ocp = OptimalControlProgram(bio_model=[model], x_bounds=x_bounds, x_init=x_init, u_bounds=u_bounds, u_init=u_init,
parameters=parameters, parameter_bounds=parameters_bounds, parameter_init=parameters_init, ...)
sol = ocp.solve()
identified_a_rest = sol.parameters["a_rest"][0]Note
See the full, runnable version in examples/getting_started/identification/muscle_model_id.py,
or examples/identification/force_model/ for the Ding2003/Ding2007/Hmed2018 variants.
Figure 3: Model identification optimization
Note
Solved in 0.343 second, computer with an AMD Ryzen Threadripper PRO 7965WXs x 48 processor.
Additional information: frequency = 33Hz, n_shooting = 66, step = 0.03s, final time = 2s, integration = Runge-Kutta 4, integration steps = 10, solver = IPOPT.
Cocofest also incorporates the recent numerical truncation method to speed up convergence.
This method limits the number of past stimulations considered in the dynamics to reduce the dependency on time-varying states.
model = ModelMaker.create_model("ding2007", stim_time=stim_time, sum_stim_truncation=10)Tip
To determine the value to use for sum_stim_truncation, you can refer to Tiago et al. (2025) or Co et al. (2024).
We are always looking for new contributors to help us improve Cocofest.
Feel free to check our contributing guidelines to get started, and please read our
code of conduct beforehand.
Don't know where to start? Issues tagged with "Good first issues" are a great place to begin!
The Cocofest companion paper is not published yet (submitted to JOSS).
Meanwhile, if you use Cocofest, please cite the software directly via its Zenodo archive:
10.5281/zenodo.17068808
Note
If you used Cocofest in your research, please let us know by submitting an issue or a pull request to add your publication to this list.
|
|
|
|
|







