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

Model creation (BIS) #358

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
53 changes: 53 additions & 0 deletions binding/python3/model_creation/mesh_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Callable

import numpy as np

from .biomechanical_model_real import BiomechanicalModelReal
from .mesh_file_real import MeshFileReal
from .protocols import Data
from .segment_coordinate_system_real import SegmentCoordinateSystemReal


class MeshFile:
def __init__(
self,
mesh_file_name: str,
mesh_color: np.ndarray[float] | list[float] | tuple[float] = None,
scaling_function: Callable = None,
rotation_function: Callable = None,
translation_function: Callable = None,
):
"""
This is a pre-constructor for the MeshFileReal class. It allows to create a generic model by marker names

Parameters
----------
mesh_file_name
The name of the mesh file
mesh_color
The color the mesh should be displayed in (RGB)
scaling_function
The function that defines the scaling of the mesh
rotation_function
The function that defines the rotation of the mesh
translation_function
The function that defines the translation of the mesh
"""
self.mesh_file_name = mesh_file_name
self.mesh_color = mesh_color
self.scaling_function = scaling_function
self.rotation_function = rotation_function
self.translation_function = translation_function

def to_mesh_file(
self,
data: Data,
) -> MeshFileReal:
return MeshFileReal.from_data(
data,
self.mesh_file_name,
self.mesh_color,
self.scaling_function,
self.rotation_function,
self.translation_function,
)
Loading
Loading