Skip to main content

BioScout Logo

BioScout

v3.1.1  ·  beta — the API may still move

A Python toolbox for musculoskeletal modelling.

Runs a full OpenSim + CEINMS pipeline from motion capture to muscle and joint contact forces, validates the results against the literature, and organises everything by subject / session / trial.
Successor to msk_modelling_python.
see License


Why BioScout

Getting from a motion-capture session to a defensible muscle-force number takes a dozen tools that do not agree on where files live. A typical study ends up with one folder per attempt, paths hard-coded into scripts, and no way to answer the question the study is actually about: does this modelling choice change the answer?

BioScout is built around that question. A session owns its raw captures once. On top of it you define as many model iterations as you want to compare — a generically scaled model, an MRI-personalised one, one with tuned moment arms — and every iteration runs the same trials through the same pipeline. Because the inputs are shared and the outputs are separated, the difference between two iterations is the effect of the modelling choice, not of a path or a re-export.

Everything a run needs is declared in one session.yaml. There are no arguments to remember and no state hidden in a notebook: the same file drives the Python API and the command line, so a session someone else hands you runs the same way yours does.

from bioscout import Session

s  = Session.open("simulations/Athlete_03/25_03_31")  # reads session.yaml
it = s.iteration("gpk_mri")                            # one runnable model variant

it.scale_model(muscle_opt=False)                       # generic -> scaled (+ MVIC for SO)
it.run(trials=["Squat_BW_01", "Walking_02"],
       do_exbiomec=True, do_so=True, do_ceinms=True, calibrate=True)

s.run(do_so=True, do_ceinms=True)   # every iteration in the session
s.summarise()                        # cross-model comparison figures

See Project structure for the folder layout and what session.yaml controls.


What it does

  • Full OpenSim pipeline — C3D → IK → ID → Muscle Analysis → Static Optimisation → muscle moments → Joint Reaction Analysis, per trial.
  • EMG-informed muscle forces — CEINMS calibration (once per session) and execution (per trial), compared against static optimisation and measured EMG.
  • Joint contact forces — SO vs CEINMS joint reaction forces, normalised to body weight, with literature validation bands overlaid (hip, knee) and gait-event marks.
  • Model validation — an independent muscle_inspect module sweeps moment arms, flags wrap discontinuities, and validates moment arms, fibre length/pennation, and joint strength against the literature (writes muscle_inspect_<model>/).
  • Batch processing — run one session or a whole project from settings.py or Session.batch_sessions(); idempotent (resumes) unless you force a rebuild.

See CHANGELOG.md for release notes.


Interface

bioscout is a Python package and a command line; it no longer ships a desktop GUI (removed in 3.1). For an interface use BioScout Web: it runs in the browser with nothing to install, opens the same project folders (project.yaml, session.yaml, 1_c3dfiles/, 2_experimental/, 3_iterations/) and writes its results where the Python package expects them, so the two can be used on the same project.

Requirements

Dependency Version Install via
Python 3.9 – 3.11 conda
OpenSim 4.6+ pip install opensim (or conda install -c opensim-org opensim)
numpy, pandas, scipy, matplotlib latest pip (auto with bioscout)
CEINMS 2.x separate install — only needed for the EMG-informed branch

Installation

1 — Create a Python 3.11 environment (OpenSim is not yet available for 3.12+):

conda create -n bioscout_env python=3.11 -y
conda activate bioscout_env

2 — Install OpenSim:

pip install opensim              # or: conda install -c opensim-org opensim -y
python -c "import opensim; print(opensim.__version__)"

3 — Install BioScout:

pip install bioscout

2.x is a beta line. It is a normal release — no --pre needed — but it is marked Development Status :: 4 - Beta on PyPI because the session/iteration API is still settling. Pin it (bioscout==3.1.1) if you need a run to stay reproducible. Coming from 1.x, see Migration from msk_modelling_python; note 2.x requires Python 3.9–3.11 and OpenSim.

For development, install editable so your edits take effect immediately:

git clone https://github.com/basgoncalves/bioscout
cd bioscout
pip install -e .

4 — Verify:

bioscout utils install     # prints a dependency status table

Command line

Nine verbs. Every one takes --help of its own, and bioscout help <verb> is the same thing.

bioscout                 the verb list
bioscout help [VERB]     the verb list, or one verb's detail
bioscout init            create a project: folders, models, templates, project.yaml
bioscout model           check / edit / compare / tune-ma / validate / scale / tps
bioscout simulate        run an OpenSim/CEINMS stage on a session (or mrs)
bioscout session         new / export / classify / ingest / reset
bioscout plot            figures, summaries, Collings ranking, JCF direction
bioscout utils           env / install / md2pdf / pylance
bioscout lab             EXPERIMENTAL 3.0: video and wearable tracking

bioscout -h needs nothing but the standard library — no conda check, no OpenSim import — so it still answers on a machine where the environment is half-built, which is when you need it most.

bioscout simulate so simulations/023/pre --replace
bioscout session new simulations/022/pre        # opens a dialog
bioscout model check models --strict
bioscout plot --list
bioscout help session

Coming from the flags

Every old flag still works — with one exception: --run_subject (and its --REPLACE/-b settings.py/--do-* stage flags) is gone outright, not aliased. It drove the old single-subject pipeline runner, which had no test coverage and no honest CLI replacement yet; see simulate above for the stages that already exist as separate steps. Every other old flag is hidden from --help and prints one line naming its replacement, so existing scripts keep running while you migrate. They will be removed one release after that.

