Skip to content
IpuchPublic

Repository files navigation

bioNC

Biomechanics with natural coordinates, in Python, forward and inverse approaches.

Build status codecov Maintainability Conda version Python License Black DOI

Install · Quickstart · Examples · Docs · Cite

Inverse kinematics on a lower-limb model

Inverse kinematics of a lower-limb model, animated with pyorerun.


✨ Why bioNC

bioNC models a biomechanism in natural coordinates: each segment is described by twelve Cartesian parameters $(u, r_p, r_d, w)$ instead of angles, so rigid bodies and joints become quadratic holonomic constraints rather than trigonometric expressions. Kinematics and dynamics stay polynomial, Jacobians are sparse and cheap, and inverse kinematics becomes a well-conditioned optimization problem, no Euler-angle singularities on the way.

🎯 Inverse kinematics Frame-per-frame solvers: ipopt, sqpmethod (CasADi) and dik, a QP-based differential IK backed by proxsuite
⚙️ Dynamics Forward and inverse dynamics under rigid-body + joint constraints, with Lagrange multipliers
🦴 Joints Spherical, hinge, universal, free, weld, constant-length, sphere-on-plane, point/two-points-on-ellipsoid, ellipsoid-on-plane, with ground variants
💪 Muscles Muscle paths with via points, and muscle-driven pendulum examples
🏋️ External forces Force sets applied in global or local frames, at arbitrary application points
📐 Model personalization Build a model template from marker functions, then scale it on a C3D static trial
🧮 Two backends numpy for numerics, casadi for symbolics and optimal control
🎬 Visualization 3D animation of models, markers and forces through pyorerun

📦 Installation

From conda-forge (recommended)

Conda-version Last-update

conda install -c conda-forge bionc

From source

pip install git+https://github.com/Ipuch/bioNC.git

For development

git clone https://github.com/Ipuch/bioNC.git && cd bioNC
conda env create -f environment.yml   # brings casadi, biorbd, ezc3d, pyorerun, proxsuite...
conda activate bionc
pip install -e .
pytest tests

biorbd and ezc3d are not on PyPI, so the conda environment is the smoothest route for the full feature set.


🚀 Quickstart

Solve inverse kinematics on an existing model and read joint angles out of it:

import numpy as np
from pyomeca import Markers

from bionc import BiomechanicalModel, InverseKinematics, NaturalCoordinates

# 1. Load a model (see `examples/model_creation` to build one from your own data)
model = BiomechanicalModel.load("examples/models/lower_limb.nc")

# 2. Load experimental markers, ordered like the model expects them
markers = Markers.from_c3d("my_trial.c3d", usecols=model.marker_names_technical).to_numpy()[:3, :, :]

# 3. Solve, "dik" is the fast QP-based differential IK, "ipopt"/"sqpmethod" are also available
ik = InverseKinematics(model, markers)
Qopt = ik.solve(method="dik")

# 4. Inspect the solution
stats = ik.sol()
print("max marker residual:", np.max(stats["marker_residuals_norm"]))
print(model.natural_coordinates_to_joint_angles(NaturalCoordinates(Qopt[:, 0])))

Then animate it:

from bionc.vizualization.pyorerun_interface import BioncModelNoMesh
from pyorerun import PhaseRerun, PyoMarkers

prr = PhaseRerun(t_span=np.linspace(0, 1, Qopt.shape[-1]))
prr.add_animated_model(
    BioncModelNoMesh(model),
    Qopt,
    tracked_markers=PyoMarkers(data=markers, marker_names=model.marker_names_technical),
)
prr.rerun()
Building your own model from a static trial

A model is described generically, axes and markers are functions of marker names, then collapsed onto real data. This is the scaling/personalization step:

from bionc import (
    BiomechanicalModelTemplate, SegmentTemplate, NaturalSegmentTemplate,
    MarkerTemplate, AxisTemplate, C3dData, JointType,
)

