Skip to content

Commit

Permalink
Merge pull request #358 from EveCharbie/model_creation_update
Browse files Browse the repository at this point in the history
Model creation (BIS)
  • Loading branch information
pariterre authored Feb 28, 2025
2 parents 6c351bb + 3e36285 commit 0a8b9a4
Show file tree
Hide file tree
Showing 38 changed files with 1,929 additions and 649 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/run_casadi_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ jobs:
- name: Run python binder tests
run: |
BIORBD_FOLDER=`pwd`
cd $BIORBD_FOLDER/$BUILD_FOLDER/test/binding/Python3
cd $BIORBD_FOLDER/$BUILD_FOLDER/test/binding/python3
export CI_MAIN_EXAMPLES_FOLDER=$BIORBD_FOLDER/$EXAMPLES_FOLDER
pytest .
cd $BIORBD_FOLDER
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/run_eigen_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ jobs:
- name: Run python binder tests
run: |
BIORBD_FOLDER=`pwd`
cd $BIORBD_FOLDER/$BUILD_FOLDER/test/binding/Python3
cd $BIORBD_FOLDER/$BUILD_FOLDER/test/binding/python3
export CI_MAIN_EXAMPLES_FOLDER=$BIORBD_FOLDER/$EXAMPLES_FOLDER
pytest .
cd $BIORBD_FOLDER
Expand Down
10 changes: 10 additions & 0 deletions binding/python3/model_creation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
from .axis import Axis
from .inertia_parameters import InertiaParameters
from .marker import Marker
from .contact import Contact
from .muscle import Muscle
from .muscle_group import MuscleGroup
from .via_point import ViaPoint
from .mesh import Mesh
from .mesh_file import MeshFile
from .protocols import Data, GenericDynamicModel
from .rotations import Rotations
from .range_of_motion import RangeOfMotion, Ranges
from .segment import Segment
from .segment_coordinate_system import SegmentCoordinateSystem
from .translations import Translations
Expand All @@ -16,7 +22,11 @@
from .biomechanical_model_real import BiomechanicalModelReal
from .axis_real import AxisReal
from .marker_real import MarkerReal
from .contact_real import ContactReal
from .muscle_real import MuscleReal, MuscleType, MuscleStateType
from .via_point_real import ViaPointReal
from .mesh_real import MeshReal
from .mesh_file_real import MeshFileReal
from .segment_real import SegmentReal
from .segment_coordinate_system_real import SegmentCoordinateSystemReal
from .inertia_parameters_real import InertiaParametersReal
Expand Down
63 changes: 50 additions & 13 deletions binding/python3/model_creation/biomechanical_model.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
from .protocols import Data
from .segment_real import SegmentReal
from .segment import Segment
from .muscle_group import MuscleGroup
from .muscle_real import MuscleReal
from .segment_coordinate_system_real import SegmentCoordinateSystemReal
from .biomechanical_model_real import BiomechanicalModelReal


class BiomechanicalModel:
def __init__(self, bio_sym_path: str = None):
self.segments = {}
self.muscle_groups = {}
self.muscles = {}
self.via_points = {}

if bio_sym_path is None:
return
raise NotImplementedError("bioMod files are not readable yet")

def __getitem__(self, name: str):
return self.segments[name]

def __setitem__(self, name: str, segment: Segment):
if segment.name is not None and segment.name != name:
raise ValueError(
"The segment name should be the same as the 'key'. Alternatively, segment.name can be left undefined"
)
segment.name = name # Make sure the name of the segment fits the internal one
self.segments[name] = segment