old new
--new-session P --from-session Q --body-mass M session new P (a dialog asks)
--c3d-export P session export P
--classifier P session classify P
--ingest-c3d F --subject S --session N session ingest F --subject S --session N
--reset --reset-dry-run --reset-raw session reset --dry-run --raw
--model-edit, --edit model edit
--compare-models, --scale-setups model compare --scale-setups
--change-moment-arms, --ma model tune-ma
--tps model tps
--joint-centres, --jc model joint-centres
--summary -overall -s -t -p plot summary --overall --subject --trial --project
--collings --skip --metric --side --top plot collings --skip --metric --side --top
--env, --env-create utils env, utils env --create
--install utils install
--md2pdf --toc --bib --outdir utils md2pdf --toc --bib --outdir
--pylance-fix utils pylance
--shots, --load-report, --add_subject lab shots, lab load-report, lab add-subject

The point was not shorter names. The flat parser had grown to 66 options, and 25 of them existed only to modify another flag — their help began "With --X:". --top and --side mean nothing without --collings; --toc and --bib mean nothing without --md2pdf. Under a verb they are simply that verb's options, and the root help is ten lines.

bioscout plot jcf — the contact force vector, on the model's bones

For anyone who has run an OpenSim JointReaction analysis. One polar panel per joint: the bearing is the force's direction in the receiving bone's own frame (SUP up, ANT right), the radius is |JCF| in body weights, and the loop is the contact-force vector traced over the trial, drawn over the bone's silhouette read straight out of the .osim. No OpenSim install needed — the model and its .vtp meshes are read as plain XML.

bioscout plot jcf --model scaled.osim \
    --jra Analyse_JRA_ReactionLoads_SO.sto Analyse_JRA_ReactionLoads_CEINMS.sto \
    --labels SO CEINMS --mass 95 --ik joint_angles.mot -o jcf_direction.png