model = BiomechanicalModelTemplate()

hip_joint = lambda m, bio: MarkerTemplate.middle_of(m, bio, "RFWT", "LFWT")

model["PELVIS"] = SegmentTemplate(
    natural_segment=NaturalSegmentTemplate(
        # from the middle of the posterior iliac spines to the middle of the anterior ones
        u_axis=AxisTemplate(
            start=lambda m, bio: MarkerTemplate.middle_of(m, bio, "RBWT", "LBWT"),
            end=lambda m, bio: MarkerTemplate.middle_of(m, bio, "RFWT", "LFWT"),
        ),
        proximal_point=lambda m, bio: MarkerTemplate.middle_of(m, bio, "RBWT", "LBWT"),
        distal_point=hip_joint,
        w_axis=AxisTemplate(start="LFWT", end="RFWT"),
    )
)
model["PELVIS"].add_marker(MarkerTemplate(name="RFWT", parent_name="PELVIS", is_technical=True))
# ... more markers, more segments ...

model.add_joint(name="hip", joint_type=JointType.SPHERICAL, parent="PELVIS", child="THIGH")

personalized_model = model.update(C3dData("statref.c3d"))  # <- the scaling step
personalized_model.save("my_model.nc")

The full, runnable version lives in examples/model_creation/right_side_lower_limb.py, and docs/model_scaling.md explains precisely what does and does not get scaled from the data.


🧪 Examples

