diff --git a/examples/stlib/SofaScene.py b/examples/stlib/SofaScene.py index 4e60200c6..84b05451e 100644 --- a/examples/stlib/SofaScene.py +++ b/examples/stlib/SofaScene.py @@ -3,7 +3,7 @@ from stlib.geometries.plane import PlaneParameters from stlib.geometries.file import FileParameters from stlib.geometries.extract import ExtractParameters -from stlib.materials.deformable import DeformableBehaviorParameters +from stlib.materials.deformable import DeformableMaterialParameters from stlib.collision import Collision, CollisionParameters from stlib.entities import Entity, EntityParameters from stlib.visual import Visual, VisualParameters @@ -66,7 +66,7 @@ def createScene(root): LogoParams = EntityParameters(name = "Logo", geometry = FileParameters(filename="mesh/SofaScene/Logo.vtk"), - material = DeformableBehaviorParameters(), + material = DeformableMaterialParameters(), collision = CollisionParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoColli.sph")), visual = VisualParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoVisu.obj"))) @@ -94,12 +94,12 @@ def createScene(root): SParams.name = "S" SParams.geometry = FileParameters(filename="mesh/SofaScene/S.vtk") SParams.geometry.elementType = ElementType.TETRAHEDRA - SParams.material = DeformableBehaviorParameters() + SParams.material = DeformableMaterialParameters() SParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC SParams.material.parameters = [200, 0.45] def SAddMaterial(node): - DeformableBehaviorParameters.addDeformableMaterial(node) + DeformableMaterialParameters.addDeformableMaterial(node) #TODO deal with that is a more smooth way in the material directly node.addObject("LinearSolverConstraintCorrection", name="ConstraintCorrection", linearSolver=SNode.LinearSolver.linkpath, ODESolver=SNode.ODESolver.linkpath) diff --git a/splib/core/utils.py b/splib/core/utils.py index 56b91f53c..706f972d5 100644 --- a/splib/core/utils.py +++ b/splib/core/utils.py @@ -32,5 +32,24 @@ def wrapper(*args, **kwargs): return MapArg +REQUIRES_COLLISIONPIPELINE = "requiresCollisionPipeline" + +def setRequiresCollisionPipeline(rootnode): + if rootnode is not None: + if rootnode.findData(REQUIRES_COLLISIONPIPELINE) is None: + rootnode.addData(name=REQUIRES_COLLISIONPIPELINE, type="bool", value=True, help="Some prefabs in the scene requires a collision pipeline.") + else: + rootnode.requiresCollisionPipeline.value = True + + +REQUIRES_LAGRANGIANCONSTRAINTSOLVER = "requiresLagrangianConstraintSolver" + +def setRequiresLagrangianConstraintSolver(rootnode): + if rootnode is not None: + if rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER) is None: + rootnode.addData(name=REQUIRES_LAGRANGIANCONSTRAINTSOLVER, type="bool", value=True, help="Some prefabs in the scene requires a Lagrangian constraint solver.") + else: + rootnode.requiresLagrangianConstraintSolver.value = True + diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py index c003c3e64..37b6d9db0 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -3,21 +3,24 @@ from splib.core.enum_types import ElementType -# 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): - node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, topology=topology, **kwargs) + 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)) : print("[warning] mass density can only be used on a surface or volumetric topology. Please use totalMass instead") if (not isDefault(lumping)) : print("[warning] lumping can only be set for surface or volumetric topology") - node.addObject("UniformMass",name="mass", totalMass=totalMass, topology=topology,**kwargs) + node.addObject("UniformMass", name="mass", totalMass=totalMass, topology=topology,**kwargs) diff --git a/stlib/__init__.py b/stlib/__init__.py index 0ad659858..25d636ecf 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["core","entities","geometries","materials","collision","visual"] +__all__ = ["core","entities","geometries","materials","collision","visual","prefabs"] import Sofa.Core from stlib.core.basePrefab import BasePrefab diff --git a/stlib/collision.py b/stlib/collision.py index b8f059249..f43e9a95c 100644 --- a/stlib/collision.py +++ b/stlib/collision.py @@ -1,27 +1,28 @@ 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 from splib.core.utils import DEFAULT_VALUE from splib.mechanics.collision_model import addCollisionModels -from Sofa.Core import Object +from splib.core.utils import setRequiresCollisionPipeline + -@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) @@ -29,6 +30,8 @@ def init(self): geom = self.add(Geometry, parameters = self.parameters.geometry) + setRequiresCollisionPipeline(rootnode=self.getRoot()) + self.addObject("MechanicalObject", template="Vec3", position=f"@{self.parameters.geometry.name}/container.position") for primitive in self.parameters.primitives: addCollisionModels(self, primitive, diff --git a/stlib/core/baseEntity.py b/stlib/core/baseEntity.py index 1456436bb..112684d9e 100644 --- a/stlib/core/baseEntity.py +++ b/stlib/core/baseEntity.py @@ -1,11 +1,11 @@ -import Sofa.Core -from baseParameters import BaseParameters +import Sofa +from stlib.core.baseParameters import BaseParameters -class BaseEntity(Sofa.Core.Prefab): +class BaseEntity(Sofa.Prefab): parameters : BaseParameters def __init__(self): - Sofa.Core.Prefab.__init__(self) + Sofa.Prefab.__init__(self) diff --git a/stlib/core/baseParameters.py b/stlib/core/baseParameters.py index be7a633b8..77c73cece 100644 --- a/stlib/core/baseParameters.py +++ b/stlib/core/baseParameters.py @@ -1,13 +1,34 @@ -import dataclasses from splib.core.utils import DEFAULT_VALUE -import dataclasses +from pydantic import BaseModel, ValidationError +from dynapydantic import SubclassTrackingModel from typing import Callable, Optional, Any +import Sofa + +class BaseParameters(SubclassTrackingModel, + discriminator_field="type", + discriminator_value_generator=lambda t: t.__name__,): -@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(): + loc = error.get("loc") + message = "" + for locPart in loc: + message += locPart.__str__() + ": " + message += error.get("msg") + Sofa.msg_error(self.__name__, message) + - def toDict(self): - return dataclasses.asdict(self) diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index 83d29f55d..07190c5f3 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -1,23 +1,29 @@ -import copy import Sofa import Sofa.Core -from stlib.core.basePrefabParameters import BasePrefabParameters + +from stlib.core.baseParameters import BaseParameters + + +class BasePrefabParameters(BaseParameters): + name : str = "object" + kwargs : dict = {} + + # Transformation information + # TODO: these data are going to be added in Node in SOFA (C++ implementation) + translation : list[float] = [0., 0., 0.] + rotation : list[float] = [0., 0., 0.] + scale : list[float] = [1., 1., 1.] + class BasePrefab(Sofa.Core.Node): - """ - A Prefab is a Sofa.Node that assembles a set of components and nodes - """ + + parameters : BasePrefabParameters def __init__(self, parameters: BasePrefabParameters): Sofa.Core.Node.__init__(self, name=parameters.name) self.parameters = parameters def init(self): - raise NotImplemented("To be overridden by child class") + raise NotImplemented("This method should be implemented in the child class to initialize the prefab's components and nodes based on the provided parameters.") - - def localToGlobalCoordinates(pointCloudInput, pointCloudOutput): - raise NotImplemented("Send an email to Damien, he will help you. Guaranteed :)") - - diff --git a/stlib/core/basePrefabParameters.py b/stlib/core/basePrefabParameters.py deleted file mode 100644 index d6a69dee9..000000000 --- a/stlib/core/basePrefabParameters.py +++ /dev/null @@ -1,15 +0,0 @@ -import dataclasses - -@dataclasses.dataclass -class BasePrefabParameters(object): - name : str = "object" - kwargs : dict = dataclasses.field(default_factory=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) diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index bc7c76711..4d0e89f6b 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -3,56 +3,69 @@ 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 RigidMaterialParameters +from splib.core.enum_types import ElementType + +from dynapydantic import Polymorphic +import Sofa + -@dataclasses.dataclass class EntityParameters(BaseParameters): + """ + Attributes: + name (str): The name of the entity. + addCollision (Optional[Callable]): Optional callable to add a collision component to the entity. + addVisual (Optional[Callable]): Optional callable to add a visual component to the entity. + geometry (GeometryParameters): Parameters for the geometry of the entity, with a default of a point at the origin. + material (MaterialParameters): Parameters for the material of the entity, with a default of a rigid material. + visual (Optional[VisualParameters]): Optional parameters for the visual component of the entity, with a default of a cube mesh. + collision (Optional[CollisionParameters]): Optional parameters for the collision component of the entity, with a default of None. + """ name : str = "Entity" - stateType : StateType = StateType.VEC3 - - ### 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 : Polymorphic[GeometryParameters] = GeometryParameters(elementType = ElementType.POINTS, data = InternalDataProvider(position = [[0., 0., 0.]])) + material : Polymorphic[MaterialParameters] = RigidMaterialParameters() + visual : Optional[VisualParameters] = VisualParameters(geometry = FileParameters(filename="mesh/cube.obj")) collision : Optional[CollisionParameters] = None - visual : Optional[VisualParameters] = None class Entity(BasePrefab): + """ + An entity is a Prefab, representing a physical object with geometry, material properties, + and optional visual and collision components. It serves as a base class for more complex entities in the simulation. + + Attributes: + parameters (EntityParameters): The parameters defining the entity, including its name, geometry, material properties, and optional visual and collision components. + """ - # A simulated object + geometry : Geometry material : Material visual : Visual collision : Collision - geometry : Geometry 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.material = self.add(Material, parameters=self.parameters.material) - self.material.States.position.parent = self.geometry.container.position.linkpath - + self.material.getMechanicalState().topology = self.geometry.container.linkpath + if self.parameters.collision is not None: self.collision = self.add(Collision, parameters=self.parameters.collision) self.addMapping(self.collision) @@ -64,18 +77,18 @@ def init(self): def addMapping(self, destinationPrefab): - template = f'{self.parameters.stateType},Vec3' # TODO: check that it is always true + template = f'{self.parameters.material.stateType},Vec3' # TODO: check that it is always true #TODO: all paths are expecting Geometry to be called Geomtry and so on. We need to robustify this by using the name parameter somehow - if( self.parameters.stateType == StateType.VEC3): + if( self.parameters.material.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/entities/bunny.py b/stlib/entities/bunny.py new file mode 100644 index 000000000..f1febde44 --- /dev/null +++ b/stlib/entities/bunny.py @@ -0,0 +1,27 @@ +from stlib.entities import Entity, EntityParameters +from stlib.visual import VisualParameters +from stlib.geometries.file import FileParameters +from stlib.materials.deformable import DeformableMaterialParameters +from splib.core.enum_types import ElementType + +from typing import Optional + + +class BunnyParameters(EntityParameters): + name : str = "Bunny" + deformable : bool = False + visual : Optional[VisualParameters] = VisualParameters(geometry = FileParameters(filename="mesh/Bunny.stl")) + + def model_post_init(self, _): + # TODO: + # 1. apply size as scale in geometry, material, collision and visual + if self.deformable: + self.geometry = FileParameters(filename="mesh/Bunny.vtk", elementType=ElementType.TETRAHEDRA) + self.material = DeformableMaterialParameters() + return + + +class Bunny(Entity): + + def __init__(self, parameters: BunnyParameters): + super().__init__(parameters) \ No newline at end of file diff --git a/stlib/entities/cube.py b/stlib/entities/cube.py new file mode 100644 index 000000000..ecf43fc53 --- /dev/null +++ b/stlib/entities/cube.py @@ -0,0 +1,22 @@ +from stlib.entities import Entity, EntityParameters +from stlib.materials.deformable import DeformableMaterialParameters +from stlib.geometries.file import FileParameters +from splib.core.enum_types import ElementType + + +class CubeParameters(EntityParameters): + name : str = "Cube" + size : float = 1 + deformable : bool = False + + def model_post_init(self, _): + if self.deformable: + self.geometry = FileParameters(filename="mesh/sphere.vtk", elementType=ElementType.TETRAHEDRA, scale=self.size) + self.material = DeformableMaterialParameters() + return + + +class Cube(Entity): + + def __init__(self, parameters: CubeParameters): + super().__init__(parameters) \ No newline at end of file diff --git a/stlib/entities/sphere.py b/stlib/entities/sphere.py new file mode 100644 index 000000000..7862938cb --- /dev/null +++ b/stlib/entities/sphere.py @@ -0,0 +1,26 @@ +from stlib.entities import Entity, EntityParameters +from stlib.visual import VisualParameters +from stlib.geometries.file import FileParameters +from stlib.materials.deformable import DeformableMaterialParameters +from splib.core.enum_types import ElementType + +from typing import Optional + + +class SphereParameters(EntityParameters): + name : str = "Sphere" + radius : float = 1 + deformable : bool = False + visual : Optional[VisualParameters] = VisualParameters(geometry = FileParameters(filename="mesh/sphere.obj")) + + def model_post_init(self, _): + if self.deformable: + self.geometry = FileParameters(filename="mesh/sphere.vtk", elementType=ElementType.TETRAHEDRA, scale=self.radius) + self.material = DeformableMaterialParameters() + return + + +class Sphere(Entity): + + def __init__(self, parameters: SphereParameters): + super().__init__(parameters) \ No newline at end of file diff --git a/stlib/geometries/__geometry__.py b/stlib/geometries/__geometry__.py index 57d0e5c76..034a0bfd2 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,35 +20,26 @@ class InternalDataProvider(object): tetrahedra : Any = DEFAULT_VALUE hexahedra : Any = DEFAULT_VALUE + @classmethod def generateAttribute(self, parent : Geometry): pass -@dataclasses.dataclass class GeometryParameters(BaseParameters): name : str = "Geometry" - # Type of the highest degree element - elementType : Optional[ElementType] = None + elementType : Optional[ElementType] = None # Type of the highest degree element data : Optional[InternalDataProvider] = None dynamicTopology : bool = False - def Data(self): - return InternalDataProvider() - - class Geometry(BasePrefab): - # container : Object # This should be more specialized into the right SOFA type - # modifier : Optional[Object] parameters : GeometryParameters def __init__(self, parameters: GeometryParameters): BasePrefab.__init__(self, parameters) - - def init(self): @@ -59,14 +49,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/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..1d3bde73e 100644 --- a/stlib/geometries/file.py +++ b/stlib/geometries/file.py @@ -1,19 +1,32 @@ 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): + +class FileParameters(GeometryParameters): + filename : str = "mesh/cube.obj" + dynamicTopology : bool = False + elementType : ElementType = None + + translation : list[float, float, float] = [0., 0., 0.] + rotation : list[float, float, float] = [0., 0., 0.] + scale : list[float, float, float] = [1., 1., 1.] + + def model_post_init(self, __context): + self.data = FileInternalDataProvider(fileParameters=self) - def __post_init__(self, **kwargs): - InternalDataProvider.__init__(self,**kwargs) +class FileInternalDataProvider(InternalDataProvider): + + fileParameters : FileParameters def generateAttribute(self, parent : Geometry): - loadMesh(parent, self.filename) + loader = loadMesh(parent, self.fileParameters.filename) + loader.translation = self.fileParameters.translation + loader.rotation = self.fileParameters.rotation + loader.scale = self.fileParameters.scale if hasattr(parent.loader, 'position'): self.position = str(parent.loader.position.linkpath) @@ -28,13 +41,4 @@ def generateAttribute(self, parent : Geometry): if hasattr(parent.loader, 'tetrahedra'): self.tetrahedra = str(parent.loader.tetrahedra.linkpath) - - -class FileParameters(GeometryParameters): - - def __init__(self, filename, dynamicTopology = False, elementType : ElementType = None ): - GeometryParameters.__init__(self, - data = FileInternalDataProvider(filename=filename), - dynamicTopology = dynamicTopology, - elementType = elementType) - + return parent.loader 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..09a59d1c2 100644 --- a/stlib/materials/__material__.py +++ b/stlib/materials/__material__.py @@ -1,23 +1,27 @@ -from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any -from splib.core.utils import defaultValueType, DEFAULT_VALUE, isDefault +from stlib.core.baseParameters import BaseParameters, Callable, Optional, Any +from splib.core.utils import DEFAULT_VALUE 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" + position : Optional[list[float]] = None + massDensity : float = DEFAULT_VALUE massLumping : bool = DEFAULT_VALUE 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 class Material(BasePrefab): parameters : MaterialParameters @@ -25,7 +29,7 @@ class Material(BasePrefab): def __init__(self, parameters: MaterialParameters): BasePrefab.__init__(self, parameters) - def init(self): - self.addObject("MechanicalObject", name="States", template=str(self.parameters.stateType)) + self.addObject("MechanicalObject", name="States", template=str(self.parameters.stateType), + position = self.parameters.position if self.parameters.position is not None else "") self.parameters.addMaterial(self) diff --git a/stlib/materials/deformable.py b/stlib/materials/deformable.py index a24763df5..d1e4e0524 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): +class DeformableMaterialParameters(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): @@ -28,25 +27,3 @@ def __addDeformableMaterial(node): addMaterial : Optional[Callable] = __addDeformableMaterial - -def createScene(root) : - from stlib.entities import Entity, EntityParameters - from stlib.visual import VisualParameters - from stlib.geometries.file import FileParameters - - root.addObject('RequiredPlugin', name='Sofa.Component.Visual') # Needed to use components [VisualStyle] - root.addObject("VisualStyle", displayFlags=["showBehavior"]) - - bunnyParameters = EntityParameters() - bunnyParameters.geometry = FileParameters(filename="mesh/Bunny.vtk") - bunnyParameters.geometry.elementType = ElementType.TETRAHEDRA # TODO: this is required by extract.py. Should it be done automatically in geometry.py ? - bunnyParameters.material = DeformableBehaviorParameters() - bunnyParameters.material.constitutiveLawType = ConstitutiveLaw.ELASTIC - bunnyParameters.material.parameters = [1000, 0.45] - bunnyParameters.visual = VisualParameters() - # bunnyParameters.visual.geometry = ExtractParameters(sourceParameters=bunnyParameters.geometry, - # destinationType=ElementType.TRIANGLES) - bunnyParameters.visual.geometry = FileParameters(filename="mesh/Bunny.stl") - bunnyParameters.visual.color = [1, 1, 1, 0.5] - bunny = root.add(Entity, parameters=bunnyParameters) - # bunny.init() \ No newline at end of file diff --git a/stlib/materials/rigid.py b/stlib/materials/rigid.py index 5668b3562..20a7dc3ab 100644 --- a/stlib/materials/rigid.py +++ b/stlib/materials/rigid.py @@ -1,13 +1,9 @@ -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 RigidMaterialParameters(MaterialParameters): -@dataclasses.dataclass -class RigidParameters(BaseParameters): + stateType : StateType = StateType.RIGID - geometry : GeometryParameters - mass : Optional[float] = None - - def toDict(self): - return dataclasses.asdict(self) + position : list[list[float]] = [[0., 0., 0., 0., 0., 0., 1.]] diff --git a/stlib/prefabs/__init__.py b/stlib/prefabs/__init__.py new file mode 100644 index 000000000..350960367 --- /dev/null +++ b/stlib/prefabs/__init__.py @@ -0,0 +1 @@ +from .__prefabs__ import * \ No newline at end of file diff --git a/stlib/prefabs/__prefabs__.py b/stlib/prefabs/__prefabs__.py new file mode 100644 index 000000000..5e85e8c31 --- /dev/null +++ b/stlib/prefabs/__prefabs__.py @@ -0,0 +1,31 @@ +from stlib.core.baseParameters import BaseParameters +from stlib.core.basePrefab import BasePrefab + + +class PrefabParameters(BaseParameters): + """ + Attributes: + name (str): The name of the entity. + addCollision (Optional[Callable]): Optional callable to add a collision component to the entity. + addVisual (Optional[Callable]): Optional callable to add a visual component to the entity. + geometry (GeometryParameters): Parameters for the geometry of the entity, with a default of a point at the origin. + material (MaterialParameters): Parameters for the material of the entity, with a default of a rigid material. + visual (Optional[VisualParameters]): Optional parameters for the visual component of the entity, with a default of a cube mesh. + collision (Optional[CollisionParameters]): Optional parameters for the collision component of the entity, with a default of None. + """ + name : str = "Prefab" + + +class Prefab(BasePrefab): + """ + A Prefab is a Sofa.Node that assembles a set of components and nodes + + Attributes: + parameters (PrefabParameters): The parameters defining the prefab, including its name and any other relevant properties. + This is a dataclass that should be defined in the child class, inheriting from BasePrefabParameters, + and can include any additional parameters needed for the specific prefab. + """ + parameters : PrefabParameters + + def __init__(self, parameters: PrefabParameters): + BasePrefab.__init__(self, parameters) diff --git a/stlib/settings/__init__.py b/stlib/settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/settings/simulation.py b/stlib/settings/simulation.py new file mode 100644 index 000000000..c7ba40525 --- /dev/null +++ b/stlib/settings/simulation.py @@ -0,0 +1,40 @@ +import Sofa.Core +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters +from splib.core.utils import REQUIRES_COLLISIONPIPELINE, REQUIRES_LAGRANGIANCONSTRAINTSOLVER + + +class SimulationSettingsParameters(BaseParameters): + name : str = "Simulation" + + +class SimulationSettings(BasePrefab): + + def __init__(self, parameters: SimulationSettingsParameters): + BasePrefab.__init__(self, parameters) + + def init(self): + rootnode = self.parents[0] + + rootnode.add('DefaultVisualManagerLoop') + rootnode.add('VisualStyle') + rootnode.add('InteractiveCamera') + + if rootnode.findData(REQUIRES_COLLISIONPIPELINE) and rootnode.findData(REQUIRES_COLLISIONPIPELINE).value: + rootnode.add('CollisionPipeline') + rootnode.add('RuleBasedContactManager', responseParams='mu=0', response='FrictionContactConstraint') + rootnode.add('ParallelBruteForceBroadPhase') + rootnode.add('ParallelBVHNarrowPhase') + rootnode.add('LocalMinDistance', alarmDistance=5, contactDistance=1) + + if rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER) and rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER).value: + rootnode.add('FreeMotionAnimationLoop') + rootnode.add('BlockGaussSeidelConstraintSolver') + else: + rootnode.add('DefaultAnimationLoop') + + self.add('EulerImplicitSolver') + self.add('SparseLDLSolver') + + if rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER) and rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER).value: + self.add('GenericConstraintCorrection') diff --git a/stlib/visual.py b/stlib/visual.py index a0f377f7d..66922d96b 100644 --- a/stlib/visual.py +++ b/stlib/visual.py @@ -1,22 +1,24 @@ 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 +from dynapydantic import Polymorphic + -@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 : Polymorphic[GeometryParameters] = None class Visual(BasePrefab): + parameters: VisualParameters + def __init__(self, parameters: VisualParameters): BasePrefab.__init__(self, parameters) @@ -25,15 +27,10 @@ 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)