--ik (the trial's IK .mot) buys two things: the hip is converted exactly from the femur frame OpenSim wrote to the pelvis frame (Newton's third law + the hip's own SpatialTransform rotation), and each panel gains the neighbouring segment dotted at its two extremes with a range-of-motion arc. --bw (newtons) replaces --mass; --plane frontal gives the MED/LAT view; --geometry DIR ... adds mesh search folders. Details: docs/JCF_DIRECTION.md.

bioscout model check — the one to run after moving anything

OpenSim resolves a mesh path relative to the folder holding the .osim. Move a model, rename a Geometry/ folder, or write a personalised model into a new subfolder, and OpenSim opens it with every muscle, marker and joint intact and no bones at all — no exception, nothing in the log.

bioscout model check                      # the project's model folders
bioscout model check models --strict      # fail on anything not portable
bioscout model check models --json geometry.json

It reads XML only — no OpenSim needed — and reports which tier resolved each mesh, because "found it" is not the useful answer:

tier meaning
local the model's own folder or its Geometry/. Portable — the only pass.
parent ../Geometry. Fine in a project tree, breaks if the model alone is copied.
bundled only bioscout's own Geometry/. Renders only because bioscout is installed.
search only via --search or $OPENSIM_HOME. Machine-local.
absolute an absolute path. Points somewhere else on another computer.
case only by ignoring filename case. Works on Windows, fails on Linux.
empty / missing zero-byte mesh / not found. No bone will be drawn.

Anything but local warns; --strict makes it a failure. The same check runs at warn level every time a model is loaded during a run.


Project structure

A project is a folder holding a settings.py and a simulations/ tree organised subject → session → iteration → trial.

my_project/
├── settings.py                    ← project config AND the runner (see below)
├── generic models/                ← unscaled .osim models shared by every subject
└── simulations/
    └── <Subject>/<Session>/
        ├── session.yaml           ← the source of truth for this session
        ├── 1_c3dfiles/            ← raw captures, flat: <Trial>.c3d
        ├── 2_experimental/        ← model-INDEPENDENT exports, written once
        │   └── <Trial>/  marker_experimental.trc  grf.mot  GRF.xml
        │                 emg.mot  emg_filtered.mot  emg_filtered_normalised.mot
        │                 grf_events.png  emg_processing.png
        ├── 3_iterations/          ← model-DEPENDENT outputs, one folder per variant
        │   ├── cateli/
        │   ├── gpk/
        │   └── gpk_mri/
        │       ├── scaled_opt_N10.osim  scaled_opt_N10_mvicx3.00.osim
        │       ├── ceinms_calibration/  ← calibrated subject, per ITERATION
        │       ├── validation/          ← muscle_inspect reports for this model
        │       └── <Trial>/
        │           ├── external_biomechanics/  IK + ID (.sto), kinematics_moments.png
        │           ├── muscle_analysis/        muscle lengths / moment arms
        │           ├── static_optimisation/    SO forces + activations + figures
        │           ├── ceinms/                 CEINMS execution outputs
        │           └── joint_contact_forces/   JRA (SO & CEINMS) + literature bands
        ├── logs/
        └── results/               ← cross-iteration comparison figures

The three levels, and why they are separate

Session — one visit to the lab. It owns the raw .c3d captures and the model-independent exports derived from them (markers, ground reaction forces, EMG). These depend only on what the participant did, so they are computed once and every iteration reads the same files. Re-exporting per model is the single most common way to make two models look different for the wrong reason.

Iteration — one model variant. Everything downstream of a scaled .osim lives here: the model itself, its CEINMS calibration, its validation reports and its per-trial results. Two iterations of the same session differ only in the model, which is what makes them comparable.

Trial — one movement, with one folder per pipeline stage. Stages are idempotent: a stage whose output already exists is skipped unless you pass replace=True, so an interrupted run resumes rather than restarting.

ceinms_calibration/ sits inside the iteration, not at the session root. The calibrated subject is a property of one model variant — cateli, gpk and gpk_mri each have their own — so a session-level folder would claim one calibration covers all of them.

session.yaml is the source of truth

One file per session defines the iterations and their labels, colours and groups; the static trial and body mass; per-trial time windows and events; the EMG channel-to-muscle map; and the CEINMS α/β/γ weights.

It outranks settings.py at run time. Where the two disagree, settings.py is the one silently doing nothing — that is the first thing to check when a setting appears to have no effect.

It is parsed strictly: duplicate keys and iteration names differing only in case are errors, not last-wins. Session.open requires exactly session.yaml (not .yml), and Session.iterations only reports an iteration whose folder exists on disk.

Create one from the captures rather than by hand — the trial list comes from the c3d filenames, so a trial cannot go missing:

bioscout --new-session "simulations/<Subject>/<Session>" --body-mass 82.5
bioscout --c3d-export  "simulations/<Subject>/<Session>"
bioscout --classifier  "simulations/<Subject>/<Session>" --write-session-yaml

The full walkthrough, including what session new deliberately does not guess, is in docs/SESSION_LAYOUT.md.

Resolving folder names — never join them by hand

Two layouts are supported. The numbered one above is current; the older flat one (c3dfiles/, experimental/, iterations directly under the session) still works, and existing sessions are never renamed behind your back. The resolvers in bioscout.session.layout answer from what is on disk, preferring the numbered names only when creating something new:

call returns
c3d_root(session) 1_c3dfiles/
experimental_root(session) 2_experimental/
iterations_root(session) 3_iterations/
iteration_path(session, name) 3_iterations/<name>/
is_numbered_layout(session) True once the session is numbered

Because they read the disk, a resolver must be called after the folders exist. Caching one in a module-level constant returns the wrong answer on a session that is half-migrated — the mistake to watch for.

Trial layout & attribute names

As of 2.0 there is no Inputs class — the canonical folder layout is owned by Analyse itself (bioscout.core.analysis._default_layout_paths). A project only needs its own Inputs class in settings.py if it wants to override the default paths (Analyse picks it up automatically when present). On an Analyse/trial object the terse layout fields have readable aliases (read/write proxies onto the same value):

alias field alias field
model_path model_dir grf grf_mot
joint_angles ik joint_reaction_so jra
inverse_dynamics id joint_reaction_ceinms jra_ceinms
static_optimisation_forces so_forces static_optimisation_activations so_activations

The short names remain the canonical keys serialised into trial_settings.xml.

Resetting

s.reset() and Session.reset_project() strip a session back to inputs-only, keeping a timestamped backup. Pass dry_run=True to see what would go first — a dry run that reports nothing removed is correct behaviour, not a failure.


Configuring a project — settings.py

Subjects, the session, and the trials to run are declared in the project's settings.py (class BatchSettings):

class BatchSettings:
    SESSION      = "25_03_31"
    session_list = [SESSION]
    trial_list   = ["Walking_02", "Squat_BW_01", "Squat_35kg_01"]

    ALL_SUBJECTS = [
        Subject("Athlete_03_Cateli",     label="Scaled (Cateli)",     session=SESSION, ...),
        Subject("Athlete_03_GPK_MRI",    label="MRI (GPK)",           session=SESSION, ...),
        # ...
    ]
    RUN_SUBJECTS  = None     # None/[] = all;  or ["Athlete_03_GPK"]
    SKIP_SUBJECTS = []
    SUBJECTS      = select_subjects(ALL_SUBJECTS, RUN_SUBJECTS, SKIP_SUBJECTS)

    dof_list      = ["hip_flexion_r", "hip_flexion_l", ...]   # bilateral set processed by IK/ID/CEINMS
    MUSCLE_GROUPS = { ... }
    # JRA_COLUMNS(model) resolves the per-model joint-contact-force columns.

class SummarySettings:
    # DOFs to PLOT / SUMMARISE (e.g. in kinematics_moments.png). Left/right merge
    # onto one column (right=blue, left=red); include the pelvis if you want it.
    dofs = ["pelvis_tilt", "hip_flexion_r", "hip_flexion_l", "knee_angle_r", ...]

Raw captures go in simulations/<Subject>/<Session>/1_c3dfiles/ as flat <Trial>.c3d; bioscout --c3d-export turns them into the shared 2_experimental/ inputs. See Project structure.


settings.py is also the runner

The project's settings.py doubles as the runner: edit the CONFIG block at the bottom (which iterations, trials, and stages to run) and launch it directly. It opens the session and drives Iteration.run / Session.summarise for you:

conda activate bioscout_env
python settings.py

Toggle DO_SCALE / DO_EXBIOMEC / DO_SO / DO_CEINMS / DO_SUMMARY and REPLACE there; per-trial config (time windows, sides, CEINMS α/β/γ, model names) lives in each session's session.yaml.


Adding a new subject

A subject needs three things: (1) an entry in settings.py, (2) a scaled .osim model, and (3) the trial .c3d files placed in the simulations tree.

Say your raw captures live as one folder per session with each file named after the trial, e.g.:

C:/…/Powerlifiting/c3dfiles/25_03_31/Walking_02.c3d
                                     Squat_BW_01.c3d
                                     Squat_35kg_01.c3d
  1. Declare the subject in settings.py → BatchSettings.subjects (plain data):

    dict(name="Athlete_04", label="Athlete 04", session="25_03_31",
         model_so="scaled.osim", model_ceinms="scaled.osim",
         generic_model="Rajagopal2015.osim", color="orange", group="generic"),
    
  2. Ingest the C3D files — distributes each <trial>.c3d into simulations/<subject>/<session>/<trial>/inputs/c3dfile.c3d and creates the models/<subject>/<session>/ folder:

    bioscout session ingest "C:/…/Powerlifiting/c3dfiles/25_03_31" \
      --subject Athlete_04 --session 25_03_31
    
  3. Drop the scaled model at models/Athlete_04/25_03_31/scaled.osim (matching the model_so/model_ceinms names from step 1).

  4. Run — session export regenerates markers/GRF/EMG + trial_settings.xml from each c3d, then simulate runs a stage:

    bioscout session export simulations/Athlete_04/25_03_31
    bioscout simulate so simulations/Athlete_04/25_03_31 --replace
    bioscout simulate ceinms simulations/Athlete_04/25_03_31 --replace
    

(session ingest is also available on the Python API as session.ingest_c3d(source=…).)


Running the pipeline

The commands below are the stable OpenSim / CEINMS pipeline (the focus of 2.x). bioscout lab holds the markerless-video / wearables features (lab shots, lab load-report, lab add-subject) — targeted for bioscout 3.0 and not stable yet. Ignore them for pipeline work; they're documented in Future add-ons.

There is no longer a single CLI verb that drives the whole pipeline (export → scale → IK → ID → MA → SO → CEINMS → JRA) for one subject in one call — the old bioscout run did that, backed by pipeline.run_subject(), and both were removed outright (no test coverage, no honest per-stage story). Two ways to run the pipeline today:

Command line — one session, one stage at a time, driven by that session's session.yaml (see bioscout session new):

cd /path/to/my_project
bioscout simulate exbiomec simulations/Athlete_03_Cateli/25_03_31   # IK -> ID -> MA
bioscout simulate so simulations/Athlete_03_Cateli/25_03_31         # SO -> moments -> JRA
bioscout simulate ceinms simulations/Athlete_03_Cateli/25_03_31     # calibrate -> execute -> JRA

Restrict scope or force a rebuild:

bioscout simulate so simulations/Athlete_03_Cateli/25_03_31 --trial Walking_02
bioscout simulate so simulations/Athlete_03_Cateli/25_03_31 --replace   # overwrite existing outputs

--trial accepts a comma-separated list; --iteration restricts to one model arm (default: every one on disk). Runs are idempotent — a stage is skipped when its output already exists unless --replace is given.

Batch, across subjects — bioscout -b settings.py (a legacy flag, not yet its own verb) still drives pipeline.run_project() / run_pipeline() from a project's settings.py, unaffected by this removal.

Running individual steps on one trial

python -m bioscout.utils.analyse <trial_dir> [method ...] calls any Analyse method by name on a single trial (the nearest parent settings.py is bound automatically). Handy for re-running one stage or regenerating a figure without a full batch:

# full SO branch on one trial (start at IK)
python -m bioscout.utils.analyse "simulations/Athlete_03_Lernagopal/25_03_31/Walking_02" run_ik run_id run_ma run_so run_jra
# (subset, e.g. just kinematics/kinetics)
python -m bioscout.utils.analyse "simulations/Athlete_03_Lernagopal/25_03_31/Walking_02" run_ik run_id

# CEINMS branch. EMG normalisation and calibration are SESSION-level (they need
# every trial's EMG for the MVC reference, and calibrate once per subject/session).
# The CLI recognises session-scoped verbs (normalise_emg, calibrate, ...) and runs
# them on the trial's parent session — point it at ANY trial in the session:
python -m bioscout.utils.analyse "simulations/Athlete_03_Lernagopal/25_03_31/Walking_02" normalise_emg calibrate

# Then execute CEINMS per trial (SO outputs + calibrated model must already exist) and JRA:
python -m bioscout.utils.analyse "simulations/Athlete_03_Lernagopal/25_03_31/Walking_02" run_ceinms_exe run_jra_ceinms

run_jra_ceinms reads the CEINMS MuscleForces.sto from the execution output, runs the JointReaction analysis, writes joint_contact_forces/Analyse_JRA_ReactionLoads_CEINMS.sto, and (when the SO JRA also exists) the JRA_SO_vs_CEINMS.png comparison figure with literature bands.

Note: normalise_emg/calibrate are methods of the Session, not the trial, so the CLI resolves <Subject>/<Session>/ from the trial path and calls them there. (The trial-level run_emg_normalise is a legacy path that writes a different file — prefer normalise_emg.)

s.normalise_emg() writes inputs/emg_filtered_normalised.mot for every trial (session-max on the filtered envelope) and s.calibrate() produces ceinms_calibration/subjectCalibrated.xml. run_ceinms_exe then needs those plus the SO outputs (run_ma run_so). It writes the CEINMS execution outputs plus the SO-vs-CEINMS-vs-EMG comparison figures (ceinms/emg_vs_activations.png: gray = EMG, red = SO, blue = CEINMS).

CEINMS execution modes

CEINMS execution minimises a weighted objective (Sartori et al. 2014, Eq. 1):

F_obj = α · E_trackMOM  +  β · E_sumEXC  +  γ · E_trackEMG
term weight what it penalises
E_trackMOM α difference between CEINMS and inverse-dynamics joint moments
E_sumEXC β sum of squared excitations — an effort penalty, not an EMG term
E_trackEMG γ difference between adjusted and measured excitations — the EMG-tracking weight

Only the ratios β/α and γ/α are identifiable: scaling an objective cannot move its minimum, so (α, β, γ) and (1, β/α, γ/α) are the same problem. That is why α is conventionally fixed at 1, and why α10 β1 γ1000 and α1 β0.1 γ100 are the same run.

Choosing γ is not settled, and it is not cosmetic — on a powerlifting dataset γ moved hip contact force by several body weights. So bioscout does not assume one way of running CEINMS. Pick a mode, and the mode decides both how many solves happen and what the result is.

# simulations/<subject>/<session>/session.yaml
ceinms:
  mode: bounds          # single | bounds | full_loop | lcurve | optimise
  alpha: 1
  beta: 1
  gamma: 30             # the PRODUCTION weighting
  gamma_bounds: [10, 100]

Default is single, so a project that never sets mode behaves exactly as before.

mode solves the result is use it when
single 1 one curve the weighting is already justified; you want the cheapest correct run
bounds 3 production estimate + a sensitivity band you must show how much the weighting moved the answer, without paying for a grid
full_loop ∏ ranges a range with a median you want the distribution over a grid, not a bracket
lcurve β × γ grid, +1 production taken from the knee you want the published tuning procedure — read the caveat below
optimise — CEINMSoptimise picks the weights you would rather search than choose; far the slowest

single

ceinms: {mode: single, alpha: 1, beta: 1, gamma: 30}

One solve into ceinms/Execution_a1_b1_g30/. Identical to pre-mode behaviour.

bounds

ceinms:
  mode: bounds
  alpha: 1
  beta: 1
  gamma: 30
  gamma_bounds: [10, 100]     # must bracket gamma

Three solves — lower, upper, then production last. Report γ = 30 as the estimate and γ = 10–100 as a band around it. The band is not a confidence interval and its middle is not a median: the brackets are chosen to expose a sensitivity, so averaging them would invent a central tendency. bioscout refuses bounds that do not bracket γ, because a band that excludes the estimate cannot be read as a sensitivity around it.

full_loop

ceinms:
  mode: full_loop
  alpha_range: [1]
  beta_range:  [0.2, 1, 3]
  gamma_range: [1, 3, 10, 30, 100, 300]

Every combination, production last. Here the median is descriptive, because the grid is dense enough for it to mean something. Cost is the product of the range lengths — 18 solves per trial above.

lcurve

ceinms:
  mode: lcurve
  lcurve_betas:  [0, 0.2, 1, 3, 10, 30, 100, 300]
  lcurve_gammas: [0, 1, 3, 10, 30, 100, 300, 1000, 3000]

Sweeps the grid, normalises each objective term to its own maximum, builds the best curve (at each γ the lowest Ê_trackMOM, breaking ties toward the lowest Ê_sumEXC as Sartori do — without that tie-break β = 0 wins every point by construction), locates the knee by the L-method, and then re-solves at the knee so production is the knee.

Check the knee before you trust it. The L-method finds the corner of a normalised picture. When the curve is nearly flat past the elbow, both axes are normalised to their own extremes and the knee tracks the range you swept rather than the data. On this project's squat trials the knee came out at exactly one tenth of the top of the γ range, in five nested ranges out of five.

The manifest reports two diagnostics:

  • elbow_ratio — two-segment fit RMSE ÷ single-line RMSE. Below about 2 there is no real elbow and the "knee" is just the best place to break a line.
  • knee_gamma_over_range_top — run two different γ ranges. If this is unchanged, the knee is a property of your grid. Use bounds or full_loop and report a band instead.

optimise

CEINMSoptimise searches the weight space itself. Nothing to configure, but it is far the slowest, and it builds its own cfg — check that the dofSet it tracks matches your execution cfg, since a silently dropped coordinate (knee adduction, typically) changes predicted hip contact force substantially.

What lands on disk

Every mode writes one folder per solve, ceinms/Execution_a<α>_b<β>_g<γ>/, plus a manifest:

ceinms/ceinms_modes_manifest.json
  {"mode": "bounds",
   "arms": [{"alpha":1,"beta":1,"gamma":10,"tag":"a1_b1_g10","production":false,
             "output_dir":"...","ok":true}, ...],
   "production": {...},
   "knee": null}

The production arm is always solved last, and its outputs keep the plain untagged filenames — so downstream code that knows nothing about modes reads the production result by default, and a run that dies half way leaves a tree that is incomplete rather than one quietly describing the wrong weighting.

The API

from bioscout.utils import ceinms
m = ceinms.ExecutionMode(trial)      # reads session.yaml via the trial
print(m.mode, m.n_solves, m.arms)    # inspect before committing the compute
manifest = m.run(trial.run_ceinms_exe_single)

ceinms.ExecutionMode.describe()      # {mode: one-line definition}

The implementation lives in bioscout/utils/ceinms/modes.py and is re-exported above, so there is one name to remember. It is a separate module on purpose: ceinms.py imports OpenSim, scipy, matplotlib and pandas at its top and is loaded inside a try/except that degrades to "binary package only" when any of those is missing. If mode selection lived there, a missing OpenSim DLL would turn a three-arm bounds run into a one-arm run with no error — the config option would be silently ignored. ceinms.modes imports only the standard library, so mode is parsed and validated identically whether or not the solver can start:

from bioscout.utils.ceinms.modes import ExecutionMode   # no OpenSim needed

To turn per-arm results into a band:

band = ceinms.ExecutionMode.aggregate({"a1_b1_g10": jcf_low,
                                       "production": jcf_prod,
                                       "a1_b1_g100": jcf_high})
# -> {"production", "low", "high", "median", "n"}

Quote production. Plot low–high as the band.

Several EMG maps in one session

Which model muscles an electrode is taken to drive is a modelling assumption, and a consequential one — widening the gastrocnemius channel to the whole triceps surae changes what CEINMS is fitting. So emg_map can hold named maps, and each iteration names the one it runs with:

# simulations/<subject>/<session>/session.yaml
emg_map:
  narrow:
    EMG_Channels_EMG09_gast_med_l: [gasmed_l, gaslat_l]
    # ... the session's other channels
  triceps:
    EMG_Channels_EMG09_gast_med_l: [gasmed_l, gaslat_l, soleus_l]
  wide:
    EMG_Channels_EMG09_gast_med_l: [gasmed_l, gaslat_l, soleus_l, perlong_l]

default_emg_map: narrow          # optional: what a silent iteration gets

iterations:
  cateli_narrow:  {generic: Catelli.osim, emg_map: narrow,  ...}
  cateli_triceps: {generic: Catelli.osim, emg_map: triceps, ...}
  gpk_wide:       {generic: GPK_v3.osim,  emg_map: wide,    ...}

The alternative was one COPIED session per grouping — same subject, same trials, same windows, same exported inputs, differing in one dict, and three places to keep a time range in step. As iterations they share all of that and compare in the same tables and figures.

A single flat emg_map (the original form) still works exactly as before: the two are told apart by value type — a channel maps to a list of muscles, a named map to a mapping of channels.

Ambiguity is an error rather than a default. With more than one map and no way to choose — no iteration selector, no default_emg_map, no map called default — the session refuses to load, as does a selector naming a map that does not exist. The failure being avoided is a run that completes normally several hours later having used the wrong electrode set, with nothing in the output to show it.

from bioscout.session.session import emg_maps, resolve_emg_map
emg_maps(cfg)                        # {'narrow': {...}, 'triceps': {...}, ...}
resolve_emg_map(cfg, "cateli_wide")  # the flat {channel: [muscles]} it runs with

Several calibration configs in one session

CEINMS's calibration bounds were one global value in settings.py, so sweeping one meant copied sessions and a runtime monkeypatch. calibration is a session.yaml block with the same shape as emg_map:

calibration:
  wide:  {optimalFiberLength: "0.5 3",        tendonSlackLength: "0.5 3"}
  tight: {optimal_fiber_length: [0.75, 1.25], tendon_slack_length: "0.75 1.25"}

default_calibration: wide

iterations:
  cateli__wide:  {generic: Catelli.osim, calibration: wide,  ...}
  cateli__tight: {generic: Catelli.osim, calibration: tight, ...}

Same rules throughout: one flat block or several named ones, told apart by value type; ambiguity refuses to load. An override is partial — a config naming only optimalFiberLength leaves every other bound to settings.py.

Both spellings of every parameter now work. settings.py has always declared optimal_fiber_length, tendon_slack_length, shape_factor and strength_coefficient, while the XML writer read the camelCase names — so four of the six ranges were unreachable and editing them in settings.py silently did nothing. Only c1 and c2 were ever live. Both spellings are canonicalised now, in settings.py and in calibration: alike.

from bioscout.session.session import calibration_configs, resolve_calibration
resolve_calibration(cfg, "cateli__tight")   # {'optimalFiberLength': '0.75 1.25', ...}

Re-cropping to edited gait events (no re-export)

Editing the <events> in a trial's trial_settings.xml does not by itself change the analysis window on a re-run — the stored start_time/end_time take precedence. Call recrop_to_events first to re-derive and persist the window from the (edited or re-detected) events, then re-run the solve steps:

# uses the current (hand-edited) events, refreshes inputs/grf_events.png, re-solves
python -m bioscout.utils.analyse "simulations/Athlete_03_Cateli/25_03_31/Walking_02" recrop_to_events run_ik run_id run_ma run_so run_jra

Raw inputs are untouched (no export); IK/ID/MA/SO/JRA simply solve within the new window. The grf_events.png labels and the <event n="…"> tags in trial_settings.xml are numbered in time order for easy cross-checking.

Re-detecting after the forces changed. A plain re-export KEEPS existing foot events (it trusts mocap-labelled events and only auto-detects when there are none). If the GRF actually changed and you want fresh events, force it:

python -m bioscout.utils.analyse ".../Walking_02" redetect_events run_ik run_id run_ma run_so run_jra

redetect_events re-detects from the GRF, overwrites the events, re-derives the window, and refreshes grf_events.png.

Choosing which leg to summarise. SummarySettings.analysis_leg ("both", "r", or "l") sets the default leg drawn in kinematics_moments.png. Override per trial by adding <analysis_leg>r</analysis_leg> to that trial's trial_settings.xml — midline DOFs (pelvis/lumbar) are always kept.

Resetting trials to inputs-only

session reset strips generated outputs (IK/ID/MA/SO/JRA results, setup_*.xml, MuscleAnalysis/, Execution*/, filtered EMG, plots, CEINMS calibration, …) back to the raw inputs, so a trial re-runs clean. It keeps each trial's inputs/ folder (the C3D lives inside it) and trial_settings.xml, and makes a timestamped backup (simulations_backup_<ts>/) before deleting anything.

session reset follows the same scoping as everything else — pass --trial to reset one trial, --session for a whole session, or no scope to reset the entire simulations/ folder:

# reset only, no run
bioscout session reset --trial Walking_02            # one trial (keeps whole inputs/)
bioscout session reset --session 25_03_31            # a whole session
bioscout session reset                               # the entire simulations/ folder
bioscout session reset --trial Walking_02 --dry-run  # preview, touch nothing
bioscout session reset --trial Walking_02 --raw      # prune inputs/ to just the c3d + trial_settings.xml

# reset-then-run is two commands now: reset, then simulate the stage(s) needed
bioscout session reset --session 25_03_31 --trial Walking_02
bioscout simulate so simulations/Athlete_03_Cateli/25_03_31 --trial Walking_02 --replace

# add `session export` first to also regenerate inputs/ (markers/GRF/EMG) from the C3D
bioscout session reset --session 25_03_31 --trial Walking_02
bioscout session export simulations/Athlete_03_Cateli/25_03_31
bioscout simulate so simulations/Athlete_03_Cateli/25_03_31 --trial Walking_02 --replace

When scoped to a trial, sibling trials and session-level files are left untouched. Because inputs/ is preserved, a plain session reset already gives a clean recompute; add --export only when you also want inputs rebuilt from the C3D.

Python API — the session-centric hierarchy (Session → Iteration → trial):

from bioscout import Session

s  = Session.open("simulations/Athlete_03/25_03_31")
it = s.iteration("gpk_mri")

# whole iteration (all its trials); do_scale builds the scaled model first
it.run(trials=["Walking_02", "Squat_BW_01"], do_scale=False,
       do_so=True, do_ceinms=True, calibrate=True, replace=True)

# one trial, step by step (a trial is an Analyse object)
t = it.trial("Walking_02")
t.run_ik(); t.run_id(); t.run_ma(); t.run_so()
t.calculate_muscle_moments(forces_type="so"); t.run_jra()

# every model iteration in the session, then cross-model comparison figures
s.run(do_so=True, do_ceinms=True); s.summarise()

# whole project — batch across every simulations/<subject>/<session>/session.yaml
Session.batch_sessions(".", subjects="Athlete_03", do_so=True, do_ceinms=True)

The per-trial stages are, in order:

IK → ID → Muscle Analysis → Static Optimisation → muscle moments → JRA        (SO branch)
EMG normalise → CEINMS calibrate (session) → CEINMS execute → muscle moments → JRA   (CEINMS branch)

To clean a session back to inputs-only (with a timestamped backup):

s.reset(dry_run=True)                                          # this session (dry_run first!)
Session.reset_project(".", sessions="25_03_31", dry_run=True)  # whole project, scoped

Muscle ranking across models — bioscout plot collings

Which muscles a model leans on, ranked, with every model side by side and the disagreement drawn on:

bioscout plot collings "simulations/Athlete_03/25_03_31"
bioscout plot collings "simulations/Athlete_03/25_03_31" --skip gpk_mri cateli_mri
bioscout plot collings . --trial Walking_03 --metric impulse --top 15 --side _l

One ranked panel per model iteration, ordered by peak muscle force of each functional group (--metric impulse for the time-integral instead). Static optimisation is the leftmost panel wherever it exists — it uses no EMG, so it is the same whatever the EMG-informed columns do, which is what makes it the right reference to anchor on.

Read the colours, not the lines. Each muscle is coloured by its rank in the leftmost panel and keeps that colour all the way right, so the left panel is always a clean dark→pale ramp and any colour disorder further right is a re-ranking. Connectors then tell you the direction: blue = ranked higher by the panel on its right, red = lower, dotted = dropped out of the top N.

flag meaning
plot collings [SESSION] session folder (the one holding session.yaml); defaults to .. Pointing at a project or subject folder works too if exactly one session sits below it.
--skip A B ... iterations to leave out, case-insensitive
--trial NAME[,NAME] restrict to these trials (default: all)
--metric peak|impulse rank by peak force (default) or force impulse
--side _r|_l which limb (default right)
--top N how many muscle groups (default 12)
-o DIR output folder (default <session>/4_outputs/collings)

After Collings et al. (2025), Med Sci Sports Exerc, who use this layout to compare exercise rankings by peak muscle force against peak EMG. The layout and the colour anchoring are theirs; the columns here are models, not two measures of one model, so output from this flag is not a replication of their result.

Joint angles and marker errors across iterations

bioscout plot kinematics "simulations/P3/session_01"
bioscout plot kinematics "simulations/P3/session_01" --skip thesis_manual ast
bioscout plot kinematics . --trial Bare1_Right,Bare2_Right --reference kainz_pwph

The step before plot collings: do the model iterations agree on the kinematics, and how well does each one track the markers. Reads what bioscout simulate exbiomec leaves in 3_iterations/<iteration>/<trial>/external_biomechanics/ (joint_angles.mot, _ik_marker_errors.sto, _ik_marker_errors_all.sto) as plain text, so it runs without OpenSim, and writes to <session>/4_outputs/kinematics/:

file what it shows
joint_angles_<trial>.png one panel per coordinate (pelvis, then right and left limb), one line per iteration
marker_errors_<trial>.png RMS and max marker error over time, and a marker x iteration map of the mean error
marker_errors_overview.png every iteration on one page: bar = mean over trials, dots = each trial
joint_angles.csv min / max / mean / range per coordinate, plus mean difference and RMSD against the reference iteration
marker_errors.csv mean and peak RMS and max error (mm), worst marker

Lines and bars use each iteration's color: and label: from session.yaml, in the order the file lists them. Rotations are unwrapped first, so a pelvis rotation crossing +/-180 deg is not read as a 360 deg range. The dashed 20 mm (RMS) and 40 mm (max) lines are OpenSim's rule-of-thumb limits for IK.

flag meaning
plot kinematics [SESSION] session folder; defaults to .. A project or subject folder works if exactly one session sits below it.
--skip A B ... iterations to leave out, case-insensitive
--iteration A,B only these iterations, in this order
--trial NAME[,NAME] restrict to these trials (default: all)
--dofs COORD ... coordinates to draw; end a name in _? for both sides
--reference ITERATION iteration the differences in joint_angles.csv are taken against (default: the first drawn)
--rms-limit MM, --max-limit MM move the guide lines (0 = none)
-o DIR output folder (default <session>/4_outputs/kinematics)

Figures & validation

Every stage writes its figures into the trial subfolders, and any figure can be regenerated without re-solving (they only read the .sto outputs):

trial.plot_kin_mom_summary()    # kinematics (top) + moments (bottom) for SummarySettings.dofs
trial.plot_residuals()          # pelvis residual forces/moments as % of |GRF| (10/25% bands)
trial.plot_so()                 # muscle forces + activations, one legible figure per leg
trial.plot_so_reserves()        # SO reserve actuators as % of each ID moment (10/25% bands)
trial.plot_jra_comparison()     # SO vs CEINMS joint contact forces (×BW) + literature bands
trial.plot_ceinms_execution_comparison()   # forces & activations: SO vs CEINMS vs EMG

The muscle-group figures (SO_muscle_groups.png) annotate each panel with R²/RMSE of the mean activation vs measured EMG. ceinms/emg_vs_activations.png uses gray = EMG, red = SO, blue = CEINMS. All figures regenerate from the .sto outputs, e.g. from the CLI:

python -m bioscout.utils.analyse ".../Walking_02" plot_kin_mom_summary plot_residuals plot_so plot_so_reserves

Literature validation. plot_jra_comparison overlays digitised literature contact-force bands on the resultant panels for gait-like trials — hip (Bergmann, Hoang, Giarmatzis) and knee (Richards) — mapped onto the trial's gait cycle. The overlay data lives in bioscout.model.inspect.literature_jcf and can be plotted on its own:

from bioscout.model.inspect import literature_jcf as ljcf
ljcf.plot_jcf_validation("hip_ref.png", entity="hip")

Model validation (muscle_inspect). An independent module checks a model's geometry and properties against the literature, writing everything into muscle_inspect_<model>/ next to the model. all runs the full battery — moment arms + fibre length/pennation + isometric & isokinetic strength (bundled literature auto-resolved, only --model required):

python -m bioscout.model.inspect all      --model scaled_mvicx3.00.osim   # full validation
python -m bioscout.model.inspect inspect  --model scaled.osim             # moment-arm sweep + wrap fix
python -m bioscout.model.inspect validate --model scaled.osim             # moment arms vs literature

Trial type & events. Each trial has a trial_type (walking, running, squat, jump, generic) and its gait events self-contained in trial_settings.xml under <events type="…"> (each <event n="…" name="…" time="…"/> is numbered in time order). The event schema per type determines the 0–100 % window used for literature overlays and the event marks drawn on figures. inputs/grf_events.png plots per-foot vertical GRF with those events (▲ contact, ▼ toe-off; generic landmarks like Start/End shown as neutral markers).

To re-derive the analysis window after editing the events, without re-exporting, use recrop_to_events (see "Re-cropping to edited gait events" above).


Migration from msk_modelling_python

pip uninstall msk-modelling-python
pip install bioscout
# Old
import msk_modelling_python as msk
# New
import bioscout as msk

Future add-ons

These features exist in early/experimental form (some already have CLI flags in the "player tracking — EXPERIMENTAL" group of --help) but are not stable and not part of the 2.x OpenSim pipeline — targeted for bioscout 3.0:

  • Player / movement tracking (computer vision). Markerless kinematics from a phone or laptop camera via pose estimation, feeding the same OpenSim pipeline. Experimental CLI: --shots (video shot analysis), --poses, --hoop, --yolo-model, and the player registry (--add_subject → subjects.json).
  • Training-load & wearables. Fatigue/load reports from fitness-tracker exports and cloud pulls. Experimental CLI: --load-report, --zepp-pull, --strava-pull, --hr-max/--hr-rest/--age/--sex, --creds.
  • Real-time muscle forces. A pre-trained ML model that maps camera-derived kinematics to muscle and joint contact forces in (semi-)real time, integrating the computer-vision and OpenSim/CEINMS pipelines.

Pipeline subjects (for the OpenSim/CEINMS batch) are declared in your project's settings.py subjects list — not via --add_subject, which targets the separate player-tracking registry.

Version: 3.1.1

Metadata

Release files for bioscout 3.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bioscout 3.1.1
File Size Uploaded
bioscout-3.1.1.tar.gz 87.2 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for bioscout 3.1.1
File Interpreter ABI Platform
bioscout-3.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 174.6 MB

Release files / bioscout-3.1.1.tar.gz

Download URL bioscout-3.1.1.tar.gz
Size 87.2 MB
Tags Source
SHA-256 checksum
How to use checksums
a06e582a2c440a307c81efd407f9885a232eda4766b806c72c93989e3a8ae3c7
BLAKE2b-256 checksum
How to use checksums
acf7223e015807c2c5275953a51f9848a47450c0742dd3626b2afee57e4594cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Release files / bioscout-3.1.1-py3-none-any.whl

Download URL bioscout-3.1.1-py3-none-any.whl
Size 87.4 MB
Tags Python 3
SHA-256 checksum
How to use checksums
dd7bf4ceb8dc6995e240e921f86e52498a17bb9fe91dbe2158af4ab53941f53d
BLAKE2b-256 checksum
How to use checksums
6ce01187e1bdc333e1f33dd6e784119ed3214a965f58f35395c6c291b101262f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Release history Release notifications | RSS feed

This release

3.1.1 This release

2 release files

3.1.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page