Example What it shows
model_creation/ Lower limb, two-side lower limbs, upper limb and markerless models built from C3D data
inverse_kinematics/ Solving IK on noisy markers, single-frame IK, solver comparison
forward_dynamics/ Pendulums (simple, 3D, n-link, universal), actuated systems, dropping a box
inverse_dynamics/ Joint torques of a three-link pendulum
muscles/ Pendulum and double pendulum driven by muscles with via points
play_with_joints/ Constant-length, point-on-ellipsoid, plane-on-ellipsoid and scapulothoracic joints
knee_parallel_mechanism/ A parallel knee mechanism (Feikes' model)
transformation_matrix/ What $\mathbf{B}$ does: segment ↔ natural ↔ global frames in plain numpy and bioNC, and the $\mathbf{B}$ conventions

🧮 Mathematical backends

Backend Import Use it for
numpy from bionc import ... (default) or bionc.bionc_numpy Numerical simulation, inverse kinematics, dynamics
casadi from bionc import bionc_casadi Symbolic expressions, gradients, optimal control problems

Both expose the same API, so a model written for one reads the same in the other.


📚 Natural coordinates in a nutshell

The $i$-th body of a biomechanism with $N$ bodies has generalized coordinates

$$Q_i = (u_{i}, r_{p_{i}}, r_{d_{i}}, w_{i}) \in \mathbf{R}^3 \times \mathbf{R}^3 \times \mathbf{R}^3 \times \mathbf{R}^3,$$

where

  • $u$, the proximal vector, in the global coordinate system,
  • $r_p$, the position of the proximal point, in the global coordinate system,
  • $r_d$, the position of the distal point, in the global coordinate system,
  • $w$, the distal vector, in the global coordinate system.

The generalized coordinates of the whole biomechanism are the concatenation of all body coordinates:

$$Q = (Q_1, \dots, Q_N).$$

To rigidify the segments and articulate them, two families of holonomic constraints are used, rigid-body constraints $\Phi^r(Q)$ and joint (kinematic) constraints $\Phi^j(Q)$, gathered in a common constraint function $\Phi$:

$$\begin{align} \Phi(Q) = \left( \Phi^r(Q) \quad \Phi^j(Q) \right)^\top \in \mathbf{R}^{6 \times N} \times \mathbf{R}^M. \end{align}$$

The transformation matrix $\mathbf{B}$: from the natural frame to an orthonormal frame

The natural vectors $u$, $v = r_p - r_d$ and $w$ are not orthogonal in general, they are constrained by angles and length parameters:

  • $\alpha$ is the angle between $v$ and $w$,
  • $\beta$ between $u$ and $w$,
  • $\gamma$ between $u$ and $v$,
  • $v$ has the length $L$ of the segment.

On the other hand, most anatomical data from the literature live in an orthonormal segment coordinate system (SCS) $(X, Y, Z)$: centre of mass, inertia, markers, muscle via points...

The transformation matrix $\mathbf{B}$ builds the SCS on the natural frame, with its columns being $u$, $v$, $w$ read in $(X, Y, Z)$, so a direct correspondance exist between the two type of frames: the natural frame and the orthonormal frame, i.e. SCS.

Building the segment coordinate system on the natural frame with B_uv

So we have the relation :

$$[u\ v\ w] = \mathbf{R}\,\mathbf{B}$$

with $\mathbf{R}$ is the rotation of the SCS with the world coordinate system, which is orthonormal like any rotation matrix. $\mathbf{B}$ is triangular and carries the shape of the segment ($L$, $\alpha$, $\beta$, $\gamma$).

We have three frames, consequently we have three relations for any point given in any of theses frames.

From → to Relation
natural → SCS $p = \mathbf{B}, n \quad\Leftrightarrow\quad n = \mathbf{B}^{-1} p$
natural → global $r = r_p + n_1 u + n_2 v + n_3 w$
SCS → global $r = r_p + \mathbf{R}, p, \quad \mathbf{R} = [u\ v\ w], \mathbf{B}^{-1}$

A point given in the SCS is reached by walking $n_1$ along $u$, $n_2$ along $v$ and $n_3$ along $w$, with $n = \mathbf{B}^{-1} p$. Unlike a rotation matrix, $\mathbf{B}$ is not orthonormal, so going back from the SCS to natural coordinates takes its inverse $\mathbf{B}^{-1}$. An orthogonal segment ($\alpha = \beta = \gamma = 90°$) has the simple $\mathbf{B} = \mathrm{diag}(1, L, 1)$.

Which $\mathbf{B}$? The Gram–Schmidt order chooses which natural axis the SCS keeps exactly:

The orthonormal frame built by each B on the same moving natural frame

Type Kept exactly In the same plane
Buv (default) $X = u$ $v$ in $(X, Y)$
Bvu $Y = v$ $u$ in $(X, Y)$
Bwu $Z = w$ $u$ in $(Z, X)$
Buw $X = u$ $w$ in $(X, Z)$

To go further, examples/transformation_matrix/ does every frame change in plain numpy next to bioNC, compares the $\mathbf{B}$ conventions in 3D, and draws the figures above. docs/note_transformation_matrix_inverses.md gives every $\mathbf{B}$, its analytical inverse and the link with Dumas' matrices.


🤝 Contributing

Contributions, bug reports and ideas are welcome, start with the contributing guide and the open issues (look for good first issue).

black . -l120   # format
pytest tests    # test

📝 How to cite

Pierre Puchaud, Alexandre Naaim, & Anais Chaumeil. Ipuch/bioNC. Zenodo. DOI

@software{puchaud_bionc,
  author    = {Pierre Puchaud and Alexandre Naaim and Anais Chaumeil},
  title     = {Ipuch/bioNC},
  year      = {2025},
  publisher = {Zenodo},
  version   = {0.11.0},
  doi       = {10.5281/zenodo.14976752},
  url       = {https://github.com/Ipuch/bioNC}
}

🙏 Reference

This work is an implementation mostly inspired by the work of Dr. Raphaël Dumas (Senior Researcher at IFSTTAR – University of Lyon) on three-dimensional multi-body modeling of the human musculoskeletal system.

Released under the MIT license.

Project activity

Release Version Last Commit Contributors Merged PRs Open Issues Closed Issues

⭐ If bioNC is useful to your research, consider starring the repo ⭐

Stars Forks