From aee94238238915092db746b8126dde203bba84da Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Thu, 9 Apr 2026 15:28:51 +0200 Subject: [PATCH] dataclasses to pydantic BaseModel --- splib/mechanics/mass.py | 4 +-- stlib/collision.py | 8 +++--- stlib/core/baseParameters.py | 28 ++++++++++++++----- stlib/core/basePrefabParameters.py | 17 +++++------- stlib/entities/__entity__.py | 43 ++++++++++++++++-------------- stlib/geometries/__geometry__.py | 32 ++++++++++------------ stlib/geometries/cube.py | 14 +++------- stlib/geometries/extract.py | 10 +------ stlib/geometries/file.py | 18 ++++++------- stlib/geometries/plane.py | 32 +++++++++++++--------- stlib/materials/__material__.py | 5 ++-- stlib/materials/deformable.py | 5 ++-- stlib/materials/rigid.py | 14 +++------- stlib/visual.py | 13 +++------ 14 files changed, 115 insertions(+), 128 deletions(-) diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py index c003c3e64..217e064fe 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -6,12 +6,12 @@ # TODO : use the massDensity ONLY and deduce totalMass if necessary from it + volume @ReusableMethod -def addMass(node, elem:ElementType, totalMass=DEFAULT_VALUE, massDensity=DEFAULT_VALUE, lumping=DEFAULT_VALUE, topology=DEFAULT_VALUE, **kwargs): +def addMass(node, elementType:ElementType, totalMass=DEFAULT_VALUE, massDensity=DEFAULT_VALUE, lumping=DEFAULT_VALUE, topology=DEFAULT_VALUE, **kwargs): if (not isDefault(totalMass)) and (not isDefault(massDensity)) : print("[warning] You defined the totalMass and the massDensity in the same time, only taking massDensity into account") del kwargs["massDensity"] - if(elem !=ElementType.POINTS and elem !=ElementType.EDGES): + if(elementType is not None and elementType !=ElementType.POINTS and elementType !=ElementType.EDGES): node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, topology=topology, **kwargs) else: if (not isDefault(massDensity)) : diff --git a/stlib/collision.py b/stlib/collision.py index b8f059249..64ef3cc98 100644 --- a/stlib/collision.py +++ b/stlib/collision.py @@ -1,5 +1,5 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses +from stlib.core.baseParameters import BaseParameters, Optional from stlib.geometries import Geometry, GeometryParameters from stlib.geometries.file import FileParameters from splib.core.enum_types import CollisionPrimitive @@ -7,21 +7,21 @@ from splib.mechanics.collision_model import addCollisionModels from Sofa.Core import Object -@dataclasses.dataclass class CollisionParameters(BaseParameters): name : str = "Collision" - primitives : list[CollisionPrimitive] = dataclasses.field(default_factory = lambda :[CollisionPrimitive.TRIANGLES]) + primitives : list[CollisionPrimitive] = [CollisionPrimitive.TRIANGLES] selfCollision : Optional[bool] = DEFAULT_VALUE bothSide : Optional[bool] = DEFAULT_VALUE group : Optional[int] = DEFAULT_VALUE contactDistance : Optional[float] = DEFAULT_VALUE - geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) + geometry : GeometryParameters = GeometryParameters() class Collision(BasePrefab): + def __init__(self, parameters: CollisionParameters): BasePrefab.__init__(self, parameters) diff --git a/stlib/core/baseParameters.py b/stlib/core/baseParameters.py index be7a633b8..648640c00 100644 --- a/stlib/core/baseParameters.py +++ b/stlib/core/baseParameters.py @@ -1,13 +1,27 @@ -import dataclasses from splib.core.utils import DEFAULT_VALUE -import dataclasses +from pydantic import BaseModel, ValidationError from typing import Callable, Optional, Any +import Sofa + +class BaseParameters(BaseModel): -@dataclasses.dataclass -class BaseParameters(object): name : str = "Object" - kwargs : dict = dataclasses.field(default_factory=dict) + kwargs : dict = {} + + # @classmethod + # def fromYaml(self, data: str): + # import yaml + # dataDict = yaml.safe_load(data) + # return self.fromDict(dataDict) + + # @classmethod + # def fromDict(self, data: dict): + # try: + # return self.model_validate(data, strict=True) + # except ValidationError as exc: + # for error in exc.errors(): + # message = error.get("loc")[0].__str__() + ": " + # message += error.get("msg") + # Sofa.msg_error(self.__name__, message) - def toDict(self): - return dataclasses.asdict(self) diff --git a/stlib/core/basePrefabParameters.py b/stlib/core/basePrefabParameters.py index d6a69dee9..f667d3c96 100644 --- a/stlib/core/basePrefabParameters.py +++ b/stlib/core/basePrefabParameters.py @@ -1,15 +1,12 @@ -import dataclasses +from stlib.core.baseParameters import BaseParameters -@dataclasses.dataclass -class BasePrefabParameters(object): +class BasePrefabParameters(BaseParameters): name : str = "object" - kwargs : dict = dataclasses.field(default_factory=dict) + kwargs : dict = {} # Transformation information # TODO: these data are going to be added in Node in SOFA (C++ implementation) - translation : list[float] = dataclasses.field(default_factory = lambda : [0., 0., 0.]) - rotation : list[float] = dataclasses.field(default_factory = lambda : [0., 0., 0.]) - scale : list[float] = dataclasses.field(default_factory = lambda : [1., 1., 1.]) - - def toDict(self): - return dataclasses.asdict(self) + translation : list[float] = [0., 0., 0.] + rotation : list[float] = [0., 0., 0.] + scale : list[float] = [1., 1., 1.] + \ No newline at end of file diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index bc7c76711..90cf15790 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -3,27 +3,29 @@ from stlib.visual import VisualParameters, Visual from stlib.materials import Material, MaterialParameters from stlib.geometries import Geometry -import dataclasses from typing import Callable, Optional -from stlib.geometries import GeometryParameters +from stlib.geometries import GeometryParameters, InternalDataProvider from splib.core.enum_types import StateType from stlib.core.basePrefab import BasePrefab +from stlib.geometries.file import FileParameters +from stlib.materials.rigid import RigidParameters +from splib.core.enum_types import ElementType + -@dataclasses.dataclass class EntityParameters(BaseParameters): name : str = "Entity" - stateType : StateType = StateType.VEC3 + stateType : StateType = StateType.RIGID ### QUID - addCollision : Optional[Callable] = lambda x : Collision(CollisionParameters()) - addVisual : Optional[Callable] = lambda x : Visual(VisualParameters()) + addCollision : Optional[Callable] = Collision(CollisionParameters()) + addVisual : Optional[Callable] = Visual(VisualParameters()) - geometry : GeometryParameters = None - material : MaterialParameters = None + geometry : GeometryParameters = GeometryParameters(elementType = ElementType.POINTS, data = InternalDataProvider(position = [[0., 0., 0.]])) + material : MaterialParameters = RigidParameters() collision : Optional[CollisionParameters] = None - visual : Optional[VisualParameters] = None + visual : Optional[VisualParameters] = VisualParameters(geometry = FileParameters(filename="mesh/cube.obj")) @@ -38,20 +40,16 @@ class Entity(BasePrefab): parameters : EntityParameters - def __init__(self, parameters=EntityParameters(), **kwargs): + def __init__(self, parameters: EntityParameters): BasePrefab.__init__(self, parameters) def init(self): self.geometry = self.add(Geometry, parameters=self.parameters.geometry) - - ### Check compatilibility of Material - if self.parameters.material.stateType != self.parameters.stateType: - print("WARNING: imcompatibility between templates of both the entity and the material") - self.parameters.material.stateType = self.parameters.stateType + self.checkMaterialCompatibility() self.material = self.add(Material, parameters=self.parameters.material) - self.material.States.position.parent = self.geometry.container.position.linkpath + self.material.getMechanicalState().position.parent = self.geometry.container.position.linkpath if self.parameters.collision is not None: self.collision = self.add(Collision, parameters=self.parameters.collision) @@ -61,6 +59,11 @@ def init(self): self.visual = self.add(Visual, parameters=self.parameters.visual) self.addMapping(self.visual) + def checkMaterialCompatibility(self): + if self.parameters.material.stateType != self.parameters.stateType: + print("WARNING: imcompatibility between templates of both the entity and the material") + self.parameters.material.stateType = self.parameters.stateType + def addMapping(self, destinationPrefab): @@ -70,12 +73,12 @@ def addMapping(self, destinationPrefab): if( self.parameters.stateType == StateType.VEC3): destinationPrefab.addObject("BarycentricMapping", output=destinationPrefab.linkpath, - output_topology=destinationPrefab.Geometry.container.linkpath, - input=self.Material.linkpath, - input_topology=self.Geometry.container.linkpath, + output_topology=destinationPrefab.geometry.container.linkpath, + input=self.material.linkpath, + input_topology=self.geometry.container.linkpath, template=template) else: destinationPrefab.addObject("RigidMapping", output=destinationPrefab.linkpath, - input=self.Material.linkpath, + input=self.material.linkpath, template=template) diff --git a/stlib/geometries/__geometry__.py b/stlib/geometries/__geometry__.py index 57d0e5c76..ce1fa7560 100644 --- a/stlib/geometries/__geometry__.py +++ b/stlib/geometries/__geometry__.py @@ -1,5 +1,5 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any +from stlib.core.baseParameters import BaseParameters, Optional, Any from splib.topology.dynamic import addDynamicTopology from splib.topology.static import addStaticTopology from splib.core.enum_types import ElementType @@ -11,8 +11,7 @@ class Geometry(BasePrefab):... -@dataclasses.dataclass -class InternalDataProvider(object): +class InternalDataProvider(BaseParameters): position : Any = None # Topology information edges : Any = DEFAULT_VALUE @@ -21,12 +20,13 @@ class InternalDataProvider(object): tetrahedra : Any = DEFAULT_VALUE hexahedra : Any = DEFAULT_VALUE + @classmethod def generateAttribute(self, parent : Geometry): pass -@dataclasses.dataclass class GeometryParameters(BaseParameters): + mytype : type = BaseParameters name : str = "Geometry" # Type of the highest degree element @@ -35,10 +35,6 @@ class GeometryParameters(BaseParameters): dynamicTopology : bool = False - def Data(self): - return InternalDataProvider() - - class Geometry(BasePrefab): # container : Object # This should be more specialized into the right SOFA type @@ -48,8 +44,6 @@ class Geometry(BasePrefab): def __init__(self, parameters: GeometryParameters): BasePrefab.__init__(self, parameters) - - def init(self): @@ -59,14 +53,16 @@ def init(self): if self.parameters.dynamicTopology : if self.parameters.elementType is not None : - addDynamicTopology(self, elementType=self.parameters.elementType, container = { - "position": self.parameters.data.position, - "edges": self.parameters.data.edges, - "triangles": self.parameters.data.triangles, - "quads": self.parameters.data.quads, - "tetrahedra": self.parameters.data.tetrahedra, - "hexahedra": self.parameters.data.hexahedra - }) + addDynamicTopology(self, + elementType=self.parameters.elementType, + container = { + "position": self.parameters.data.position, + "edges": self.parameters.data.edges, + "triangles": self.parameters.data.triangles, + "quads": self.parameters.data.quads, + "tetrahedra": self.parameters.data.tetrahedra, + "hexahedra": self.parameters.data.hexahedra + }) else: raise ValueError else: diff --git a/stlib/geometries/cube.py b/stlib/geometries/cube.py index 4d5a52ca3..78f1c2658 100644 --- a/stlib/geometries/cube.py +++ b/stlib/geometries/cube.py @@ -1,14 +1,6 @@ -from stlib.geometries import GeometryParameters +from stlib.geometries import FileParameters -class CubeParameters(GeometryParameters): - def __init__(self, center, edgeLength, pointPerEdge, dynamicTopology = False): +class CubeParameters(FileParameters): - customGeom = CubeParameters.createData(center, edgeLength, pointPerEdge) - GeometryParameters.__init__(data = customGeom, dynamicTopology = dynamicTopology) - - @staticmethod - def createData(center, edgeLength, pointPerEdge) -> GeometryParameters.Data : - data = GeometryParameters.Data() - #Fill data - return data + filename : str = "mesh/cube.obj" \ No newline at end of file diff --git a/stlib/geometries/extract.py b/stlib/geometries/extract.py index 5ee1b3762..d640e0938 100644 --- a/stlib/geometries/extract.py +++ b/stlib/geometries/extract.py @@ -1,5 +1,4 @@ from stlib.geometries import GeometryParameters, InternalDataProvider, Geometry -from stlib.core.baseParameters import dataclasses from splib.topology.dynamic import addDynamicTopology from splib.topology.loader import loadMesh from splib.core.enum_types import ElementType @@ -13,18 +12,11 @@ class ExtractInternalDataProvider(InternalDataProvider): sourceType : ElementType sourceName : str - def __init__(self, destinationType : ElementType, sourceType : ElementType, sourceName : str): - self.destinationType = destinationType - self.sourceType = sourceType - self.sourceName = sourceName - - def __post_init__(self): + def model_post_init(self, __context): if(not (self.sourceType == ElementType.TETRAHEDRA and self.destinationType == ElementType.TRIANGLES) and not (self.sourceType == ElementType.HEXAHEDRA and self.destinationType == ElementType.QUADS) ): raise ValueError("Only configuration possible are 'Tetrahedra to Triangles' and 'Hexahedra to Quads'") - InternalDataProvider.__init__(self) - def generateAttribute(self, parent : Geometry): node = parent.addChild("ExtractedGeometry") diff --git a/stlib/geometries/file.py b/stlib/geometries/file.py index 601782f9d..2d2267629 100644 --- a/stlib/geometries/file.py +++ b/stlib/geometries/file.py @@ -1,17 +1,12 @@ from stlib.geometries import GeometryParameters, InternalDataProvider, Geometry -from stlib.core.baseParameters import dataclasses from splib.topology.loader import loadMesh from splib.core.enum_types import ElementType from Sofa.Core import Node -@dataclasses.dataclass class FileInternalDataProvider(InternalDataProvider): filename : str = "mesh/cube.obj" - def __post_init__(self, **kwargs): - InternalDataProvider.__init__(self,**kwargs) - def generateAttribute(self, parent : Geometry): loadMesh(parent, self.filename) @@ -32,9 +27,12 @@ def generateAttribute(self, parent : Geometry): class FileParameters(GeometryParameters): - def __init__(self, filename, dynamicTopology = False, elementType : ElementType = None ): - GeometryParameters.__init__(self, - data = FileInternalDataProvider(filename=filename), - dynamicTopology = dynamicTopology, - elementType = elementType) + filename : str = "mesh/cube.obj" + dynamicTopology : bool = False + elementType : ElementType = None + + def model_post_init(self, __context): + self.data = FileInternalDataProvider(filename=self.filename) + + diff --git a/stlib/geometries/plane.py b/stlib/geometries/plane.py index 2c3c637a4..1a35cace8 100644 --- a/stlib/geometries/plane.py +++ b/stlib/geometries/plane.py @@ -1,20 +1,15 @@ from stlib.geometries import GeometryParameters, InternalDataProvider, Geometry -import dataclasses import numpy as np -@dataclasses.dataclass class PlaneDataProvider(InternalDataProvider): - center : np.ndarray[float] = dataclasses.field(default_factory = lambda : np.array([0,0,0])) - normal : np.ndarray[float] = dataclasses.field(default_factory = lambda : np.array([0,0,1])) - lengthNormal : np.ndarray[float] = dataclasses.field(default_factory = lambda : np.array([1,0,0])) + center : np.ndarray[float] = np.array([0,0,0]) + normal : np.ndarray[float] = np.array([0,0,1]) + lengthNormal : np.ndarray[float] = np.array([1,0,0]) lengthNbEdge : int = 1 widthNbEdge : int = 1 lengthSize : float = 1.0 widthSize : float = 1.0 - def __post_init__(self, **kwargs): - InternalDataProvider.__init__(self,**kwargs) - def generateAttribute(self, parent : Geometry): lengthEdgeSize = self.lengthSize / self.lengthNbEdge @@ -37,9 +32,22 @@ def generateAttribute(self, parent : Geometry): - class PlaneParameters(GeometryParameters): - def __init__(self, center, normal, lengthNormal, lengthNbEdge, widthNbEdge, lengthSize, widthSize, dynamicTopology = False): - GeometryParameters.__init__(self, data = PlaneDataProvider(center=center, normal=normal, lengthNormal=lengthNormal, lengthNbEdge=lengthNbEdge, widthNbEdge=widthNbEdge, lengthSize=lengthSize, widthSize=widthSize), - dynamicTopology = dynamicTopology) + center: list[float, float, float] = [0, 0, 0] + normal: list[float, float, float] = [0, 0, 1] + lengthNormal: list[float, float, float] = [1, 0, 0] + lengthNbEdge: int = 1 + widthNbEdge: int = 1 + lengthSize: float = 1.0 + widthSize: float = 1.0 + dynamicTopology: bool = False + + def model_post_init(self, __context): + self.data = PlaneDataProvider(center=self.center, + normal=self.normal, + lengthNormal=self.lengthNormal, + lengthNbEdge=self.lengthNbEdge, + widthNbEdge=self.widthNbEdge, + lengthSize=self.lengthSize, + widthSize=self.widthSize) diff --git a/stlib/materials/__material__.py b/stlib/materials/__material__.py index ff88dbd43..7790c6dc3 100644 --- a/stlib/materials/__material__.py +++ b/stlib/materials/__material__.py @@ -1,11 +1,10 @@ -from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any +from stlib.core.baseParameters import BaseParameters, Callable, Optional, Any from splib.core.utils import defaultValueType, DEFAULT_VALUE, isDefault from splib.core.enum_types import StateType from stlib.core.basePrefab import BasePrefab from splib.mechanics.mass import addMass -@dataclasses.dataclass class MaterialParameters(BaseParameters): name : str = "Material" @@ -14,7 +13,7 @@ class MaterialParameters(BaseParameters): stateType : StateType = StateType.VEC3 - addMaterial : Optional[Callable] = lambda node : addMass(node, node.parameters.stateType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) + addMaterial : Optional[Callable] = lambda node : addMass(node, elementType=None, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) # TODO : previously called Behavior diff --git a/stlib/materials/deformable.py b/stlib/materials/deformable.py index a24763df5..16be6e1e1 100644 --- a/stlib/materials/deformable.py +++ b/stlib/materials/deformable.py @@ -1,17 +1,16 @@ from stlib.materials import MaterialParameters from splib.core.enum_types import ConstitutiveLaw -from stlib.core.baseParameters import Callable, Optional, dataclasses +from stlib.core.baseParameters import Callable, Optional from splib.mechanics.linear_elasticity import * from splib.mechanics.hyperelasticity import * from splib.mechanics.mass import addMass -@dataclasses.dataclass class DeformableBehaviorParameters(MaterialParameters): constitutiveLawType : ConstitutiveLaw = ConstitutiveLaw.ELASTIC elementType : ElementType = ElementType.TETRAHEDRA - parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio + parameters : list[float] = [1000, 0.45] # young modulus, poisson ratio def __addDeformableMaterial(node): diff --git a/stlib/materials/rigid.py b/stlib/materials/rigid.py index 5668b3562..9fa72026f 100644 --- a/stlib/materials/rigid.py +++ b/stlib/materials/rigid.py @@ -1,13 +1,7 @@ -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses -from stlib.geometries import GeometryParameters +from stlib.materials import MaterialParameters +from splib.core.enum_types import StateType +class RigidParameters(MaterialParameters): -@dataclasses.dataclass -class RigidParameters(BaseParameters): - - geometry : GeometryParameters - mass : Optional[float] = None - - def toDict(self): - return dataclasses.asdict(self) + stateType : StateType = StateType.RIGID diff --git a/stlib/visual.py b/stlib/visual.py index a0f377f7d..8aa40b5f8 100644 --- a/stlib/visual.py +++ b/stlib/visual.py @@ -1,18 +1,17 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses +from stlib.core.baseParameters import BaseParameters, Optional from stlib.geometries import Geometry, GeometryParameters from stlib.geometries.file import FileParameters from splib.core.utils import DEFAULT_VALUE from Sofa.Core import Object -@dataclasses.dataclass class VisualParameters(BaseParameters): name : str = "Visual" color : Optional[list[float]] = DEFAULT_VALUE texture : Optional[str] = DEFAULT_VALUE - geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) + geometry : GeometryParameters = None class Visual(BasePrefab): @@ -25,15 +24,11 @@ def init(self): self.addObject("OglModel", color=self.parameters.color, src=self.geometry.container.linkpath) - @staticmethod - def getParameters(**kwargs) -> VisualParameters: - return VisualParameters(**kwargs) - def createScene(root): # Create a visual from a mesh file - parameters = Visual.getParameters() + parameters = VisualParameters() parameters.name = "LiverVisual" parameters.geometry = FileParameters(filename="mesh/liver.obj") - root.add(Visual, parameters) \ No newline at end of file + root.add(Visual, parameters=parameters) \ No newline at end of file