def to_real(self, data: Data) -> BiomechanicalModelReal:
"""
Collapse the model to an actual personalized biomechanical model based on the generic model and the data
Expand Down Expand Up @@ -54,19 +47,63 @@ def to_real(self, data: Data) -> BiomechanicalModelReal:
if s.mesh is not None:
mesh = s.mesh.to_mesh(data, model, scs)

model[s.name] = SegmentReal(
mesh_file = None
if s.mesh_file is not None:
mesh_file = s.mesh_file.to_mesh_file(data)

model.segments[s.name] = SegmentReal(
name=s.name,
parent_name=s.parent_name,
segment_coordinate_system=scs,
translations=s.translations,
rotations=s.rotations,
q_ranges=s.q_ranges,
qdot_ranges=s.qdot_ranges,
inertia_parameters=inertia_parameters,
mesh=mesh,
mesh_file=mesh_file,
)

for marker in s.markers:
model.segments[name].add_marker(marker.to_marker(data, model, scs))

for contact in s.contacts:
model.segments[name].add_contact(contact.to_contact(data))

for name in self.muscle_groups:
mg = self.muscle_groups[name]

model.muscle_groups[mg.name] = MuscleGroup(
name=mg.name,
origin_parent_name=mg.origin_parent_name,
insertion_parent_name=mg.insertion_parent_name,
)

for name in self.muscles:
m = self.muscles[name]

if m.muscle_group not in model.muscle_groups:
raise RuntimeError(
f"Please create the muscle group {m.muscle_group} before putting the muscle {m.name} in it."
)

model.muscles[m.name] = m.to_muscle(model, data)

for name in self.via_points:
vp = self.via_points[name]

if vp.muscle_name not in model.muscles:
raise RuntimeError(
f"Please create the muscle {vp.muscle_name} before putting the via point {vp.name} in it."
)

if vp.muscle_group not in model.muscle_groups:
raise RuntimeError(
f"Please create the muscle group {vp.muscle_group} before putting the via point {vp.name} in it."
)

model.via_points[vp.name] = vp.to_via_point(data)

return model

def write(self, save_path: str, data: Data):
Expand Down
40 changes: 33 additions & 7 deletions binding/python3/model_creation/biomechanical_model_real.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
class BiomechanicalModelReal:
def __init__(self):
from .segment_real import SegmentReal # Imported here to prevent from circular imports
from .muscle_group import MuscleGroup
from .muscle_real import MuscleReal
from .via_point_real import ViaPointReal

self.segments: dict[str:SegmentReal, ...] = {}
# From Pythom 3.7 the insertion order in a dict is preserved. This is important because when writing a new
# .bioMod file, the order of the segment matters

def __getitem__(self, name: str):
return self.segments[name]

def __setitem__(self, name: str, segment: "SegmentReal"):
segment.name = name # Make sure the name of the segment fits the internal one
self.segments[name] = segment
self.muscle_groups: dict[str:MuscleGroup, ...] = {}
self.muscles: dict[str:MuscleReal, ...] = {}
self.via_points: dict[str:ViaPointReal, ...] = {}

def __str__(self):
out_string = "version 4\n\n"

out_string += "// --------------------------------------------------------------\n"
out_string += "// SEGMENTS\n"
out_string += "// --------------------------------------------------------------\n\n"
for name in self.segments:
out_string += str(self.segments[name])
out_string += "\n\n\n" # Give some space between segments

out_string += "// --------------------------------------------------------------\n"
out_string += "// MUSCLE GROUPS\n"
out_string += "// --------------------------------------------------------------\n\n"
for name in self.muscle_groups:
out_string += str(self.muscle_groups[name])
out_string += "\n"
out_string += "\n\n\n" # Give some space after muscle groups

out_string += "// --------------------------------------------------------------\n"
out_string += "// MUSCLES\n"
out_string += "// --------------------------------------------------------------\n\n"
for name in self.muscles:
out_string += str(self.muscles[name])
out_string += "\n\n\n" # Give some space between muscles

out_string += "// --------------------------------------------------------------\n"
out_string += "// MUSCLES VIA POINTS\n"
out_string += "// --------------------------------------------------------------\n\n"
for name in self.via_points:
out_string += str(self.via_points[name])
out_string += "\n\n\n" # Give some space between via points

return out_string

def write(self, file_path: str):
Expand Down
41 changes: 41 additions & 0 deletions binding/python3/model_creation/contact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Callable

from .protocols import Data
from .translations import Translations
from .contact_real import ContactReal


class Contact:
def __init__(
self,
name: str,
function: Callable | str = None,
parent_name: str = None,
axis: Translations = None,
):
"""
Parameters
----------
name
The name of the new contact
function
The function (f(m) -> np.ndarray, where m is a dict of markers) that defines the contact with.
parent_name
The name of the parent the contact is attached to
axis
The axis of the contact
"""
self.name = name
function = function if function is not None else self.name
self.function = (lambda m, bio: m[function]) if isinstance(function, str) else function
self.parent_name = parent_name
self.axis = axis

def to_contact(self, data: Data) -> ContactReal:
return ContactReal.from_data(
data,
self.name,
self.function,
self.parent_name,
self.axis,
)
80 changes: 80 additions & 0 deletions binding/python3/model_creation/contact_real.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Callable

import numpy as np

from .protocols import Data
from .translations import Translations


class ContactReal:
def __init__(
self,
name: str,
parent_name: str,
position: tuple[int | float, int | float, int | float] | np.ndarray = None,
axis: Translations = None,
):
"""
Parameters
----------
name
The name of the new contact
parent_name
The name of the parent the contact is attached to
position
The 3d position of the contact
axis
The axis of the contact
"""
self.name = name
self.parent_name = parent_name
if position is None:
position = np.array((0, 0, 0, 1))
self.position = position if isinstance(position, np.ndarray) else np.array(position)
self.axis = axis

@staticmethod
def from_data(
data: Data,
name: str,
function: Callable,
parent_name: str,
axis: Translations = None,
):
"""
This is a constructor for the Contact class. It evaluates the function that defines the contact to get an
actual position
Parameters
----------
data
The data to pick the data from
name
The name of the new contact
function
The function (f(m) -> np.ndarray, where m is a dict of markers (XYZ1 x time)) that defines the contacts in the local joint coordinates.
parent_name
The name of the parent the contact is attached to
axis
The axis of the contact
"""

# Get the position of the contact points and do some sanity checks
p: np.ndarray = function(data.values)
if not isinstance(p, np.ndarray):
raise RuntimeError(f"The function {function} must return a np.ndarray of dimension 3xT (XYZ x time)")
if p.shape == (3, 1):
p = p.reshape((3,))
elif p.shape != (3,):
raise RuntimeError(f"The function {function} must return a vector of dimension 3 (XYZ)")

return ContactReal(name, parent_name, p, axis)

def __str__(self):
# Define the print function, so it automatically formats things in the file properly
out_string = f"contact\t{self.name}\n"
out_string += f"\tparent\t{self.parent_name}\n"
out_string += f"\tposition\t{np.round(self.position[0], 4)}\t{np.round(self.position[1], 4)}\t{np.round(self.position[2], 4)}\n"
out_string += f"\taxis\t{self.axis.value}\n"
out_string += "endcontact\n"
return out_string
6 changes: 3 additions & 3 deletions binding/python3/model_creation/inertia_parameters_real.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def __str__(self):
# Define the print function, so it automatically formats things in the file properly
com = np.nanmean(self.center_of_mass, axis=1)[:3]

out_string = f"\tmass {self.mass}\n"
out_string += f"\tCenterOfMass {com[0]:0.5f} {com[1]:0.5f} {com[2]:0.5f}\n"
out_string += f"\tinertia_xxyyzz {self.inertia[0]} {self.inertia[1]} {self.inertia[2]}\n"
out_string = f"\tmass\t{self.mass}\n"
out_string += f"\tCenterOfMass\t{com[0]:0.5f}\t{com[1]:0.5f}\t{com[2]:0.5f}\n"
out_string += f"\tinertia_xxyyzz\t{self.inertia[0]}\t{self.inertia[1]}\t{self.inertia[2]}\n"
return out_string
10 changes: 5 additions & 5 deletions binding/python3/model_creation/marker_real.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ def mean_position(self) -> np.ndarray:

def __str__(self):
# Define the print function, so it automatically formats things in the file properly
out_string = f"marker {self.name}\n"
out_string += f"\tparent {self.parent_name}\n"
out_string = f"marker\t{self.name}\n"
out_string += f"\tparent\t{self.parent_name}\n"

p = self.mean_position
out_string += f"\tposition {p[0]:0.4f} {p[1]:0.4f} {p[2]:0.4f}\n"
out_string += f"\ttechnical {1 if self.is_technical else 0}\n"
out_string += f"\tanatomical {1 if self.is_anatomical else 0}\n"
out_string += f"\tposition\t{p[0]:0.4f}\t{p[1]:0.4f}\t{p[2]:0.4f}\n"
out_string += f"\ttechnical\t{1 if self.is_technical else 0}\n"
out_string += f"\tanatomical\t{1 if self.is_anatomical else 0}\n"
out_string += "endmarker\n"
return out_string

Expand Down
Loading

0 comments on commit 0a8b9a4

Please sign in to comment.