From a6ff8859b6bec6853969d8c93d9ced273fc1ade4 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 10:03:50 +0200 Subject: [PATCH 01/49] add folder for STLIB --- stlib/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 stlib/__init__.py diff --git a/stlib/__init__.py b/stlib/__init__.py new file mode 100644 index 000000000..34e9c316c --- /dev/null +++ b/stlib/__init__.py @@ -0,0 +1,4 @@ +"""SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it""" +raise Exception("SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it") + + From 60352a4c0534be2bc1d10c2ed7c2a2db9eda2c67 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 10:14:51 +0200 Subject: [PATCH 02/49] Add reusable methods --- splib/Testing.py | 122 +++++++++++++++++++++++++ splib/__init__.py | 4 +- splib/core/__init__.py | 1 + splib/core/enum_types.py | 34 +++++++ splib/core/node_wrapper.py | 73 +++++++++++++++ splib/core/utils.py | 36 ++++++++ splib/mechanics/__init__.py | 1 + splib/mechanics/collision_model.py | 18 ++++ splib/mechanics/fix_points.py | 30 +++++++ splib/mechanics/hyperelasticity.py | 14 +++ splib/mechanics/linear_elasticity.py | 26 ++++++ splib/mechanics/mass.py | 19 ++++ splib/simulation/__init__.py | 1 + splib/simulation/headers.py | 128 +++++++++++++++++++++++++++ splib/simulation/linear_solvers.py | 31 +++++++ splib/simulation/ode_solvers.py | 14 +++ splib/topology/__init__.py | 1 + splib/topology/dynamic.py | 67 ++++++++++++++ splib/topology/loader.py | 22 +++++ splib/topology/static.py | 7 ++ 20 files changed, 646 insertions(+), 3 deletions(-) create mode 100644 splib/Testing.py create mode 100644 splib/core/__init__.py create mode 100644 splib/core/enum_types.py create mode 100644 splib/core/node_wrapper.py create mode 100644 splib/core/utils.py create mode 100644 splib/mechanics/__init__.py create mode 100644 splib/mechanics/collision_model.py create mode 100644 splib/mechanics/fix_points.py create mode 100644 splib/mechanics/hyperelasticity.py create mode 100644 splib/mechanics/linear_elasticity.py create mode 100644 splib/mechanics/mass.py create mode 100644 splib/simulation/__init__.py create mode 100644 splib/simulation/headers.py create mode 100644 splib/simulation/linear_solvers.py create mode 100644 splib/simulation/ode_solvers.py create mode 100644 splib/topology/__init__.py create mode 100644 splib/topology/dynamic.py create mode 100644 splib/topology/loader.py create mode 100644 splib/topology/static.py diff --git a/splib/Testing.py b/splib/Testing.py new file mode 100644 index 000000000..9347f8c2d --- /dev/null +++ b/splib/Testing.py @@ -0,0 +1,122 @@ +from splib.topology.dynamic import * +from splib.simulation.headers import * +from splib.simulation.ode_solvers import * +from splib.simulation.linear_solvers import * +from splib.mechanics.linear_elasticity import * +from splib.mechanics.mass import * +from splib.mechanics.fix_points import * +from splib.topology.loader import * +from splib.core.node_wrapper import * + +from splib.core.node_wrapper import ChildWrapper, ObjectWrapper + + +class displayNode(): + def __init__(self,_level=0): + self.prefix = "" + for i in range(_level): + self.prefix += "| " + + def addObject(self,type:str,**kwargs): + print(self.prefix + type + " with " + str(kwargs)) + + def addChild(self,name:str): + print(self.prefix + "-> Node : " + name) + return displayNode(len(self.prefix) + 1) + +class exportScene(): + def __init__(self,name="rootNode"): + self.name = name + + def addObject(self,type:str,**kwargs): + suffix = "" + for i in kwargs: + suffix += "," + str(i) + "=\"" + str(kwargs[i]) + "\"" + print(self.name+".addObject(\"" + type + "\"" + suffix + ")") + + def addChild(self,name:str): + print(name + '=' + self.name+".addChild(\"" + name + "\")") + setattr(self,name,exportScene(name)) + return getattr(self,name) + + def __setattr__(self, key, value): + if(not(key == "name")): + print(self.__dict__["name"] + "." + key + " = " + str(value)) + self.__dict__[key] = value + else: + self.__dict__[key] = value + + +@PrefabSimulation +def createScene(rootNode): + rootNode.dt = 0.03 + rootNode.gravity = [0,-9.81,0] + + setupLagrangianCollision(rootNode,requiredPlugins={"pluginName":['Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual']}) + # + # + # ## TODO : Being able to call "childNode.addAnything" by using the __getattr__ method + Liver0=rootNode.addChild("Liver0") + Liver0.addObject("EulerImplicitSolver",name="ODESolver",rayleighStiffness="0",rayleighMass="0") + Liver0.addObject("SparseLDLSolver",name="LinearSolver",template="CompressedRowSparseMatrixMat3x3",parallelInverseProduct="False") + Liver0.addObject("MeshGmshLoader",name="meshLoader",filename="mesh/liver.msh") + Liver0.addObject("TetrahedronSetTopologyModifier",name="modifier") + Liver0.addObject("TetrahedronSetTopologyContainer",name="container",src="@meshLoader") + Liver0.addObject("TetrahedronSetGeometryAlgorithms",name="algorithms") + Liver0.addObject("MechanicalObject",name="mstate",template="Vec3d") + Liver0.addObject("LinearSolverConstraintCorrection",name="constraintCorrection") + Liver0.addObject("TetrahedronFEMForceField",name="constitutiveLaw",youngModulus="3000",poissonRatio="0.3",method="large") + Liver0.addObject("MeshMatrixMass",name="mass",massDensity="1") + Liver0.addObject("BoxROI",name="fixedBoxROI",box="0 3 0 2 5 2") + Liver0.addObject("FixedProjectiveConstraint",name="fixedConstraints",indices="@fixedBoxROI.indices") + Visu=Liver0.addChild("Visu") + Visu.addObject("TriangleSetTopologyModifier",name="modifier") + Visu.addObject("TriangleSetTopologyContainer",name="container") + Visu.addObject("TriangleSetGeometryAlgorithms",name="algorithms") + Visu.addObject("Tetra2TriangleTopologicalMapping",name="TopologicalMapping",input="@../container",output="@container") + Visu.addObject("OglModel",name="OglModel",topology="@container",color="[1.0, 0.2, 0.8]") + Visu.addObject("IdentityMapping",name="Mapping",isMechanical="False") + + + SimulatedLiver1 = rootNode.addChild("Liver1") + addImplicitODE(SimulatedLiver1) + addLinearSolver(SimulatedLiver1,iterative=False, template="CompressedRowSparseMatrixMat3x3") + loadMesh(SimulatedLiver1,filename="mesh/liver.msh") + addDynamicTopology(SimulatedLiver1,type=ElementType.TETRA,source="@meshLoader") + SimulatedLiver1.addObject("MechanicalObject",name="mstate", template='Vec3d') + SimulatedLiver1.addObject("LinearSolverConstraintCorrection",name="constraintCorrection") + addLinearElasticity(SimulatedLiver1,ElementType.TETRA, poissonRatio="0.3", youngModulus="3000", method='large') + addMass(SimulatedLiver1,template='Vec3d',massDensity="2") + addFixation(SimulatedLiver1,ConstraintType.PROJECTIVE,boxROIs=[0, 3, 0, 2, 5, 2]) + + SimulatedLiverVisu = SimulatedLiver1.addChild("Visu") + addDynamicTopology(SimulatedLiverVisu,ElementType.TRIANGLES) + SimulatedLiverVisu.addObject("Tetra2TriangleTopologicalMapping", name="TopologicalMapping", input="@../container", output="@container") + SimulatedLiverVisu.addObject("OglModel", name="OglModel", topology="@container",color=[1.0,0.2,0.8]) + SimulatedLiverVisu.addObject("IdentityMapping",name="Mapping",isMechanical=False) + + + return rootNode + + + +if __name__ == "__main__": + Node = exportScene() + createScene(Node) + + + + + + diff --git a/splib/__init__.py b/splib/__init__.py index 34e9c316c..a04a5119e 100644 --- a/splib/__init__.py +++ b/splib/__init__.py @@ -1,4 +1,2 @@ -"""SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it""" -raise Exception("SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it") - +__all__ = ["core","topology", "simulation","modeler","mechanics","Testing"] diff --git a/splib/core/__init__.py b/splib/core/__init__.py new file mode 100644 index 000000000..78deb3fad --- /dev/null +++ b/splib/core/__init__.py @@ -0,0 +1 @@ +__all__ = ["node_wrapper", "utils","enum_types"] diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py new file mode 100644 index 000000000..dc1b5e864 --- /dev/null +++ b/splib/core/enum_types.py @@ -0,0 +1,34 @@ +from enum import Enum + +class ConstitutiveLaw(Enum): + LINEAR_COROT = 1 + HYPERELASTIC = 2 + +class ODEType(Enum): + EXPLICIT = 1 + IMPLICIT = 2 + +class SolverType(Enum): + DIRECT = 1 + ITERATIVE = 2 + +class MappingType(Enum): + BARYCENTRIC = 1 + IDENTITY = 2 + RIGID = 3 + + +class ConstraintType(Enum): + PROJECTIVE = 1 + WEAK = 2 + LAGRANGIAN = 3 + + + +class ElementType(Enum): + POINTS = 1 + EDGES = 2 + TRIANGLES = 3 + QUAD = 4 + TETRA = 5 + HEXA = 6 diff --git a/splib/core/node_wrapper.py b/splib/core/node_wrapper.py new file mode 100644 index 000000000..7646b9e84 --- /dev/null +++ b/splib/core/node_wrapper.py @@ -0,0 +1,73 @@ +from functools import wraps +from splib.core.utils import defaultValueType +# The two classes are not merged because one could want to use a ReusableMethod +# (enabling to pass dictionary fixing params) without wanting to use a full PrefabSimulation + + +class BaseWrapper(object): + + def __init__(self,node): + self.node = node + + def __getattr__(self, item): + return getattr(self.node,item) + + +class ObjectWrapper(BaseWrapper): + def __init__(self,*args,**kwargs): + super().__init__(*args,**kwargs) + + def addObject(self,*args, **kwargs): + parameters = {} + # Expand any parameter that has been set by the user in a custom dictionary + # and having the same key as the component name + if "name" in kwargs: + parameters["name"] = kwargs["name"] + if kwargs["name"] in kwargs: + if isinstance(kwargs[kwargs["name"]], dict): + parameters = {**parameters, **kwargs[kwargs["name"]]} + else: + print("[Warning] You are passing a keyword arg with the same name as one obj without it being a Dict, it will not be used. ") + + # Add all the parameters from kwargs that are not dictionary + for param in kwargs: + if param in parameters: + if not(param == "name"): + print("[Warning] You are redefining the parameter '"+ param + "' of object " + str(args[0])) + elif not(isinstance(kwargs[param], dict)) and not(isinstance(kwargs[param],defaultValueType)): + parameters = {**parameters,param:kwargs[param]} + + return self.node.addObject(*args,**parameters) + + + + +class ChildWrapper(ObjectWrapper): + def __init__(self,*args,**kwargs): + super().__init__(*args,**kwargs) + + # This method enforces that the object that is created by the addChild method keeps the prefab type + def addChild(self,*args, **kwargs): + child = self.node.addChild(*args,**kwargs) + returnObject = self.__new__(type(self)) + returnObject.__init__(child) + return returnObject + + + + +def ReusableMethod(method): + @wraps(method) + def wrapper(*args, **kwargs): + + node = args[0] + # We don't want to wrap an object that is already wrapped + # It wouldn't break anything but nodes might get wrapped and wrapped again multiplying redirections... + if( not isinstance(node,ObjectWrapper) ): + node = ObjectWrapper(node) + + if len(args)>1: + return method(node,*args[1:],**kwargs) + else: + return method(node,**kwargs) + return wrapper diff --git a/splib/core/utils.py b/splib/core/utils.py new file mode 100644 index 000000000..56b91f53c --- /dev/null +++ b/splib/core/utils.py @@ -0,0 +1,36 @@ +from typing import List, Callable, Tuple, Dict +from functools import wraps + +class defaultValueType(): + def __init__(self): + pass + +DEFAULT_VALUE = defaultValueType() + +def isDefault(obj): + return isinstance(obj,defaultValueType) + + +def getParameterSet(name : str,parameterSet : Dict) -> Dict: + if name in parameterSet: + if isinstance(parameterSet[name], dict): + return parameterSet[name] + return {} + + +def MapKeywordArg(objectName,*argumentMaps): + def MapArg(method): + @wraps(method) + def wrapper(*args, **kwargs): + containerParams = getParameterSet(objectName,kwargs) + for argMap in argumentMaps: + if (argMap[0] in kwargs) and not(kwargs[argMap[0]] is None): + containerParams[argMap[1]] = kwargs[argMap[0]] + kwargs[objectName] = containerParams + return method(*args,**kwargs) + return wrapper + return MapArg + + + + diff --git a/splib/mechanics/__init__.py b/splib/mechanics/__init__.py new file mode 100644 index 000000000..3761021a2 --- /dev/null +++ b/splib/mechanics/__init__.py @@ -0,0 +1 @@ +__all__ = ["linear_elasticity","hyperelasticity","fix_points","collision_model","mass"] \ No newline at end of file diff --git a/splib/mechanics/collision_model.py b/splib/mechanics/collision_model.py new file mode 100644 index 000000000..40bf0c1d8 --- /dev/null +++ b/splib/mechanics/collision_model.py @@ -0,0 +1,18 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import ElementType + + + +@ReusableMethod +def addCollisionModels(node,points=False, edges=False,triangles=False, spheres=False,tetrahedron=False,selfCollision=DEFAULT_VALUE, proximity=DEFAULT_VALUE, group=DEFAULT_VALUE, contactStiffness=DEFAULT_VALUE, contactFriction=DEFAULT_VALUE,spheresRadius=DEFAULT_VALUE,**kwargs): + if(points): + node.addObject("PointCollisionModel",name="PointCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) + if(edges): + node.addObject("LineCollisionModel",name="EdgeCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) + if(triangles): + node.addObject("TriangleCollisionModel",name="TriangleCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) + if(spheres): + node.addObject("SphereCollisionModel",name="SphereCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) + if(tetrahedron): + node.addObject("TetrahedronCollisionModel",name="TetraCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) diff --git a/splib/mechanics/fix_points.py b/splib/mechanics/fix_points.py new file mode 100644 index 000000000..ddbbd6308 --- /dev/null +++ b/splib/mechanics/fix_points.py @@ -0,0 +1,30 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import isDefault, DEFAULT_VALUE +from splib.core.enum_types import ConstraintType +from enum import Enum + + +##box +@ReusableMethod +def addFixation(node,type:ConstraintType,boxROIs=DEFAULT_VALUE, sphereROIs=DEFAULT_VALUE, indices=DEFAULT_VALUE, fixAll=DEFAULT_VALUE,**kwargs): + if (isDefault(indices)): + if(not isDefault(boxROIs)): + node.addObject("BoxROI",name='fixedBoxROI',box=boxROIs,**kwargs) + indices="@fixedBoxROI.indices" + if(not isDefault(sphereROIs)): + node.addObject("SphereROI",name='fixedSphereROI',centers=sphereROIs[0],radii=sphereROIs[1],**kwargs) + indices="@fixedSphereROI.indices" + + match type: + case ConstraintType.WEAK: + node.addObject("FixedWeakConstraint",name="fixedConstraints", indices=indices, fixAll=fixAll, **kwargs) + return + case ConstraintType.PROJECTIVE: + node.addObject("FixedProjectiveConstraint",name="fixedConstraints", indices=indices, fixAll=fixAll, **kwargs) + return + case ConstraintType.LAGRANGIAN: + node.addObject("LagrangianFixedConstraint",name="fixedConstraints", indices=indices, fixAll=fixAll, **kwargs) + return + case _: + print('Contraint type is either ConstraintType.PROJECTIVE, ConstraintType.WEAK or ConstraintType.LAGRANGIAN') + return diff --git a/splib/mechanics/hyperelasticity.py b/splib/mechanics/hyperelasticity.py new file mode 100644 index 000000000..1e79c4206 --- /dev/null +++ b/splib/mechanics/hyperelasticity.py @@ -0,0 +1,14 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import ElementType + + +@ReusableMethod +def addHyperelasticity(node,elem:ElementType,materialName=DEFAULT_VALUE, parameterSet=DEFAULT_VALUE, matrixRegularization=DEFAULT_VALUE,**kwargs): + match elem: + case ElementType.TETRA: + node.addObject("TetrahedronHyperelasticityFEMForceField",name="constitutiveLaw", materialName=materialName, parameterSet=parameterSet, matrixRegularization=matrixRegularization, **kwargs) + return + case _: + print('Hyperelasticity model only exist for Tetrahedron elements.') + return \ No newline at end of file diff --git a/splib/mechanics/linear_elasticity.py b/splib/mechanics/linear_elasticity.py new file mode 100644 index 000000000..8675afe19 --- /dev/null +++ b/splib/mechanics/linear_elasticity.py @@ -0,0 +1,26 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import ElementType + + +@ReusableMethod +def addLinearElasticity(node,elem:ElementType,youngModulus=DEFAULT_VALUE, poissonRatio=DEFAULT_VALUE, method=DEFAULT_VALUE,**kwargs): + match elem: + case ElementType.EDGES: + node.addObject("BeamFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) + return + case ElementType.TRIANGLES: + node.addObject("TriangleFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + return + case ElementType.QUAD: + node.addObject("QuadBendingFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + return + case ElementType.TETRA: + node.addObject("TetrahedronFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + return + case ElementType.HEXA: + node.addObject("HexahedronFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + return + case _: + print('Linear elasticity is only available for topology of type EDGES, TRIANGLES, QUADS, TETRAHEDRON, HEXAHEDRON') + return \ No newline at end of file diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py new file mode 100644 index 000000000..e3fe68fe3 --- /dev/null +++ b/splib/mechanics/mass.py @@ -0,0 +1,19 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import defaultValueType, DEFAULT_VALUE, isDefault +from splib.core.enum_types import ElementType + + +@ReusableMethod +def addMass(node,template,totalMass=DEFAULT_VALUE,massDensity=DEFAULT_VALUE,lumping=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") + kwargs.pop('massDensity') + + if(template=="Rigid3"): + node.addObject("UniformMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) + else: + node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) + + + + diff --git a/splib/simulation/__init__.py b/splib/simulation/__init__.py new file mode 100644 index 000000000..d59a665ef --- /dev/null +++ b/splib/simulation/__init__.py @@ -0,0 +1 @@ +__all__ = ["headers","linear_solvers","ode_solvers"] diff --git a/splib/simulation/headers.py b/splib/simulation/headers.py new file mode 100644 index 000000000..9734d3062 --- /dev/null +++ b/splib/simulation/headers.py @@ -0,0 +1,128 @@ +from splib.core.node_wrapper import ReusableMethod +from enum import Enum + +from splib.core.utils import DEFAULT_VALUE + + +class CollisionType(Enum): + NONE = 1 + PENALITY = 2 + LAGRANGIAN = 3 + + +@ReusableMethod +def setupDefaultHeader(node, displayFlags = "showVisualModels", backgroundColor=[1,1,1,1], parallelComputing=False,**kwargs): + + node.addObject('VisualStyle', displayFlags=displayFlags) + node.addObject('BackgroundSetting', color=backgroundColor) + + node.addObject("RequiredPlugin", name="requiredPlugins", pluginName=['Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual', + ], + **kwargs) + node.addObject('DefaultAnimationLoop',name="animation", parallelODESolving=parallelComputing, **kwargs) + + return node + + +@ReusableMethod +def setupPenalityCollisionHeader(node, displayFlags = "showVisualModels",backgroundColor=[1,1,1,1], stick=False, parallelComputing=False, alarmDistance=DEFAULT_VALUE, contactDistance=DEFAULT_VALUE, **kwargs): + node.addObject('VisualStyle', displayFlags=displayFlags) + node.addObject('BackgroundSetting', color=backgroundColor) + + node.addObject("RequiredPlugin", name="requiredPlugins", pluginName=['Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual', + ], + **kwargs) + + parallelPrefix = "" + if(parallelComputing): + parallelPrefix="Parallel" + + node.addObject('DefaultAnimationLoop',name="animation", **kwargs) + node.addObject('CollisionPipeline', name="collisionPipeline", **kwargs) + node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase", **kwargs) + node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase", **kwargs) + + if(stick): + node.addObject('CollisionResponse',name="ContactManager", response="BarycentricStickContact",**kwargs) + else: + node.addObject('CollisionResponse',name="ContactManager", response="BarycentricPenalityContact",**kwargs) + node.addObject('LocalMinDistance' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs) + + return node + + +@ReusableMethod +def setupLagrangianCollision(node, displayFlags = "showVisualModels",backgroundColor=[1,1,1,1], parallelComputing=False, stick=False, alarmDistance=DEFAULT_VALUE, contactDistance=DEFAULT_VALUE, frictionCoef=0.0, tolerance=0.0, maxIterations=100, **kwargs): + node.addObject('VisualStyle', displayFlags=displayFlags) + node.addObject('BackgroundSetting', color=backgroundColor) + + node.addObject("RequiredPlugin", name="requiredPlugins", pluginName=['Sofa.Component.Constraint.Lagrangian', + 'Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual', + ], + **kwargs) + + + node.addObject('FreeMotionAnimationLoop',name="animation", + parallelCollisionDetectionAndFreeMotion=parallelComputing, + parallelODESolving=parallelComputing, + **kwargs) + + parallelPrefix = "" + if(parallelComputing): + parallelPrefix="Parallel" + + node.addObject('CollisionPipeline', name="collisionPipeline", + **kwargs) + + node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase", + **kwargs) + + node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase", + **kwargs) + + if(stick): + node.addObject('CollisionResponse',name="ContactManager", response="StickContactConstraint", responseParams="tol="+str(tolerance),**kwargs) + else: + node.addObject('CollisionResponse',name="ContactManager", response="FrictionContactConstraint", responseParams="mu="+str(frictionCoef),**kwargs) + + node.addObject('NewProximityIntersection' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs) + node.addObject('GenericConstraintSolver',name="ConstraintSolver", tolerance=tolerance, maxIterations=maxIterations, multithreading=parallelComputing,**kwargs) + node.addObject("ConstraintAttachButtonSetting") + + return node diff --git a/splib/simulation/linear_solvers.py b/splib/simulation/linear_solvers.py new file mode 100644 index 000000000..b6b90f027 --- /dev/null +++ b/splib/simulation/linear_solvers.py @@ -0,0 +1,31 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import * + +@ReusableMethod +def addLinearSolver(node,iterative=False,iterations=DEFAULT_VALUE,tolerance=DEFAULT_VALUE,threshold=DEFAULT_VALUE,template=DEFAULT_VALUE,constantSparsity=False,parallelInverseProduct=DEFAULT_VALUE,**kwargs): + containerParams = getParameterSet("LinearSolver",kwargs) + if iterative: + if not isDefault(iterations): + containerParams["iterations"] = iterations + if not isDefault(tolerance): + containerParams["tolerance"] = tolerance + if not isDefault(threshold): + containerParams["threshold"] = threshold + else: + if isDefault(template) and not("template" in containerParams): + containerParams["template"] = 'CompressedRowSparseMatrix' + elif not isDefault(template) : + containerParams["template"] = template + kwargs["LinearSolver"] = containerParams + + if(constantSparsity): + node.addObject("ConstantSparsityPatternSystem",name='LinearSystem',**kwargs) + kwargs["LinearSolver"]["template"] = 'CompressedRowSparseMatrix' + kwargs["LinearSolver"]["linearSystem"]="@LinearSystem" + + + + if iterative: + node.addObject('CGLinearSolver', name='LinearSolver', parallelInverseProduct=parallelInverseProduct, **kwargs) + else: + node.addObject('SparseLDLSolver', name='LinearSolver', parallelInverseProduct=parallelInverseProduct, **kwargs) \ No newline at end of file diff --git a/splib/simulation/ode_solvers.py b/splib/simulation/ode_solvers.py new file mode 100644 index 000000000..eed57bab8 --- /dev/null +++ b/splib/simulation/ode_solvers.py @@ -0,0 +1,14 @@ +from splib.core.node_wrapper import ReusableMethod + +@ReusableMethod +def addImplicitODE(node,static=False,**kwargs): + if( not(static) ): + node.addObject("EulerImplicitSolver",name="ODESolver",**kwargs) + else: + node.addObject("StaticSolver",name="ODESolver",**kwargs) + +@ReusableMethod +def addExplicitODE(node,**kwargs): + node.addObject("EulerExplicitSolver",name="ODESolver",**kwargs) + + diff --git a/splib/topology/__init__.py b/splib/topology/__init__.py new file mode 100644 index 000000000..23469f77d --- /dev/null +++ b/splib/topology/__init__.py @@ -0,0 +1 @@ +__all__ = ["static","dynamic","loader"] \ No newline at end of file diff --git a/splib/topology/dynamic.py b/splib/topology/dynamic.py new file mode 100644 index 000000000..b5a829c39 --- /dev/null +++ b/splib/topology/dynamic.py @@ -0,0 +1,67 @@ +from splib.core.node_wrapper import ReusableMethod +from enum import Enum +from splib.core.utils import MapKeywordArg, DEFAULT_VALUE +from splib.core.enum_types import ElementType + + + +@ReusableMethod +def addPointTopology(node,position=DEFAULT_VALUE,source=DEFAULT_VALUE, **kwargs): + node.addObject("PointSetTopologyModifier", name="modifier", **kwargs) + node.addObject("PointSetTopologyContainer", name="container", src=source, position=position, **kwargs) + # node.addObject("PointSetGeometryAlgorithms", name="algorithms", **kwargs) + +@ReusableMethod +def addEdgeTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("EdgeSetTopologyModifier", name="modifier",**kwargs) + node.addObject("EdgeSetTopologyContainer", name="container", src=source, position=position, edges=edges, **kwargs) + # node.addObject("EdgeSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addTriangleTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,triangles=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("TriangleSetTopologyModifier", name="modifier",**kwargs) + node.addObject("TriangleSetTopologyContainer", name="container", src=source, position=position, edges=edges, triangles=triangles, **kwargs) + # node.addObject("TriangleSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addQuadTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,quads=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("QuadSetTopologyModifier", name="modifier",**kwargs) + node.addObject("QuadSetTopologyContainer", name="container", src=source, position=position, edges=edges, quads=quads, **kwargs) + # node.addObject("QuadSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addTetrahedronTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,triangles=DEFAULT_VALUE,tetrahedra=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("TetrahedronSetTopologyModifier", name="modifier",**kwargs) + node.addObject("TetrahedronSetTopologyContainer", name="container", src=source, position=position, edges=edges, triangles=triangles, tetrahedra=tetrahedra, **kwargs) + # node.addObject("TetrahedronSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addHexahedronTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,quads=DEFAULT_VALUE,hexahedra=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("HexahedronSetTopologyModifier", name="modifier",**kwargs) + node.addObject("HexahedronSetTopologyContainer", name="container", src=source, position=position, edges=edges, quads=quads, hexahedra=hexahedra, **kwargs) + # node.addObject("HexahedronSetGeometryAlgorithms", name="algorithms",**kwargs) + +def addDynamicTopology(node,type:ElementType,**kwargs): + + match type: + case ElementType.POINTS: + addPointTopology(node,**kwargs) + return + case ElementType.EDGES: + addEdgeTopology(node,**kwargs) + return + case ElementType.TRIANGLES: + addTriangleTopology(node,**kwargs) + return + case ElementType.QUAD: + addQuadTopology(node,**kwargs) + return + case ElementType.TETRA: + addTetrahedronTopology(node,**kwargs) + return + case ElementType.HEXA: + addHexahedronTopology(node,**kwargs) + return + case _: + print('Topology type should be one of the following : "ElementType.POINTS, ElementType.EDGES, ElementType.TRIANGLES, ElementType.QUAD, ElementType.TETRA, ElementType.HEXA" ') + return diff --git a/splib/topology/loader.py b/splib/topology/loader.py new file mode 100644 index 000000000..f9dd55fbb --- /dev/null +++ b/splib/topology/loader.py @@ -0,0 +1,22 @@ +from splib.core.node_wrapper import ReusableMethod + +@ReusableMethod +def loadMesh(node,filename,**kwargs): + splitedName = filename.split('.') + if len(splitedName) == 1: + print('[Error] : A file name without extension was provided.') + return + + if splitedName[-1] in ['vtk', 'obj', 'stl', 'msh', 'sph']: + if splitedName[-1] == "msh": + return node.addObject("MeshGmshLoader", name="meshLoader",filename=filename, **kwargs) + elif splitedName[-1] == "sph": + return node.addObject("SphereLoader", name="meshLoader",filename=filename, **kwargs) + else: + return node.addObject("Mesh"+splitedName[-1].upper()+"Loader", name="meshLoader",filename=filename, **kwargs) + else: + print('[Error] : File extension ' + splitedName[-1] + ' not recognised.') + + + + diff --git a/splib/topology/static.py b/splib/topology/static.py new file mode 100644 index 000000000..2e49fcf95 --- /dev/null +++ b/splib/topology/static.py @@ -0,0 +1,7 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE + +@ReusableMethod +def addStaticTopology(node,source=DEFAULT_VALUE,**kwargs): + node.addObject("MeshTopology", name="container", src=source, **kwargs) + From d0241052b82aeed4786fb8a3e8e03cfb6b3025c6 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 10:17:23 +0200 Subject: [PATCH 03/49] Add working files --- stlib/misc/entity.py | 188 +++++++++++++++++++++++++++++++++++++++ stlib/misc/entity2.py | 12 +++ stlib/misc/softrobots.py | 23 +++++ stlib/misc/test-1.py | 53 +++++++++++ stlib/misc/test2.py | 65 ++++++++++++++ 5 files changed, 341 insertions(+) create mode 100644 stlib/misc/entity.py create mode 100644 stlib/misc/entity2.py create mode 100644 stlib/misc/softrobots.py create mode 100644 stlib/misc/test-1.py create mode 100644 stlib/misc/test2.py diff --git a/stlib/misc/entity.py b/stlib/misc/entity.py new file mode 100644 index 000000000..fbf702439 --- /dev/null +++ b/stlib/misc/entity.py @@ -0,0 +1,188 @@ +from typing import Callable, Optional, overload + +import Sofa +import dataclasses + +def addBidule(self): + return self.addChild("Bidule") + +DEFAULT_VALUE = object() + +def NONE(*args, **kwargs): + pass + +def to_dict(o): + if isinstance(o, dict): + return o + if hasattr(o, "to_dict"): + return o.to_dict() + return {} + +@dataclasses.dataclass +class PrefabParameters(object): + name : str = "Prefab" + kwargs : dict = dataclasses.field(default_factory=dict) + + def __getattr__(self, name: str) : + if name == "__getstate__": + getattr(PrefabParameters, "__getstate__") + if name == "__setstate__": + getattr(PrefabParameters, "__setstate__") + + try: + a = self.__getattribute__(name) + except Exception as e: + return NONE + return a + + def to_dict(self): + return dataclasses.asdict(self) + +@dataclasses.dataclass +class VisualModelParameters(PrefabParameters): + name : str = "VisualModel" + + filename : str = "mesh/sphere_02.obj" + + renderer : dict = dataclasses.field(default_factory=dict) + mapping : dict = dataclasses.field(default_factory=dict) + +class VisualModel(Sofa.Core.Node): + + def __init__(self, parent=None, parameters : VisualModelParameters = VisualModelParameters()): + Sofa.Core.Node.__init__(self, name=parameters.name) + + if parent != None: + parent.addChild(self) + + self.addObject("MeshOBJLoader", name="loader", filename=parameters.filename) + self.addRenderer(**to_dict(parameters.renderer) | {"src" : "@loader"} ) + self.addMapping(**to_dict(parameters.mapping) ) + + def addRenderer(self, **kwargs): + self.addObject("OglModel", name="renderer", **kwargs) + + def addMapping(self, **kwargs): + self.addObject("RigidMapping", name="mapping", **kwargs) + +class CollisionModel(Sofa.Core.BasePrefab): + def __init__(self, params, **kwargs): + Sofa.Core.Node.__init__(self, **kwargs) + + class Parameters(object): + enabled : bool = False + +class MechanicalObject(Sofa.Core.Object): + positions : list[float] + + @dataclasses.dataclass + class Parameters(object): + name : str = "MechanicalObject" + + def to_dict(self): + return dataclasses.asdict(self) + + +@dataclasses.dataclass +class SimulationParameters(PrefabParameters): + name : str = "Simulation" + iterations : Optional[int] = None + template: Optional[str] = None + solver : dict = dataclasses.field(default_factory=dict) + integration : dict = dataclasses.field(default_factory=dict) + + def to_dict(self): + return self.asdict() + +class Simulation(Sofa.Core.Node): + solver : Sofa.Core.Object + integration : Sofa.Core.Object + iterations : int + + def __init__(self, parent : Sofa.Core.Node = None, parameters : SimulationParameters = SimulationParameters()): + Sofa.Core.Node.__init__(self, name=parameters.name) + if parent is not None: + parent.addChild(self) + + if parameters.iterations != NONE and "iterations" in parameters.solver: + raise Exception("Cannot set direct attribute and internal hack... ") + + self.addObject("EulerImplicitSolver", name = "integration", **to_dict(parameters.integration)) + self.addObject("CGLinearSolver", name = "solver", iterations=parameters.iterations, **to_dict(parameters.solver)) + + + +#@dataclasses.dataclass +#class Solver(object): +# integrationscheme : str +# numericalsolver : str + +@dataclasses.dataclass +class EntityParameters(PrefabParameters): + name : str = "Entity" + + addSimulation : Callable = Simulation + addCollisionModel : Callable = CollisionModel + addVisualModel : Callable = VisualModel + + #setConstitutiveLaw # : Callable = addBidule + #setBoundaryCondition #: Callable = addBidule + + mechanical : dict = dataclasses.field(default_factory=dict) + collision : CollisionModel.Parameters = CollisionModel.Parameters() + visual : VisualModelParameters = VisualModelParameters() + simulation : SimulationParameters = SimulationParameters() + +class Entity(Sofa.Core.Node): + # A simulated object + simulation : Simulation + visual : VisualModel + collision : CollisionModel + + parameters : EntityParameters + + def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): + Sofa.Core.Node.__init__(self, name=parameters.name) + + if parent is not None: + parent.addChild(self) + + self.parameters = parameters + + self.addMechanicalModel(**parameters.mechanical) + self.addSimulation(parameters=parameters.simulation) + self.addVisualModel(parameters=parameters.visual) + self.addCollisionModel() + + def addMechanicalModel(self, **kwargs): + self.addObject("MechanicalObject", **kwargs) + + def addSimulation(self, **kwargs): + self.parameters.addSimulation(self, **kwargs) + + def addVisualModel(self, **kwargs): + self.parameters.addVisualModel(self, **kwargs) + + def addCollisionModel(self): + pass + +class Rigid(Entity): + def __init__(self, **kwargs): + Entity.__init__(self, **kwargs) + + +class Deformable(Entity): + def __init__(self, **kwargs): + Entity.__init__(self, **kwargs) + +@dataclasses.dataclass +class DeformableEntityParameters(EntityParameters): + addConstitutiveLaw : Callable = lambda x: x + + mass : Optional[float] = None + + def to_dict(self): + return dataclasses.asdict(self) + + + diff --git a/stlib/misc/entity2.py b/stlib/misc/entity2.py new file mode 100644 index 000000000..2baf73b28 --- /dev/null +++ b/stlib/misc/entity2.py @@ -0,0 +1,12 @@ +from typing import Callable, Optional, overload +import Sofa + +class Parameters(object): + addConstitutiveLaw : Callable + mass = 3.4 + +def Deformable(parent, name="Deformable"): + self = parent.addChild(name) + return self + +Deformable.get_parameters = Parameters \ No newline at end of file diff --git a/stlib/misc/softrobots.py b/stlib/misc/softrobots.py new file mode 100644 index 000000000..d8e7871a7 --- /dev/null +++ b/stlib/misc/softrobots.py @@ -0,0 +1,23 @@ +class SoftRobots: + class Cable(Sofa.Core.BasePrefab): + length : float + + def __init__(self,**kwargs): + pass + + def Parameters(object): + lenght : float + +class Trunk(Sofa.Core.BasePrefab): + body : Entity.Deformable + cables : list [SoftRobots.Cable] + + def __init__(self, params): + body = Entity.Deformable() + + for param in range(params.cables): + cables.append(SoftRobots.Cable(body, param)) + + class Parameters(object): + body : Entity.Deformable.Parameters + cables : list[SoftRobots.Cable.Parameters] diff --git a/stlib/misc/test-1.py b/stlib/misc/test-1.py new file mode 100644 index 000000000..289f1ff02 --- /dev/null +++ b/stlib/misc/test-1.py @@ -0,0 +1,53 @@ +from typing import Callable, Optional, overload +import Sofa.Core + + +import entity +import entity2 + +# Monkey patch for demonstration purpose +def newAdd(self, creator, **kwargs): + if callable(creator): + creator(parent=self, **kwargs) +Sofa.Core.Node.add = newAdd + +def createScene(root): + #root.add(entity.Deformable) + #root.addChild(entity2.Deformable(root)) + + params = entity.Deformable.Parameters() + params.name = "Deformable2" + root.add(entity.Deformable, params=auto_load) + + #def addCustomVisual(self, **kwargs): + # Rigid.addVisualModel( mapping={"toto":"in"} ) + + #params = Entity.Parameters() + #params.addVisualModel = addCustomVisual + #root.add(Entity, params) + + #  + #params = Rigid.new_parameters() + #params.mass = 4.5 + #root.add(Entity, params) + #root.add(Entity) + + #params.addVisualModelOverride = addCustomVisual + + ###  + #Entity._addVisualModel = addCustomVisual + #root.add(Entity, params) + + #root.add(Entity.Rigid) + #root.add(Entity.Deformable) + + #root.add(Entity) + #root.add(VisualModel, params) + + #root.add(VisualModel) + + #params = Entity.Deformable.Parameters() + #params.visual = None + #a = root.add(Entity.Deformable, params) + + return root \ No newline at end of file diff --git a/stlib/misc/test2.py b/stlib/misc/test2.py new file mode 100644 index 000000000..4dc12d147 --- /dev/null +++ b/stlib/misc/test2.py @@ -0,0 +1,65 @@ +import Sofa.Core +import copy +import entity +from entity import PrefabParameters, EntityParameters, Entity, Simulation + + +oldAdd=Sofa.Core.Node.addObject +def myAddObject(self : Sofa.Core.Node, tname, **kwargs): + kwargs = copy.copy(kwargs) + previouslen = len(self.objects) + try: + oldAdd(self, tname, **kwargs) + except Exception as e: + target = self + if len(self.objects) != previouslen: + target = list(self.objects)[-1] + Sofa.msg_error(target, str(e)) + +Sofa.Core.Node.addObject = myAddObject + + +def myAdd(self : Sofa.Core.Node, c, params = PrefabParameters(), **kwargs): + def findName(cname, node): + """Compute a working unique name in the node""" + rname = cname + for i in range(0, len(node.children)): + if rname not in node.children: + return rname + rname = cname + str(i+1) + return rname + + for k,v in kwargs.items(): + if hasattr(params, k): + setattr(params, k, v) + + params = copy.copy(params) + if params.name in self.children: + params.name = findName(params.name, self) + + return c(parent = self, parameters=params) +Sofa.Core.Node.add = myAdd + +def createScene(root): + #@optionalkwargs + + #def eulalieAddOde(self, **kwargs): + # self.addObject("EulerExplicitSolver", name="numericalintegration") + # self.addObject("LinearSolver", name="numericalsolver", firstOrder=True) + + params = EntityParameters() + params.simulation.iterations = 10 + params.simulation.integration["rayleighStiffness"] = 2.0 + params.addSimulation = entity.NONE + + params.mechanical["template"] = "Rigid3" + + #params.simulation.integration["rayleightStiffnessXXX"] = 2.0 + + #params.solver.kwargs["numericalintegration"] = { "firstOrder" : True } + + root.add(Entity, params) + root.add(Entity, params) + root.add(Entity, params) + + #root.add(Simulation, name="mySimulation") From 10082bfeaf5132e01866bc9b3f7f2529de4e2b99 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 10:29:32 +0200 Subject: [PATCH 04/49] Created example scenes --- stlib/examples/PrefabScene_beginner.py | 35 ++++++++++++++++++++++ stlib/examples/PrefabScene_expert.py | 0 stlib/examples/PrefabScene_intermediate.py | 0 stlib/examples/helper.py | 23 ++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 stlib/examples/PrefabScene_beginner.py create mode 100644 stlib/examples/PrefabScene_expert.py create mode 100644 stlib/examples/PrefabScene_intermediate.py create mode 100644 stlib/examples/helper.py diff --git a/stlib/examples/PrefabScene_beginner.py b/stlib/examples/PrefabScene_beginner.py new file mode 100644 index 000000000..bbace5a0a --- /dev/null +++ b/stlib/examples/PrefabScene_beginner.py @@ -0,0 +1,35 @@ + +class exportScene(): + def __init__(self,name="rootNode"): + self.name = name + + def addObject(self,type:str,**kwargs): + suffix = "" + for i in kwargs: + suffix += "," + str(i) + "=\"" + str(kwargs[i]) + "\"" + print(self.name+".addObject(\"" + type + "\"" + suffix + ")") + + def addChild(self,name:str): + print(name + '=' + self.name+".addChild(\"" + name + "\")") + setattr(self,name,exportScene(name)) + return getattr(self,name) + + def __setattr__(self, key, value): + if(not(key == "name")): + print(self.__dict__["name"] + "." + key + " = " + str(value)) + self.__dict__[key] = value + else: + self.__dict__[key] = value + + + +def createScene(root): + + + return root + + +if __name__=="__main__": + + + pass \ No newline at end of file diff --git a/stlib/examples/PrefabScene_expert.py b/stlib/examples/PrefabScene_expert.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/examples/PrefabScene_intermediate.py b/stlib/examples/PrefabScene_intermediate.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/examples/helper.py b/stlib/examples/helper.py new file mode 100644 index 000000000..da5dc5205 --- /dev/null +++ b/stlib/examples/helper.py @@ -0,0 +1,23 @@ + + +class exportScene(): + def __init__(self,name="rootNode"): + self.name = name + + def addObject(self,type:str,**kwargs): + suffix = "" + for i in kwargs: + suffix += "," + str(i) + "=\"" + str(kwargs[i]) + "\"" + print(self.name+".addObject(\"" + type + "\"" + suffix + ")") + + def addChild(self,name:str): + print(name + '=' + self.name+".addChild(\"" + name + "\")") + setattr(self,name,exportScene(name)) + return getattr(self,name) + + def __setattr__(self, key, value): + if(not(key == "name")): + print(self.__dict__["name"] + "." + key + " = " + str(value)) + self.__dict__[key] = value + else: + self.__dict__[key] = value From d167479f0ba36ad33366135d558219cf5fbc07d9 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 10:33:01 +0200 Subject: [PATCH 05/49] Fix example --- stlib/examples/PrefabScene_beginner.py | 35 ++++++-------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/stlib/examples/PrefabScene_beginner.py b/stlib/examples/PrefabScene_beginner.py index bbace5a0a..11a16a2df 100644 --- a/stlib/examples/PrefabScene_beginner.py +++ b/stlib/examples/PrefabScene_beginner.py @@ -1,35 +1,14 @@ - -class exportScene(): - def __init__(self,name="rootNode"): - self.name = name - - def addObject(self,type:str,**kwargs): - suffix = "" - for i in kwargs: - suffix += "," + str(i) + "=\"" + str(kwargs[i]) + "\"" - print(self.name+".addObject(\"" + type + "\"" + suffix + ")") - - def addChild(self,name:str): - print(name + '=' + self.name+".addChild(\"" + name + "\")") - setattr(self,name,exportScene(name)) - return getattr(self,name) - - def __setattr__(self, key, value): - if(not(key == "name")): - print(self.__dict__["name"] + "." + key + " = " + str(value)) - self.__dict__[key] = value - else: - self.__dict__[key] = value - +from helper import exportScene def createScene(root): - - return root + + return root if __name__=="__main__": - - - pass \ No newline at end of file + root = exportScene() + createScene(root) + + pass \ No newline at end of file From dd9cc34a371809d5e738dd98721b3bd36af599ac Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Mon, 14 Apr 2025 10:52:31 +0200 Subject: [PATCH 06/49] orga folders --- stlib/__init__.py | 5 +---- stlib/examples/PrefabScene_beginner.py | 4 ++-- stlib/misc/entity2.py | 12 ------------ 3 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 stlib/misc/entity2.py diff --git a/stlib/__init__.py b/stlib/__init__.py index 34e9c316c..723fc1b46 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1,4 +1 @@ -"""SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it""" -raise Exception("SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it") - - +__all__ = ["misc"] diff --git a/stlib/examples/PrefabScene_beginner.py b/stlib/examples/PrefabScene_beginner.py index 11a16a2df..d28b98e7d 100644 --- a/stlib/examples/PrefabScene_beginner.py +++ b/stlib/examples/PrefabScene_beginner.py @@ -1,8 +1,9 @@ from helper import exportScene +from stlib.misc.entity import Entity def createScene(root): - + Entity.Deformable(Mesh,MechaProperties) return root @@ -10,5 +11,4 @@ def createScene(root): if __name__=="__main__": root = exportScene() createScene(root) - pass \ No newline at end of file diff --git a/stlib/misc/entity2.py b/stlib/misc/entity2.py deleted file mode 100644 index 2baf73b28..000000000 --- a/stlib/misc/entity2.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Callable, Optional, overload -import Sofa - -class Parameters(object): - addConstitutiveLaw : Callable - mass = 3.4 - -def Deformable(parent, name="Deformable"): - self = parent.addChild(name) - return self - -Deformable.get_parameters = Parameters \ No newline at end of file From 7589706e87289f0adb98a5210e512497b1350a21 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 10:55:02 +0200 Subject: [PATCH 07/49] Fix package --- splib/Testing.py | 38 +---------------------------- splib/__init__.py | 2 +- {stlib/examples => splib}/helper.py | 12 +++++++++ 3 files changed, 14 insertions(+), 38 deletions(-) rename {stlib/examples => splib}/helper.py (66%) diff --git a/splib/Testing.py b/splib/Testing.py index 9347f8c2d..fc31093cf 100644 --- a/splib/Testing.py +++ b/splib/Testing.py @@ -9,45 +9,9 @@ from splib.core.node_wrapper import * from splib.core.node_wrapper import ChildWrapper, ObjectWrapper +from splib.helper import exportScene -class displayNode(): - def __init__(self,_level=0): - self.prefix = "" - for i in range(_level): - self.prefix += "| " - - def addObject(self,type:str,**kwargs): - print(self.prefix + type + " with " + str(kwargs)) - - def addChild(self,name:str): - print(self.prefix + "-> Node : " + name) - return displayNode(len(self.prefix) + 1) - -class exportScene(): - def __init__(self,name="rootNode"): - self.name = name - - def addObject(self,type:str,**kwargs): - suffix = "" - for i in kwargs: - suffix += "," + str(i) + "=\"" + str(kwargs[i]) + "\"" - print(self.name+".addObject(\"" + type + "\"" + suffix + ")") - - def addChild(self,name:str): - print(name + '=' + self.name+".addChild(\"" + name + "\")") - setattr(self,name,exportScene(name)) - return getattr(self,name) - - def __setattr__(self, key, value): - if(not(key == "name")): - print(self.__dict__["name"] + "." + key + " = " + str(value)) - self.__dict__[key] = value - else: - self.__dict__[key] = value - - -@PrefabSimulation def createScene(rootNode): rootNode.dt = 0.03 rootNode.gravity = [0,-9.81,0] diff --git a/splib/__init__.py b/splib/__init__.py index a04a5119e..6b380c11a 100644 --- a/splib/__init__.py +++ b/splib/__init__.py @@ -1,2 +1,2 @@ -__all__ = ["core","topology", "simulation","modeler","mechanics","Testing"] +__all__ = ["core","topology", "simulation","modeler","mechanics","Testing","helper"] diff --git a/stlib/examples/helper.py b/splib/helper.py similarity index 66% rename from stlib/examples/helper.py rename to splib/helper.py index da5dc5205..b4881af5f 100644 --- a/stlib/examples/helper.py +++ b/splib/helper.py @@ -1,3 +1,15 @@ +class displayNode(): + def __init__(self,_level=0): + self.prefix = "" + for i in range(_level): + self.prefix += "| " + + def addObject(self,type:str,**kwargs): + print(self.prefix + type + " with " + str(kwargs)) + + def addChild(self,name:str): + print(self.prefix + "-> Node : " + name) + return displayNode(len(self.prefix) + 1) class exportScene(): From 2f844d71d00f8be6765904802347ced5298f2160 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 10:57:28 +0200 Subject: [PATCH 08/49] bootstrap examples --- stlib/examples/PrefabScene_beginner.py | 2 +- stlib/examples/PrefabScene_expert.py | 14 ++++++++++++++ stlib/examples/PrefabScene_intermediate.py | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/stlib/examples/PrefabScene_beginner.py b/stlib/examples/PrefabScene_beginner.py index d28b98e7d..0bc427d37 100644 --- a/stlib/examples/PrefabScene_beginner.py +++ b/stlib/examples/PrefabScene_beginner.py @@ -1,4 +1,4 @@ -from helper import exportScene +from splib.helper import exportScene from stlib.misc.entity import Entity diff --git a/stlib/examples/PrefabScene_expert.py b/stlib/examples/PrefabScene_expert.py index e69de29bb..0bc427d37 100644 --- a/stlib/examples/PrefabScene_expert.py +++ b/stlib/examples/PrefabScene_expert.py @@ -0,0 +1,14 @@ +from splib.helper import exportScene +from stlib.misc.entity import Entity + + +def createScene(root): + Entity.Deformable(Mesh,MechaProperties) + + return root + + +if __name__=="__main__": + root = exportScene() + createScene(root) + pass \ No newline at end of file diff --git a/stlib/examples/PrefabScene_intermediate.py b/stlib/examples/PrefabScene_intermediate.py index e69de29bb..0bc427d37 100644 --- a/stlib/examples/PrefabScene_intermediate.py +++ b/stlib/examples/PrefabScene_intermediate.py @@ -0,0 +1,14 @@ +from splib.helper import exportScene +from stlib.misc.entity import Entity + + +def createScene(root): + Entity.Deformable(Mesh,MechaProperties) + + return root + + +if __name__=="__main__": + root = exportScene() + createScene(root) + pass \ No newline at end of file From 2c55e7dc4fab0e8b184d75a73dc845e6131f1ff7 Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Mon, 14 Apr 2025 11:28:18 +0200 Subject: [PATCH 09/49] start beginner scene --- stlib/examples/PrefabScene_beginner.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stlib/examples/PrefabScene_beginner.py b/stlib/examples/PrefabScene_beginner.py index 0bc427d37..7cd4312c0 100644 --- a/stlib/examples/PrefabScene_beginner.py +++ b/stlib/examples/PrefabScene_beginner.py @@ -3,8 +3,10 @@ def createScene(root): - Entity.Deformable(Mesh,MechaProperties) - + params = Entity.Deformable.Parameters() + params.name = "Logo" + params.topology.filename="share/mesh/SOFA-logo.obj" + root.add(Entity.Deformable, params=params) return root From 9b9e057fa9edfaa8da64dc710e471e7378bc232e Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 11:31:18 +0200 Subject: [PATCH 10/49] Move around files --- stlib/core/entity.py | 57 ++++++++++++++++++++++++++ stlib/core/parameters.py | 24 +++++++++++ stlib/core/visual_model.py | 32 +++++++++++++++ stlib/examples/PrefabScene_beginner.py | 6 +-- stlib/simulated/deformable.py | 17 ++++++++ stlib/simulated/rigid.py | 0 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 stlib/core/entity.py create mode 100644 stlib/core/parameters.py create mode 100644 stlib/core/visual_model.py create mode 100644 stlib/simulated/deformable.py create mode 100644 stlib/simulated/rigid.py diff --git a/stlib/core/entity.py b/stlib/core/entity.py new file mode 100644 index 000000000..ef756dc8f --- /dev/null +++ b/stlib/core/entity.py @@ -0,0 +1,57 @@ +from stlib.core.parameters import EntityParameters +import dataclasses +from typing import Callable, Optional, overload +import Sofa + + + +class Entity(Sofa.Core.BasePrefab): + + # A simulated object + simulation : Simulation + visual : VisualModel + collision : CollisionModel + + parameters : EntityParameters + + @dataclasses.dataclass + class Parameters(EntityParameters): + name : str = "Entity" + + addSimulation : Callable = Simulation + addCollisionModel : Callable = CollisionModel + addVisualModel : Callable = VisualModel + + #setConstitutiveLaw # : Callable = addBidule + #setBoundaryCondition #: Callable = addBidule + + mechanical : dict = dataclasses.field(default_factory=dict) + collision : CollisionModel.Parameters = CollisionModel.Parameters() + visual : VisualModelParameters = VisualModelParameters() + simulation : SimulationParameters = SimulationParameters() + + + def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): + Sofa.Core.Node.__init__(self, name=parameters.name) + + if parent is not None: + parent.addChild(self) + + self.parameters = parameters + + self.addMechanicalModel(**parameters.mechanical) + self.addSimulation(parameters=parameters.simulation) + self.addVisualModel(parameters=parameters.visual) + self.addCollisionModel() + + def addMechanicalModel(self, **kwargs): + self.addObject("MechanicalObject", **kwargs) + + def addSimulation(self, **kwargs): + self.parameters.addSimulation(self, **kwargs) + + def addVisualModel(self, **kwargs): + self.parameters.addVisualModel(self, **kwargs) + + def addCollisionModel(self): + pass diff --git a/stlib/core/parameters.py b/stlib/core/parameters.py new file mode 100644 index 000000000..b1df61d12 --- /dev/null +++ b/stlib/core/parameters.py @@ -0,0 +1,24 @@ +import dataclasses +from splib.core.utils import DEFAULT_VALUE + +#defaultValueType + +@dataclasses.dataclass +class PrefabParameters(object): + name : str = "Prefab" + kwargs : dict = dataclasses.field(default_factory=dict) + + def __getattr__(self, name: str) : + if name == "__getstate__": + getattr(PrefabParameters, "__getstate__") + if name == "__setstate__": + getattr(PrefabParameters, "__setstate__") + + try: + a = self.__getattribute__(name) + except Exception as e: + return DEFAULT_VALUE + return a + + def to_dict(self): + return dataclasses.asdict(self) diff --git a/stlib/core/visual_model.py b/stlib/core/visual_model.py new file mode 100644 index 000000000..7081c9b2c --- /dev/null +++ b/stlib/core/visual_model.py @@ -0,0 +1,32 @@ +from stlib.core.parameters import EntityParameters +import dataclasses +import Sofa + +class VisualModel(Sofa.Core.BasePrefab): + @dataclasses.dataclass + class Parameters(EntityParameters): + name : str = "VisualModel" + + filename : str = "mesh/sphere_02.obj" + + renderer : dict = dataclasses.field(default_factory=dict) + mapping : dict = dataclasses.field(default_factory=dict) + + + + def __init__(self, parent=None, parameters : VisualModelParameters = VisualModelParameters()): + Sofa.Core.Node.__init__(self, name=parameters.name) + + if parent != None: + parent.addChild(self) + + self.addObject("MeshOBJLoader", name="loader", filename=parameters.filename) + self.addRenderer(**to_dict(parameters.renderer) | {"src" : "@loader"} ) + self.addMapping(**to_dict(parameters.mapping) ) + + def addRenderer(self, **kwargs): + self.addObject("OglModel", name="renderer", **kwargs) + + def addMapping(self, **kwargs): + self.addObject("RigidMapping", name="mapping", **kwargs) + \ No newline at end of file diff --git a/stlib/examples/PrefabScene_beginner.py b/stlib/examples/PrefabScene_beginner.py index 7cd4312c0..a507bd529 100644 --- a/stlib/examples/PrefabScene_beginner.py +++ b/stlib/examples/PrefabScene_beginner.py @@ -1,12 +1,12 @@ from splib.helper import exportScene -from stlib.misc.entity import Entity +from stlib.simulated.deformable import Deformable def createScene(root): - params = Entity.Deformable.Parameters() + params = Deformable.Parameters() params.name = "Logo" params.topology.filename="share/mesh/SOFA-logo.obj" - root.add(Entity.Deformable, params=params) + root.add(Deformable, params=params) return root diff --git a/stlib/simulated/deformable.py b/stlib/simulated/deformable.py new file mode 100644 index 000000000..2c7d43c97 --- /dev/null +++ b/stlib/simulated/deformable.py @@ -0,0 +1,17 @@ +from stlib.core.entity import Entity +from stlib.core.parameters import EntityParameters +import dataclasses + +class Deformable(Entity): + @dataclasses.dataclass + class Parameters(EntityParameters): + addConstitutiveLaw : Callable = lambda x: x + + mass : Optional[float] = None + + def to_dict(self): + return dataclasses.asdict(self) + + def __init__(self, **kwargs): + Entity.__init__(self, **kwargs) + diff --git a/stlib/simulated/rigid.py b/stlib/simulated/rigid.py new file mode 100644 index 000000000..e69de29bb From b989159330158c37036660aaad84ff4a3e247589 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 11:32:57 +0200 Subject: [PATCH 11/49] Move examples into SofaPython3 main example folder --- {stlib/examples => examples/stlib}/PrefabScene_beginner.py | 0 {stlib/examples => examples/stlib}/PrefabScene_expert.py | 0 {stlib/examples => examples/stlib}/PrefabScene_intermediate.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {stlib/examples => examples/stlib}/PrefabScene_beginner.py (100%) rename {stlib/examples => examples/stlib}/PrefabScene_expert.py (100%) rename {stlib/examples => examples/stlib}/PrefabScene_intermediate.py (100%) diff --git a/stlib/examples/PrefabScene_beginner.py b/examples/stlib/PrefabScene_beginner.py similarity index 100% rename from stlib/examples/PrefabScene_beginner.py rename to examples/stlib/PrefabScene_beginner.py diff --git a/stlib/examples/PrefabScene_expert.py b/examples/stlib/PrefabScene_expert.py similarity index 100% rename from stlib/examples/PrefabScene_expert.py rename to examples/stlib/PrefabScene_expert.py diff --git a/stlib/examples/PrefabScene_intermediate.py b/examples/stlib/PrefabScene_intermediate.py similarity index 100% rename from stlib/examples/PrefabScene_intermediate.py rename to examples/stlib/PrefabScene_intermediate.py From b42833b2bd9023a3de3857aac392d1940b6789b1 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 11:35:27 +0200 Subject: [PATCH 12/49] Add unsaved modifications --- stlib/__init__.py | 2 +- stlib/core/parameters.py | 2 +- stlib/simulated/deformable.py | 1 + stlib/simulated/rigid.py | 9 +++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/stlib/__init__.py b/stlib/__init__.py index 723fc1b46..d3aa4e81c 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1 +1 @@ -__all__ = ["misc"] +__all__ = ["core","simulated"] diff --git a/stlib/core/parameters.py b/stlib/core/parameters.py index b1df61d12..55cd025fe 100644 --- a/stlib/core/parameters.py +++ b/stlib/core/parameters.py @@ -4,7 +4,7 @@ #defaultValueType @dataclasses.dataclass -class PrefabParameters(object): +class EntityParameters(object): name : str = "Prefab" kwargs : dict = dataclasses.field(default_factory=dict) diff --git a/stlib/simulated/deformable.py b/stlib/simulated/deformable.py index 2c7d43c97..933c8806a 100644 --- a/stlib/simulated/deformable.py +++ b/stlib/simulated/deformable.py @@ -1,5 +1,6 @@ from stlib.core.entity import Entity from stlib.core.parameters import EntityParameters + import dataclasses class Deformable(Entity): diff --git a/stlib/simulated/rigid.py b/stlib/simulated/rigid.py index e69de29bb..f830eab39 100644 --- a/stlib/simulated/rigid.py +++ b/stlib/simulated/rigid.py @@ -0,0 +1,9 @@ +from stlib.core.entity import Entity +from stlib.core.parameters import EntityParameters + +import dataclasses + + +class Rigid(Entity): + def __init__(self, **kwargs): + Entity.__init__(self, **kwargs) From fa0bd7429d9feaac04a2d98f4afb7a2759461227 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Mon, 14 Apr 2025 13:47:30 +0200 Subject: [PATCH 13/49] reoganisation after discussion --- stlib/core/base_entity.py | 9 +++++++++ stlib/core/{parameters.py => base_parameters.py} | 12 +++++++----- stlib/core/base_prefab.py | 10 ++++++++++ stlib/{core/entity.py => entities/__entity__.py} | 3 +-- stlib/entities/__init__.py | 0 .../deformable/__deformable__.py} | 0 stlib/entities/deformable/__init__.py | 0 stlib/entities/deformable/parameters.py | 0 stlib/entities/rigid/__init__.py | 0 .../rigid.py => entities/rigid/__rigid__.py} | 0 stlib/entities/rigid/parameters.py | 0 stlib/{core/visual_model.py => prefabs/visual.py} | 0 stlib/shapes/__init__.py | 0 stlib/shapes/cube.py | 0 stlib/shapes/sphere.py | 0 15 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 stlib/core/base_entity.py rename stlib/core/{parameters.py => base_parameters.py} (67%) create mode 100644 stlib/core/base_prefab.py rename stlib/{core/entity.py => entities/__entity__.py} (97%) create mode 100644 stlib/entities/__init__.py rename stlib/{simulated/deformable.py => entities/deformable/__deformable__.py} (100%) create mode 100644 stlib/entities/deformable/__init__.py create mode 100644 stlib/entities/deformable/parameters.py create mode 100644 stlib/entities/rigid/__init__.py rename stlib/{simulated/rigid.py => entities/rigid/__rigid__.py} (100%) create mode 100644 stlib/entities/rigid/parameters.py rename stlib/{core/visual_model.py => prefabs/visual.py} (100%) create mode 100644 stlib/shapes/__init__.py create mode 100644 stlib/shapes/cube.py create mode 100644 stlib/shapes/sphere.py diff --git a/stlib/core/base_entity.py b/stlib/core/base_entity.py new file mode 100644 index 000000000..a9be8df3a --- /dev/null +++ b/stlib/core/base_entity.py @@ -0,0 +1,9 @@ +import Sofa.Core +from base_parameters import BaseParameters + +class BaseEntity(Sofa.Core.Prefab): + + parameters : BaseParameters + + def __init__(self): + Sofa.Core.Prefab.__init__(self) diff --git a/stlib/core/parameters.py b/stlib/core/base_parameters.py similarity index 67% rename from stlib/core/parameters.py rename to stlib/core/base_parameters.py index 55cd025fe..d1257b4bb 100644 --- a/stlib/core/parameters.py +++ b/stlib/core/base_parameters.py @@ -1,18 +1,20 @@ import dataclasses from splib.core.utils import DEFAULT_VALUE -#defaultValueType +from stlib.shapes import Shape + @dataclasses.dataclass -class EntityParameters(object): - name : str = "Prefab" +class BaseParameters(object): + name : str = "Entity" + shape : Shape = Shape() kwargs : dict = dataclasses.field(default_factory=dict) def __getattr__(self, name: str) : if name == "__getstate__": - getattr(PrefabParameters, "__getstate__") + getattr(BaseParameters, "__getstate__") if name == "__setstate__": - getattr(PrefabParameters, "__setstate__") + getattr(BaseParameters, "__setstate__") try: a = self.__getattribute__(name) diff --git a/stlib/core/base_prefab.py b/stlib/core/base_prefab.py new file mode 100644 index 000000000..067bc5bae --- /dev/null +++ b/stlib/core/base_prefab.py @@ -0,0 +1,10 @@ +import Sofa.Core + + +class BasePrefab(Sofa.Core.Prefab): + """ + A Prefab is a Sofa.Node that assembles a set of components and nodes + """ + + def __init__(self): + Sofa.Core.Prefab.__init__(self) \ No newline at end of file diff --git a/stlib/core/entity.py b/stlib/entities/__entity__.py similarity index 97% rename from stlib/core/entity.py rename to stlib/entities/__entity__.py index ef756dc8f..8d2dd6f55 100644 --- a/stlib/core/entity.py +++ b/stlib/entities/__entity__.py @@ -4,8 +4,7 @@ import Sofa - -class Entity(Sofa.Core.BasePrefab): +class Entity(Sofa.Core.BaseEntity): # A simulated object simulation : Simulation diff --git a/stlib/entities/__init__.py b/stlib/entities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/simulated/deformable.py b/stlib/entities/deformable/__deformable__.py similarity index 100% rename from stlib/simulated/deformable.py rename to stlib/entities/deformable/__deformable__.py diff --git a/stlib/entities/deformable/__init__.py b/stlib/entities/deformable/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/entities/deformable/parameters.py b/stlib/entities/deformable/parameters.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/entities/rigid/__init__.py b/stlib/entities/rigid/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/simulated/rigid.py b/stlib/entities/rigid/__rigid__.py similarity index 100% rename from stlib/simulated/rigid.py rename to stlib/entities/rigid/__rigid__.py diff --git a/stlib/entities/rigid/parameters.py b/stlib/entities/rigid/parameters.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/core/visual_model.py b/stlib/prefabs/visual.py similarity index 100% rename from stlib/core/visual_model.py rename to stlib/prefabs/visual.py diff --git a/stlib/shapes/__init__.py b/stlib/shapes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/shapes/cube.py b/stlib/shapes/cube.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/shapes/sphere.py b/stlib/shapes/sphere.py new file mode 100644 index 000000000..e69de29bb From 4380356e636425a4f1fe7047f4d7cd53798a61f4 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 15:18:53 +0200 Subject: [PATCH 14/49] Add gemetries --- examples/stlib/PrefabScene_beginner.py | 12 ++------ stlib/core/{base_entity.py => baseEntity.py} | 0 .../parameters.py => core/baseGeometry.py} | 0 stlib/core/baseParameters.py | 13 +++++++++ stlib/core/basePrefab.py | 11 +++++++ stlib/core/base_parameters.py | 26 ----------------- stlib/core/base_prefab.py | 10 ------- stlib/entities/deformable/__deformable__.py | 20 +++++-------- stlib/entities/deformable/__parameters__.py | 12 ++++++++ stlib/geometry/__geometry__.py | 29 +++++++++++++++++++ stlib/{shapes => geometry}/__init__.py | 0 stlib/{shapes => geometry}/cube.py | 0 stlib/{shapes => geometry}/sphere.py | 0 13 files changed, 75 insertions(+), 58 deletions(-) rename stlib/core/{base_entity.py => baseEntity.py} (100%) rename stlib/{entities/deformable/parameters.py => core/baseGeometry.py} (100%) create mode 100644 stlib/core/baseParameters.py create mode 100644 stlib/core/basePrefab.py delete mode 100644 stlib/core/base_parameters.py delete mode 100644 stlib/core/base_prefab.py create mode 100644 stlib/entities/deformable/__parameters__.py create mode 100644 stlib/geometry/__geometry__.py rename stlib/{shapes => geometry}/__init__.py (100%) rename stlib/{shapes => geometry}/cube.py (100%) rename stlib/{shapes => geometry}/sphere.py (100%) diff --git a/examples/stlib/PrefabScene_beginner.py b/examples/stlib/PrefabScene_beginner.py index a507bd529..2c95cc013 100644 --- a/examples/stlib/PrefabScene_beginner.py +++ b/examples/stlib/PrefabScene_beginner.py @@ -1,16 +1,8 @@ -from splib.helper import exportScene -from stlib.simulated.deformable import Deformable - +from stlib.entities.deformable import Deformable def createScene(root): - params = Deformable.Parameters() + params = Deformable.getParameters() params.name = "Logo" params.topology.filename="share/mesh/SOFA-logo.obj" root.add(Deformable, params=params) return root - - -if __name__=="__main__": - root = exportScene() - createScene(root) - pass \ No newline at end of file diff --git a/stlib/core/base_entity.py b/stlib/core/baseEntity.py similarity index 100% rename from stlib/core/base_entity.py rename to stlib/core/baseEntity.py diff --git a/stlib/entities/deformable/parameters.py b/stlib/core/baseGeometry.py similarity index 100% rename from stlib/entities/deformable/parameters.py rename to stlib/core/baseGeometry.py diff --git a/stlib/core/baseParameters.py b/stlib/core/baseParameters.py new file mode 100644 index 000000000..5d9a66a02 --- /dev/null +++ b/stlib/core/baseParameters.py @@ -0,0 +1,13 @@ +import dataclasses +from splib.core.utils import DEFAULT_VALUE + +import dataclasses +from types import Callable, Optional + +@dataclasses.dataclass +class BaseParameters(object): + name : str = "Entity" + kwargs : dict = dataclasses.field(default_factory=dict) + + def toDict(self): + return dataclasses.asdict(self) diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py new file mode 100644 index 000000000..22652f744 --- /dev/null +++ b/stlib/core/basePrefab.py @@ -0,0 +1,11 @@ +import Sofa.Core +from baseParameters import BaseParameters + + +class BasePrefab(Sofa.Core.Prefab): + """ + A Prefab is a Sofa.Node that assembles a set of components and nodes + """ + + def __init__(self, params: BaseParameters): + Sofa.Core.Prefab.__init__(self, name=params.name) \ No newline at end of file diff --git a/stlib/core/base_parameters.py b/stlib/core/base_parameters.py deleted file mode 100644 index d1257b4bb..000000000 --- a/stlib/core/base_parameters.py +++ /dev/null @@ -1,26 +0,0 @@ -import dataclasses -from splib.core.utils import DEFAULT_VALUE - -from stlib.shapes import Shape - - -@dataclasses.dataclass -class BaseParameters(object): - name : str = "Entity" - shape : Shape = Shape() - kwargs : dict = dataclasses.field(default_factory=dict) - - def __getattr__(self, name: str) : - if name == "__getstate__": - getattr(BaseParameters, "__getstate__") - if name == "__setstate__": - getattr(BaseParameters, "__setstate__") - - try: - a = self.__getattribute__(name) - except Exception as e: - return DEFAULT_VALUE - return a - - def to_dict(self): - return dataclasses.asdict(self) diff --git a/stlib/core/base_prefab.py b/stlib/core/base_prefab.py deleted file mode 100644 index 067bc5bae..000000000 --- a/stlib/core/base_prefab.py +++ /dev/null @@ -1,10 +0,0 @@ -import Sofa.Core - - -class BasePrefab(Sofa.Core.Prefab): - """ - A Prefab is a Sofa.Node that assembles a set of components and nodes - """ - - def __init__(self): - Sofa.Core.Prefab.__init__(self) \ No newline at end of file diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 933c8806a..1e91f84d0 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -1,18 +1,14 @@ -from stlib.core.entity import Entity -from stlib.core.parameters import EntityParameters +from stlib.entities import Entity +from stlib.entities.deformable.__parameters__ import DeformableParameters -import dataclasses class Deformable(Entity): - @dataclasses.dataclass - class Parameters(EntityParameters): - addConstitutiveLaw : Callable = lambda x: x + + @staticmethod + def getParameters() -> DeformableParameters: + return DeformableParameters() + - mass : Optional[float] = None - - def to_dict(self): - return dataclasses.asdict(self) - - def __init__(self, **kwargs): + def __init__(self, params : DeformableParameters, **kwargs): Entity.__init__(self, **kwargs) diff --git a/stlib/entities/deformable/__parameters__.py b/stlib/entities/deformable/__parameters__.py new file mode 100644 index 000000000..e7126b159 --- /dev/null +++ b/stlib/entities/deformable/__parameters__.py @@ -0,0 +1,12 @@ + +import dataclasses +from stlib.core.base_parameters import BaseParameters + +@dataclasses.dataclass +class DeformableParameters(BaseParameters): + addConstitutiveLaw : Callable = lambda x: x + + mass : Optional[float] = None + + def to_dict(self): + return dataclasses.asdict(self) diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py new file mode 100644 index 000000000..f4af08050 --- /dev/null +++ b/stlib/geometry/__geometry__.py @@ -0,0 +1,29 @@ +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters, Optional +from splib.topology.loader import loadMesh +from splib.core.enum_types import ElementType +from Sofa.Core import Object + +class Geometry(BasePrefab): + topology : Object # This should be more specialized into the right SOFA type + topologyModifier : Optional[Object] + + def __init__(self, *args, **kwargs): + BasePrefab.__init__(self, *args, **kwargs) + + +class GeometryParameter(BaseParameters): + isDynamic : bool + elementType : Optional[ElementType] + + +class FileGeometryParamtetrs(GeometryParameter): + filename : str + + +class FileGeometry(Geometry): + fileLoader : Object + + def __init__(self): + pass + \ No newline at end of file diff --git a/stlib/shapes/__init__.py b/stlib/geometry/__init__.py similarity index 100% rename from stlib/shapes/__init__.py rename to stlib/geometry/__init__.py diff --git a/stlib/shapes/cube.py b/stlib/geometry/cube.py similarity index 100% rename from stlib/shapes/cube.py rename to stlib/geometry/cube.py diff --git a/stlib/shapes/sphere.py b/stlib/geometry/sphere.py similarity index 100% rename from stlib/shapes/sphere.py rename to stlib/geometry/sphere.py From ba978cea1f4420311daed5798036ed7e5c821ba4 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 15:41:39 +0200 Subject: [PATCH 15/49] WIP FileParameters --- stlib/entities/__entity__.py | 35 +++++++++-------- stlib/geometry/__geometry__.py | 71 ++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index 8d2dd6f55..78a2487b1 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -1,8 +1,25 @@ -from stlib.core.parameters import EntityParameters +from stlib.core.baseParameters import BaseParameters import dataclasses from typing import Callable, Optional, overload import Sofa +@dataclasses.dataclass +class EntityParameters(BaseParameters): + name : str = "Entity" + + addSimulation : Callable = Simulation + addCollisionModel : Callable = CollisionModel + addVisualModel : Callable = VisualModel + + #setConstitutiveLaw # : Callable = addBidule + #setBoundaryCondition #: Callable = addBidule + + mechanical : dict = dataclasses.field(default_factory=dict) + collision : CollisionModel.Parameters = CollisionModel.Parameters() + visual : VisualModelParameters = VisualModelParameters() + simulation : SimulationParameters = SimulationParameters() + + class Entity(Sofa.Core.BaseEntity): @@ -13,22 +30,6 @@ class Entity(Sofa.Core.BaseEntity): parameters : EntityParameters - @dataclasses.dataclass - class Parameters(EntityParameters): - name : str = "Entity" - - addSimulation : Callable = Simulation - addCollisionModel : Callable = CollisionModel - addVisualModel : Callable = VisualModel - - #setConstitutiveLaw # : Callable = addBidule - #setBoundaryCondition #: Callable = addBidule - - mechanical : dict = dataclasses.field(default_factory=dict) - collision : CollisionModel.Parameters = CollisionModel.Parameters() - visual : VisualModelParameters = VisualModelParameters() - simulation : SimulationParameters = SimulationParameters() - def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): Sofa.Core.Node.__init__(self, name=parameters.name) diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index f4af08050..b0d8a6f3a 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -1,29 +1,76 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses from splib.topology.loader import loadMesh +from splib.topology.dynamic import addDynamicTopology from splib.core.enum_types import ElementType from Sofa.Core import Object -class Geometry(BasePrefab): - topology : Object # This should be more specialized into the right SOFA type - topologyModifier : Optional[Object] - - def __init__(self, *args, **kwargs): - BasePrefab.__init__(self, *args, **kwargs) +@dataclasses.dataclass class GeometryParameter(BaseParameters): - isDynamic : bool + isDynamic : bool = False elementType : Optional[ElementType] + positions : Optional[list] + edges : Optional[list] + triangles : Optional[list] + tetrahedra : Optional[list] + hexahedra : Optional[list] + quads : Optional[list] + + +class Geometry(BasePrefab): + container : Object # This should be more specialized into the right SOFA type + modifier : Optional[Object] + def __init__(self, params: GeometryParameter): + BasePrefab.__init__(self, params) + if(param.isDynamic): + if(param.elementType is not None): + addDynamicTopology(self, param.elementType, **kargs) + else: + raise ValueError -class FileGeometryParamtetrs(GeometryParameter): + +@dataclasses.dataclass +class FileGeometryParameters(GeometryParameter): filename : str class FileGeometry(Geometry): - fileLoader : Object + loader : Object + + def __init__(self,param : FileGeometryParameters): + loadMesh(self, param.filename, **param.kwargs) + param.positions = "@loader.positions" + param.edges = "@loader.edges" + param.positions = "@loader.positions" + param.positions = "@loader.positions" + Geometry.__init__(self,param) - def __init__(self): pass - \ No newline at end of file + + + +def createScene(root): + from stlib.entities.deformable import Deformable + + fileParameters = FileGeometryParameters(filename = "MyFile.obj", + isDynamic = True, + elementType=ElementType.TETRA, + name="geometry") + + + + cubeGeometry = FileGeometry(FileGeometryParameters) + + entityParam = Deformable.getParameters( geometry = cubeGeometry, name="MyCube") + + + myCube = root.addChild("MyCube") + cubeGeomtry = myCube.addChild("geometry") + + cubeGeomtry.addObject("MeshObjLoader", name="loader", filenmae="MyFile.obj") + cubeGeomtry.addObject("TetrahedronSetTopologyContainer", name="container", positions="@loader.positions") + cubeGeomtry.addObject("TetrahedronSetTopologyModifier", name="modifier") + From fc881002fbba1cc42206b54312b5c195676dd6ac Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 17:49:47 +0200 Subject: [PATCH 16/49] Add beginner example --- examples/stlib/PrefabScene_beginner.py | 32 +++++++-- splib/topology/loader.py | 6 +- stlib/__init__.py | 2 +- stlib/core/baseEntity.py | 2 +- stlib/core/baseParameters.py | 2 +- stlib/entities/__entity__.py | 23 +++--- stlib/entities/__init__.py | 1 + stlib/entities/deformable/__deformable__.py | 4 +- stlib/entities/deformable/__init__.py | 2 + stlib/entities/deformable/__parameters__.py | 6 +- stlib/entities/rigid/__init__.py | 1 + stlib/entities/rigid/__parameters__.py | 10 +++ stlib/entities/rigid/__rigid__.py | 10 ++- stlib/entities/rigid/parameters.py | 0 stlib/geometry/__geometry__.py | 79 ++++++--------------- stlib/geometry/__init__.py | 1 + stlib/geometry/cube.py | 14 ++++ stlib/geometry/file.py | 24 +++++++ stlib/geometry/sphere.py | 14 ++++ 19 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 stlib/entities/rigid/__parameters__.py delete mode 100644 stlib/entities/rigid/parameters.py create mode 100644 stlib/geometry/file.py diff --git a/examples/stlib/PrefabScene_beginner.py b/examples/stlib/PrefabScene_beginner.py index 2c95cc013..9d7ec24ac 100644 --- a/examples/stlib/PrefabScene_beginner.py +++ b/examples/stlib/PrefabScene_beginner.py @@ -1,8 +1,32 @@ +from stlib.entities.rigid import Rigid from stlib.entities.deformable import Deformable +from stlib.geometry.cube import CubeParameters +from stlib.geometry.file import FileParameters +from splib.simulation.headers import setupLagrangianCollision +from splib.simulation.linear_solvers import addLinearSolver +from splib.simulation.ode_solvers import addImplicitODE + +#To be added in splib +def addSolvers(root): + addLinearSolver(root) + addImplicitODE(root) + root.addObject("LinearSolverConstraintCorrection", linearsolver="@LinearSolver") def createScene(root): - params = Deformable.getParameters() - params.name = "Logo" - params.topology.filename="share/mesh/SOFA-logo.obj" - root.add(Deformable, params=params) + root.gravity = [0, 0, -9.81] + + setupLagrangianCollision(root) + addSolvers(root) + + rigidParams = Rigid.getParameters() + rigidParams.geometry = CubeParameters([0, 0, 0], 1, 3) + root.add(Rigid,rigidParams) + + + deformableParams = Deformable.getParameters() + #Add transformation somewhere here + deformableParams.geometry = FileParameters("SofaScene/Logo.vtk") + root.add(Deformable,deformableParams) + + return root diff --git a/splib/topology/loader.py b/splib/topology/loader.py index f9dd55fbb..b040491aa 100644 --- a/splib/topology/loader.py +++ b/splib/topology/loader.py @@ -9,11 +9,11 @@ def loadMesh(node,filename,**kwargs): if splitedName[-1] in ['vtk', 'obj', 'stl', 'msh', 'sph']: if splitedName[-1] == "msh": - return node.addObject("MeshGmshLoader", name="meshLoader",filename=filename, **kwargs) + return node.addObject("MeshGmshLoader", name="loader",filename=filename, **kwargs) elif splitedName[-1] == "sph": - return node.addObject("SphereLoader", name="meshLoader",filename=filename, **kwargs) + return node.addObject("SphereLoader", name="loader",filename=filename, **kwargs) else: - return node.addObject("Mesh"+splitedName[-1].upper()+"Loader", name="meshLoader",filename=filename, **kwargs) + return node.addObject("Mesh"+splitedName[-1].upper()+"Loader", name="loader",filename=filename, **kwargs) else: print('[Error] : File extension ' + splitedName[-1] + ' not recognised.') diff --git a/stlib/__init__.py b/stlib/__init__.py index d3aa4e81c..70108a419 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1 +1 @@ -__all__ = ["core","simulated"] +__all__ = ["core","entities","prefabs","shapes"] diff --git a/stlib/core/baseEntity.py b/stlib/core/baseEntity.py index a9be8df3a..f93fc050e 100644 --- a/stlib/core/baseEntity.py +++ b/stlib/core/baseEntity.py @@ -1,5 +1,5 @@ import Sofa.Core -from base_parameters import BaseParameters +from baseParameters import BaseParameters class BaseEntity(Sofa.Core.Prefab): diff --git a/stlib/core/baseParameters.py b/stlib/core/baseParameters.py index 5d9a66a02..8d8ada38f 100644 --- a/stlib/core/baseParameters.py +++ b/stlib/core/baseParameters.py @@ -6,7 +6,7 @@ @dataclasses.dataclass class BaseParameters(object): - name : str = "Entity" + name : str = "object" kwargs : dict = dataclasses.field(default_factory=dict) def toDict(self): diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index 78a2487b1..181fb941f 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -1,23 +1,26 @@ from stlib.core.baseParameters import BaseParameters import dataclasses -from typing import Callable, Optional, overload +from typing import Callable, Optional, overload, Any +from stlib.geometry import GeometryParameters import Sofa @dataclasses.dataclass class EntityParameters(BaseParameters): - name : str = "Entity" + name = "Entity" + + # addSimulation : Callable = Simulation + # addCollisionModel : Callable = CollisionModel + # addVisualModel : Callable = VisualModel + - addSimulation : Callable = Simulation - addCollisionModel : Callable = CollisionModel - addVisualModel : Callable = VisualModel #setConstitutiveLaw # : Callable = addBidule #setBoundaryCondition #: Callable = addBidule - - mechanical : dict = dataclasses.field(default_factory=dict) - collision : CollisionModel.Parameters = CollisionModel.Parameters() - visual : VisualModelParameters = VisualModelParameters() - simulation : SimulationParameters = SimulationParameters() + geometry : GeometryParameters + # mechanical : dict = dataclasses.field(default_factory=dict) + # collision : CollisionModel.Parameters = CollisionModel.Parameters() + # visual : VisualModelParameters = VisualModelParameters() + # simulation : SimulationParameters = SimulationParameters() diff --git a/stlib/entities/__init__.py b/stlib/entities/__init__.py index e69de29bb..a207a568f 100644 --- a/stlib/entities/__init__.py +++ b/stlib/entities/__init__.py @@ -0,0 +1 @@ +from __entity__ import * \ No newline at end of file diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 1e91f84d0..db629c9a0 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -5,8 +5,8 @@ class Deformable(Entity): @staticmethod - def getParameters() -> DeformableParameters: - return DeformableParameters() + def getParameters(**kwargs) -> DeformableParameters: + return DeformableParameters(**kwargs) def __init__(self, params : DeformableParameters, **kwargs): diff --git a/stlib/entities/deformable/__init__.py b/stlib/entities/deformable/__init__.py index e69de29bb..e23887d67 100644 --- a/stlib/entities/deformable/__init__.py +++ b/stlib/entities/deformable/__init__.py @@ -0,0 +1,2 @@ +from __deformable__ import * +from __parameters__ import * \ No newline at end of file diff --git a/stlib/entities/deformable/__parameters__.py b/stlib/entities/deformable/__parameters__.py index e7126b159..e41ec3707 100644 --- a/stlib/entities/deformable/__parameters__.py +++ b/stlib/entities/deformable/__parameters__.py @@ -1,6 +1,4 @@ - -import dataclasses -from stlib.core.base_parameters import BaseParameters +from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses @dataclasses.dataclass class DeformableParameters(BaseParameters): @@ -8,5 +6,5 @@ class DeformableParameters(BaseParameters): mass : Optional[float] = None - def to_dict(self): + def toDict(self): return dataclasses.asdict(self) diff --git a/stlib/entities/rigid/__init__.py b/stlib/entities/rigid/__init__.py index e69de29bb..17b4b3b61 100644 --- a/stlib/entities/rigid/__init__.py +++ b/stlib/entities/rigid/__init__.py @@ -0,0 +1 @@ +from __rigid__ import * \ No newline at end of file diff --git a/stlib/entities/rigid/__parameters__.py b/stlib/entities/rigid/__parameters__.py new file mode 100644 index 000000000..868ec86b4 --- /dev/null +++ b/stlib/entities/rigid/__parameters__.py @@ -0,0 +1,10 @@ +from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses + +@dataclasses.dataclass +class DRigidParameters(BaseParameters): + addConstitutiveLaw : Callable = lambda x: x + + mass : Optional[float] = None + + def toDict(self): + return dataclasses.asdict(self) diff --git a/stlib/entities/rigid/__rigid__.py b/stlib/entities/rigid/__rigid__.py index f830eab39..69c68f6d0 100644 --- a/stlib/entities/rigid/__rigid__.py +++ b/stlib/entities/rigid/__rigid__.py @@ -1,9 +1,15 @@ -from stlib.core.entity import Entity -from stlib.core.parameters import EntityParameters +from stlib.entities import Entity +from stlib.entities.rigid.__parameters__ import RigidParameters import dataclasses class Rigid(Entity): + + @staticmethod + def getParameters(**kwargs) -> RigidParameters: + return RigidParameters(**kwargs) + + def __init__(self, **kwargs): Entity.__init__(self, **kwargs) diff --git a/stlib/entities/rigid/parameters.py b/stlib/entities/rigid/parameters.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index b0d8a6f3a..1927ddf27 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -1,76 +1,37 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses -from splib.topology.loader import loadMesh +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any from splib.topology.dynamic import addDynamicTopology +from splib.topology.static import addStaticTopology from splib.core.enum_types import ElementType from Sofa.Core import Object - - @dataclasses.dataclass -class GeometryParameter(BaseParameters): - isDynamic : bool = False - elementType : Optional[ElementType] - positions : Optional[list] - edges : Optional[list] - triangles : Optional[list] - tetrahedra : Optional[list] - hexahedra : Optional[list] - quads : Optional[list] - +class GeometryParameters(BaseParameters): + @dataclasses.dataclass + class Data(object): + elementType : Optional[ElementType] + positions : Any + edges : Optional[Any] + triangles : Optional[Any] + tetrahedra : Optional[Any] + hexahedra : Optional[Any] + quads : Optional[Any] + dynamicTopology : bool = False + data : Data class Geometry(BasePrefab): container : Object # This should be more specialized into the right SOFA type modifier : Optional[Object] - def __init__(self, params: GeometryParameter): + def __init__(self, params: GeometryParameters): BasePrefab.__init__(self, params) - if(param.isDynamic): - if(param.elementType is not None): - addDynamicTopology(self, param.elementType, **kargs) + if(params.dynamicTopology): + if(params.elementType is not None): + addDynamicTopology(self, **dataclasses.asdict(params.data), **params.kargs) else: raise ValueError + else: + addStaticTopology(self, **dataclasses.asdict(params.data), **params.kargs) -@dataclasses.dataclass -class FileGeometryParameters(GeometryParameter): - filename : str - - -class FileGeometry(Geometry): - loader : Object - - def __init__(self,param : FileGeometryParameters): - loadMesh(self, param.filename, **param.kwargs) - param.positions = "@loader.positions" - param.edges = "@loader.edges" - param.positions = "@loader.positions" - param.positions = "@loader.positions" - Geometry.__init__(self,param) - - pass - - - -def createScene(root): - from stlib.entities.deformable import Deformable - - fileParameters = FileGeometryParameters(filename = "MyFile.obj", - isDynamic = True, - elementType=ElementType.TETRA, - name="geometry") - - - - cubeGeometry = FileGeometry(FileGeometryParameters) - - entityParam = Deformable.getParameters( geometry = cubeGeometry, name="MyCube") - - - myCube = root.addChild("MyCube") - cubeGeomtry = myCube.addChild("geometry") - cubeGeomtry.addObject("MeshObjLoader", name="loader", filenmae="MyFile.obj") - cubeGeomtry.addObject("TetrahedronSetTopologyContainer", name="container", positions="@loader.positions") - cubeGeomtry.addObject("TetrahedronSetTopologyModifier", name="modifier") - diff --git a/stlib/geometry/__init__.py b/stlib/geometry/__init__.py index e69de29bb..e954fbdcb 100644 --- a/stlib/geometry/__init__.py +++ b/stlib/geometry/__init__.py @@ -0,0 +1 @@ +from __geometry__ import * \ No newline at end of file diff --git a/stlib/geometry/cube.py b/stlib/geometry/cube.py index e69de29bb..0c8ac7615 100644 --- a/stlib/geometry/cube.py +++ b/stlib/geometry/cube.py @@ -0,0 +1,14 @@ +from stlib.geometry import GeometryParameters + +class CubeParameters(GeometryParameters): + def __init__(self, center, edgeLength, pointPerEdge, dynamicTopology = False): + + 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 + \ No newline at end of file diff --git a/stlib/geometry/file.py b/stlib/geometry/file.py new file mode 100644 index 000000000..7504c321e --- /dev/null +++ b/stlib/geometry/file.py @@ -0,0 +1,24 @@ +from stlib.geometry import GeometryParameters +from splib.topology.loader import loadMesh +from Sofa.Core import Node + +class FileParameters(GeometryParameters): + def __init__(self, filename, dynamicTopology = False): + + customGeom = FileParameters.createData(filename) + GeometryParameters.__init__(data = customGeom, dynamicTopology = dynamicTopology) + + @staticmethod + def createData(filename) -> GeometryParameters.Data : + data = GeometryParameters.Data() + node = Sofa.Core.Node() + loadMesh(node, filename) + node.init() + + data.positions = node.loader.position.value + data.edges = node.loader.edges.value + data.triangles = node.loader.triangles.value + data.tetrahedra = node.loader.tetras.value + + return data + \ No newline at end of file diff --git a/stlib/geometry/sphere.py b/stlib/geometry/sphere.py index e69de29bb..2c3a58ce6 100644 --- a/stlib/geometry/sphere.py +++ b/stlib/geometry/sphere.py @@ -0,0 +1,14 @@ +from stlib.geometry import GeometryParameters + +class SphereParameters(GeometryParameters): + def __init__(self, center, radius, pointPerRad, dynamicTopology = False): + + customGeom = SphereParameters.createData(center, radius, pointPerRad) + GeometryParameters.__init__(data = customGeom, dynamicTopology = dynamicTopology) + + @staticmethod + def createData(center, radius, pointPerRad) -> GeometryParameters.Data : + data = GeometryParameters.Data() + #Fill data + return data + \ No newline at end of file From 7c314c6a5ff58b9b571bef5dbf5c77807d1f4a9c Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 17:50:24 +0200 Subject: [PATCH 17/49] Add unsaved changes --- stlib/entities/rigid/__parameters__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stlib/entities/rigid/__parameters__.py b/stlib/entities/rigid/__parameters__.py index 868ec86b4..ce8e0a04f 100644 --- a/stlib/entities/rigid/__parameters__.py +++ b/stlib/entities/rigid/__parameters__.py @@ -1,8 +1,7 @@ from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses @dataclasses.dataclass -class DRigidParameters(BaseParameters): - addConstitutiveLaw : Callable = lambda x: x +class RigidParameters(BaseParameters): mass : Optional[float] = None From b70a52bc2247dff239cc4a2c5e93143b4efbba55 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 14 Apr 2025 18:22:59 +0200 Subject: [PATCH 18/49] Start work on visual and collision --- stlib/entities/deformable/__deformable__.py | 26 +++++++++++-- stlib/entities/deformable/__parameters__.py | 12 +++++- stlib/entities/rigid/__parameters__.py | 2 + stlib/geometry/file.py | 1 + stlib/prefabs/collision.py | 18 +++++++++ stlib/prefabs/visual.py | 41 ++++++++------------- 6 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 stlib/prefabs/collision.py diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index db629c9a0..3d8f94f20 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -1,14 +1,34 @@ from stlib.entities import Entity from stlib.entities.deformable.__parameters__ import DeformableParameters - +from stlib.geometry import Geometry +from stlib.prefabs.visual import Visual +from stlib.prefabs.collision import Collision class Deformable(Entity): - + params : DeformableParameters + @staticmethod def getParameters(**kwargs) -> DeformableParameters: return DeformableParameters(**kwargs) def __init__(self, params : DeformableParameters, **kwargs): - Entity.__init__(self, **kwargs) + Entity.__init__(self, **kwargs) + self.add(Geometry, params.geometry) + self.__addConstitutiveLaw__() + self.add(Collision, params.collision) + + #@customizable + # Need generic way of defining paramaters (linear/hyper...) + def __addConstitutiveLaw__(self): + self.params.addConstitutiveLaw() + + + + #@customizable + def __addVisual__(self): + #Exctract surface and add indentity mapping + + self.add(Visual, params.visual) + pass diff --git a/stlib/entities/deformable/__parameters__.py b/stlib/entities/deformable/__parameters__.py index e41ec3707..d64c29f0e 100644 --- a/stlib/entities/deformable/__parameters__.py +++ b/stlib/entities/deformable/__parameters__.py @@ -1,10 +1,18 @@ from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses +from stlib.geometry import GeometryParameters +from stlib.prefabs.visual import VisualParameters +from stlib.prefabs.collision import CollisionParameters @dataclasses.dataclass class DeformableParameters(BaseParameters): - addConstitutiveLaw : Callable = lambda x: x - mass : Optional[float] = None + geometry : GeometryParameters + # Add default value + visual : VisualParameters + collision : CollisionParameters + mass : float = float + + addConstitutiveLaw : Callable = lambda x: x def toDict(self): return dataclasses.asdict(self) diff --git a/stlib/entities/rigid/__parameters__.py b/stlib/entities/rigid/__parameters__.py index ce8e0a04f..f7be03c77 100644 --- a/stlib/entities/rigid/__parameters__.py +++ b/stlib/entities/rigid/__parameters__.py @@ -1,8 +1,10 @@ from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses +from stlib.geometry import GeometryParameters @dataclasses.dataclass class RigidParameters(BaseParameters): + geometry : GeometryParameters mass : Optional[float] = None def toDict(self): diff --git a/stlib/geometry/file.py b/stlib/geometry/file.py index 7504c321e..18e40ebb8 100644 --- a/stlib/geometry/file.py +++ b/stlib/geometry/file.py @@ -11,6 +11,7 @@ def __init__(self, filename, dynamicTopology = False): @staticmethod def createData(filename) -> GeometryParameters.Data : data = GeometryParameters.Data() + node = Sofa.Core.Node() loadMesh(node, filename) node.init() diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py new file mode 100644 index 000000000..bb73fe4bc --- /dev/null +++ b/stlib/prefabs/collision.py @@ -0,0 +1,18 @@ +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any +from stlib.geometry import GeometryParameters +from Sofa.Core import Object + +@dataclasses.dataclass +class CollisionParameters(BaseParameters): + primitives : list[Any] #TODO: add enum type in splib + + geometry : GeometryParameters + addMapping : Callable + + +class Collision(BasePrefab): + + def __init__(self, params: GeometryParameters): + BasePrefab.__init__(self, params) + diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index 7081c9b2c..2a9590b84 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -1,32 +1,21 @@ -from stlib.core.parameters import EntityParameters -import dataclasses -import Sofa +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any +from stlib.geometry import GeometryParameters +from Sofa.Core import Object -class VisualModel(Sofa.Core.BasePrefab): - @dataclasses.dataclass - class Parameters(EntityParameters): - name : str = "VisualModel" +@dataclasses.dataclass +class VisualParameters(BaseParameters): + color : Optional[list[float]] + texture : Optional[str] - filename : str = "mesh/sphere_02.obj" - - renderer : dict = dataclasses.field(default_factory=dict) - mapping : dict = dataclasses.field(default_factory=dict) + geometry : GeometryParameters + addMapping : Optional[Callable] +class Visual(BasePrefab): + container : Object # This should be more specialized into the right SOFA type + modifier : Optional[Object] - def __init__(self, parent=None, parameters : VisualModelParameters = VisualModelParameters()): - Sofa.Core.Node.__init__(self, name=parameters.name) + def __init__(self, params: GeometryParameters): + BasePrefab.__init__(self, params) - if parent != None: - parent.addChild(self) - - self.addObject("MeshOBJLoader", name="loader", filename=parameters.filename) - self.addRenderer(**to_dict(parameters.renderer) | {"src" : "@loader"} ) - self.addMapping(**to_dict(parameters.mapping) ) - - def addRenderer(self, **kwargs): - self.addObject("OglModel", name="renderer", **kwargs) - - def addMapping(self, **kwargs): - self.addObject("RigidMapping", name="mapping", **kwargs) - \ No newline at end of file From 7cedc423e3f29de73dea38e8e760d7627fffe1d5 Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Tue, 15 Apr 2025 13:53:42 +0200 Subject: [PATCH 19/49] work on Tuesday : visual, geometry --- stlib/core/basePrefab.py | 32 +++++++++++++++++++-- stlib/entities/__entity__.py | 2 ++ stlib/entities/deformable/__deformable__.py | 6 ++-- stlib/geometry/__geometry__.py | 13 +++++---- stlib/geometry/file.py | 2 ++ stlib/prefabs/visual.py | 11 +++++-- 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index 22652f744..cd391c582 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -1,5 +1,29 @@ +import copy import Sofa.Core -from baseParameters import BaseParameters +from stlib.core.basePrefabParameters import BasePrefabParameters + + +def addFromTypeName(self : Sofa.Core.Node, typeName, params = BasePrefabParameters, **kwargs): + def findName(cname, node): + """Compute a working unique name in the node""" + rname = cname + for i in range(0, len(node.children)): + if rname not in node.children: + return rname + rname = cname + str(i+1) + return rname + + for k,v in kwargs.items(): + if hasattr(params, k): + setattr(params, k, v) + + params = copy.copy(params) + if params.name in self.children: + params.name = findName(params.name, self) + + return typeName(parent = self, parameters=params) + +Sofa.Core.Node.add = addFromTypeName class BasePrefab(Sofa.Core.Prefab): @@ -8,4 +32,8 @@ class BasePrefab(Sofa.Core.Prefab): """ def __init__(self, params: BaseParameters): - Sofa.Core.Prefab.__init__(self, name=params.name) \ No newline at end of file + Sofa.Core.Prefab.__init__(self, name=params.name) + + def localToGlobalCoordinates(pointCloudInput, pointCloudOutput): + raise NotImplemented("Send an email to Damien, he will help you. Guaranteed :)") + diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index 181fb941f..705a154ab 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -43,6 +43,7 @@ def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): self.parameters = parameters self.addMechanicalModel(**parameters.mechanical) + # ???? self.addSimulation(parameters=parameters.simulation) self.addVisualModel(parameters=parameters.visual) self.addCollisionModel() @@ -50,6 +51,7 @@ def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): def addMechanicalModel(self, **kwargs): self.addObject("MechanicalObject", **kwargs) + # ???? def addSimulation(self, **kwargs): self.parameters.addSimulation(self, **kwargs) diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 3d8f94f20..3f024b230 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -18,17 +18,15 @@ def __init__(self, params : DeformableParameters, **kwargs): self.__addConstitutiveLaw__() self.add(Collision, params.collision) + #@customizable # Need generic way of defining paramaters (linear/hyper...) def __addConstitutiveLaw__(self): self.params.addConstitutiveLaw() - #@customizable def __addVisual__(self): - #Exctract surface and add indentity mapping - + #Extract surface and add identity mapping self.add(Visual, params.visual) - pass diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index 1927ddf27..58cfcea82 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -9,13 +9,19 @@ class GeometryParameters(BaseParameters): @dataclasses.dataclass class Data(object): - elementType : Optional[ElementType] + # Initial positions positions : Any + + # Type of the highest degree element + elementType : Optional[ElementType] + + # Topology information edges : Optional[Any] triangles : Optional[Any] + quads : Optional[Any] tetrahedra : Optional[Any] hexahedra : Optional[Any] - quads : Optional[Any] + dynamicTopology : bool = False data : Data @@ -32,6 +38,3 @@ def __init__(self, params: GeometryParameters): raise ValueError else: addStaticTopology(self, **dataclasses.asdict(params.data), **params.kargs) - - - diff --git a/stlib/geometry/file.py b/stlib/geometry/file.py index 18e40ebb8..5247577ed 100644 --- a/stlib/geometry/file.py +++ b/stlib/geometry/file.py @@ -21,5 +21,7 @@ def createData(filename) -> GeometryParameters.Data : data.triangles = node.loader.triangles.value data.tetrahedra = node.loader.tetras.value + node.cleanUp() + return data \ No newline at end of file diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index 2a9590b84..4a41d2736 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -1,6 +1,6 @@ from stlib.core.basePrefab import BasePrefab from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any -from stlib.geometry import GeometryParameters +from stlib.geometry import Geometry, GeometryParameters from Sofa.Core import Object @dataclasses.dataclass @@ -16,6 +16,13 @@ class Visual(BasePrefab): container : Object # This should be more specialized into the right SOFA type modifier : Optional[Object] - def __init__(self, params: GeometryParameters): + def __init__(self, params: VisualParameters): BasePrefab.__init__(self, params) + geom = self.add(Geometry, params.geometry) + # TODO : handle optional color, texture using DEFAULT_VALUE mechanism (as implemented by Paul) + self.addObject("OglModel", color=params.color, src=geom.container.linkpath) + + if params.addMapping is not None: + params.addMapping(self) + From 5f183d5ab673ed9c36d2313b670865eecc6df2bc Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Tue, 15 Apr 2025 14:17:18 +0200 Subject: [PATCH 20/49] add basePrefabParameters.py --- stlib/core/basePrefabParameters.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 stlib/core/basePrefabParameters.py diff --git a/stlib/core/basePrefabParameters.py b/stlib/core/basePrefabParameters.py new file mode 100644 index 000000000..9ea8911bd --- /dev/null +++ b/stlib/core/basePrefabParameters.py @@ -0,0 +1,15 @@ +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] = [0., 0., 0.] + rotation : list[float] = [0., 0., 0.] + scale : list[float] = [1., 1., 1.] + + def toDict(self): + return dataclasses.asdict(self) From 97a23813f83926e88aab434fbd9cd80917d02a48 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 15 Apr 2025 15:03:42 +0200 Subject: [PATCH 21/49] Add extracted geometry --- stlib/geometry/__geometry__.py | 46 ++++++++++++++++++++++------------ stlib/geometry/extract.py | 46 ++++++++++++++++++++++++++++++++++ stlib/geometry/file.py | 42 +++++++++++++++++-------------- 3 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 stlib/geometry/extract.py diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index 58cfcea82..d2f7bd5fc 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -3,38 +3,52 @@ from splib.topology.dynamic import addDynamicTopology from splib.topology.static import addStaticTopology from splib.core.enum_types import ElementType +from splib.core.utils import DEFAULT_VALUE from Sofa.Core import Object + +class Geometry(BasePrefab):... + @dataclasses.dataclass -class GeometryParameters(BaseParameters): - @dataclasses.dataclass - class Data(object): - # Initial positions - positions : Any +class InternalDataProvider(object): + positions : Any + # Topology information + edges : Any = DEFAULT_VALUE + triangles : Any = DEFAULT_VALUE + quads : Any = DEFAULT_VALUE + tetrahedra : Any = DEFAULT_VALUE + hexahedra : Any = DEFAULT_VALUE + + def generateAttribute(self, parent : Geometry): + pass + - # Type of the highest degree element - elementType : Optional[ElementType] +@dataclasses.dataclass +class GeometryParameters(BaseParameters): - # Topology information - edges : Optional[Any] - triangles : Optional[Any] - quads : Optional[Any] - tetrahedra : Optional[Any] - hexahedra : Optional[Any] + # Type of the highest degree element + elementType : Optional[ElementType] dynamicTopology : bool = False - data : Data + data : Optional[InternalDataProvider] + + class Geometry(BasePrefab): container : Object # This should be more specialized into the right SOFA type modifier : Optional[Object] + params : GeometryParameters + def __init__(self, params: GeometryParameters): BasePrefab.__init__(self, params) + self.params = params + if params.data is not None : + params.data.generateAttribute(self) if(params.dynamicTopology): if(params.elementType is not None): - addDynamicTopology(self, **dataclasses.asdict(params.data), **params.kargs) + addDynamicTopology(self, container = {dataclasses.asdict(params.data)}) else: raise ValueError else: - addStaticTopology(self, **dataclasses.asdict(params.data), **params.kargs) + addStaticTopology(self, container = {dataclasses.asdict(params.data)}) diff --git a/stlib/geometry/extract.py b/stlib/geometry/extract.py new file mode 100644 index 000000000..ae9c97e69 --- /dev/null +++ b/stlib/geometry/extract.py @@ -0,0 +1,46 @@ +from stlib.geometry 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 + +from Sofa.Core import Node + + +class ExtractInternalDataProvider(InternalDataProvider): + destElemType : ElementType + fromElemType : ElementType + fromNodeName : str + + def __post_init__(self): + if(not (self.fromElemType == ElementType.TETRA and self.destElemType == ElementType.TRIANGLES) + and not (self.fromElemType == ElementType.HEXA and self.destElemType == ElementType.QUAD) ): + raise ValueError("Only configuration possible are 'Tetra to Triangle' and 'Hexa to quad'") + + InternalDataProvider.__init__(self) + + + def generateAttribute(self, parent : Geometry): + tmn = parent.addChild("topologicalMappingNode") + + #TODO: Specify somewhere in the doc that this should only be used for mapped topologies that extract parent topology surface + fromLink = parent.parents[0].parents[0].getChild(self.fromNodeName).container.linkpath + addDynamicTopology(tmn, type=self.destElemType) + if self.fromElemType == ElementType.TETRA: + tmn.addObject("Tetra2TriangleTopologicalMapping", input=fromLink, output=tmn.container.linkpath) + elif self.fromElemType == ElementType.HEXA: + tmn.addObject("Hexa2QuadTopologicalMapping", input=fromLink, output=tmn.container.linkpath) + + self.positions = tmn.container.position.linkpath + self.edges = tmn.container.edges.linkpath + self.triangles = tmn.container.triangles.linkpath + self.quads = tmn.container.quads.linkpath + self.hexaedra = tmn.container.hexaedra.linkpath + self.tetrahedra = tmn.container.tetras.linkpath + + + +class ExtractParameters(GeometryParameters): + def __init__(self, fromGeometry : GeometryParameters, destElementType : ElementType, dynamicTopology = False, ): + GeometryParameters.__init__(data = ExtractInternalDataProvider(destElemType = destElementType, fromElemType = fromGeometry.elementType,fromNodeName = fromGeometry.name), dynamicTopology = dynamicTopology, elementType = destElementType) + diff --git a/stlib/geometry/file.py b/stlib/geometry/file.py index 5247577ed..1c15597df 100644 --- a/stlib/geometry/file.py +++ b/stlib/geometry/file.py @@ -1,27 +1,31 @@ -from stlib.geometry import GeometryParameters +from stlib.geometry 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 -class FileParameters(GeometryParameters): - def __init__(self, filename, dynamicTopology = False): +@dataclasses.dataclass +class FileInternalDataProvider(InternalDataProvider): + filename : str - customGeom = FileParameters.createData(filename) - GeometryParameters.__init__(data = customGeom, dynamicTopology = dynamicTopology) + def __post_init__(self, **kwargs): + InternalDataProvider.__init__(self,**kwargs) - @staticmethod - def createData(filename) -> GeometryParameters.Data : - data = GeometryParameters.Data() - - node = Sofa.Core.Node() - loadMesh(node, filename) - node.init() + def generateAttribute(self, parent : Geometry): + + loadMesh(parent, self.filename) - data.positions = node.loader.position.value - data.edges = node.loader.edges.value - data.triangles = node.loader.triangles.value - data.tetrahedra = node.loader.tetras.value + self.positions = parent.loader.position.linkpath + self.edges = parent.loader.edges.linkpath + self.triangles = parent.loader.triangles.linkpath + self.quads = parent.loader.quads.linkpath + self.hexaedra = parent.loader.hexaedra.linkpath + self.tetrahedra = parent.loader.tetras.linkpath - node.cleanUp() - return data - \ No newline at end of file + +class FileParameters(GeometryParameters): + def __init__(self, filename, dynamicTopology = False, elementType : ElementType = None ): + GeometryParameters.__init__(data = FileInternalDataProvider(filename), dynamicTopology = dynamicTopology, elementType = elementType) + From 00fd16b4f22838570e7a18c755efac6315effd9e Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 15 Apr 2025 15:48:01 +0200 Subject: [PATCH 22/49] Fix visual example --- examples/stlib/PrefabScene_beginner.py | 2 -- examples/stlib/PrefabScene_intermediate.py | 8 +------- stlib/core/baseParameters.py | 2 +- stlib/core/basePrefab.py | 11 ++++++----- stlib/core/basePrefabParameters.py | 6 +++--- stlib/entities/deformable/__init__.py | 4 ++-- stlib/entities/rigid/__init__.py | 2 +- stlib/geometry/__geometry__.py | 12 +++++++----- stlib/geometry/__init__.py | 2 +- stlib/geometry/extract.py | 4 ++-- stlib/geometry/file.py | 16 ++++++++-------- stlib/prefabs/visual.py | 20 ++++++++++++++++---- 12 files changed, 48 insertions(+), 41 deletions(-) diff --git a/examples/stlib/PrefabScene_beginner.py b/examples/stlib/PrefabScene_beginner.py index 9d7ec24ac..ca59a63e5 100644 --- a/examples/stlib/PrefabScene_beginner.py +++ b/examples/stlib/PrefabScene_beginner.py @@ -22,11 +22,9 @@ def createScene(root): rigidParams.geometry = CubeParameters([0, 0, 0], 1, 3) root.add(Rigid,rigidParams) - deformableParams = Deformable.getParameters() #Add transformation somewhere here deformableParams.geometry = FileParameters("SofaScene/Logo.vtk") root.add(Deformable,deformableParams) - return root diff --git a/examples/stlib/PrefabScene_intermediate.py b/examples/stlib/PrefabScene_intermediate.py index 0bc427d37..7ce22c745 100644 --- a/examples/stlib/PrefabScene_intermediate.py +++ b/examples/stlib/PrefabScene_intermediate.py @@ -3,12 +3,6 @@ def createScene(root): - Entity.Deformable(Mesh,MechaProperties) - return root - -if __name__=="__main__": - root = exportScene() - createScene(root) - pass \ No newline at end of file + return root \ No newline at end of file diff --git a/stlib/core/baseParameters.py b/stlib/core/baseParameters.py index 8d8ada38f..883b17b5b 100644 --- a/stlib/core/baseParameters.py +++ b/stlib/core/baseParameters.py @@ -2,7 +2,7 @@ from splib.core.utils import DEFAULT_VALUE import dataclasses -from types import Callable, Optional +from typing import Callable, Optional, Any @dataclasses.dataclass class BaseParameters(object): diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index cd391c582..820e0e2b2 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -1,5 +1,6 @@ import copy -import Sofa.Core +import Sofa +import Sofa.Core from stlib.core.basePrefabParameters import BasePrefabParameters @@ -21,18 +22,18 @@ def findName(cname, node): if params.name in self.children: params.name = findName(params.name, self) - return typeName(parent = self, parameters=params) + return self.addChild(typeName(params)) Sofa.Core.Node.add = addFromTypeName -class BasePrefab(Sofa.Core.Prefab): +class BasePrefab(Sofa.Core.Node): """ A Prefab is a Sofa.Node that assembles a set of components and nodes """ - def __init__(self, params: BaseParameters): - Sofa.Core.Prefab.__init__(self, name=params.name) + def __init__(self, params: BasePrefabParameters): + Sofa.Core.Node.__init__(self, name=params.name) 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 index 9ea8911bd..d6a69dee9 100644 --- a/stlib/core/basePrefabParameters.py +++ b/stlib/core/basePrefabParameters.py @@ -7,9 +7,9 @@ class BasePrefabParameters(object): # 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.] + 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/deformable/__init__.py b/stlib/entities/deformable/__init__.py index e23887d67..d17dc4db2 100644 --- a/stlib/entities/deformable/__init__.py +++ b/stlib/entities/deformable/__init__.py @@ -1,2 +1,2 @@ -from __deformable__ import * -from __parameters__ import * \ No newline at end of file +from .__deformable__ import * +from .__parameters__ import * \ No newline at end of file diff --git a/stlib/entities/rigid/__init__.py b/stlib/entities/rigid/__init__.py index 17b4b3b61..04efc302b 100644 --- a/stlib/entities/rigid/__init__.py +++ b/stlib/entities/rigid/__init__.py @@ -1 +1 @@ -from __rigid__ import * \ No newline at end of file +from .__rigid__ import * \ No newline at end of file diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index d2f7bd5fc..32ed6b610 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -11,7 +11,7 @@ class Geometry(BasePrefab):... @dataclasses.dataclass class InternalDataProvider(object): - positions : Any + position : Any = None # Topology information edges : Any = DEFAULT_VALUE triangles : Any = DEFAULT_VALUE @@ -27,10 +27,12 @@ def generateAttribute(self, parent : Geometry): class GeometryParameters(BaseParameters): # Type of the highest degree element - elementType : Optional[ElementType] + elementType : Optional[ElementType] = None + data : Optional[InternalDataProvider] = None dynamicTopology : bool = False - data : Optional[InternalDataProvider] + + @@ -47,8 +49,8 @@ def __init__(self, params: GeometryParameters): params.data.generateAttribute(self) if(params.dynamicTopology): if(params.elementType is not None): - addDynamicTopology(self, container = {dataclasses.asdict(params.data)}) + addDynamicTopology(self, container = dataclasses.asdict(params.data)) else: raise ValueError else: - addStaticTopology(self, container = {dataclasses.asdict(params.data)}) + addStaticTopology(self, container = dataclasses.asdict(params.data)) diff --git a/stlib/geometry/__init__.py b/stlib/geometry/__init__.py index e954fbdcb..34a466c77 100644 --- a/stlib/geometry/__init__.py +++ b/stlib/geometry/__init__.py @@ -1 +1 @@ -from __geometry__ import * \ No newline at end of file +from .__geometry__ import * \ No newline at end of file diff --git a/stlib/geometry/extract.py b/stlib/geometry/extract.py index ae9c97e69..8edba4c65 100644 --- a/stlib/geometry/extract.py +++ b/stlib/geometry/extract.py @@ -31,11 +31,11 @@ def generateAttribute(self, parent : Geometry): elif self.fromElemType == ElementType.HEXA: tmn.addObject("Hexa2QuadTopologicalMapping", input=fromLink, output=tmn.container.linkpath) - self.positions = tmn.container.position.linkpath + self.position = tmn.container.position.linkpath self.edges = tmn.container.edges.linkpath self.triangles = tmn.container.triangles.linkpath self.quads = tmn.container.quads.linkpath - self.hexaedra = tmn.container.hexaedra.linkpath + self.hexahedra = tmn.container.hexahedra.linkpath self.tetrahedra = tmn.container.tetras.linkpath diff --git a/stlib/geometry/file.py b/stlib/geometry/file.py index 1c15597df..e2e4a60d9 100644 --- a/stlib/geometry/file.py +++ b/stlib/geometry/file.py @@ -7,7 +7,7 @@ @dataclasses.dataclass class FileInternalDataProvider(InternalDataProvider): - filename : str + filename : str = "mesh/cube.obj" def __post_init__(self, **kwargs): InternalDataProvider.__init__(self,**kwargs) @@ -16,16 +16,16 @@ def generateAttribute(self, parent : Geometry): loadMesh(parent, self.filename) - self.positions = parent.loader.position.linkpath - self.edges = parent.loader.edges.linkpath - self.triangles = parent.loader.triangles.linkpath - self.quads = parent.loader.quads.linkpath - self.hexaedra = parent.loader.hexaedra.linkpath - self.tetrahedra = parent.loader.tetras.linkpath + self.position = str(parent.loader.position.linkpath) + self.edges = str(parent.loader.edges.linkpath) + self.triangles = str(parent.loader.triangles.linkpath) + self.quads = str(parent.loader.quads.linkpath) + self.hexahedra = str(parent.loader.hexahedra.linkpath) + self.tetrahedra = str(parent.loader.tetras.linkpath) class FileParameters(GeometryParameters): def __init__(self, filename, dynamicTopology = False, elementType : ElementType = None ): - GeometryParameters.__init__(data = FileInternalDataProvider(filename), dynamicTopology = dynamicTopology, elementType = elementType) + GeometryParameters.__init__(self,data = FileInternalDataProvider(filename), dynamicTopology = dynamicTopology, elementType = elementType) diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index 4a41d2736..c7c8d6308 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -1,15 +1,16 @@ from stlib.core.basePrefab import BasePrefab from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any from stlib.geometry import Geometry, GeometryParameters +from stlib.geometry.file import FileParameters from Sofa.Core import Object @dataclasses.dataclass class VisualParameters(BaseParameters): - color : Optional[list[float]] - texture : Optional[str] + color : Optional[list[float]] = None + texture : Optional[str] = None - geometry : GeometryParameters - addMapping : Optional[Callable] + geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) + addMapping : Optional[Callable] = None class Visual(BasePrefab): @@ -26,3 +27,14 @@ def __init__(self, params: VisualParameters): if params.addMapping is not None: params.addMapping(self) + @staticmethod + def getParameters(**kwargs) -> VisualParameters: + return VisualParameters(**kwargs) + + +def createScene(root): + + # Create a visual from a mesh file + params = Visual.getParameters() + params.geometry = FileParameters(filename="mesh/cube.obj") + root.add(Visual, params) From 423a484b6fa7b3aca21b34778446b4b07240b74d Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 15 Apr 2025 16:13:44 +0200 Subject: [PATCH 23/49] Finished collisions --- splib/core/enum_types.py | 6 +++- splib/mechanics/collision_model.py | 33 +++++++++++++------- stlib/prefabs/collision.py | 50 ++++++++++++++++++++++++++---- stlib/prefabs/visual.py | 3 -- 4 files changed, 70 insertions(+), 22 deletions(-) diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py index dc1b5e864..dbce47e14 100644 --- a/splib/core/enum_types.py +++ b/splib/core/enum_types.py @@ -23,7 +23,11 @@ class ConstraintType(Enum): WEAK = 2 LAGRANGIAN = 3 - +class CollisionPrimitive(Enum): + POINTS = 1 + LINES = 2 + TRIANGLES = 3 + SPHERES = 4 class ElementType(Enum): POINTS = 1 diff --git a/splib/mechanics/collision_model.py b/splib/mechanics/collision_model.py index 40bf0c1d8..ad8effade 100644 --- a/splib/mechanics/collision_model.py +++ b/splib/mechanics/collision_model.py @@ -1,18 +1,27 @@ from splib.core.node_wrapper import ReusableMethod from splib.core.utils import DEFAULT_VALUE -from splib.core.enum_types import ElementType +from splib.core.enum_types import CollisionPrimitive @ReusableMethod -def addCollisionModels(node,points=False, edges=False,triangles=False, spheres=False,tetrahedron=False,selfCollision=DEFAULT_VALUE, proximity=DEFAULT_VALUE, group=DEFAULT_VALUE, contactStiffness=DEFAULT_VALUE, contactFriction=DEFAULT_VALUE,spheresRadius=DEFAULT_VALUE,**kwargs): - if(points): - node.addObject("PointCollisionModel",name="PointCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) - if(edges): - node.addObject("LineCollisionModel",name="EdgeCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) - if(triangles): - node.addObject("TriangleCollisionModel",name="TriangleCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) - if(spheres): - node.addObject("SphereCollisionModel",name="SphereCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) - if(tetrahedron): - node.addObject("TetrahedronCollisionModel",name="TetraCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) +def addCollisionModels(node, primitive : CollisionPrimitive, topology=DEFAULT_VALUE, selfCollision=DEFAULT_VALUE, proximity=DEFAULT_VALUE, group=DEFAULT_VALUE, contactStiffness=DEFAULT_VALUE, contactFriction=DEFAULT_VALUE,spheresRadius=DEFAULT_VALUE,**kwargs): + match primitive: + case CollisionPrimitive.POINTS: + node.addObject("PointCollisionModel",name="PointCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) + return + case CollisionPrimitive.LINES: + node.addObject("LineCollisionModel",name="EdgeCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) + return + case CollisionPrimitive.TRIANGLES: + node.addObject("TriangleCollisionModel",name="TriangleCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) + return + case CollisionPrimitive.SPHERES: + node.addObject("SphereCollisionModel",name="SphereCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) + return + case _: + return + + #Cube and tetra are missing. + + \ No newline at end of file diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py index bb73fe4bc..28915629b 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/prefabs/collision.py @@ -1,18 +1,56 @@ from stlib.core.basePrefab import BasePrefab from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any -from stlib.geometry import GeometryParameters +from stlib.geometry import Geometry, GeometryParameters +from stlib.geometry.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 @dataclasses.dataclass class CollisionParameters(BaseParameters): - primitives : list[Any] #TODO: add enum type in splib + primitives : list[CollisionPrimitive] = dataclasses.field(default_factory = lambda :[CollisionPrimitive.POINTS]) - geometry : GeometryParameters - addMapping : Callable + selfCollision : Optional[bool] = DEFAULT_VALUE + proximity : Optional[float] = DEFAULT_VALUE + group : Optional[int] = DEFAULT_VALUE + contactStiffness : Optional[float] = DEFAULT_VALUE + contactFriction : Optional[float] = DEFAULT_VALUE + spheresRadius : Optional[float] = DEFAULT_VALUE + geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) + addMapping : Optional[Callable] = None -class Collision(BasePrefab): - def __init__(self, params: GeometryParameters): +class Collision(BasePrefab): + def __init__(self, params: CollisionParameters): BasePrefab.__init__(self, params) + geom = self.add(Geometry, params.geometry) + + self.addObject("MechanicalObject",template="Vec3", position=f"@{params.geometry.name}/container.position") + for primitive in params.primitives: + addCollisionModels(self,primitive, + topology=f"@{params.geometry.name}/container", + selfCollision=params.selfCollision, + proximity=params.proximity, + group=params.group, + contactStiffness=params.contactStiffness, + contactFriction=params.contactFriction, + spheresRadius=params.spheresRadius, + **params.kwargs) + + if params.addMapping is not None: + params.addMapping(self) + + @staticmethod + def getParameters(**kwargs) -> CollisionParameters: + return CollisionParameters(**kwargs) + + +def createScene(root): + + # Create a visual from a mesh file + params = Collision.getParameters() + params.geometry = FileParameters(filename="mesh/cube.obj") + root.add(Collision, params) diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index c7c8d6308..2bf61ed95 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -14,9 +14,6 @@ class VisualParameters(BaseParameters): class Visual(BasePrefab): - container : Object # This should be more specialized into the right SOFA type - modifier : Optional[Object] - def __init__(self, params: VisualParameters): BasePrefab.__init__(self, params) From c97bfa36be4a4f83987b6fdffaaf360a46290d36 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Mon, 2 Jun 2025 14:55:37 +0200 Subject: [PATCH 24/49] [stlib-splib] work on prefabs: cleaning (#501) * [stlib-splib] cleaning * revert collisionModel --- splib/core/enum_types.py | 41 ++++++++++++++-------------- splib/mechanics/collision_model.py | 21 +++++++++----- splib/mechanics/hyperelasticity.py | 2 +- splib/mechanics/linear_elasticity.py | 6 ++-- splib/topology/dynamic.py | 8 +++--- stlib/geometry/__geometry__.py | 5 +--- stlib/geometry/extract.py | 35 ++++++++++++++++-------- stlib/geometry/file.py | 12 ++++---- stlib/prefabs/collision.py | 21 +++++++------- stlib/prefabs/visual.py | 13 ++++++++- 10 files changed, 96 insertions(+), 68 deletions(-) diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py index dbce47e14..568245734 100644 --- a/splib/core/enum_types.py +++ b/splib/core/enum_types.py @@ -5,34 +5,33 @@ class ConstitutiveLaw(Enum): HYPERELASTIC = 2 class ODEType(Enum): - EXPLICIT = 1 - IMPLICIT = 2 + EXPLICIT = 1 + IMPLICIT = 2 class SolverType(Enum): - DIRECT = 1 - ITERATIVE = 2 + DIRECT = 1 + ITERATIVE = 2 class MappingType(Enum): - BARYCENTRIC = 1 - IDENTITY = 2 - RIGID = 3 - + BARYCENTRIC = 1 + IDENTITY = 2 + RIGID = 3 class ConstraintType(Enum): - PROJECTIVE = 1 - WEAK = 2 - LAGRANGIAN = 3 + PROJECTIVE = 1 + WEAK = 2 + LAGRANGIAN = 3 class CollisionPrimitive(Enum): - POINTS = 1 - LINES = 2 - TRIANGLES = 3 - SPHERES = 4 + POINTS = 1 + LINES = 2 + TRIANGLES = 3 + SPHERES = 4 class ElementType(Enum): - POINTS = 1 - EDGES = 2 - TRIANGLES = 3 - QUAD = 4 - TETRA = 5 - HEXA = 6 + POINTS = 1 + EDGES = 2 + TRIANGLES = 3 + QUADS = 4 + TETRAHEDRONS = 5 + HEXAHEDRONS = 6 diff --git a/splib/mechanics/collision_model.py b/splib/mechanics/collision_model.py index ad8effade..e1abdf457 100644 --- a/splib/mechanics/collision_model.py +++ b/splib/mechanics/collision_model.py @@ -3,21 +3,28 @@ from splib.core.enum_types import CollisionPrimitive - @ReusableMethod -def addCollisionModels(node, primitive : CollisionPrimitive, topology=DEFAULT_VALUE, selfCollision=DEFAULT_VALUE, proximity=DEFAULT_VALUE, group=DEFAULT_VALUE, contactStiffness=DEFAULT_VALUE, contactFriction=DEFAULT_VALUE,spheresRadius=DEFAULT_VALUE,**kwargs): +def addCollisionModels(node, primitive : CollisionPrimitive, + topology=DEFAULT_VALUE, + selfCollision=DEFAULT_VALUE, + proximity=DEFAULT_VALUE, + group=DEFAULT_VALUE, + contactStiffness=DEFAULT_VALUE, + contactFriction=DEFAULT_VALUE, + spheresRadius=DEFAULT_VALUE, + **kwargs): match primitive: case CollisionPrimitive.POINTS: - node.addObject("PointCollisionModel",name="PointCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) + node.addObject("PointCollisionModel", name="PointCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) return - case CollisionPrimitive.LINES: - node.addObject("LineCollisionModel",name="EdgeCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) + case CollisionPrimitive.LINES: + node.addObject("LineCollisionModel", name="EdgeCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) return case CollisionPrimitive.TRIANGLES: - node.addObject("TriangleCollisionModel",name="TriangleCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) + node.addObject("TriangleCollisionModel", name="TriangleCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) return case CollisionPrimitive.SPHERES: - node.addObject("SphereCollisionModel",name="SphereCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) + node.addObject("SphereCollisionModel", name="SphereCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) return case _: return diff --git a/splib/mechanics/hyperelasticity.py b/splib/mechanics/hyperelasticity.py index 1e79c4206..27c306358 100644 --- a/splib/mechanics/hyperelasticity.py +++ b/splib/mechanics/hyperelasticity.py @@ -6,7 +6,7 @@ @ReusableMethod def addHyperelasticity(node,elem:ElementType,materialName=DEFAULT_VALUE, parameterSet=DEFAULT_VALUE, matrixRegularization=DEFAULT_VALUE,**kwargs): match elem: - case ElementType.TETRA: + case ElementType.TETRAHEDRONS: node.addObject("TetrahedronHyperelasticityFEMForceField",name="constitutiveLaw", materialName=materialName, parameterSet=parameterSet, matrixRegularization=matrixRegularization, **kwargs) return case _: diff --git a/splib/mechanics/linear_elasticity.py b/splib/mechanics/linear_elasticity.py index 8675afe19..33de61895 100644 --- a/splib/mechanics/linear_elasticity.py +++ b/splib/mechanics/linear_elasticity.py @@ -12,13 +12,13 @@ def addLinearElasticity(node,elem:ElementType,youngModulus=DEFAULT_VALUE, poisso case ElementType.TRIANGLES: node.addObject("TriangleFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) return - case ElementType.QUAD: + case ElementType.QUADS: node.addObject("QuadBendingFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) return - case ElementType.TETRA: + case ElementType.TETRAHEDRONS: node.addObject("TetrahedronFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) return - case ElementType.HEXA: + case ElementType.HEXAHEDRONS: node.addObject("HexahedronFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) return case _: diff --git a/splib/topology/dynamic.py b/splib/topology/dynamic.py index b5a829c39..c84bd1850 100644 --- a/splib/topology/dynamic.py +++ b/splib/topology/dynamic.py @@ -53,15 +53,15 @@ def addDynamicTopology(node,type:ElementType,**kwargs): case ElementType.TRIANGLES: addTriangleTopology(node,**kwargs) return - case ElementType.QUAD: + case ElementType.QUADS: addQuadTopology(node,**kwargs) return - case ElementType.TETRA: + case ElementType.TETRAHEDRONS: addTetrahedronTopology(node,**kwargs) return - case ElementType.HEXA: + case ElementType.HEXAHEDRONS: addHexahedronTopology(node,**kwargs) return case _: - print('Topology type should be one of the following : "ElementType.POINTS, ElementType.EDGES, ElementType.TRIANGLES, ElementType.QUAD, ElementType.TETRA, ElementType.HEXA" ') + print('Topology type should be one of the following : "ElementType.POINTS, ElementType.EDGES, ElementType.TRIANGLES, ElementType.QUADS, ElementType.TETRAHEDRONS, ElementType.HEXAHEDRONS" ') return diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index 32ed6b610..8b63a20cc 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -33,11 +33,8 @@ class GeometryParameters(BaseParameters): dynamicTopology : bool = False - - - class Geometry(BasePrefab): - container : Object # This should be more specialized into the right SOFA type + container : Object # This should be more specialized into the right SOFA type modifier : Optional[Object] params : GeometryParameters diff --git a/stlib/geometry/extract.py b/stlib/geometry/extract.py index 8edba4c65..0e7c38c1a 100644 --- a/stlib/geometry/extract.py +++ b/stlib/geometry/extract.py @@ -8,27 +8,32 @@ class ExtractInternalDataProvider(InternalDataProvider): - destElemType : ElementType - fromElemType : ElementType + destElementType : ElementType + fromElemenType : ElementType fromNodeName : str + def __init__(self, destElementType : ElementType, fromElementType : ElementType, fromNodeName : str): + self.destElementType = destElementType, + self.fromElementType = fromElementType, + self.fromNodeName = fromNodeName + def __post_init__(self): - if(not (self.fromElemType == ElementType.TETRA and self.destElemType == ElementType.TRIANGLES) - and not (self.fromElemType == ElementType.HEXA and self.destElemType == ElementType.QUAD) ): - raise ValueError("Only configuration possible are 'Tetra to Triangle' and 'Hexa to quad'") + if(not (self.fromElementType == ElementType.TETRAHEDRONS and self.destElementType == ElementType.TRIANGLES) + and not (self.fromElementType == ElementType.HEXAHEDRONS and self.destElementType == ElementType.QUADS) ): + raise ValueError("Only configuration possible are 'Tetrahedrons to Triangles' and 'Hexahedrons to Quads'") InternalDataProvider.__init__(self) def generateAttribute(self, parent : Geometry): - tmn = parent.addChild("topologicalMappingNode") + tmn = parent.addChild("TopologicalMappingNode") #TODO: Specify somewhere in the doc that this should only be used for mapped topologies that extract parent topology surface fromLink = parent.parents[0].parents[0].getChild(self.fromNodeName).container.linkpath - addDynamicTopology(tmn, type=self.destElemType) - if self.fromElemType == ElementType.TETRA: + addDynamicTopology(tmn, type=self.destElementType) + if self.fromElementType == ElementType.TETRAHEDRONS: tmn.addObject("Tetra2TriangleTopologicalMapping", input=fromLink, output=tmn.container.linkpath) - elif self.fromElemType == ElementType.HEXA: + elif self.fromElementType == ElementType.HEXAHEDRONS: tmn.addObject("Hexa2QuadTopologicalMapping", input=fromLink, output=tmn.container.linkpath) self.position = tmn.container.position.linkpath @@ -41,6 +46,14 @@ def generateAttribute(self, parent : Geometry): class ExtractParameters(GeometryParameters): - def __init__(self, fromGeometry : GeometryParameters, destElementType : ElementType, dynamicTopology = False, ): - GeometryParameters.__init__(data = ExtractInternalDataProvider(destElemType = destElementType, fromElemType = fromGeometry.elementType,fromNodeName = fromGeometry.name), dynamicTopology = dynamicTopology, elementType = destElementType) + def __init__(self, + fromGeometry : GeometryParameters, + destElementType : ElementType, + dynamicTopology = False, ): + GeometryParameters.__init__(self, + data = ExtractInternalDataProvider(destElementType = destElementType, + fromElementType = fromGeometry.elementType, + fromNodeName = fromGeometry.name), + dynamicTopology = dynamicTopology, + elementType = destElementType) diff --git a/stlib/geometry/file.py b/stlib/geometry/file.py index e2e4a60d9..70738b02f 100644 --- a/stlib/geometry/file.py +++ b/stlib/geometry/file.py @@ -12,9 +12,8 @@ class FileInternalDataProvider(InternalDataProvider): def __post_init__(self, **kwargs): InternalDataProvider.__init__(self,**kwargs) - def generateAttribute(self, parent : Geometry): - - loadMesh(parent, self.filename) + def generateAttribute(self, parent : Geometry): + loadMesh(parent, self.filename) self.position = str(parent.loader.position.linkpath) self.edges = str(parent.loader.edges.linkpath) @@ -26,6 +25,9 @@ def generateAttribute(self, parent : Geometry): class FileParameters(GeometryParameters): - def __init__(self, filename, dynamicTopology = False, elementType : ElementType = None ): - GeometryParameters.__init__(self,data = FileInternalDataProvider(filename), dynamicTopology = dynamicTopology, elementType = elementType) + def __init__(self, filename, dynamicTopology = False, elementType : ElementType = None ): + GeometryParameters.__init__(self, + data = FileInternalDataProvider(filename), + dynamicTopology = dynamicTopology, + elementType = elementType) diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py index 28915629b..8ed1f8a67 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/prefabs/collision.py @@ -9,14 +9,10 @@ @dataclasses.dataclass class CollisionParameters(BaseParameters): - primitives : list[CollisionPrimitive] = dataclasses.field(default_factory = lambda :[CollisionPrimitive.POINTS]) + primitives : list[CollisionPrimitive] = dataclasses.field(default_factory = lambda :[CollisionPrimitive.TRIANGLES]) selfCollision : Optional[bool] = DEFAULT_VALUE - proximity : Optional[float] = DEFAULT_VALUE group : Optional[int] = DEFAULT_VALUE - contactStiffness : Optional[float] = DEFAULT_VALUE - contactFriction : Optional[float] = DEFAULT_VALUE - spheresRadius : Optional[float] = DEFAULT_VALUE geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) addMapping : Optional[Callable] = None @@ -28,16 +24,12 @@ def __init__(self, params: CollisionParameters): geom = self.add(Geometry, params.geometry) - self.addObject("MechanicalObject",template="Vec3", position=f"@{params.geometry.name}/container.position") + self.addObject("MechanicalObject", template="Vec3", position=f"@{params.geometry.name}/container.position") for primitive in params.primitives: - addCollisionModels(self,primitive, + addCollisionModels(self, primitive, topology=f"@{params.geometry.name}/container", selfCollision=params.selfCollision, - proximity=params.proximity, group=params.group, - contactStiffness=params.contactStiffness, - contactFriction=params.contactFriction, - spheresRadius=params.spheresRadius, **params.kwargs) if params.addMapping is not None: @@ -50,7 +42,14 @@ def getParameters(**kwargs) -> CollisionParameters: def createScene(root): + root.addObject("VisualStyle", displayFlags="showCollisionModels") + # Create a visual from a mesh file params = Collision.getParameters() params.geometry = FileParameters(filename="mesh/cube.obj") + # Expert parameters + params.kwargs = { + "contactStiffness": 100.0, + "contactFriction": 0.5 + } root.add(Collision, params) diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index 2bf61ed95..70790463e 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -1,7 +1,9 @@ from stlib.core.basePrefab import BasePrefab from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any from stlib.geometry import Geometry, GeometryParameters +from stlib.geometry.extract import ExtractParameters from stlib.geometry.file import FileParameters +from splib.core.enum_types import ElementType from Sofa.Core import Object @dataclasses.dataclass @@ -32,6 +34,15 @@ def getParameters(**kwargs) -> VisualParameters: def createScene(root): # Create a visual from a mesh file - params = Visual.getParameters() + params = Visual.getParameters() + params.name = "VisualFromFile" params.geometry = FileParameters(filename="mesh/cube.obj") root.add(Visual, params) + + + # Create a visual from a mesh file + # params = Visual.getParameters() + # params.name = "ExtractedVisual" + # params.geometry = ExtractParameters(fromGeometry=FileParameters(filename="mesh/cube.vtk"), + # destElementType=ElementType.TRIANGLES) + # root.add(Visual, params) From 995d1b3befe1665b86dd5653f9431ef7ecb84e48 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Mon, 2 Jun 2025 14:57:36 +0200 Subject: [PATCH 25/49] [stlib] adds README --- stlib/README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 stlib/README.md diff --git a/stlib/README.md b/stlib/README.md new file mode 100644 index 000000000..ff1881cd8 --- /dev/null +++ b/stlib/README.md @@ -0,0 +1,42 @@ +# STLIB + +## Terminology + +| Term | Description | +| -------------- | -------------------------------------------------------------- | +| Component* | Element of the scene hierarchy implementing a given behavior | +| ~~Object~~ | A deprecated synonym of a Component | +| Node* | Element of the scene hierarchy holding other Node (often refered as childs) or Objects | +| Data* | Attribute of a Component or a Node | +| Prefab | A Sofa.Node assembling of Objects and Nodes (a "fragment" of a scene) | +| Geometry | A prefab that describe shapes with their topologies (i.e a shape with its space descritization and its associated connectivity) | +| Entity | A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry.| +| Parameters | Every prefab has a set of parameters. These parameters can contain data, links, callable or being composed of other parameters. Some of them can be optional. ~~Must inherit from `stlib.core.baseParameter.BaseParameter` and have a `@dataclasses.dataclass` decorator~~. Must have a `@stlib.parameters` decorator. | + +*Defined in SOFA documentation [here](https://www.sofa-framework.org/doc/using-sofa/terminology). + +## Concepts & Structure + +This library is structured to provide a set of _Prefabs_ that can be used to build complex simulations in SOFA. +Prefabs are reusable fragments of a scene that can be specified through Parameters. +We introduce two different concepts, Prefab and Parameters: +- Prefabs defining the logic of instantiation +- Parameters providing the information (e.g data, links, callable) needed by a Prefab for its own instantiation + +We introduce two types of Prefabs: +- __Geometry__: A prefab that describes shapes with their topologies (i.e a shape with its space discretization and its associated connectivity). +- __Entity__: A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry. + +## Usage + +STLIB has been designed to suit the following levels of use: + +- __Beginners__: + - Create simple simulations using predefined Prefabs. + - Use the provided Prefabs and Parameters without needing to understand the underlying implementation. +- __Intermediate users__: + - Create more complex simulations by combining existing Prefabs. + - Redefine Parameters for their own usage. +- __Advanced users__: + - Create their own Prefabs from scratch or by extending the provided ones. + - Enrich the library with new Prefabs and Parameters. From 01531ca5de7792e8973f23e58a0e5cd20ff127b4 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Mon, 2 Jun 2025 14:58:51 +0200 Subject: [PATCH 26/49] [stlib] minor changes from discussion --- stlib/geometry/extract.py | 13 +++++++------ stlib/prefabs/collision.py | 12 +++++++++--- stlib/prefabs/visual.py | 7 +++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/stlib/geometry/extract.py b/stlib/geometry/extract.py index 0e7c38c1a..b7b17cbc6 100644 --- a/stlib/geometry/extract.py +++ b/stlib/geometry/extract.py @@ -29,6 +29,7 @@ def generateAttribute(self, parent : Geometry): tmn = parent.addChild("TopologicalMappingNode") #TODO: Specify somewhere in the doc that this should only be used for mapped topologies that extract parent topology surface + fromLink = parent.parents[0].parents[0].getChild(self.fromNodeName).container.linkpath addDynamicTopology(tmn, type=self.destElementType) if self.fromElementType == ElementType.TETRAHEDRONS: @@ -47,13 +48,13 @@ def generateAttribute(self, parent : Geometry): class ExtractParameters(GeometryParameters): def __init__(self, - fromGeometry : GeometryParameters, - destElementType : ElementType, + sourceParameters : GeometryParameters, + destinationType : ElementType, dynamicTopology = False, ): GeometryParameters.__init__(self, - data = ExtractInternalDataProvider(destElementType = destElementType, - fromElementType = fromGeometry.elementType, - fromNodeName = fromGeometry.name), + data = ExtractInternalDataProvider(destElementType = sourceParameters, + fromElementType = destinationType, + fromNodeName = destinationType.name), dynamicTopology = dynamicTopology, - elementType = destElementType) + elementType = destinationType) diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py index 8ed1f8a67..7e8704c48 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/prefabs/collision.py @@ -46,10 +46,16 @@ def createScene(root): # Create a visual from a mesh file params = Collision.getParameters() + params.group = 1 params.geometry = FileParameters(filename="mesh/cube.obj") # Expert parameters params.kwargs = { - "contactStiffness": 100.0, - "contactFriction": 0.5 + "TriangleCollisionModel":{"contactStiffness": 100.0, "contactFriction": 0.5} } - root.add(Collision, params) + collision = root.add(Collision, params) + + # OR set the parameters post creation + collision.TriangleCollisionModel.contactStiffness = 100.0 + collision.TriangleCollisionModel.contactFriction = 0.5 + collision.TriangleCollisionModel.set(contactStiffness=100.0, contactFriction=0.5) # we have information of what is possible + collision.TriangleCollisionModel.set({"contactStiffness": 100.0, "contactFriction": 0.5}) # we can do n'importe quoi diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index 70790463e..7752a2db0 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -39,10 +39,9 @@ def createScene(root): params.geometry = FileParameters(filename="mesh/cube.obj") root.add(Visual, params) - - # Create a visual from a mesh file + # # Create a visual from a node # params = Visual.getParameters() # params.name = "ExtractedVisual" - # params.geometry = ExtractParameters(fromGeometry=FileParameters(filename="mesh/cube.vtk"), - # destElementType=ElementType.TRIANGLES) + # params.geometry = ExtractParameters(sourceParameters=FileParameters(filename="mesh/cube.vtk"), + # destinationType=ElementType.TRIANGLES) # root.add(Visual, params) From fc1404ae6863fee2cf40f07dffc61bbbc9a9eaa3 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Tue, 3 Jun 2025 12:54:04 +0200 Subject: [PATCH 27/49] [splib] updates entity --- splib/Testing.py | 4 +- splib/core/enum_types.py | 7 ++- stlib/README.md | 8 ++++ stlib/entities/__entity__.py | 50 ++++++++++----------- stlib/entities/deformable/__deformable__.py | 21 +++++---- stlib/prefabs/behavior.py | 15 +++++++ stlib/prefabs/collision.py | 16 ++++--- 7 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 stlib/prefabs/behavior.py diff --git a/splib/Testing.py b/splib/Testing.py index fc31093cf..ee0f83097 100644 --- a/splib/Testing.py +++ b/splib/Testing.py @@ -57,10 +57,10 @@ def createScene(rootNode): addImplicitODE(SimulatedLiver1) addLinearSolver(SimulatedLiver1,iterative=False, template="CompressedRowSparseMatrixMat3x3") loadMesh(SimulatedLiver1,filename="mesh/liver.msh") - addDynamicTopology(SimulatedLiver1,type=ElementType.TETRA,source="@meshLoader") + addDynamicTopology(SimulatedLiver1,type=ElementType.TETRAHEDRONS,source="@meshLoader") SimulatedLiver1.addObject("MechanicalObject",name="mstate", template='Vec3d') SimulatedLiver1.addObject("LinearSolverConstraintCorrection",name="constraintCorrection") - addLinearElasticity(SimulatedLiver1,ElementType.TETRA, poissonRatio="0.3", youngModulus="3000", method='large') + addLinearElasticity(SimulatedLiver1,ElementType.TETRAHEDRONS, poissonRatio="0.3", youngModulus="3000", method='large') addMass(SimulatedLiver1,template='Vec3d',massDensity="2") addFixation(SimulatedLiver1,ConstraintType.PROJECTIVE,boxROIs=[0, 3, 0, 2, 5, 2]) diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py index 568245734..9f5f05c09 100644 --- a/splib/core/enum_types.py +++ b/splib/core/enum_types.py @@ -1,7 +1,7 @@ from enum import Enum class ConstitutiveLaw(Enum): - LINEAR_COROT = 1 + ELASTIC = 1 HYPERELASTIC = 2 class ODEType(Enum): @@ -35,3 +35,8 @@ class ElementType(Enum): QUADS = 4 TETRAHEDRONS = 5 HEXAHEDRONS = 6 + +class StateType(Enum): + VEC3 = 3 + VEC1 = 1 + RIGID = 7 \ No newline at end of file diff --git a/stlib/README.md b/stlib/README.md index ff1881cd8..2cb668d87 100644 --- a/stlib/README.md +++ b/stlib/README.md @@ -40,3 +40,11 @@ STLIB has been designed to suit the following levels of use: - __Advanced users__: - Create their own Prefabs from scratch or by extending the provided ones. - Enrich the library with new Prefabs and Parameters. + +## Available Parameters + +Each Prefab comes with a set of Parameters, these have been selected when the following criteria are met: +- Data which corresponds to an intraseque property of the Prefab, required for its instantiation +- Data which does not have a default value +- Data which cannot be inferred +- Data which are commun to all possible usage of the Prefab diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index 705a154ab..f65de8df6 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -1,25 +1,29 @@ from stlib.core.baseParameters import BaseParameters +from stlib.prefabs.collision import CollisionParameters, Collision +from stlib.prefabs.visual import VisualParameters, Visual +from stlib.prefabs.behavior import Behavior, BehaviorParameters +from stlib.geometry import Geometry import dataclasses from typing import Callable, Optional, overload, Any from stlib.geometry import GeometryParameters import Sofa + @dataclasses.dataclass class EntityParameters(BaseParameters): name = "Entity" # addSimulation : Callable = Simulation - # addCollisionModel : Callable = CollisionModel - # addVisualModel : Callable = VisualModel - - - #setConstitutiveLaw # : Callable = addBidule #setBoundaryCondition #: Callable = addBidule + + addCollision : Optional[Callable] = lambda x : Collision(CollisionParameters()) + addVisual : Optional[Callable] = lambda x : Visual(VisualParameters()) + geometry : GeometryParameters # mechanical : dict = dataclasses.field(default_factory=dict) - # collision : CollisionModel.Parameters = CollisionModel.Parameters() - # visual : VisualModelParameters = VisualModelParameters() + collision : Optional[CollisionParameters] = None + visual : Optional[VisualParameters] = None # simulation : SimulationParameters = SimulationParameters() @@ -27,9 +31,10 @@ class EntityParameters(BaseParameters): class Entity(Sofa.Core.BaseEntity): # A simulated object - simulation : Simulation - visual : VisualModel - collision : CollisionModel + behavior : Behavior + visual : Visual + collision : Collision + geometry : Geometry parameters : EntityParameters @@ -42,21 +47,16 @@ def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): self.parameters = parameters - self.addMechanicalModel(**parameters.mechanical) - # ???? - self.addSimulation(parameters=parameters.simulation) - self.addVisualModel(parameters=parameters.visual) - self.addCollisionModel() - - def addMechanicalModel(self, **kwargs): - self.addObject("MechanicalObject", **kwargs) + self.add(Geometry, self.parameters.geometry) + self.addBehavior(parameters=parameters.behavior) + self.addVisual(parameters=parameters.visual) + self.addCollision(parameters=parameters.collision) - # ???? - def addSimulation(self, **kwargs): - self.parameters.addSimulation(self, **kwargs) + def addBehavior(self, **kwargs): + self.parameters.addBehavior(self, **kwargs) - def addVisualModel(self, **kwargs): - self.parameters.addVisualModel(self, **kwargs) + def addVisual(self, **kwargs): + self.parameters.addVisual(self, **kwargs) - def addCollisionModel(self): - pass + def addCollision(self, **kwargs): + self.parameters.addCollision(self, **kwargs) diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 3f024b230..05b12d603 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -1,10 +1,15 @@ -from stlib.entities import Entity +from stlib.entities import Entity, EntityParameters from stlib.entities.deformable.__parameters__ import DeformableParameters -from stlib.geometry import Geometry from stlib.prefabs.visual import Visual -from stlib.prefabs.collision import Collision +from stlib.mixins.collision import CollisionMixin +from splib.core.enum_types import ConstitutiveLaw + +class DeformableParameters(EntityParameters): + + constitutiveLaw : ConstitutiveLaw = ConstitutiveLaw.ELASTIC + +class Deformable(CollisionMixin, Entity): -class Deformable(Entity): params : DeformableParameters @staticmethod @@ -13,10 +18,10 @@ def getParameters(**kwargs) -> DeformableParameters: def __init__(self, params : DeformableParameters, **kwargs): - Entity.__init__(self, **kwargs) - self.add(Geometry, params.geometry) + Entity.__init__(self, **kwargs) + self.__addConstitutiveLaw__() - self.add(Collision, params.collision) + self.addCollision(params.collision) #@customizable @@ -28,5 +33,5 @@ def __addConstitutiveLaw__(self): #@customizable def __addVisual__(self): #Extract surface and add identity mapping - self.add(Visual, params.visual) + self.add(Visual, self.params.visual) diff --git a/stlib/prefabs/behavior.py b/stlib/prefabs/behavior.py new file mode 100644 index 000000000..70fe932db --- /dev/null +++ b/stlib/prefabs/behavior.py @@ -0,0 +1,15 @@ +from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any +from splib.core.enum_types import StateType + + +@dataclasses.dataclass +class BehaviorParameters(BaseParameters): + name: str = "Behavior" + + stateType: StateType = StateType.VEC3 + massDensity: float # TODO : discuss with Hugo massDensity/totalMass + + + + + diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py index 7e8704c48..8ab59559b 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/prefabs/collision.py @@ -12,7 +12,9 @@ class CollisionParameters(BaseParameters): primitives : list[CollisionPrimitive] = dataclasses.field(default_factory = lambda :[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()) addMapping : Optional[Callable] = None @@ -49,13 +51,13 @@ def createScene(root): params.group = 1 params.geometry = FileParameters(filename="mesh/cube.obj") # Expert parameters - params.kwargs = { - "TriangleCollisionModel":{"contactStiffness": 100.0, "contactFriction": 0.5} - } + # params.kwargs = { + # "TriangleCollisionModel":{"contactStiffness": 100.0, "contactFriction": 0.5} + # } collision = root.add(Collision, params) # OR set the parameters post creation - collision.TriangleCollisionModel.contactStiffness = 100.0 - collision.TriangleCollisionModel.contactFriction = 0.5 - collision.TriangleCollisionModel.set(contactStiffness=100.0, contactFriction=0.5) # we have information of what is possible - collision.TriangleCollisionModel.set({"contactStiffness": 100.0, "contactFriction": 0.5}) # we can do n'importe quoi + # collision.TriangleCollisionModel.contactStiffness = 100.0 + # collision.TriangleCollisionModel.contactFriction = 0.5 + # collision.TriangleCollisionModel.set(contactStiffness=100.0, contactFriction=0.5) # we have information of what is possible + # collision.TriangleCollisionModel.set({"contactStiffness": 100.0, "contactFriction": 0.5}) # we can do n'importe quoi From dd230b9bf2c182743735face02d6af1c50602cc8 Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Tue, 3 Jun 2025 13:10:40 +0200 Subject: [PATCH 28/49] update README --- stlib/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stlib/README.md b/stlib/README.md index 2cb668d87..fd71c6d1c 100644 --- a/stlib/README.md +++ b/stlib/README.md @@ -10,10 +10,11 @@ | Data* | Attribute of a Component or a Node | | Prefab | A Sofa.Node assembling of Objects and Nodes (a "fragment" of a scene) | | Geometry | A prefab that describe shapes with their topologies (i.e a shape with its space descritization and its associated connectivity) | -| Entity | A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry.| +| Entity | A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry but includes neither a linear solver nor an integration scheme.| | Parameters | Every prefab has a set of parameters. These parameters can contain data, links, callable or being composed of other parameters. Some of them can be optional. ~~Must inherit from `stlib.core.baseParameter.BaseParameter` and have a `@dataclasses.dataclass` decorator~~. Must have a `@stlib.parameters` decorator. | -*Defined in SOFA documentation [here](https://www.sofa-framework.org/doc/using-sofa/terminology). +\*Defined in SOFA documentation [here](https://www.sofa-framework.org/doc/using-sofa/terminology). + ## Concepts & Structure @@ -25,7 +26,8 @@ We introduce two different concepts, Prefab and Parameters: We introduce two types of Prefabs: - __Geometry__: A prefab that describes shapes with their topologies (i.e a shape with its space discretization and its associated connectivity). -- __Entity__: A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry. +- __Entity__: A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry but includes neither a linear solver nor an integration scheme. + ## Usage From 57efa1dc0383656d3a6bff975c485cf013594223 Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Tue, 3 Jun 2025 13:34:51 +0200 Subject: [PATCH 29/49] add init in prefab --- stlib/prefabs/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 stlib/prefabs/__init__.py diff --git a/stlib/prefabs/__init__.py b/stlib/prefabs/__init__.py new file mode 100644 index 000000000..2d2771cfc --- /dev/null +++ b/stlib/prefabs/__init__.py @@ -0,0 +1 @@ +__all__ = ["behavior","collision","visual"] From 938f05b779a7950c8bd5f994daa8d3c31ae89162 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 3 Jun 2025 13:57:08 +0200 Subject: [PATCH 30/49] Add first implem of entity --- stlib/entities/__entity__.py | 43 +++++++++++++++++++++++------------- stlib/prefabs/behavior.py | 4 ++++ stlib/prefabs/collision.py | 3 --- stlib/prefabs/visual.py | 1 - 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index f65de8df6..19e6be231 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -3,9 +3,11 @@ from stlib.prefabs.visual import VisualParameters, Visual from stlib.prefabs.behavior import Behavior, BehaviorParameters from stlib.geometry import Geometry +from stlib.geometry.extract import ExtractParameters import dataclasses from typing import Callable, Optional, overload, Any from stlib.geometry import GeometryParameters +from splib.core.enum_types import StateType import Sofa @@ -13,18 +15,16 @@ class EntityParameters(BaseParameters): name = "Entity" - # addSimulation : Callable = Simulation - #setConstitutiveLaw # : Callable = addBidule - #setBoundaryCondition #: Callable = addBidule + template : StateType + ### QUID addCollision : Optional[Callable] = lambda x : Collision(CollisionParameters()) addVisual : Optional[Callable] = lambda x : Visual(VisualParameters()) geometry : GeometryParameters - # mechanical : dict = dataclasses.field(default_factory=dict) + behavior : BehaviorParameters collision : Optional[CollisionParameters] = None visual : Optional[VisualParameters] = None - # simulation : SimulationParameters = SimulationParameters() @@ -47,16 +47,29 @@ def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): self.parameters = parameters - self.add(Geometry, self.parameters.geometry) - self.addBehavior(parameters=parameters.behavior) - self.addVisual(parameters=parameters.visual) - self.addCollision(parameters=parameters.collision) + self.geometry = self.add(Geometry, self.parameters.geometry) - def addBehavior(self, **kwargs): - self.parameters.addBehavior(self, **kwargs) + ### Check compatilibility of Behavior + if self.parameters.behavior.stateType != self.parameters.template: + print("WARNING: imcompatibility between templates of entity and behavior") + self.parameters.behavior.stateType = self.parameters.template - def addVisual(self, **kwargs): - self.parameters.addVisual(self, **kwargs) + self.behavior = self.add(Behavior,self.parameters.behavior) + + if self.parameters.collision is not None: + self.collision = self.add(Collision,self.parameters.collision) + self.addMapping(self.parameters.collision, self.collision) + + + if self.parameters.visual is not None: + self.visual = self.add(Visual,self.parameters.visual) + self.addMapping(self.parameters.visual, self.visual) - def addCollision(self, **kwargs): - self.parameters.addCollision(self, **kwargs) + def addMapping(self, destParameter, destPrefab): + if( self.parameters.template == StateType.VEC3): + if isinstance(destParameter.geometry,ExtractParameters): + destPrefab.addObject("IdentityMapping", input="@../behavior/", output="@.", template='Vec3,Vec3') + else : + destPrefab.addObject("BarycentricMapping", input="@../behavior/", output="@.", template='Vec3,Vec3') + else: + destPrefab.addObject("RigidMapping", input="@../behavior", output="@.", template='Rigid3,Vec3') diff --git a/stlib/prefabs/behavior.py b/stlib/prefabs/behavior.py index 70fe932db..5e5960e5e 100644 --- a/stlib/prefabs/behavior.py +++ b/stlib/prefabs/behavior.py @@ -1,5 +1,6 @@ from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any from splib.core.enum_types import StateType +from stlib.core.basePrefab import BasePrefab @dataclasses.dataclass @@ -13,3 +14,6 @@ class BehaviorParameters(BaseParameters): +class Behavior(BasePrefab): + def __init__(self, params: BehaviorParameters): + BasePrefab.__init__(self, params) diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py index 8ab59559b..218c0c469 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/prefabs/collision.py @@ -17,7 +17,6 @@ class CollisionParameters(BaseParameters): contactDistance : Optional[float] = DEFAULT_VALUE geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) - addMapping : Optional[Callable] = None class Collision(BasePrefab): @@ -34,8 +33,6 @@ def __init__(self, params: CollisionParameters): group=params.group, **params.kwargs) - if params.addMapping is not None: - params.addMapping(self) @staticmethod def getParameters(**kwargs) -> CollisionParameters: diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index 7752a2db0..2326b5d19 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -12,7 +12,6 @@ class VisualParameters(BaseParameters): texture : Optional[str] = None geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) - addMapping : Optional[Callable] = None class Visual(BasePrefab): From 592c13d96d4eb68c827877fcc39586ed9ee33fff Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Tue, 3 Jun 2025 16:16:11 +0200 Subject: [PATCH 31/49] before the train 3/3 --- splib/core/enum_types.py | 10 ++- splib/mechanics/mass.py | 1 + stlib/entities/__entity__.py | 26 ++++--- stlib/entities/deformable/__deformable__.py | 76 +++++++++++++++------ stlib/entities/deformable/__parameters__.py | 18 ----- stlib/prefabs/behavior.py | 19 ------ stlib/prefabs/material.py | 26 +++++++ 7 files changed, 105 insertions(+), 71 deletions(-) delete mode 100644 stlib/entities/deformable/__parameters__.py delete mode 100644 stlib/prefabs/behavior.py create mode 100644 stlib/prefabs/material.py diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py index 9f5f05c09..eec743076 100644 --- a/splib/core/enum_types.py +++ b/splib/core/enum_types.py @@ -39,4 +39,12 @@ class ElementType(Enum): class StateType(Enum): VEC3 = 3 VEC1 = 1 - RIGID = 7 \ No newline at end of file + RIGID = 7 + + def __str__(self): + if self == StateType.VEC3: + return "Vec3" + if self == StateType.VEC1: + return "Vec1" + if self == StateType.RIGID: + return "Rigid3" diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py index e3fe68fe3..2dc8dbdf7 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -3,6 +3,7 @@ from splib.core.enum_types import ElementType +# TODO : use the massDensity only and deduce totalMass if necessary from it + volume @ReusableMethod def addMass(node,template,totalMass=DEFAULT_VALUE,massDensity=DEFAULT_VALUE,lumping=DEFAULT_VALUE,**kwargs): if (not isDefault(totalMass)) and (not isDefault(massDensity)) : diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index 19e6be231..6304b8d0e 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -1,7 +1,7 @@ from stlib.core.baseParameters import BaseParameters from stlib.prefabs.collision import CollisionParameters, Collision from stlib.prefabs.visual import VisualParameters, Visual -from stlib.prefabs.behavior import Behavior, BehaviorParameters +from stlib.prefabs.material import Material, MaterialParameters from stlib.geometry import Geometry from stlib.geometry.extract import ExtractParameters import dataclasses @@ -22,7 +22,7 @@ class EntityParameters(BaseParameters): addVisual : Optional[Callable] = lambda x : Visual(VisualParameters()) geometry : GeometryParameters - behavior : BehaviorParameters + material : MaterialParameters collision : Optional[CollisionParameters] = None visual : Optional[VisualParameters] = None @@ -31,7 +31,7 @@ class EntityParameters(BaseParameters): class Entity(Sofa.Core.BaseEntity): # A simulated object - behavior : Behavior + material : Material visual : Visual collision : Collision geometry : Geometry @@ -49,12 +49,12 @@ def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): self.geometry = self.add(Geometry, self.parameters.geometry) - ### Check compatilibility of Behavior - if self.parameters.behavior.stateType != self.parameters.template: - print("WARNING: imcompatibility between templates of entity and behavior") - self.parameters.behavior.stateType = self.parameters.template + ### Check compatilibility of Material + if self.parameters.material.stateType != self.parameters.template: + print("WARNING: imcompatibility between templates of both the entity and the material") + self.parameters.material.stateType = self.parameters.template - self.behavior = self.add(Behavior,self.parameters.behavior) + self.material = self.add(Material,self.parameters.material) if self.parameters.collision is not None: self.collision = self.add(Collision,self.parameters.collision) @@ -65,11 +65,15 @@ def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): self.visual = self.add(Visual,self.parameters.visual) self.addMapping(self.parameters.visual, self.visual) + def addMapping(self, destParameter, destPrefab): + + templateString = f'{self.parameters.template},{destParameter.template}' + if( self.parameters.template == StateType.VEC3): if isinstance(destParameter.geometry,ExtractParameters): - destPrefab.addObject("IdentityMapping", input="@../behavior/", output="@.", template='Vec3,Vec3') + destPrefab.addObject("IdentityMapping", input="@../material/", output="@.", template=templateString) else : - destPrefab.addObject("BarycentricMapping", input="@../behavior/", output="@.", template='Vec3,Vec3') + destPrefab.addObject("BarycentricMapping", input="@../material/", output="@.", template=templateString) else: - destPrefab.addObject("RigidMapping", input="@../behavior", output="@.", template='Rigid3,Vec3') + destPrefab.addObject("RigidMapping", input="@../material", output="@.", template=templateString) diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 05b12d603..007bc64f0 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -1,37 +1,69 @@ from stlib.entities import Entity, EntityParameters -from stlib.entities.deformable.__parameters__ import DeformableParameters +from stlib.prefabs.material import Material, MaterialParameters from stlib.prefabs.visual import Visual -from stlib.mixins.collision import CollisionMixin -from splib.core.enum_types import ConstitutiveLaw +from splib.core.enum_types import ConstitutiveLaw, ElementType +from splib.mechanics.linear_elasticity import * +from splib.mechanics.hyperelasticity import * +from splib.mechanics.mass import addMass -class DeformableParameters(EntityParameters): - constitutiveLaw : ConstitutiveLaw = ConstitutiveLaw.ELASTIC +class DeformableBehaviorParameters(MaterialParameters): -class Deformable(CollisionMixin, Entity): + constitutiveLawType : ConstitutiveLaw + elementType : ElementType + parameters : list[float] - params : DeformableParameters + def addMaterial(self, node): - @staticmethod - def getParameters(**kwargs) -> DeformableParameters: - return DeformableParameters(**kwargs) + addMass(node, node.stateType, massDensity=node.massDensity, lumping=node.massLumping) + + # TODO : change this with inheritance + if(self.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): + addHyperelasticity(node,self.elementType, self.parameters) + else: + addLinearElasticity(node,self.elementType, self.parameters[0], self.parameters[1]) + + +# class Deformable(Entity): + +# params : DeformableParameters + +# @staticmethod +# def getParameters(**kwargs) -> DeformableParameters: +# return DeformableParameters(**kwargs) - def __init__(self, params : DeformableParameters, **kwargs): - Entity.__init__(self, **kwargs) +# def __init__(self, params : DeformableParameters, **kwargs): +# Entity.__init__(self, **kwargs) - self.__addConstitutiveLaw__() - self.addCollision(params.collision) +# self.__addConstitutiveLaw__() +# self.addCollision(params.collision) - #@customizable - # Need generic way of defining paramaters (linear/hyper...) - def __addConstitutiveLaw__(self): - self.params.addConstitutiveLaw() +# #@customizable +# # Need generic way of defining paramaters (linear/hyper...) +# def __addConstitutiveLaw__(self): +# self.params.addConstitutiveLaw() - #@customizable - def __addVisual__(self): - #Extract surface and add identity mapping - self.add(Visual, self.params.visual) +# #@customizable +# def __addVisual__(self): +# #Extract surface and add identity mapping +# self.add(Visual, self.params.visual) + + + + + +def createScene(root) : + + from stlib.geometry.file import FileParameters + from stlib.geometry.extract import ExtractParameters + liverParameters = EntityParameters() + liverParameters.behavior = DeformableBehaviorParameters() + liverParameters.behavior.constitutiveLawType = ConstitutiveLaw.ELASTIC + liverParameters.behavior.parameters = [1000, 0.45] + liverParameters.geometry = FileParameters("liver.vtk") + liverParameters.visual = ExtractParameters() + myDeformableObject = root.add(Entity, liverParameters) \ No newline at end of file diff --git a/stlib/entities/deformable/__parameters__.py b/stlib/entities/deformable/__parameters__.py deleted file mode 100644 index d64c29f0e..000000000 --- a/stlib/entities/deformable/__parameters__.py +++ /dev/null @@ -1,18 +0,0 @@ -from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses -from stlib.geometry import GeometryParameters -from stlib.prefabs.visual import VisualParameters -from stlib.prefabs.collision import CollisionParameters - -@dataclasses.dataclass -class DeformableParameters(BaseParameters): - - geometry : GeometryParameters - # Add default value - visual : VisualParameters - collision : CollisionParameters - mass : float = float - - addConstitutiveLaw : Callable = lambda x: x - - def toDict(self): - return dataclasses.asdict(self) diff --git a/stlib/prefabs/behavior.py b/stlib/prefabs/behavior.py deleted file mode 100644 index 5e5960e5e..000000000 --- a/stlib/prefabs/behavior.py +++ /dev/null @@ -1,19 +0,0 @@ -from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any -from splib.core.enum_types import StateType -from stlib.core.basePrefab import BasePrefab - - -@dataclasses.dataclass -class BehaviorParameters(BaseParameters): - name: str = "Behavior" - - stateType: StateType = StateType.VEC3 - massDensity: float # TODO : discuss with Hugo massDensity/totalMass - - - - - -class Behavior(BasePrefab): - def __init__(self, params: BehaviorParameters): - BasePrefab.__init__(self, params) diff --git a/stlib/prefabs/material.py b/stlib/prefabs/material.py new file mode 100644 index 000000000..f3418cd1f --- /dev/null +++ b/stlib/prefabs/material.py @@ -0,0 +1,26 @@ +from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any +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" + + stateType: StateType = StateType.VEC3 + massDensity: float + massLumping: bool + + addMaterial : Optional[Callable] = lambda node : addMass(node, node.stateType, massDensity=node.massDensity, lumping=node.massLumping) + + +# TODO : previously called Behavior +class Material(BasePrefab): + def __init__(self, params: MaterialParameters): + BasePrefab.__init__(self, params) + + self.params = params + self.addObject("MechanicalObject", name="States", template=str(self.params.stateType)) + + if(params.addMaterial is not None): + params.addMaterial(self) From ca864d89e0d30cb7878020f3a3e206513cb676b9 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Tue, 3 Jun 2025 16:23:22 +0200 Subject: [PATCH 32/49] very last commit befor train --- stlib/entities/__init__.py | 2 +- stlib/entities/deformable/__deformable__.py | 4 ++-- stlib/prefabs/material.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/stlib/entities/__init__.py b/stlib/entities/__init__.py index a207a568f..bfad7c6c5 100644 --- a/stlib/entities/__init__.py +++ b/stlib/entities/__init__.py @@ -1 +1 @@ -from __entity__ import * \ No newline at end of file +from .__entity__ import * \ No newline at end of file diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 007bc64f0..229299a5c 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -58,12 +58,12 @@ def addMaterial(self, node): def createScene(root) : from stlib.geometry.file import FileParameters - from stlib.geometry.extract import ExtractParameters + # from stlib.geometry.extract import ExtractParameters liverParameters = EntityParameters() liverParameters.behavior = DeformableBehaviorParameters() liverParameters.behavior.constitutiveLawType = ConstitutiveLaw.ELASTIC liverParameters.behavior.parameters = [1000, 0.45] liverParameters.geometry = FileParameters("liver.vtk") - liverParameters.visual = ExtractParameters() + # liverParameters.visual = ExtractParameters() myDeformableObject = root.add(Entity, liverParameters) \ No newline at end of file diff --git a/stlib/prefabs/material.py b/stlib/prefabs/material.py index f3418cd1f..c4313244b 100644 --- a/stlib/prefabs/material.py +++ b/stlib/prefabs/material.py @@ -7,9 +7,10 @@ class MaterialParameters(BaseParameters): name: str = "Material" - stateType: StateType = StateType.VEC3 massDensity: float massLumping: bool + + stateType: StateType = StateType.VEC3 addMaterial : Optional[Callable] = lambda node : addMass(node, node.stateType, massDensity=node.massDensity, lumping=node.massLumping) From 1d8cdd3c90373dc94a8348002cedf9284fd355b7 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 3 Jun 2025 16:53:50 +0200 Subject: [PATCH 33/49] WIP --- splib/core/enum_types.py | 1 + stlib/entities/__entity__.py | 13 ++++++------- stlib/entities/deformable/__deformable__.py | 18 ++++++++++-------- stlib/prefabs/material.py | 8 +++++--- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py index eec743076..d27bd458c 100644 --- a/splib/core/enum_types.py +++ b/splib/core/enum_types.py @@ -48,3 +48,4 @@ def __str__(self): return "Vec1" if self == StateType.RIGID: return "Rigid3" + diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index 6304b8d0e..f17375329 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -9,26 +9,27 @@ from stlib.geometry import GeometryParameters from splib.core.enum_types import StateType import Sofa +from stlib.core.basePrefab import BasePrefab @dataclasses.dataclass class EntityParameters(BaseParameters): name = "Entity" - template : StateType + template : StateType = None ### QUID addCollision : Optional[Callable] = lambda x : Collision(CollisionParameters()) addVisual : Optional[Callable] = lambda x : Visual(VisualParameters()) - geometry : GeometryParameters - material : MaterialParameters + geometry : GeometryParameters = None + material : MaterialParameters = None collision : Optional[CollisionParameters] = None visual : Optional[VisualParameters] = None -class Entity(Sofa.Core.BaseEntity): +class Entity(BasePrefab): # A simulated object material : Material @@ -39,11 +40,9 @@ class Entity(Sofa.Core.BaseEntity): parameters : EntityParameters - def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): + def __init__(self, parameters=EntityParameters(), **kwargs): Sofa.Core.Node.__init__(self, name=parameters.name) - if parent is not None: - parent.addChild(self) self.parameters = parameters diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 229299a5c..7213d6a74 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -1,7 +1,7 @@ from stlib.entities import Entity, EntityParameters from stlib.prefabs.material import Material, MaterialParameters from stlib.prefabs.visual import Visual -from splib.core.enum_types import ConstitutiveLaw, ElementType +from splib.core.enum_types import ConstitutiveLaw, ElementType, StateType from splib.mechanics.linear_elasticity import * from splib.mechanics.hyperelasticity import * from splib.mechanics.mass import addMass @@ -9,13 +9,13 @@ class DeformableBehaviorParameters(MaterialParameters): - constitutiveLawType : ConstitutiveLaw - elementType : ElementType - parameters : list[float] + constitutiveLawType : ConstitutiveLaw = None + elementType : ElementType = None + parameters : list[float] = None def addMaterial(self, node): - addMass(node, node.stateType, massDensity=node.massDensity, lumping=node.massLumping) + addMass(node, str(node.stateType), massDensity=node.massDensity, lumping=node.massLumping) # TODO : change this with inheritance if(self.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): @@ -61,9 +61,11 @@ def createScene(root) : # from stlib.geometry.extract import ExtractParameters liverParameters = EntityParameters() - liverParameters.behavior = DeformableBehaviorParameters() - liverParameters.behavior.constitutiveLawType = ConstitutiveLaw.ELASTIC - liverParameters.behavior.parameters = [1000, 0.45] + liverParameters.template = StateType.VEC3 + liverParameters.material = DeformableBehaviorParameters() + liverParameters.material.stateType = StateType.VEC3 + liverParameters.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + liverParameters.material.parameters = [1000, 0.45] liverParameters.geometry = FileParameters("liver.vtk") # liverParameters.visual = ExtractParameters() myDeformableObject = root.add(Entity, liverParameters) \ No newline at end of file diff --git a/stlib/prefabs/material.py b/stlib/prefabs/material.py index c4313244b..cfc374286 100644 --- a/stlib/prefabs/material.py +++ b/stlib/prefabs/material.py @@ -1,5 +1,5 @@ from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any -from splib.core.enum_types import StateType +from splib.core.enum_types import * from stlib.core.basePrefab import BasePrefab from splib.mechanics.mass import addMass @@ -7,8 +7,8 @@ class MaterialParameters(BaseParameters): name: str = "Material" - massDensity: float - massLumping: bool + massDensity: float = 0 + massLumping: bool = False stateType: StateType = StateType.VEC3 @@ -21,6 +21,8 @@ def __init__(self, params: MaterialParameters): BasePrefab.__init__(self, params) self.params = params + + print(params.stateType.toString()) self.addObject("MechanicalObject", name="States", template=str(self.params.stateType)) if(params.addMaterial is not None): From bacb2589193fc964ecbb65f1033d7e09aeafa415 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 3 Jun 2025 17:07:13 +0200 Subject: [PATCH 34/49] Retore original. Still crash due to wrong string conversion --- splib/core/enum_types.py | 2 +- stlib/prefabs/material.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py index d27bd458c..8968e2f5c 100644 --- a/splib/core/enum_types.py +++ b/splib/core/enum_types.py @@ -48,4 +48,4 @@ def __str__(self): return "Vec1" if self == StateType.RIGID: return "Rigid3" - + return 'Unknown' diff --git a/stlib/prefabs/material.py b/stlib/prefabs/material.py index cfc374286..3102f0e3c 100644 --- a/stlib/prefabs/material.py +++ b/stlib/prefabs/material.py @@ -22,7 +22,6 @@ def __init__(self, params: MaterialParameters): self.params = params - print(params.stateType.toString()) self.addObject("MechanicalObject", name="States", template=str(self.params.stateType)) if(params.addMaterial is not None): From 09f45c5985a5aed388502fb5b958f140a7349666 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Fri, 13 Jun 2025 14:19:29 +0200 Subject: [PATCH 35/49] [stlib] Deformable Entity (#502) * working deformable entity * [splib] fixes match case * [stlib] WIP on extract.py --- splib/Testing.py | 4 +- splib/core/enum_types.py | 4 +- splib/mechanics/hyperelasticity.py | 2 +- splib/mechanics/linear_elasticity.py | 18 ++--- splib/mechanics/mass.py | 10 +-- splib/topology/dynamic.py | 12 +-- splib/topology/loader.py | 2 +- splib/topology/static.py | 2 +- stlib/README.md | 10 +-- stlib/core/baseParameters.py | 2 +- stlib/entities/__entity__.py | 45 ++++++----- stlib/entities/deformable/__deformable__.py | 85 ++++++++------------- stlib/geometry/__geometry__.py | 52 ++++++++----- stlib/geometry/extract.py | 69 ++++++++++------- stlib/geometry/file.py | 5 +- stlib/misc/entity.py | 2 +- stlib/misc/softrobots.py | 4 +- stlib/misc/test-1.py | 30 ++++---- stlib/misc/test2.py | 34 ++++----- stlib/prefabs/collision.py | 30 ++++---- stlib/prefabs/material.py | 26 ++++--- stlib/prefabs/visual.py | 37 ++++----- 22 files changed, 242 insertions(+), 243 deletions(-) diff --git a/splib/Testing.py b/splib/Testing.py index ee0f83097..146b4096b 100644 --- a/splib/Testing.py +++ b/splib/Testing.py @@ -57,10 +57,10 @@ def createScene(rootNode): addImplicitODE(SimulatedLiver1) addLinearSolver(SimulatedLiver1,iterative=False, template="CompressedRowSparseMatrixMat3x3") loadMesh(SimulatedLiver1,filename="mesh/liver.msh") - addDynamicTopology(SimulatedLiver1,type=ElementType.TETRAHEDRONS,source="@meshLoader") + addDynamicTopology(SimulatedLiver1,type=ElementType.TETRAHEDRA,source="@meshLoader") SimulatedLiver1.addObject("MechanicalObject",name="mstate", template='Vec3d') SimulatedLiver1.addObject("LinearSolverConstraintCorrection",name="constraintCorrection") - addLinearElasticity(SimulatedLiver1,ElementType.TETRAHEDRONS, poissonRatio="0.3", youngModulus="3000", method='large') + addLinearElasticity(SimulatedLiver1,ElementType.TETRAHEDRA, poissonRatio="0.3", youngModulus="3000", method='large') addMass(SimulatedLiver1,template='Vec3d',massDensity="2") addFixation(SimulatedLiver1,ConstraintType.PROJECTIVE,boxROIs=[0, 3, 0, 2, 5, 2]) diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py index 8968e2f5c..5d4504311 100644 --- a/splib/core/enum_types.py +++ b/splib/core/enum_types.py @@ -33,8 +33,8 @@ class ElementType(Enum): EDGES = 2 TRIANGLES = 3 QUADS = 4 - TETRAHEDRONS = 5 - HEXAHEDRONS = 6 + TETRAHEDRA = 5 + HEXAHEDRA = 6 class StateType(Enum): VEC3 = 3 diff --git a/splib/mechanics/hyperelasticity.py b/splib/mechanics/hyperelasticity.py index 27c306358..fb255df41 100644 --- a/splib/mechanics/hyperelasticity.py +++ b/splib/mechanics/hyperelasticity.py @@ -6,7 +6,7 @@ @ReusableMethod def addHyperelasticity(node,elem:ElementType,materialName=DEFAULT_VALUE, parameterSet=DEFAULT_VALUE, matrixRegularization=DEFAULT_VALUE,**kwargs): match elem: - case ElementType.TETRAHEDRONS: + case ElementType.TETRAHEDRA: node.addObject("TetrahedronHyperelasticityFEMForceField",name="constitutiveLaw", materialName=materialName, parameterSet=parameterSet, matrixRegularization=matrixRegularization, **kwargs) return case _: diff --git a/splib/mechanics/linear_elasticity.py b/splib/mechanics/linear_elasticity.py index 33de61895..1e9b30290 100644 --- a/splib/mechanics/linear_elasticity.py +++ b/splib/mechanics/linear_elasticity.py @@ -4,22 +4,22 @@ @ReusableMethod -def addLinearElasticity(node,elem:ElementType,youngModulus=DEFAULT_VALUE, poissonRatio=DEFAULT_VALUE, method=DEFAULT_VALUE,**kwargs): - match elem: +def addLinearElasticity(node, elementType:ElementType, youngModulus=DEFAULT_VALUE, poissonRatio=DEFAULT_VALUE, method=DEFAULT_VALUE, **kwargs): + match elementType: case ElementType.EDGES: - node.addObject("BeamFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) + node.addObject("BeamFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) return case ElementType.TRIANGLES: - node.addObject("TriangleFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + node.addObject("TriangleFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) return case ElementType.QUADS: - node.addObject("QuadBendingFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + node.addObject("QuadBendingFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) return - case ElementType.TETRAHEDRONS: - node.addObject("TetrahedronFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + case ElementType.TETRAHEDRA: + node.addObject("TetrahedronFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) return - case ElementType.HEXAHEDRONS: - node.addObject("HexahedronFEMForceField",name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method,**kwargs) + case ElementType.HEXAHEDRA: + node.addObject("HexahedronFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) return case _: print('Linear elasticity is only available for topology of type EDGES, TRIANGLES, QUADS, TETRAHEDRON, HEXAHEDRON') diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py index 2dc8dbdf7..917fc9365 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -5,15 +5,15 @@ # TODO : use the massDensity only and deduce totalMass if necessary from it + volume @ReusableMethod -def addMass(node,template,totalMass=DEFAULT_VALUE,massDensity=DEFAULT_VALUE,lumping=DEFAULT_VALUE,**kwargs): +def addMass(node, template, totalMass=DEFAULT_VALUE, massDensity=DEFAULT_VALUE, lumping=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") kwargs.pop('massDensity') - if(template=="Rigid3"): - node.addObject("UniformMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) - else: - node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) + # if(template=="Rigid3"): + node.addObject("UniformMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) + # else: + # node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) diff --git a/splib/topology/dynamic.py b/splib/topology/dynamic.py index c84bd1850..1f18be29d 100644 --- a/splib/topology/dynamic.py +++ b/splib/topology/dynamic.py @@ -41,9 +41,8 @@ def addHexahedronTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,quads= node.addObject("HexahedronSetTopologyContainer", name="container", src=source, position=position, edges=edges, quads=quads, hexahedra=hexahedra, **kwargs) # node.addObject("HexahedronSetGeometryAlgorithms", name="algorithms",**kwargs) -def addDynamicTopology(node,type:ElementType,**kwargs): - - match type: +def addDynamicTopology(node, elementType:ElementType, **kwargs): + match elementType: case ElementType.POINTS: addPointTopology(node,**kwargs) return @@ -56,12 +55,13 @@ def addDynamicTopology(node,type:ElementType,**kwargs): case ElementType.QUADS: addQuadTopology(node,**kwargs) return - case ElementType.TETRAHEDRONS: + case ElementType.TETRAHEDRA: addTetrahedronTopology(node,**kwargs) return - case ElementType.HEXAHEDRONS: + case ElementType.HEXAHEDRA: addHexahedronTopology(node,**kwargs) return case _: - print('Topology type should be one of the following : "ElementType.POINTS, ElementType.EDGES, ElementType.TRIANGLES, ElementType.QUADS, ElementType.TETRAHEDRONS, ElementType.HEXAHEDRONS" ') + print('Topology type should be one of the following : "ElementType.POINTS, ElementType.EDGES, ElementType.TRIANGLES, ElementType.QUADS, ElementType.TETRAHEDRA, ElementType.HEXAHEDRA" ') return + \ No newline at end of file diff --git a/splib/topology/loader.py b/splib/topology/loader.py index b040491aa..c4f54b01f 100644 --- a/splib/topology/loader.py +++ b/splib/topology/loader.py @@ -13,7 +13,7 @@ def loadMesh(node,filename,**kwargs): elif splitedName[-1] == "sph": return node.addObject("SphereLoader", name="loader",filename=filename, **kwargs) else: - return node.addObject("Mesh"+splitedName[-1].upper()+"Loader", name="loader",filename=filename, **kwargs) + return node.addObject("Mesh"+splitedName[-1].upper()+"Loader", name="loader", filename=filename, **kwargs) else: print('[Error] : File extension ' + splitedName[-1] + ' not recognised.') diff --git a/splib/topology/static.py b/splib/topology/static.py index 2e49fcf95..591460425 100644 --- a/splib/topology/static.py +++ b/splib/topology/static.py @@ -2,6 +2,6 @@ from splib.core.utils import DEFAULT_VALUE @ReusableMethod -def addStaticTopology(node,source=DEFAULT_VALUE,**kwargs): +def addStaticTopology(node, source=DEFAULT_VALUE, **kwargs): node.addObject("MeshTopology", name="container", src=source, **kwargs) diff --git a/stlib/README.md b/stlib/README.md index fd71c6d1c..c4e7dd901 100644 --- a/stlib/README.md +++ b/stlib/README.md @@ -6,12 +6,12 @@ | -------------- | -------------------------------------------------------------- | | Component* | Element of the scene hierarchy implementing a given behavior | | ~~Object~~ | A deprecated synonym of a Component | -| Node* | Element of the scene hierarchy holding other Node (often refered as childs) or Objects | +| Node* | Element of the scene hierarchy holding other Nodes (often refered as childs) or Components | | Data* | Attribute of a Component or a Node | -| Prefab | A Sofa.Node assembling of Objects and Nodes (a "fragment" of a scene) | -| Geometry | A prefab that describe shapes with their topologies (i.e a shape with its space descritization and its associated connectivity) | -| Entity | A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry but includes neither a linear solver nor an integration scheme.| -| Parameters | Every prefab has a set of parameters. These parameters can contain data, links, callable or being composed of other parameters. Some of them can be optional. ~~Must inherit from `stlib.core.baseParameter.BaseParameter` and have a `@dataclasses.dataclass` decorator~~. Must have a `@stlib.parameters` decorator. | +| Prefab | A Node assembling of Components and Nodes (a "fragment" of a scene) | +| Geometry | A Prefab that describes shapes with their topologies (i.e a shapes with their space descritization and their associated connectivity) | +| Entity | A physical Prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry but includes neither a linear solver nor an integration scheme.| +| Parameters | Every Prefab has a set of parameters. These parameters can contain data, links, callable or being composed of other parameters. Some of them can be optional. ~~Must inherit from `stlib.core.baseParameter.BaseParameter` and have a `@dataclasses.dataclass` decorator~~. Must have a `@stlib.parameters` decorator. | \*Defined in SOFA documentation [here](https://www.sofa-framework.org/doc/using-sofa/terminology). diff --git a/stlib/core/baseParameters.py b/stlib/core/baseParameters.py index 883b17b5b..be7a633b8 100644 --- a/stlib/core/baseParameters.py +++ b/stlib/core/baseParameters.py @@ -6,7 +6,7 @@ @dataclasses.dataclass class BaseParameters(object): - name : str = "object" + name : str = "Object" kwargs : dict = dataclasses.field(default_factory=dict) def toDict(self): diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index f17375329..f39d8b4ed 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -1,4 +1,5 @@ from stlib.core.baseParameters import BaseParameters +from stlib.core.basePrefab import BasePrefab from stlib.prefabs.collision import CollisionParameters, Collision from stlib.prefabs.visual import VisualParameters, Visual from stlib.prefabs.material import Material, MaterialParameters @@ -14,9 +15,9 @@ @dataclasses.dataclass class EntityParameters(BaseParameters): - name = "Entity" + name : str = "Entity" - template : StateType = None + stateType : StateType = StateType.VEC3 ### QUID addCollision : Optional[Callable] = lambda x : Collision(CollisionParameters()) @@ -41,38 +42,40 @@ class Entity(BasePrefab): def __init__(self, parameters=EntityParameters(), **kwargs): - Sofa.Core.Node.__init__(self, name=parameters.name) - - + BasePrefab.__init__(self, parameters) self.parameters = parameters self.geometry = self.add(Geometry, self.parameters.geometry) ### Check compatilibility of Material - if self.parameters.material.stateType != self.parameters.template: + 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.template + self.parameters.material.stateType = self.parameters.stateType - self.material = self.add(Material,self.parameters.material) + self.material = self.add(Material, self.parameters.material) + self.material.States.position.parent = self.geometry.container.position.linkpath if self.parameters.collision is not None: - self.collision = self.add(Collision,self.parameters.collision) - self.addMapping(self.parameters.collision, self.collision) - + self.collision = self.add(Collision, self.parameters.collision) + self.addMapping(self.collision) if self.parameters.visual is not None: - self.visual = self.add(Visual,self.parameters.visual) - self.addMapping(self.parameters.visual, self.visual) + self.visual = self.add(Visual, self.parameters.visual) + self.addMapping(self.visual) - def addMapping(self, destParameter, destPrefab): + def addMapping(self, destinationPrefab): - templateString = f'{self.parameters.template},{destParameter.template}' + template = f'{self.parameters.stateType},Vec3' # TODO: check that it is always true - if( self.parameters.template == StateType.VEC3): - if isinstance(destParameter.geometry,ExtractParameters): - destPrefab.addObject("IdentityMapping", input="@../material/", output="@.", template=templateString) - else : - destPrefab.addObject("BarycentricMapping", input="@../material/", output="@.", template=templateString) + if( self.parameters.stateType == StateType.VEC3): + destinationPrefab.addObject("BarycentricMapping", + input="@../Material/", + input_topology=destinationPrefab.geometry.container.linkpath, + output="@.", + template=template) else: - destPrefab.addObject("RigidMapping", input="@../material", output="@.", template=templateString) + destinationPrefab.addObject("RigidMapping", + input="@../Material", + output="@.", + template=template) diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 7213d6a74..1c73b2d9a 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -1,71 +1,46 @@ -from stlib.entities import Entity, EntityParameters -from stlib.prefabs.material import Material, MaterialParameters -from stlib.prefabs.visual import Visual -from splib.core.enum_types import ConstitutiveLaw, ElementType, StateType +from stlib.prefabs.material import MaterialParameters +from splib.core.enum_types import ConstitutiveLaw, ElementType +from stlib.core.baseParameters import Callable, Optional, dataclasses from splib.mechanics.linear_elasticity import * from splib.mechanics.hyperelasticity import * from splib.mechanics.mass import addMass +@dataclasses.dataclass class DeformableBehaviorParameters(MaterialParameters): - constitutiveLawType : ConstitutiveLaw = None - elementType : ElementType = None - parameters : list[float] = None - - def addMaterial(self, node): - - addMass(node, str(node.stateType), massDensity=node.massDensity, lumping=node.massLumping) + constitutiveLawType : ConstitutiveLaw = ConstitutiveLaw.ELASTIC + elementType : ElementType = ElementType.TETRAHEDRA + parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio + def addDeformableMaterial(node): + addMass(node, node.parameters.stateType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) # TODO : change this with inheritance - if(self.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): - addHyperelasticity(node,self.elementType, self.parameters) + if(node.parameters.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): + addHyperelasticity(node, node.parameters.elementType, node.parameters.parameters, topology="@../Geometry/container") else: - addLinearElasticity(node,self.elementType, self.parameters[0], self.parameters[1]) - - -# class Deformable(Entity): - -# params : DeformableParameters - -# @staticmethod -# def getParameters(**kwargs) -> DeformableParameters: -# return DeformableParameters(**kwargs) - - -# def __init__(self, params : DeformableParameters, **kwargs): -# Entity.__init__(self, **kwargs) - -# self.__addConstitutiveLaw__() -# self.addCollision(params.collision) - - -# #@customizable -# # Need generic way of defining paramaters (linear/hyper...) -# def __addConstitutiveLaw__(self): -# self.params.addConstitutiveLaw() - - -# #@customizable -# def __addVisual__(self): -# #Extract surface and add identity mapping -# self.add(Visual, self.params.visual) - - + addLinearElasticity(node, node.parameters.elementType, node.parameters.parameters[0], node.parameters.parameters[1], topology="@../Geometry/container") + addMaterial : Optional[Callable] = addDeformableMaterial def createScene(root) : - + from stlib.entities import Entity, EntityParameters + from stlib.prefabs.visual import VisualParameters + from stlib.geometry.extract import ExtractParameters from stlib.geometry.file import FileParameters - # from stlib.geometry.extract import ExtractParameters - liverParameters = EntityParameters() - liverParameters.template = StateType.VEC3 - liverParameters.material = DeformableBehaviorParameters() - liverParameters.material.stateType = StateType.VEC3 - liverParameters.material.constitutiveLawType = ConstitutiveLaw.ELASTIC - liverParameters.material.parameters = [1000, 0.45] - liverParameters.geometry = FileParameters("liver.vtk") - # liverParameters.visual = ExtractParameters() - myDeformableObject = root.add(Entity, liverParameters) \ No newline at end of file + 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, bunnyParameters) \ No newline at end of file diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index 8b63a20cc..085215da8 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -13,11 +13,11 @@ class Geometry(BasePrefab):... class InternalDataProvider(object): position : Any = None # Topology information - edges : Any = DEFAULT_VALUE - triangles : Any = DEFAULT_VALUE - quads : Any = DEFAULT_VALUE - tetrahedra : Any = DEFAULT_VALUE - hexahedra : Any = DEFAULT_VALUE + edges : Any = DEFAULT_VALUE + triangles : Any = DEFAULT_VALUE + quads : Any = DEFAULT_VALUE + tetrahedra : Any = DEFAULT_VALUE + hexahedra : Any = DEFAULT_VALUE def generateAttribute(self, parent : Geometry): pass @@ -25,6 +25,7 @@ def generateAttribute(self, parent : Geometry): @dataclasses.dataclass class GeometryParameters(BaseParameters): + name : str = "Geometry" # Type of the highest degree element elementType : Optional[ElementType] = None @@ -34,20 +35,33 @@ class GeometryParameters(BaseParameters): class Geometry(BasePrefab): - container : Object # This should be more specialized into the right SOFA type - modifier : Optional[Object] - - params : GeometryParameters - - def __init__(self, params: GeometryParameters): - BasePrefab.__init__(self, params) - self.params = params - if params.data is not None : - params.data.generateAttribute(self) - if(params.dynamicTopology): - if(params.elementType is not None): - addDynamicTopology(self, container = dataclasses.asdict(params.data)) + # 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) + + self.parameters = parameters + + # Generate attribute (positions, edges, triangles, quads, tetrahedra, hexahedra) from the internal data provider + if parameters.data is not None : + parameters.data.generateAttribute(self) + if parameters.dynamicTopology : + if parameters.elementType is not None : + addDynamicTopology(self, container = dataclasses.asdict(parameters.data)) else: raise ValueError else: - addStaticTopology(self, container = dataclasses.asdict(params.data)) + addStaticTopology(self, + container = + { + "position": parameters.data.position, + "edges": parameters.data.edges, + "triangles": parameters.data.triangles, + "quads": parameters.data.quads, + "tetrahedra": parameters.data.tetrahedra, + "hexahedra": parameters.data.hexahedra + }) diff --git a/stlib/geometry/extract.py b/stlib/geometry/extract.py index b7b17cbc6..c20249666 100644 --- a/stlib/geometry/extract.py +++ b/stlib/geometry/extract.py @@ -4,45 +4,56 @@ from splib.topology.loader import loadMesh from splib.core.enum_types import ElementType +import Sofa from Sofa.Core import Node class ExtractInternalDataProvider(InternalDataProvider): - destElementType : ElementType - fromElemenType : ElementType - fromNodeName : str + destinationType : ElementType + sourceType : ElementType + sourceName : str - def __init__(self, destElementType : ElementType, fromElementType : ElementType, fromNodeName : str): - self.destElementType = destElementType, - self.fromElementType = fromElementType, - self.fromNodeName = fromNodeName + def __init__(self, destinationType : ElementType, sourceType : ElementType, sourceName : str): + self.destinationType = destinationType + self.sourceType = sourceType + self.sourceName = sourceName def __post_init__(self): - if(not (self.fromElementType == ElementType.TETRAHEDRONS and self.destElementType == ElementType.TRIANGLES) - and not (self.fromElementType == ElementType.HEXAHEDRONS and self.destElementType == ElementType.QUADS) ): - raise ValueError("Only configuration possible are 'Tetrahedrons to Triangles' and 'Hexahedrons to Quads'") + 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): - tmn = parent.addChild("TopologicalMappingNode") + node = parent.addChild("ExtractedGeometry") #TODO: Specify somewhere in the doc that this should only be used for mapped topologies that extract parent topology surface - - fromLink = parent.parents[0].parents[0].getChild(self.fromNodeName).container.linkpath - addDynamicTopology(tmn, type=self.destElementType) - if self.fromElementType == ElementType.TETRAHEDRONS: - tmn.addObject("Tetra2TriangleTopologicalMapping", input=fromLink, output=tmn.container.linkpath) - elif self.fromElementType == ElementType.HEXAHEDRONS: - tmn.addObject("Hexa2QuadTopologicalMapping", input=fromLink, output=tmn.container.linkpath) + # fromLink = parent.parents[0].parents[0].getChild(self.SourceName).container.linkpath + # TODO: the line above cannot work if the nodes and objects are not added to the graph prior the end of __init__() call + # !!! also, on a fail, nothing is added to the graph, which makes things harder to debug + # !!! also, does not work because of the function canCreate(), which checks the input (not yet created?) + # this is all related + fromLink = "@../../Geometry.container" # TODO: can we do better than this? + addDynamicTopology(node, elementType=self.sourceType) + if self.sourceType == ElementType.TETRAHEDRA: + node.addObject("Tetra2TriangleTopologicalMapping", input=fromLink, output=node.container.linkpath) + elif self.sourceType == ElementType.HEXAHEDRA: + node.addObject("Hexa2QuadTopologicalMapping", input=fromLink, output=node.container.linkpath) + else: + Sofa.msg_error("[stlib/geometry/exctrat.py]", "Element type: " + str(self.sourceType) + " not supported.") - self.position = tmn.container.position.linkpath - self.edges = tmn.container.edges.linkpath - self.triangles = tmn.container.triangles.linkpath - self.quads = tmn.container.quads.linkpath - self.hexahedra = tmn.container.hexahedra.linkpath - self.tetrahedra = tmn.container.tetras.linkpath + self.position = node.container.position.linkpath + if node.container.findData("edges") is not None: + self.edges = node.container.edges.linkpath + if node.container.findData("triangles") is not None: + self.triangles = node.container.triangles.linkpath + if node.container.findData("quads") is not None: + self.quads = node.container.quads.linkpath + if node.container.findData("hexahedra") is not None: + self.hexahedra = node.container.hexahedra.linkpath + if node.container.findData("tetras") is not None: + self.tetrahedra = node.container.tetras.linkpath @@ -50,11 +61,11 @@ class ExtractParameters(GeometryParameters): def __init__(self, sourceParameters : GeometryParameters, destinationType : ElementType, - dynamicTopology = False, ): + dynamicTopology : bool = False, ): GeometryParameters.__init__(self, - data = ExtractInternalDataProvider(destElementType = sourceParameters, - fromElementType = destinationType, - fromNodeName = destinationType.name), + data = ExtractInternalDataProvider(destinationType = destinationType, + sourceType = sourceParameters.elementType, + sourceName = sourceParameters.name), dynamicTopology = dynamicTopology, elementType = destinationType) diff --git a/stlib/geometry/file.py b/stlib/geometry/file.py index 70738b02f..cc51c077d 100644 --- a/stlib/geometry/file.py +++ b/stlib/geometry/file.py @@ -12,7 +12,7 @@ class FileInternalDataProvider(InternalDataProvider): def __post_init__(self, **kwargs): InternalDataProvider.__init__(self,**kwargs) - def generateAttribute(self, parent : Geometry): + def generateAttribute(self, parent : Geometry): loadMesh(parent, self.filename) self.position = str(parent.loader.position.linkpath) @@ -25,9 +25,10 @@ def generateAttribute(self, parent : Geometry): class FileParameters(GeometryParameters): + def __init__(self, filename, dynamicTopology = False, elementType : ElementType = None ): GeometryParameters.__init__(self, - data = FileInternalDataProvider(filename), + data = FileInternalDataProvider(filename=filename), dynamicTopology = dynamicTopology, elementType = elementType) diff --git a/stlib/misc/entity.py b/stlib/misc/entity.py index fbf702439..a3549b083 100644 --- a/stlib/misc/entity.py +++ b/stlib/misc/entity.py @@ -66,7 +66,7 @@ def addMapping(self, **kwargs): self.addObject("RigidMapping", name="mapping", **kwargs) class CollisionModel(Sofa.Core.BasePrefab): - def __init__(self, params, **kwargs): + def __init__(self, parameters, **kwargs): Sofa.Core.Node.__init__(self, **kwargs) class Parameters(object): diff --git a/stlib/misc/softrobots.py b/stlib/misc/softrobots.py index d8e7871a7..aca8ab50e 100644 --- a/stlib/misc/softrobots.py +++ b/stlib/misc/softrobots.py @@ -12,10 +12,10 @@ class Trunk(Sofa.Core.BasePrefab): body : Entity.Deformable cables : list [SoftRobots.Cable] - def __init__(self, params): + def __init__(self, parameters): body = Entity.Deformable() - for param in range(params.cables): + for param in range(parameters.cables): cables.append(SoftRobots.Cable(body, param)) class Parameters(object): diff --git a/stlib/misc/test-1.py b/stlib/misc/test-1.py index 289f1ff02..2b8987031 100644 --- a/stlib/misc/test-1.py +++ b/stlib/misc/test-1.py @@ -15,39 +15,39 @@ def createScene(root): #root.add(entity.Deformable) #root.addChild(entity2.Deformable(root)) - params = entity.Deformable.Parameters() - params.name = "Deformable2" - root.add(entity.Deformable, params=auto_load) + parameters = entity.Deformable.Parameters() + parameters.name = "Deformable2" + root.add(entity.Deformable, parameters=auto_load) #def addCustomVisual(self, **kwargs): # Rigid.addVisualModel( mapping={"toto":"in"} ) - #params = Entity.Parameters() - #params.addVisualModel = addCustomVisual - #root.add(Entity, params) + #parameters = Entity.Parameters() + #parameters.addVisualModel = addCustomVisual + #root.add(Entity, parameters) #  - #params = Rigid.new_parameters() - #params.mass = 4.5 - #root.add(Entity, params) + #parameters = Rigid.new_parameters() + #parameters.mass = 4.5 + #root.add(Entity, parameters) #root.add(Entity) - #params.addVisualModelOverride = addCustomVisual + #parameters.addVisualModelOverride = addCustomVisual ###  #Entity._addVisualModel = addCustomVisual - #root.add(Entity, params) + #root.add(Entity, parameters) #root.add(Entity.Rigid) #root.add(Entity.Deformable) #root.add(Entity) - #root.add(VisualModel, params) + #root.add(VisualModel, parameters) #root.add(VisualModel) - #params = Entity.Deformable.Parameters() - #params.visual = None - #a = root.add(Entity.Deformable, params) + #parameters = Entity.Deformable.Parameters() + #parameters.visual = None + #a = root.add(Entity.Deformable, parameters) return root \ No newline at end of file diff --git a/stlib/misc/test2.py b/stlib/misc/test2.py index 4dc12d147..cdebfad1c 100644 --- a/stlib/misc/test2.py +++ b/stlib/misc/test2.py @@ -19,7 +19,7 @@ def myAddObject(self : Sofa.Core.Node, tname, **kwargs): Sofa.Core.Node.addObject = myAddObject -def myAdd(self : Sofa.Core.Node, c, params = PrefabParameters(), **kwargs): +def myAdd(self : Sofa.Core.Node, c, parameters = PrefabParameters(), **kwargs): def findName(cname, node): """Compute a working unique name in the node""" rname = cname @@ -30,14 +30,14 @@ def findName(cname, node): return rname for k,v in kwargs.items(): - if hasattr(params, k): - setattr(params, k, v) + if hasattr(parameters, k): + setattr(parameters, k, v) - params = copy.copy(params) - if params.name in self.children: - params.name = findName(params.name, self) + parameters = copy.copy(parameters) + if parameters.name in self.children: + parameters.name = findName(parameters.name, self) - return c(parent = self, parameters=params) + return c(parent = self, parameters=parameters) Sofa.Core.Node.add = myAdd def createScene(root): @@ -47,19 +47,19 @@ def createScene(root): # self.addObject("EulerExplicitSolver", name="numericalintegration") # self.addObject("LinearSolver", name="numericalsolver", firstOrder=True) - params = EntityParameters() - params.simulation.iterations = 10 - params.simulation.integration["rayleighStiffness"] = 2.0 - params.addSimulation = entity.NONE + parameters = EntityParameters() + parameters.simulation.iterations = 10 + parameters.simulation.integration["rayleighStiffness"] = 2.0 + parameters.addSimulation = entity.NONE - params.mechanical["template"] = "Rigid3" + parameters.mechanical["template"] = "Rigid3" - #params.simulation.integration["rayleightStiffnessXXX"] = 2.0 + #parameters.simulation.integration["rayleightStiffnessXXX"] = 2.0 - #params.solver.kwargs["numericalintegration"] = { "firstOrder" : True } + #parameters.solver.kwargs["numericalintegration"] = { "firstOrder" : True } - root.add(Entity, params) - root.add(Entity, params) - root.add(Entity, params) + root.add(Entity, parameters) + root.add(Entity, parameters) + root.add(Entity, parameters) #root.add(Simulation, name="mySimulation") diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py index 218c0c469..da603146f 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/prefabs/collision.py @@ -9,6 +9,8 @@ @dataclasses.dataclass class CollisionParameters(BaseParameters): + name : str = "Collision" + primitives : list[CollisionPrimitive] = dataclasses.field(default_factory = lambda :[CollisionPrimitive.TRIANGLES]) selfCollision : Optional[bool] = DEFAULT_VALUE @@ -20,18 +22,18 @@ class CollisionParameters(BaseParameters): class Collision(BasePrefab): - def __init__(self, params: CollisionParameters): - BasePrefab.__init__(self, params) + def __init__(self, parameters: CollisionParameters): + BasePrefab.__init__(self, parameters) - geom = self.add(Geometry, params.geometry) + geom = self.add(Geometry, parameters.geometry) - self.addObject("MechanicalObject", template="Vec3", position=f"@{params.geometry.name}/container.position") - for primitive in params.primitives: + self.addObject("MechanicalObject", template="Vec3", position=f"@{parameters.geometry.name}/container.position") + for primitive in parameters.primitives: addCollisionModels(self, primitive, - topology=f"@{params.geometry.name}/container", - selfCollision=params.selfCollision, - group=params.group, - **params.kwargs) + topology=f"@{parameters.geometry.name}/container", + selfCollision=parameters.selfCollision, + group=parameters.group, + **parameters.kwargs) @staticmethod @@ -44,14 +46,14 @@ def createScene(root): root.addObject("VisualStyle", displayFlags="showCollisionModels") # Create a visual from a mesh file - params = Collision.getParameters() - params.group = 1 - params.geometry = FileParameters(filename="mesh/cube.obj") + parameters = Collision.getParameters() + parameters.group = 1 + parameters.geometry = FileParameters(filename="mesh/cube.obj") # Expert parameters - # params.kwargs = { + # parameters.kwargs = { # "TriangleCollisionModel":{"contactStiffness": 100.0, "contactFriction": 0.5} # } - collision = root.add(Collision, params) + collision = root.add(Collision, parameters) # OR set the parameters post creation # collision.TriangleCollisionModel.contactStiffness = 100.0 diff --git a/stlib/prefabs/material.py b/stlib/prefabs/material.py index 3102f0e3c..07009f421 100644 --- a/stlib/prefabs/material.py +++ b/stlib/prefabs/material.py @@ -1,28 +1,30 @@ from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any -from splib.core.enum_types import * +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" + name : str = "Material" - massDensity: float = 0 - massLumping: bool = False + massDensity : float = DEFAULT_VALUE + massLumping : bool = DEFAULT_VALUE - stateType: StateType = StateType.VEC3 + stateType : StateType = StateType.VEC3 - addMaterial : Optional[Callable] = lambda node : addMass(node, node.stateType, massDensity=node.massDensity, lumping=node.massLumping) + addMaterial : Optional[Callable] = lambda node : addMass(node, node.parameters.stateType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) # TODO : previously called Behavior class Material(BasePrefab): - def __init__(self, params: MaterialParameters): - BasePrefab.__init__(self, params) - self.params = params + parameters : MaterialParameters - self.addObject("MechanicalObject", name="States", template=str(self.params.stateType)) + def __init__(self, parameters: MaterialParameters): + BasePrefab.__init__(self, parameters) + self.parameters = parameters - if(params.addMaterial is not None): - params.addMaterial(self) + self.addObject("MechanicalObject", name="States", template=str(parameters.stateType)) + parameters.addMaterial(self) diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index 2326b5d19..bbc34bee1 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -1,29 +1,27 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any from stlib.geometry import Geometry, GeometryParameters -from stlib.geometry.extract import ExtractParameters from stlib.geometry.file import FileParameters -from splib.core.enum_types import ElementType +from splib.core.utils import DEFAULT_VALUE from Sofa.Core import Object @dataclasses.dataclass class VisualParameters(BaseParameters): - color : Optional[list[float]] = None - texture : Optional[str] = None + name : str = "Visual" + + color : Optional[list[float]] = DEFAULT_VALUE + texture : Optional[str] = DEFAULT_VALUE geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) class Visual(BasePrefab): - def __init__(self, params: VisualParameters): - BasePrefab.__init__(self, params) - geom = self.add(Geometry, params.geometry) - # TODO : handle optional color, texture using DEFAULT_VALUE mechanism (as implemented by Paul) - self.addObject("OglModel", color=params.color, src=geom.container.linkpath) + def __init__(self, parameters: VisualParameters): + BasePrefab.__init__(self, parameters) - if params.addMapping is not None: - params.addMapping(self) + self.geometry = self.add(Geometry, parameters.geometry) + self.addObject("OglModel", color=parameters.color, src=self.geometry.container.linkpath) @staticmethod def getParameters(**kwargs) -> VisualParameters: @@ -33,14 +31,7 @@ def getParameters(**kwargs) -> VisualParameters: def createScene(root): # Create a visual from a mesh file - params = Visual.getParameters() - params.name = "VisualFromFile" - params.geometry = FileParameters(filename="mesh/cube.obj") - root.add(Visual, params) - - # # Create a visual from a node - # params = Visual.getParameters() - # params.name = "ExtractedVisual" - # params.geometry = ExtractParameters(sourceParameters=FileParameters(filename="mesh/cube.vtk"), - # destinationType=ElementType.TRIANGLES) - # root.add(Visual, params) + parameters = Visual.getParameters() + parameters.name = "LiverVisual" + parameters.geometry = FileParameters(filename="mesh/liver.obj") + root.add(Visual, parameters) \ No newline at end of file From d4b60425e94c47742d2ac0da14991f950b81290e Mon Sep 17 00:00:00 2001 From: bakpaul Date: Fri, 13 Jun 2025 15:03:47 +0200 Subject: [PATCH 36/49] Fix linkpath by introducing init method + make addDeformableMaterial private --- stlib/core/baseEntity.py | 2 ++ stlib/core/basePrefab.py | 12 ++++++++- stlib/entities/__entity__.py | 14 +++++----- stlib/entities/deformable/__deformable__.py | 4 +-- stlib/geometry/__geometry__.py | 29 +++++++++++---------- stlib/prefabs/collision.py | 16 +++++++----- stlib/prefabs/material.py | 7 ++--- stlib/prefabs/visual.py | 6 +++-- 8 files changed, 55 insertions(+), 35 deletions(-) diff --git a/stlib/core/baseEntity.py b/stlib/core/baseEntity.py index f93fc050e..1456436bb 100644 --- a/stlib/core/baseEntity.py +++ b/stlib/core/baseEntity.py @@ -7,3 +7,5 @@ class BaseEntity(Sofa.Core.Prefab): def __init__(self): Sofa.Core.Prefab.__init__(self) + + diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index 820e0e2b2..c2e101483 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -22,7 +22,10 @@ def findName(cname, node): if params.name in self.children: params.name = findName(params.name, self) - return self.addChild(typeName(params)) + pref = self.addChild(typeName(params)) + pref.init() + + return pref Sofa.Core.Node.add = addFromTypeName @@ -34,7 +37,14 @@ class BasePrefab(Sofa.Core.Node): def __init__(self, params: BasePrefabParameters): Sofa.Core.Node.__init__(self, name=params.name) + self.parameters = params + def init(self): + raise NotImplemented("To be overridden by child class") + + def localToGlobalCoordinates(pointCloudInput, pointCloudOutput): raise NotImplemented("Send an email to Damien, he will help you. Guaranteed :)") + + diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index f39d8b4ed..d8d5d8ca6 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -43,8 +43,9 @@ class Entity(BasePrefab): def __init__(self, parameters=EntityParameters(), **kwargs): BasePrefab.__init__(self, parameters) - self.parameters = parameters + + def init(self): self.geometry = self.add(Geometry, self.parameters.geometry) ### Check compatilibility of Material @@ -70,12 +71,13 @@ def addMapping(self, destinationPrefab): if( self.parameters.stateType == StateType.VEC3): destinationPrefab.addObject("BarycentricMapping", - input="@../Material/", - input_topology=destinationPrefab.geometry.container.linkpath, - output="@.", + output=destinationPrefab.linkpath, + output_topology=destinationPrefab.geometry.container.linkpath, + input=self.material.linkpath, + input_topology=self.geometry.container.linkpath, template=template) else: destinationPrefab.addObject("RigidMapping", - input="@../Material", - output="@.", + output=destinationPrefab.linkpath, + input=self.material.linkpath, template=template) diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 1c73b2d9a..458289c74 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -13,7 +13,7 @@ class DeformableBehaviorParameters(MaterialParameters): elementType : ElementType = ElementType.TETRAHEDRA parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio - def addDeformableMaterial(node): + def __addDeformableMaterial(node): addMass(node, node.parameters.stateType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) # TODO : change this with inheritance if(node.parameters.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): @@ -21,7 +21,7 @@ def addDeformableMaterial(node): else: addLinearElasticity(node, node.parameters.elementType, node.parameters.parameters[0], node.parameters.parameters[1], topology="@../Geometry/container") - addMaterial : Optional[Callable] = addDeformableMaterial + addMaterial : Optional[Callable] = __addDeformableMaterial def createScene(root) : diff --git a/stlib/geometry/__geometry__.py b/stlib/geometry/__geometry__.py index 085215da8..10b5084b3 100644 --- a/stlib/geometry/__geometry__.py +++ b/stlib/geometry/__geometry__.py @@ -41,27 +41,28 @@ class Geometry(BasePrefab): parameters : GeometryParameters def __init__(self, parameters: GeometryParameters): - BasePrefab.__init__(self, parameters) - self.parameters = parameters + + + def init(self): # Generate attribute (positions, edges, triangles, quads, tetrahedra, hexahedra) from the internal data provider - if parameters.data is not None : - parameters.data.generateAttribute(self) - if parameters.dynamicTopology : - if parameters.elementType is not None : - addDynamicTopology(self, container = dataclasses.asdict(parameters.data)) + if self.parameters.data is not None : + self.parameters.data.generateAttribute(self) + if self.parameters.dynamicTopology : + if self.parameters.elementType is not None : + addDynamicTopology(self, container = dataclasses.asdict(self.parameters.data)) else: raise ValueError else: addStaticTopology(self, container = { - "position": parameters.data.position, - "edges": parameters.data.edges, - "triangles": parameters.data.triangles, - "quads": parameters.data.quads, - "tetrahedra": parameters.data.tetrahedra, - "hexahedra": parameters.data.hexahedra - }) + "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 + }) \ No newline at end of file diff --git a/stlib/prefabs/collision.py b/stlib/prefabs/collision.py index da603146f..aba95d68b 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/prefabs/collision.py @@ -25,15 +25,17 @@ class Collision(BasePrefab): def __init__(self, parameters: CollisionParameters): BasePrefab.__init__(self, parameters) - geom = self.add(Geometry, parameters.geometry) + def init(self): + + geom = self.add(Geometry, self.parameters.geometry) - self.addObject("MechanicalObject", template="Vec3", position=f"@{parameters.geometry.name}/container.position") - for primitive in parameters.primitives: + self.addObject("MechanicalObject", template="Vec3", position=f"@{self.parameters.geometry.name}/container.position") + for primitive in self.parameters.primitives: addCollisionModels(self, primitive, - topology=f"@{parameters.geometry.name}/container", - selfCollision=parameters.selfCollision, - group=parameters.group, - **parameters.kwargs) + topology=f"@{self.parameters.geometry.name}/container", + selfCollision=self.parameters.selfCollision, + group=self.parameters.group, + **self.parameters.kwargs) @staticmethod diff --git a/stlib/prefabs/material.py b/stlib/prefabs/material.py index 07009f421..070c7ac55 100644 --- a/stlib/prefabs/material.py +++ b/stlib/prefabs/material.py @@ -24,7 +24,8 @@ class Material(BasePrefab): def __init__(self, parameters: MaterialParameters): BasePrefab.__init__(self, parameters) - self.parameters = parameters + - self.addObject("MechanicalObject", name="States", template=str(parameters.stateType)) - parameters.addMaterial(self) + def init(self): + self.addObject("MechanicalObject", name="States", template=str(self.parameters.stateType)) + self.parameters.addMaterial(self) diff --git a/stlib/prefabs/visual.py b/stlib/prefabs/visual.py index bbc34bee1..2ced99a7f 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/prefabs/visual.py @@ -20,8 +20,10 @@ class Visual(BasePrefab): def __init__(self, parameters: VisualParameters): BasePrefab.__init__(self, parameters) - self.geometry = self.add(Geometry, parameters.geometry) - self.addObject("OglModel", color=parameters.color, src=self.geometry.container.linkpath) + def init(self): + self.geometry = self.add(Geometry, self.parameters.geometry) + self.addObject("OglModel", color=self.parameters.color, src=self.geometry.container.linkpath) + @staticmethod def getParameters(**kwargs) -> VisualParameters: From aad2a5e4c4d3f54ff412d06d55f8b3f0a49a0be5 Mon Sep 17 00:00:00 2001 From: Damien Marchal Date: Thu, 17 Jul 2025 09:30:28 +0200 Subject: [PATCH 37/49] [STLIB] Add the unified "add" in Sofa.Core.Node (#503) * Add Sofa.Core.Node.add with preliminary test * rename the "add" function and make it private * Apply suggestions from code review Co-authored-by: Hugo --------- Co-authored-by: Hugo --- stlib/__init__.py | 55 ++++++++++++++++++++++++++++++++++ stlib/core/basePrefab.py | 27 ----------------- tests/test_new_add.py | 64 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 tests/test_new_add.py diff --git a/stlib/__init__.py b/stlib/__init__.py index 70108a419..01c21a403 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1 +1,56 @@ __all__ = ["core","entities","prefabs","shapes"] + +import Sofa.Core +def __genericAdd(self : Sofa.Core.Node, typeName, **kwargs): + def findName(cname, names): + """Compute a working unique name in the node""" + rname = cname + for i in range(0, len(names)): + if rname not in names: + return rname + rname = cname + str(i+1) + return rname + + # Check if a name is provided, if not, use the one of the class + params = kwargs.copy() + isNode = False + if "name" not in params: + if isinstance(typeName, str): + params["name"] = typeName + isNode=True + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Node): + params["name"] = "Node" + isNode=True + elif isinstance(typeName, Sofa.Core.Node): + params["name"] = "Node" + isNode=True + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): + params["name"] = typeName.name.value + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.ObjectDeclaration): + params["name"] = typeName.__name__ + else: + raise RuntimeError("Invalid argument ", typeName) + + # Check if the name already exists, if this happens, create a new one. + if params["name"] in self.children or params["name"] in self.objects: + names = {node.name.value for node in self.children} + names = names.union({object.name.value for object in self.objects}) + params["name"] = findName(params["name"], names) + + # Dispatch the creation to either addObject or addChild + if isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Node): + pref = self.addChild(typeName(params["name"])) + elif isinstance(typeName, Sofa.Core.Node): + pref = self.addChild(typeName) + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): + pref = self.addObject(typeName(**params)) + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.ObjectDeclaration): + pref = self.addObject(typeName.__name__, **params) + elif isinstance(typeName, str): + pref = self.addObject(typeName, **params) + else: + raise RuntimeError("Invalid argument", typeName) + return pref + +# Inject the method so it become available as if it was part of Sofa.Core.Node +Sofa.Core.Node.add = __genericAdd diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index c2e101483..1dd5665a3 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -3,33 +3,6 @@ import Sofa.Core from stlib.core.basePrefabParameters import BasePrefabParameters - -def addFromTypeName(self : Sofa.Core.Node, typeName, params = BasePrefabParameters, **kwargs): - def findName(cname, node): - """Compute a working unique name in the node""" - rname = cname - for i in range(0, len(node.children)): - if rname not in node.children: - return rname - rname = cname + str(i+1) - return rname - - for k,v in kwargs.items(): - if hasattr(params, k): - setattr(params, k, v) - - params = copy.copy(params) - if params.name in self.children: - params.name = findName(params.name, self) - - pref = self.addChild(typeName(params)) - pref.init() - - return pref - -Sofa.Core.Node.add = addFromTypeName - - class BasePrefab(Sofa.Core.Node): """ A Prefab is a Sofa.Node that assembles a set of components and nodes diff --git a/tests/test_new_add.py b/tests/test_new_add.py new file mode 100644 index 000000000..fcadda6f1 --- /dev/null +++ b/tests/test_new_add.py @@ -0,0 +1,64 @@ +import unittest +import Sofa +import SofaRuntime +import Sofa.Core +import stlib + +class ObjectDeclaration(object): + ... + +Sofa.Core.ObjectDeclaration = ObjectDeclaration + +class MechanicalObject(ObjectDeclaration): + pass + +class TestNewAdd(unittest.TestCase): + def test_add_node_with_node_type(self): + root = Sofa.Core.Node("root") + root.add(Sofa.Core.Node, name="aNodeA") + self.assertEqual(len(root.children), 1) + self.assertEqual(root.children[0].name.value, "aNodeA") + + def test_add_node_with_node_instance(self): + root = Sofa.Core.Node("root") + root.add(Sofa.Core.Node("aNodeB")) + self.assertEqual(len(root.children), 1) + self.assertEqual(root.children[0].name.value, "aNodeB") + + def test_add_object_with_string_type(self): + root = Sofa.Core.Node("root") + root.add("MechanicalObject", name="anObject1", position=[[1,2,3]]) + self.assertEqual(len(root.objects), 1) + self.assertEqual(root.objects[0].name.value, "anObject1") + self.assertEqual(root.objects[0].position.value.shape, (1,3)) + + def test_add_object_with_object_type(self): + root = Sofa.Core.Node("root") + root.add(MechanicalObject, name="anObject2", position=[[1,2,3]]) + self.assertEqual(len(root.objects), 1) + self.assertEqual(root.objects[0].name.value, "anObject2") + self.assertEqual(root.objects[0].position.value.shape, (1,3)) + + def test_automatic_name_generation(self): + root = Sofa.Core.Node("root") + root.add(MechanicalObject, position=[[1,2,3]]) + root.add(MechanicalObject, position=[[1,2,3]]) + root.add(MechanicalObject, position=[[1,2,3]]) + self.assertEqual(root.objects[0].name.value, "MechanicalObject") + self.assertEqual(root.objects[1].name.value, "MechanicalObject1") + self.assertEqual(root.objects[2].name.value, "MechanicalObject2") + + root.add(Sofa.Core.Node, name="TestNode") + root.add(Sofa.Core.Node, name="TestNode") + self.assertEqual(root.children[0].name.value, "TestNode") + self.assertEqual(root.children[1].name.value, "TestNode1") + + root.add(Sofa.Core.Node) + root.add(Sofa.Core.Node) + self.assertEqual(root.children[2].name.value, "Node") + self.assertEqual(root.children[3].name.value, "Node1") + +if __name__ == '__main__': + + SofaRuntime.importPlugin("Sofa.Component.StateContainer") + unittest.main() From 1b15154879a1839135b672d49855a7a9b27d7cac Mon Sep 17 00:00:00 2001 From: Paul Baksic Date: Thu, 17 Jul 2025 10:32:45 +0200 Subject: [PATCH 38/49] Add new tests for prefabs --- stlib/__init__.py | 4 +++- tests/test_new_add.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/stlib/__init__.py b/stlib/__init__.py index 01c21a403..51d5cd849 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -39,7 +39,9 @@ def findName(cname, names): # Dispatch the creation to either addObject or addChild if isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Node): - pref = self.addChild(typeName(params["name"])) + name = params["name"] + params.pop("name") + pref = self.addChild(typeName(name,**params)) elif isinstance(typeName, Sofa.Core.Node): pref = self.addChild(typeName) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): diff --git a/tests/test_new_add.py b/tests/test_new_add.py index fcadda6f1..0c1036b65 100644 --- a/tests/test_new_add.py +++ b/tests/test_new_add.py @@ -58,6 +58,26 @@ def test_automatic_name_generation(self): self.assertEqual(root.children[2].name.value, "Node") self.assertEqual(root.children[3].name.value, "Node1") + def test_add_node_with_kwargs(self): + root = Sofa.Core.Node("root") + root.add(Sofa.Core.Node, name="aNodeC", gravity=[1,2,3]) + self.assertEqual(root.children[0].gravity.value, [1,2,3]) + + def test_add_instanciated_prefab(self): + root = Sofa.Core.Node("root") + from stlib.entities import Entity, EntityParameters + + bunnyParameters = EntityParameters() + bunny = root.add(Entity(bunnyParameters)) + + def test_add_prefab_with_parameter_object(self): + root = Sofa.Core.Node("root") + from stlib.entities import Entity, EntityParameters + + bunnyParameters = EntityParameters() + bunny = root.add(Entity, bunnyParameters) + + if __name__ == '__main__': SofaRuntime.importPlugin("Sofa.Component.StateContainer") From e2fff56b9ad801dd3b7763a21bf5029039c39e51 Mon Sep 17 00:00:00 2001 From: Paul Baksic Date: Thu, 17 Jul 2025 10:33:49 +0200 Subject: [PATCH 39/49] Revert modification of __genericAdd --- stlib/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stlib/__init__.py b/stlib/__init__.py index 51d5cd849..01c21a403 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -39,9 +39,7 @@ def findName(cname, names): # Dispatch the creation to either addObject or addChild if isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Node): - name = params["name"] - params.pop("name") - pref = self.addChild(typeName(name,**params)) + pref = self.addChild(typeName(params["name"])) elif isinstance(typeName, Sofa.Core.Node): pref = self.addChild(typeName) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): From 9de87d48465322ade1d1d3065be33935d1f20833 Mon Sep 17 00:00:00 2001 From: hugtalbot Date: Thu, 17 Jul 2025 16:44:15 +0200 Subject: [PATCH 40/49] update addMass using elementType --- splib/mechanics/mass.py | 21 ++++++++++++--------- stlib/entities/deformable/__deformable__.py | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py index 917fc9365..cb3478564 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -3,18 +3,21 @@ from splib.core.enum_types import ElementType -# TODO : use the massDensity only and deduce totalMass if necessary from it + volume +# TODO : use the massDensity ONLY and deduce totalMass if necessary from it + volume + @ReusableMethod -def addMass(node, template, totalMass=DEFAULT_VALUE, massDensity=DEFAULT_VALUE, lumping=DEFAULT_VALUE, **kwargs): +def addMass(node, elem:ElementType, totalMass=DEFAULT_VALUE, massDensity=DEFAULT_VALUE, lumping=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") - kwargs.pop('massDensity') - - # if(template=="Rigid3"): - node.addObject("UniformMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) - # else: - # node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **kwargs) - + del kwargs["massDensity"] + if(elem !=ElementType.POINTS and elem !=ElementType.EDGES): + node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **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, **kwargs) diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/entities/deformable/__deformable__.py index 458289c74..4cbdf1296 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/entities/deformable/__deformable__.py @@ -14,7 +14,7 @@ class DeformableBehaviorParameters(MaterialParameters): parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio def __addDeformableMaterial(node): - addMass(node, node.parameters.stateType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) + addMass(node, node.parameters.elementType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) # TODO : change this with inheritance if(node.parameters.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): addHyperelasticity(node, node.parameters.elementType, node.parameters.parameters, topology="@../Geometry/container") From 75fa28f6b6b6989fdfe5fbb993da1d8bb8726337 Mon Sep 17 00:00:00 2001 From: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:24:27 +0100 Subject: [PATCH 41/49] [Prefabs] Reorder files to match concepts (#547) * Reorder files to match concepts * Put every prefab in root to avoid having a useless prefix * Fix bad import in bunny example * Update examples/stlib/PrefabScene_beginner.py Co-authored-by: EulalieCoevoet --------- Co-authored-by: EulalieCoevoet --- examples/stlib/PrefabScene_beginner.py | 8 +- stlib/__init__.py | 6 +- stlib/{prefabs => }/collision.py | 6 +- stlib/entities/__entity__.py | 15 +- stlib/entities/deformable/__init__.py | 2 - stlib/entities/rigid/__init__.py | 1 - stlib/entities/rigid/__parameters__.py | 11 - stlib/entities/rigid/__rigid__.py | 15 -- .../{geometry => geometries}/__geometry__.py | 0 stlib/{geometry => geometries}/__init__.py | 0 stlib/{geometry => geometries}/cube.py | 2 +- stlib/{geometry => geometries}/extract.py | 2 +- stlib/{geometry => geometries}/file.py | 2 +- stlib/{geometry => geometries}/sphere.py | 2 +- stlib/materials/__init__.py | 1 + .../material.py => materials/__material__.py} | 4 +- .../deformable.py} | 9 +- stlib/materials/rigid.py | 13 ++ stlib/misc/entity.py | 188 ------------------ stlib/misc/softrobots.py | 23 --- stlib/misc/test-1.py | 53 ----- stlib/misc/test2.py | 65 ------ stlib/prefabs/__init__.py | 1 - stlib/{prefabs => }/visual.py | 6 +- 24 files changed, 43 insertions(+), 392 deletions(-) rename stlib/{prefabs => }/collision.py (92%) delete mode 100644 stlib/entities/deformable/__init__.py delete mode 100644 stlib/entities/rigid/__init__.py delete mode 100644 stlib/entities/rigid/__parameters__.py delete mode 100644 stlib/entities/rigid/__rigid__.py rename stlib/{geometry => geometries}/__geometry__.py (100%) rename stlib/{geometry => geometries}/__init__.py (100%) rename stlib/{geometry => geometries}/cube.py (91%) rename stlib/{geometry => geometries}/extract.py (97%) rename stlib/{geometry => geometries}/file.py (93%) rename stlib/{geometry => geometries}/sphere.py (90%) create mode 100644 stlib/materials/__init__.py rename stlib/{prefabs/material.py => materials/__material__.py} (98%) rename stlib/{entities/deformable/__deformable__.py => materials/deformable.py} (88%) create mode 100644 stlib/materials/rigid.py delete mode 100644 stlib/misc/entity.py delete mode 100644 stlib/misc/softrobots.py delete mode 100644 stlib/misc/test-1.py delete mode 100644 stlib/misc/test2.py delete mode 100644 stlib/prefabs/__init__.py rename stlib/{prefabs => }/visual.py (90%) diff --git a/examples/stlib/PrefabScene_beginner.py b/examples/stlib/PrefabScene_beginner.py index ca59a63e5..2c4552c59 100644 --- a/examples/stlib/PrefabScene_beginner.py +++ b/examples/stlib/PrefabScene_beginner.py @@ -1,7 +1,7 @@ -from stlib.entities.rigid import Rigid -from stlib.entities.deformable import Deformable -from stlib.geometry.cube import CubeParameters -from stlib.geometry.file import FileParameters +from stlib.materials.rigid import Rigid +from stlib.materials.deformable import Deformable +from stlib.geometries.cube import CubeParameters +from stlib.geometries.file import FileParameters from splib.simulation.headers import setupLagrangianCollision from splib.simulation.linear_solvers import addLinearSolver from splib.simulation.ode_solvers import addImplicitODE diff --git a/stlib/__init__.py b/stlib/__init__.py index 01c21a403..94db0b9c1 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["core","entities","prefabs","shapes"] +__all__ = ["core","entities","geometries","materials","collision","visual"] import Sofa.Core def __genericAdd(self : Sofa.Core.Node, typeName, **kwargs): @@ -11,7 +11,7 @@ def findName(cname, names): rname = cname + str(i+1) return rname - # Check if a name is provided, if not, use the one of the class + # Check if a name is provided, if not, use the one of the class params = kwargs.copy() isNode = False if "name" not in params: @@ -31,7 +31,7 @@ def findName(cname, names): else: raise RuntimeError("Invalid argument ", typeName) - # Check if the name already exists, if this happens, create a new one. + # Check if the name already exists, if this happens, create a new one. if params["name"] in self.children or params["name"] in self.objects: names = {node.name.value for node in self.children} names = names.union({object.name.value for object in self.objects}) diff --git a/stlib/prefabs/collision.py b/stlib/collision.py similarity index 92% rename from stlib/prefabs/collision.py rename to stlib/collision.py index aba95d68b..ebffd7b75 100644 --- a/stlib/prefabs/collision.py +++ b/stlib/collision.py @@ -1,7 +1,7 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses, Any -from stlib.geometry import Geometry, GeometryParameters -from stlib.geometry.file import FileParameters +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses +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 diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index d8d5d8ca6..a60aa5a41 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -1,15 +1,12 @@ from stlib.core.baseParameters import BaseParameters -from stlib.core.basePrefab import BasePrefab -from stlib.prefabs.collision import CollisionParameters, Collision -from stlib.prefabs.visual import VisualParameters, Visual -from stlib.prefabs.material import Material, MaterialParameters -from stlib.geometry import Geometry -from stlib.geometry.extract import ExtractParameters +from stlib.collision import CollisionParameters, Collision +from stlib.visual import VisualParameters, Visual +from stlib.materials import Material, MaterialParameters +from stlib.geometries import Geometry import dataclasses -from typing import Callable, Optional, overload, Any -from stlib.geometry import GeometryParameters +from typing import Callable, Optional +from stlib.geometries import GeometryParameters from splib.core.enum_types import StateType -import Sofa from stlib.core.basePrefab import BasePrefab diff --git a/stlib/entities/deformable/__init__.py b/stlib/entities/deformable/__init__.py deleted file mode 100644 index d17dc4db2..000000000 --- a/stlib/entities/deformable/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .__deformable__ import * -from .__parameters__ import * \ No newline at end of file diff --git a/stlib/entities/rigid/__init__.py b/stlib/entities/rigid/__init__.py deleted file mode 100644 index 04efc302b..000000000 --- a/stlib/entities/rigid/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .__rigid__ import * \ No newline at end of file diff --git a/stlib/entities/rigid/__parameters__.py b/stlib/entities/rigid/__parameters__.py deleted file mode 100644 index f7be03c77..000000000 --- a/stlib/entities/rigid/__parameters__.py +++ /dev/null @@ -1,11 +0,0 @@ -from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses -from stlib.geometry import GeometryParameters - -@dataclasses.dataclass -class RigidParameters(BaseParameters): - - geometry : GeometryParameters - mass : Optional[float] = None - - def toDict(self): - return dataclasses.asdict(self) diff --git a/stlib/entities/rigid/__rigid__.py b/stlib/entities/rigid/__rigid__.py deleted file mode 100644 index 69c68f6d0..000000000 --- a/stlib/entities/rigid/__rigid__.py +++ /dev/null @@ -1,15 +0,0 @@ -from stlib.entities import Entity -from stlib.entities.rigid.__parameters__ import RigidParameters - -import dataclasses - - -class Rigid(Entity): - - @staticmethod - def getParameters(**kwargs) -> RigidParameters: - return RigidParameters(**kwargs) - - - def __init__(self, **kwargs): - Entity.__init__(self, **kwargs) diff --git a/stlib/geometry/__geometry__.py b/stlib/geometries/__geometry__.py similarity index 100% rename from stlib/geometry/__geometry__.py rename to stlib/geometries/__geometry__.py diff --git a/stlib/geometry/__init__.py b/stlib/geometries/__init__.py similarity index 100% rename from stlib/geometry/__init__.py rename to stlib/geometries/__init__.py diff --git a/stlib/geometry/cube.py b/stlib/geometries/cube.py similarity index 91% rename from stlib/geometry/cube.py rename to stlib/geometries/cube.py index 0c8ac7615..4d5a52ca3 100644 --- a/stlib/geometry/cube.py +++ b/stlib/geometries/cube.py @@ -1,4 +1,4 @@ -from stlib.geometry import GeometryParameters +from stlib.geometries import GeometryParameters class CubeParameters(GeometryParameters): def __init__(self, center, edgeLength, pointPerEdge, dynamicTopology = False): diff --git a/stlib/geometry/extract.py b/stlib/geometries/extract.py similarity index 97% rename from stlib/geometry/extract.py rename to stlib/geometries/extract.py index c20249666..7143fb07d 100644 --- a/stlib/geometry/extract.py +++ b/stlib/geometries/extract.py @@ -1,4 +1,4 @@ -from stlib.geometry import GeometryParameters, InternalDataProvider, Geometry +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 diff --git a/stlib/geometry/file.py b/stlib/geometries/file.py similarity index 93% rename from stlib/geometry/file.py rename to stlib/geometries/file.py index cc51c077d..b057728c1 100644 --- a/stlib/geometry/file.py +++ b/stlib/geometries/file.py @@ -1,4 +1,4 @@ -from stlib.geometry import GeometryParameters, InternalDataProvider, Geometry +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 diff --git a/stlib/geometry/sphere.py b/stlib/geometries/sphere.py similarity index 90% rename from stlib/geometry/sphere.py rename to stlib/geometries/sphere.py index 2c3a58ce6..2cb64f6ee 100644 --- a/stlib/geometry/sphere.py +++ b/stlib/geometries/sphere.py @@ -1,4 +1,4 @@ -from stlib.geometry import GeometryParameters +from stlib.geometries import GeometryParameters class SphereParameters(GeometryParameters): def __init__(self, center, radius, pointPerRad, dynamicTopology = False): diff --git a/stlib/materials/__init__.py b/stlib/materials/__init__.py new file mode 100644 index 000000000..6a748749a --- /dev/null +++ b/stlib/materials/__init__.py @@ -0,0 +1 @@ +from .__material__ import * \ No newline at end of file diff --git a/stlib/prefabs/material.py b/stlib/materials/__material__.py similarity index 98% rename from stlib/prefabs/material.py rename to stlib/materials/__material__.py index 070c7ac55..ff88dbd43 100644 --- a/stlib/prefabs/material.py +++ b/stlib/materials/__material__.py @@ -11,7 +11,7 @@ class MaterialParameters(BaseParameters): 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) @@ -24,7 +24,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)) diff --git a/stlib/entities/deformable/__deformable__.py b/stlib/materials/deformable.py similarity index 88% rename from stlib/entities/deformable/__deformable__.py rename to stlib/materials/deformable.py index 4cbdf1296..cb424ed43 100644 --- a/stlib/entities/deformable/__deformable__.py +++ b/stlib/materials/deformable.py @@ -1,5 +1,5 @@ -from stlib.prefabs.material import MaterialParameters -from splib.core.enum_types import ConstitutiveLaw, ElementType +from stlib.materials import MaterialParameters +from splib.core.enum_types import ConstitutiveLaw from stlib.core.baseParameters import Callable, Optional, dataclasses from splib.mechanics.linear_elasticity import * from splib.mechanics.hyperelasticity import * @@ -26,9 +26,8 @@ def __addDeformableMaterial(node): def createScene(root) : from stlib.entities import Entity, EntityParameters - from stlib.prefabs.visual import VisualParameters - from stlib.geometry.extract import ExtractParameters - from stlib.geometry.file import FileParameters + from stlib.visual import VisualParameters + from stlib.geometries.file import FileParameters root.addObject("VisualStyle", displayFlags=["showBehavior"]) diff --git a/stlib/materials/rigid.py b/stlib/materials/rigid.py new file mode 100644 index 000000000..5668b3562 --- /dev/null +++ b/stlib/materials/rigid.py @@ -0,0 +1,13 @@ +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses +from stlib.geometries import GeometryParameters + + + +@dataclasses.dataclass +class RigidParameters(BaseParameters): + + geometry : GeometryParameters + mass : Optional[float] = None + + def toDict(self): + return dataclasses.asdict(self) diff --git a/stlib/misc/entity.py b/stlib/misc/entity.py deleted file mode 100644 index a3549b083..000000000 --- a/stlib/misc/entity.py +++ /dev/null @@ -1,188 +0,0 @@ -from typing import Callable, Optional, overload - -import Sofa -import dataclasses - -def addBidule(self): - return self.addChild("Bidule") - -DEFAULT_VALUE = object() - -def NONE(*args, **kwargs): - pass - -def to_dict(o): - if isinstance(o, dict): - return o - if hasattr(o, "to_dict"): - return o.to_dict() - return {} - -@dataclasses.dataclass -class PrefabParameters(object): - name : str = "Prefab" - kwargs : dict = dataclasses.field(default_factory=dict) - - def __getattr__(self, name: str) : - if name == "__getstate__": - getattr(PrefabParameters, "__getstate__") - if name == "__setstate__": - getattr(PrefabParameters, "__setstate__") - - try: - a = self.__getattribute__(name) - except Exception as e: - return NONE - return a - - def to_dict(self): - return dataclasses.asdict(self) - -@dataclasses.dataclass -class VisualModelParameters(PrefabParameters): - name : str = "VisualModel" - - filename : str = "mesh/sphere_02.obj" - - renderer : dict = dataclasses.field(default_factory=dict) - mapping : dict = dataclasses.field(default_factory=dict) - -class VisualModel(Sofa.Core.Node): - - def __init__(self, parent=None, parameters : VisualModelParameters = VisualModelParameters()): - Sofa.Core.Node.__init__(self, name=parameters.name) - - if parent != None: - parent.addChild(self) - - self.addObject("MeshOBJLoader", name="loader", filename=parameters.filename) - self.addRenderer(**to_dict(parameters.renderer) | {"src" : "@loader"} ) - self.addMapping(**to_dict(parameters.mapping) ) - - def addRenderer(self, **kwargs): - self.addObject("OglModel", name="renderer", **kwargs) - - def addMapping(self, **kwargs): - self.addObject("RigidMapping", name="mapping", **kwargs) - -class CollisionModel(Sofa.Core.BasePrefab): - def __init__(self, parameters, **kwargs): - Sofa.Core.Node.__init__(self, **kwargs) - - class Parameters(object): - enabled : bool = False - -class MechanicalObject(Sofa.Core.Object): - positions : list[float] - - @dataclasses.dataclass - class Parameters(object): - name : str = "MechanicalObject" - - def to_dict(self): - return dataclasses.asdict(self) - - -@dataclasses.dataclass -class SimulationParameters(PrefabParameters): - name : str = "Simulation" - iterations : Optional[int] = None - template: Optional[str] = None - solver : dict = dataclasses.field(default_factory=dict) - integration : dict = dataclasses.field(default_factory=dict) - - def to_dict(self): - return self.asdict() - -class Simulation(Sofa.Core.Node): - solver : Sofa.Core.Object - integration : Sofa.Core.Object - iterations : int - - def __init__(self, parent : Sofa.Core.Node = None, parameters : SimulationParameters = SimulationParameters()): - Sofa.Core.Node.__init__(self, name=parameters.name) - if parent is not None: - parent.addChild(self) - - if parameters.iterations != NONE and "iterations" in parameters.solver: - raise Exception("Cannot set direct attribute and internal hack... ") - - self.addObject("EulerImplicitSolver", name = "integration", **to_dict(parameters.integration)) - self.addObject("CGLinearSolver", name = "solver", iterations=parameters.iterations, **to_dict(parameters.solver)) - - - -#@dataclasses.dataclass -#class Solver(object): -# integrationscheme : str -# numericalsolver : str - -@dataclasses.dataclass -class EntityParameters(PrefabParameters): - name : str = "Entity" - - addSimulation : Callable = Simulation - addCollisionModel : Callable = CollisionModel - addVisualModel : Callable = VisualModel - - #setConstitutiveLaw # : Callable = addBidule - #setBoundaryCondition #: Callable = addBidule - - mechanical : dict = dataclasses.field(default_factory=dict) - collision : CollisionModel.Parameters = CollisionModel.Parameters() - visual : VisualModelParameters = VisualModelParameters() - simulation : SimulationParameters = SimulationParameters() - -class Entity(Sofa.Core.Node): - # A simulated object - simulation : Simulation - visual : VisualModel - collision : CollisionModel - - parameters : EntityParameters - - def __init__(self, parent=None, parameters=EntityParameters(), **kwargs): - Sofa.Core.Node.__init__(self, name=parameters.name) - - if parent is not None: - parent.addChild(self) - - self.parameters = parameters - - self.addMechanicalModel(**parameters.mechanical) - self.addSimulation(parameters=parameters.simulation) - self.addVisualModel(parameters=parameters.visual) - self.addCollisionModel() - - def addMechanicalModel(self, **kwargs): - self.addObject("MechanicalObject", **kwargs) - - def addSimulation(self, **kwargs): - self.parameters.addSimulation(self, **kwargs) - - def addVisualModel(self, **kwargs): - self.parameters.addVisualModel(self, **kwargs) - - def addCollisionModel(self): - pass - -class Rigid(Entity): - def __init__(self, **kwargs): - Entity.__init__(self, **kwargs) - - -class Deformable(Entity): - def __init__(self, **kwargs): - Entity.__init__(self, **kwargs) - -@dataclasses.dataclass -class DeformableEntityParameters(EntityParameters): - addConstitutiveLaw : Callable = lambda x: x - - mass : Optional[float] = None - - def to_dict(self): - return dataclasses.asdict(self) - - - diff --git a/stlib/misc/softrobots.py b/stlib/misc/softrobots.py deleted file mode 100644 index aca8ab50e..000000000 --- a/stlib/misc/softrobots.py +++ /dev/null @@ -1,23 +0,0 @@ -class SoftRobots: - class Cable(Sofa.Core.BasePrefab): - length : float - - def __init__(self,**kwargs): - pass - - def Parameters(object): - lenght : float - -class Trunk(Sofa.Core.BasePrefab): - body : Entity.Deformable - cables : list [SoftRobots.Cable] - - def __init__(self, parameters): - body = Entity.Deformable() - - for param in range(parameters.cables): - cables.append(SoftRobots.Cable(body, param)) - - class Parameters(object): - body : Entity.Deformable.Parameters - cables : list[SoftRobots.Cable.Parameters] diff --git a/stlib/misc/test-1.py b/stlib/misc/test-1.py deleted file mode 100644 index 2b8987031..000000000 --- a/stlib/misc/test-1.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Callable, Optional, overload -import Sofa.Core - - -import entity -import entity2 - -# Monkey patch for demonstration purpose -def newAdd(self, creator, **kwargs): - if callable(creator): - creator(parent=self, **kwargs) -Sofa.Core.Node.add = newAdd - -def createScene(root): - #root.add(entity.Deformable) - #root.addChild(entity2.Deformable(root)) - - parameters = entity.Deformable.Parameters() - parameters.name = "Deformable2" - root.add(entity.Deformable, parameters=auto_load) - - #def addCustomVisual(self, **kwargs): - # Rigid.addVisualModel( mapping={"toto":"in"} ) - - #parameters = Entity.Parameters() - #parameters.addVisualModel = addCustomVisual - #root.add(Entity, parameters) - - #  - #parameters = Rigid.new_parameters() - #parameters.mass = 4.5 - #root.add(Entity, parameters) - #root.add(Entity) - - #parameters.addVisualModelOverride = addCustomVisual - - ###  - #Entity._addVisualModel = addCustomVisual - #root.add(Entity, parameters) - - #root.add(Entity.Rigid) - #root.add(Entity.Deformable) - - #root.add(Entity) - #root.add(VisualModel, parameters) - - #root.add(VisualModel) - - #parameters = Entity.Deformable.Parameters() - #parameters.visual = None - #a = root.add(Entity.Deformable, parameters) - - return root \ No newline at end of file diff --git a/stlib/misc/test2.py b/stlib/misc/test2.py deleted file mode 100644 index cdebfad1c..000000000 --- a/stlib/misc/test2.py +++ /dev/null @@ -1,65 +0,0 @@ -import Sofa.Core -import copy -import entity -from entity import PrefabParameters, EntityParameters, Entity, Simulation - - -oldAdd=Sofa.Core.Node.addObject -def myAddObject(self : Sofa.Core.Node, tname, **kwargs): - kwargs = copy.copy(kwargs) - previouslen = len(self.objects) - try: - oldAdd(self, tname, **kwargs) - except Exception as e: - target = self - if len(self.objects) != previouslen: - target = list(self.objects)[-1] - Sofa.msg_error(target, str(e)) - -Sofa.Core.Node.addObject = myAddObject - - -def myAdd(self : Sofa.Core.Node, c, parameters = PrefabParameters(), **kwargs): - def findName(cname, node): - """Compute a working unique name in the node""" - rname = cname - for i in range(0, len(node.children)): - if rname not in node.children: - return rname - rname = cname + str(i+1) - return rname - - for k,v in kwargs.items(): - if hasattr(parameters, k): - setattr(parameters, k, v) - - parameters = copy.copy(parameters) - if parameters.name in self.children: - parameters.name = findName(parameters.name, self) - - return c(parent = self, parameters=parameters) -Sofa.Core.Node.add = myAdd - -def createScene(root): - #@optionalkwargs - - #def eulalieAddOde(self, **kwargs): - # self.addObject("EulerExplicitSolver", name="numericalintegration") - # self.addObject("LinearSolver", name="numericalsolver", firstOrder=True) - - parameters = EntityParameters() - parameters.simulation.iterations = 10 - parameters.simulation.integration["rayleighStiffness"] = 2.0 - parameters.addSimulation = entity.NONE - - parameters.mechanical["template"] = "Rigid3" - - #parameters.simulation.integration["rayleightStiffnessXXX"] = 2.0 - - #parameters.solver.kwargs["numericalintegration"] = { "firstOrder" : True } - - root.add(Entity, parameters) - root.add(Entity, parameters) - root.add(Entity, parameters) - - #root.add(Simulation, name="mySimulation") diff --git a/stlib/prefabs/__init__.py b/stlib/prefabs/__init__.py deleted file mode 100644 index 2d2771cfc..000000000 --- a/stlib/prefabs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ["behavior","collision","visual"] diff --git a/stlib/prefabs/visual.py b/stlib/visual.py similarity index 90% rename from stlib/prefabs/visual.py rename to stlib/visual.py index 2ced99a7f..271aad3c7 100644 --- a/stlib/prefabs/visual.py +++ b/stlib/visual.py @@ -1,7 +1,7 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any -from stlib.geometry import Geometry, GeometryParameters -from stlib.geometry.file import FileParameters +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses +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 ae1d496f020cb43a61f1c445eb227d5904b61365 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Tue, 28 Oct 2025 16:29:33 +0100 Subject: [PATCH 42/49] [Prefabs] cleaning to fix deformable example (#549) * cleaning to fix deformable example * Restore checks on name and add check on the presence of parameters * Allow to use default parameters (passing nothing to the add method except the class name --------- Co-authored-by: Paul Baksic --- splib/mechanics/mass.py | 16 +++++++------- stlib/__init__.py | 40 +++++++++++++++++++++++------------ stlib/core/basePrefab.py | 6 +++--- stlib/entities/__entity__.py | 8 +++---- stlib/materials/deformable.py | 4 +++- stlib/visual.py | 2 +- 6 files changed, 45 insertions(+), 31 deletions(-) diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py index cb3478564..a3ee1fe28 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -11,13 +11,13 @@ def addMass(node, elem:ElementType, totalMass=DEFAULT_VALUE, massDensity=DEFAULT 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, **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") + # if(elem !=ElementType.POINTS and elem !=ElementType.EDGES): + # node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, **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, **kwargs) + node.addObject("UniformMass",name="mass", totalMass=totalMass, **kwargs) diff --git a/stlib/__init__.py b/stlib/__init__.py index 94db0b9c1..0ad659858 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1,6 +1,8 @@ __all__ = ["core","entities","geometries","materials","collision","visual"] import Sofa.Core +from stlib.core.basePrefab import BasePrefab + def __genericAdd(self : Sofa.Core.Node, typeName, **kwargs): def findName(cname, names): """Compute a working unique name in the node""" @@ -11,19 +13,29 @@ def findName(cname, names): rname = cname + str(i+1) return rname + + def checkName(context : Sofa.Core.Node, name): + # Check if the name already exists, if this happens, create a new one. + if name in context.children or name in context.objects: + names = {node.name.value for node in context.children} + names = names.union({object.name.value for object in context.objects}) + name = findName(name, names) + return name + + # Check if a name is provided, if not, use the one of the class params = kwargs.copy() - isNode = False - if "name" not in params: + if isinstance(typeName, type) and issubclass(typeName, BasePrefab): #Only for prefabs + if len(params.keys()) > 1 or (len(params.keys()) == 1 and "parameters" not in params): + raise RuntimeError("Invalid argument, a prefab takes only the \"parameters\" kwargs as input") + + elif "name" not in params : #This doesn't apply to prefab if isinstance(typeName, str): params["name"] = typeName - isNode=True elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Node): - params["name"] = "Node" - isNode=True + params["name"] = typeName.__name__ elif isinstance(typeName, Sofa.Core.Node): params["name"] = "Node" - isNode=True elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): params["name"] = typeName.name.value elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.ObjectDeclaration): @@ -31,17 +43,17 @@ def findName(cname, names): else: raise RuntimeError("Invalid argument ", typeName) - # Check if the name already exists, if this happens, create a new one. - if params["name"] in self.children or params["name"] in self.objects: - names = {node.name.value for node in self.children} - names = names.union({object.name.value for object in self.objects}) - params["name"] = findName(params["name"], names) + if isinstance(typeName, type) and issubclass(typeName, BasePrefab) and len(params.keys()) == 1: + params["parameters"].name = checkName(self, params["parameters"].name) + else: + params["name"] = checkName(self, params["name"]) # Dispatch the creation to either addObject or addChild - if isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Node): - pref = self.addChild(typeName(params["name"])) + if isinstance(typeName, type) and issubclass(typeName, BasePrefab): + pref = self.addChild(typeName(**params)) + pref.init() elif isinstance(typeName, Sofa.Core.Node): - pref = self.addChild(typeName) + pref = self.addChild(typeName(**params)) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): pref = self.addObject(typeName(**params)) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.ObjectDeclaration): diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index 1dd5665a3..83d29f55d 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -8,9 +8,9 @@ class BasePrefab(Sofa.Core.Node): A Prefab is a Sofa.Node that assembles a set of components and nodes """ - def __init__(self, params: BasePrefabParameters): - Sofa.Core.Node.__init__(self, name=params.name) - self.parameters = params + 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") diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index a60aa5a41..9a332dca2 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -43,22 +43,22 @@ def __init__(self, parameters=EntityParameters(), **kwargs): def init(self): - self.geometry = self.add(Geometry, self.parameters.geometry) + 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, self.parameters.material) + self.material = self.add(Material, parameters=self.parameters.material) self.material.States.position.parent = self.geometry.container.position.linkpath if self.parameters.collision is not None: - self.collision = self.add(Collision, self.parameters.collision) + self.collision = self.add(Collision, parameters=self.parameters.collision) self.addMapping(self.collision) if self.parameters.visual is not None: - self.visual = self.add(Visual, self.parameters.visual) + self.visual = self.add(Visual, parameters=self.parameters.visual) self.addMapping(self.visual) diff --git a/stlib/materials/deformable.py b/stlib/materials/deformable.py index cb424ed43..69a9558bb 100644 --- a/stlib/materials/deformable.py +++ b/stlib/materials/deformable.py @@ -29,6 +29,7 @@ def createScene(root) : 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() @@ -42,4 +43,5 @@ def createScene(root) : # destinationType=ElementType.TRIANGLES) bunnyParameters.visual.geometry = FileParameters(filename="mesh/Bunny.stl") bunnyParameters.visual.color = [1, 1, 1, 0.5] - bunny = root.add(Entity, bunnyParameters) \ No newline at end of file + bunny = root.add(Entity, parameters=bunnyParameters) + # bunny.init() \ No newline at end of file diff --git a/stlib/visual.py b/stlib/visual.py index 271aad3c7..a0f377f7d 100644 --- a/stlib/visual.py +++ b/stlib/visual.py @@ -21,7 +21,7 @@ def __init__(self, parameters: VisualParameters): BasePrefab.__init__(self, parameters) def init(self): - self.geometry = self.add(Geometry, self.parameters.geometry) + self.geometry = self.add(Geometry, parameters=self.parameters.geometry) self.addObject("OglModel", color=self.parameters.color, src=self.geometry.container.linkpath) From ac3ed748c3151f63694ac8dd81aa6d590cd68acd Mon Sep 17 00:00:00 2001 From: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Tue, 28 Oct 2025 18:21:39 +0100 Subject: [PATCH 43/49] Fix by passing state link when using other topology than edges (#550) --- splib/mechanics/mass.py | 18 +++++++++--------- stlib/materials/deformable.py | 7 ++++++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py index a3ee1fe28..c003c3e64 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -6,18 +6,18 @@ # 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, **kwargs): +def addMass(node, elem: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, **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") + if(elem !=ElementType.POINTS and elem !=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, **kwargs) + node.addObject("UniformMass",name="mass", totalMass=totalMass, topology=topology,**kwargs) diff --git a/stlib/materials/deformable.py b/stlib/materials/deformable.py index 69a9558bb..a24763df5 100644 --- a/stlib/materials/deformable.py +++ b/stlib/materials/deformable.py @@ -14,7 +14,12 @@ class DeformableBehaviorParameters(MaterialParameters): parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio def __addDeformableMaterial(node): - addMass(node, node.parameters.elementType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) + + massKwargs = {} + if node.parameters.elementType != ElementType.EDGES: #If we use the MeshMatrixMass, then the mass will need us to specify the mstate to use + massKwargs["geometryState"] = "@States" + + addMass(node, node.parameters.elementType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping, topology="@../Geometry/container", mass=massKwargs) # TODO : change this with inheritance if(node.parameters.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): addHyperelasticity(node, node.parameters.elementType, node.parameters.parameters, topology="@../Geometry/container") From 539badcb1b6d98f1b640fb838b50f68afff6104f Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 2 Mar 2026 09:10:20 +0100 Subject: [PATCH 44/49] Squashed commit of the following: commit 833bd690c1337f59a0238265e0a9cced392ded21 Author: Hugo Date: Fri Feb 27 00:40:59 2026 +0100 [doc] Use sphinx-design in order to fix doc generation (#586) Co-authored-by: hugtalbot commit d982bc81270822c1cd22d09b57ff69387eac6bbc Author: Alex Bilger Date: Thu Feb 26 23:11:32 2026 +0100 Remove unappropriate comment (#591) commit ae7e8e1476389fd611bcd91750764119c8885fa7 Author: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Thu Feb 26 18:39:56 2026 +0100 Ignore the meshing scenes (#587) commit 6593f348fd7e9cacdea980d2ea95683ff5b93dee Author: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Thu Feb 26 18:38:58 2026 +0100 [RPyC] Add new exposed methods to SOFA server (#570) * Add a way to init the root remotly * Add method to change working dir on remote commit bdbaeceee5ff4aa0a19b0e77a122bfe0f8decaf3 Author: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Thu Feb 26 10:29:37 2026 +0100 Add python modules that uses the CGAL binding (#567) * Add python modules that uses the CGAL binding instead of the CGALPlugin to generate mesh from a pointcloud or a surfacic polyhedron * Add a SOFA scene opening and using the generated mesh with CGAL * Sort all files into a meshing repo and add a Gmsh example * Make display optionnal * Fix typo --------- Co-authored-by: hugtalbot Co-authored-by: Themis Skamagkis <70031729+th-skam@users.noreply.github.com> commit 617d50637f5c322688aa64b1686fcbb161c148cc Author: Hugo Date: Thu Feb 26 10:29:13 2026 +0100 [examples] Add a thermo-elastic example (#204) * [examples] Add a thermo-elastic example * minor comment to remove * Get compatible with #198 and add legend * Remove option of topo changes * Update scene further to recently introduced templates in Mass * avoid using strings * update scene with imgui, linkpath etc --------- Co-authored-by: hugtalbot commit 4fb7e897b9eb639f618c730eb4ce30514f06fb8c Author: leobois67 Date: Wed Feb 25 18:58:24 2026 +0100 Example of a ForceField implemented with JAX (#557) * Add the example * Igne jax example on main CI * Fix ignoring the jax examples * Fix addKToMatrix() with different options * Apply suggestions from code review --------- Co-authored-by: Paul Baksic Co-authored-by: Hugo commit b3a79c67b9a759e25cc29f204ffa4f1e3fafa3a4 Author: Hugo Date: Wed Feb 25 15:50:54 2026 +0100 Fix code block indentation for createScene function (#582) Updated code block formatting for createScene function. This should fix the doc generation commit fc759676d153d1cc0278e2d0a4e955690bd7fa3b Author: Alex Bilger Date: Fri Feb 13 03:42:51 2026 +0100 fix wrong parameter order in addDForce and addKToMatrix (#579) * fix wrong parameter order in addDForce * also fix addKToMatrix commit ca2be7c3321cc6249804522ab2da20369e4a9b99 Author: Alex Bilger Date: Fri Feb 13 03:42:37 2026 +0100 Add bindings for prism and pyramid topology functions (#578) commit 346d2988efbd57dea87ae9f13fcc6f4725ac6b69 Author: Alex Bilger Date: Fri Feb 13 03:42:20 2026 +0100 ordering field is deprecated, use NaturalOrderingMethod instead (#577) commit 959b4cd9978739c41bf559bd53f05438b5109d85 Author: Alex Bilger Date: Fri Feb 13 03:42:02 2026 +0100 Redirect SOFA logs to Python's sys.stdout using a custom PythonMessageHandler (#576) * Redirect SOFA logs to Python's sys.stdout using a custom PythonMessageHandler. * cleaning * Redirect SOFA logs to Python's sys.stdout using a custom PythonMessageHandler. commit c0dc30e37467838527d5fba15200572d49983647 Author: Alex Bilger Date: Thu Feb 12 10:43:05 2026 +0100 Clean getRecords in Timer module (#561) * Clean getRecords in Timer module * remove extra /** commit d2ba4759e6848fa9e0ed717fc8f8e870f0664644 Author: Alex Bilger Date: Wed Feb 4 23:59:52 2026 +0100 include `sofa/core/fwd.h` instead of the forward declaration (#575) commit c209f3a110f52dbf3b5ca2430fee015bde1b0315 Author: Frederick Roy Date: Wed Feb 4 22:50:20 2026 +0900 fix mixed-up quote/doublequote, plus consistent quoting (#574) commit 7760402271e791d19954bf8570f7589ffb5ad4fb Author: Hugo Date: Tue Feb 3 08:05:33 2026 +0100 Generalize use of initRoot (#566) Co-authored-by: hugtalbot commit b29cfe5b77a40bbc6603e2a95ece1ca1e89e5c8e Author: Damien Marchal Date: Thu Jan 29 20:19:43 2026 +0100 Remove the implementation of getPathName as it is now in BaseData (#556) See: https://github.com/sofa-framework/sofa/pull/5759 Co-authored-by: Hugo commit c7fb25453c382d0350d61b14bfe06a288fdc815b Author: Alex Bilger Date: Thu Jan 29 20:08:22 2026 +0100 Introduce an example of a custom message handler (#569) commit f4e90d4b67250855650ed8f60361b1bc36be6859 Author: Hugo Date: Thu Jan 29 09:00:43 2026 +0100 Follow lifecycle v26.06 deprecated header (#571) Co-authored-by: hugtalbot commit 8bf0162ba54b14c0e7a148a4e1be789159ba4c5b Author: Frederick Roy Date: Wed Jan 7 06:48:05 2026 +0900 remove import of scipy.misc, which is deprecated (#564) commit 1016b94aadf5d95caaa0be8f7ca6ee4090f89347 Author: Alex Bilger Date: Thu Dec 18 16:37:52 2025 +0100 Update advanced_timer.py with comments and fix deprecated data (#562) commit 29c4e87cea2844394a4941c2d279324b5f4b9267 Author: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Tue Dec 16 12:01:32 2025 +0100 Add example scene using CCDTightInclusion (#559) * Add example scene using CCDTightInclusion * Replace ProjectedGaussSeidel with BlockGaussSeidel * Fix scene error commit d38d3c4e8ab8353d2d07fdb3e3cb47228b4359f6 Author: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Fri Dec 12 11:53:20 2025 +0100 Apply name changing on code and examples (#560) commit b09d92171b5ac37f6bb4a0b3412c419fc46277ff Author: Damien Marchal Date: Thu Nov 13 15:28:05 2025 +0100 Add Quaternion to python factory so we can create data field with this type. (#555) commit a66c0549bec11e4490c6b8ff7a4487d6472aafb7 Author: Damien Marchal Date: Thu Nov 13 15:27:48 2025 +0100 Improve error message when the SetDataFromArray does not match (#554) * Improve error message when the SetDataFromArray does not match * Apply suggestions from code review --------- Co-authored-by: Hugo commit 746745c450d60f08ca1617b3c748d3a23132884b Author: Damien Marchal Date: Thu Nov 13 15:27:28 2025 +0100 Add context manager behavior to Node() (#545) This allows to use with statment to have good looking indentation in sofa scene. eg: with Node("YOLO") as w: w.addObject("YY") commit af98611042d18f4bc53a678c5338afc8b6bcdebc Author: Alex Bilger Date: Thu Nov 13 15:25:55 2025 +0100 Support more types of linear systems (#542) * Support more types of linear systems * add support for BTD commit ae6b3300b314a64d8e32a8b82012211b28bfaed7 Author: Alex Bilger Date: Thu Nov 13 10:35:28 2025 +0100 add missing required plugin in test (#558) commit 6ef05edecc78d4c44064da373caf54e09af45374 Author: Alex Bilger Date: Tue Nov 11 14:34:26 2025 +0100 Fix binding after #5648 (#528) Co-authored-by: Hugo commit 5029c050f68078ab5173d1ffcace69598caa7f8b Author: Alex Bilger Date: Thu Nov 6 10:24:24 2025 +0100 Add validation for numpy array as return type in AddKToMatrix bindings (#541) * Add validation for numpy array as return type in AddKToMatrix bindings * Refactor AddKToMatrix return type validation and simplify conditional checks commit 8ba1e2c137bb46c6692ed540d659eca7b1420aa3 Author: Damien Marchal Date: Thu Oct 30 16:06:25 2025 +0100 Register in the PythonFactory Rigid3::Coord (#544) Because currently only Rigid3::VecCoord was supported. commit 72ed0e6364699f9d51a977b5951693a04ca18f23 Author: Damien Marchal Date: Thu Oct 30 16:06:16 2025 +0100 Add proper return value to event overriden in python (#543) Currently the controllers implemented in python cannot report the the event processing system in sofa that there is no need anymore to process the event. The PR allows to do. Every onXXXXXX() that implement an event processor can no return a value. If it is None or False then processing of event use this to continue the propagation If it is True (aka: has been processed/no more further), then the SOFA system will not proposed to other component to process the same event --- Plugin/src/SofaPython3/DataHelper.cpp | 10 +- Plugin/src/SofaPython3/PythonEnvironment.cpp | 2 +- Plugin/src/SofaPython3/PythonFactory.cpp | 12 +- .../Binding_ConstraintSolver_doc.h | 6 +- .../SofaLinearSolver/Binding_LinearSolver.cpp | 6 +- .../SofaLinearSystem/Binding_LinearSystem.cpp | 143 +- .../SofaLinearSystem/Binding_LinearSystem.inl | 101 + .../Binding_LinearSystem_doc.h | 32 +- .../SofaLinearSystem/CMakeLists.txt | 1 + .../SofaConstraintSolver/matrix_access.py | 4 +- .../tests/SofaDeformable/SpringForceField.py | 2 +- .../tests/SofaLinearSolver/matrix_access.py | 2 +- bindings/Sofa/package/__init__.py | 2 +- .../SofaPython3/Sofa/Core/Binding_Base.cpp | 8 +- .../Sofa/Core/Binding_BaseData.cpp | 16 +- .../Sofa/Core/Binding_BaseMeshTopology.cpp | 30 +- .../Sofa/Core/Binding_BaseObject.h | 8 +- .../Sofa/Core/Binding_Controller.cpp | 17 +- .../Sofa/Core/Binding_Controller.h | 2 +- .../Sofa/Core/Binding_ForceField.cpp | 45 +- .../SofaPython3/Sofa/Core/Binding_Node.cpp | 13 + .../SofaPython3/Sofa/Core/Binding_Node_doc.h | 2 +- .../Sofa/Core/Binding_TaskScheduler.cpp | 4 +- .../SofaPython3/Sofa/Core/Submodule_Core.cpp | 2 +- .../Sofa/Simulation/Binding_SceneCheck.cpp | 1 - .../Simulation/Submodule_Simulation_doc.h | 2 +- bindings/Sofa/tests/Core/BaseData.py | 6 +- bindings/Sofa/tests/Core/Controller.py | 2 +- bindings/Sofa/tests/Core/Events.py | 4 +- bindings/Sofa/tests/Core/ForceField.py | 19 +- bindings/Sofa/tests/Core/Mass.py | 2 +- bindings/SofaRuntime/CMakeLists.txt | 2 + .../SofaRuntime/Module_SofaRuntime.cpp | 6 +- .../SofaRuntime/PythonMessageHandler.cpp | 94 + .../SofaRuntime/PythonMessageHandler.h | 41 + .../SofaRuntime/Timer/Submodule_Timer.cpp | 149 +- docs/sphinx/source/conf.py | 2 +- docs/sphinx/source/content/Compilation.rst | 8 +- docs/sphinx/source/content/FirstSteps.rst | 10 +- docs/sphinx/source/content/Installation.rst | 16 +- docs/sphinx/source/content/UsingThePlugin.rst | 28 +- docs/sphinx/source/requirements.txt | 2 +- examples/.scene-tests | 6 + examples/CCDIntersection.py | 159 + examples/CCDIntersection.py.view | 17 + examples/MessageHandler.py | 99 + .../RPYC/ExampleManipulatingTheNodeLocally.py | 97 + examples/RPYC/SOFAService.py | 26 +- examples/ReadTheDocs_Example.py | 2 +- examples/access_compliance_matrix.py | 2 +- examples/access_constraint_matrix.py | 2 +- examples/access_contact_forces.py | 2 +- examples/access_energy.py | 4 +- examples/access_mass_matrix.py | 25 +- examples/access_matrix.py | 65 +- examples/advanced_timer.py | 65 +- examples/example-forcefield.py | 4 +- examples/jax/forcefield.py | 180 + examples/meshing/CGAL/README.txt | 5 + examples/meshing/CGAL/cgal_utils.py | 259 ++ examples/meshing/CGAL/data/point_cloud.pcd | Bin 0 -> 313 bytes examples/meshing/CGAL/data/torus.obj | 3263 +++++++++++++++++ ...mesh_from_point_cloud_using_convex_hull.py | 150 + examples/meshing/CGAL/mesh_from_polyhedron.py | 173 + examples/meshing/CGAL/requirements.txt | 4 + examples/meshing/Gmsh/generateMesh.py | 97 + examples/thermoelasticity.py | 115 + 67 files changed, 5340 insertions(+), 345 deletions(-) create mode 100644 bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.inl create mode 100644 bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp create mode 100644 bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.h create mode 100644 examples/CCDIntersection.py create mode 100644 examples/CCDIntersection.py.view create mode 100644 examples/MessageHandler.py create mode 100644 examples/RPYC/ExampleManipulatingTheNodeLocally.py create mode 100644 examples/jax/forcefield.py create mode 100644 examples/meshing/CGAL/README.txt create mode 100644 examples/meshing/CGAL/cgal_utils.py create mode 100644 examples/meshing/CGAL/data/point_cloud.pcd create mode 100644 examples/meshing/CGAL/data/torus.obj create mode 100644 examples/meshing/CGAL/mesh_from_point_cloud_using_convex_hull.py create mode 100644 examples/meshing/CGAL/mesh_from_polyhedron.py create mode 100644 examples/meshing/CGAL/requirements.txt create mode 100644 examples/meshing/Gmsh/generateMesh.py create mode 100644 examples/thermoelasticity.py diff --git a/Plugin/src/SofaPython3/DataHelper.cpp b/Plugin/src/SofaPython3/DataHelper.cpp index 2f4ab166d..df6f59866 100644 --- a/Plugin/src/SofaPython3/DataHelper.cpp +++ b/Plugin/src/SofaPython3/DataHelper.cpp @@ -95,15 +95,7 @@ std::ostream& operator<<(std::ostream& out, const py::buffer_info& p) std::string getPathTo(Base* b) { - BaseNode* node = dynamic_cast(b); - if(node) - return node->getPathName(); - BaseObject* object = dynamic_cast(b); - if(object) - return object->getPathName(); - - assert(true && "Only Base & BaseObject are supported"); - return ""; + return b->getPathName(); } const char* getFormat(const AbstractTypeInfo& nfo) diff --git a/Plugin/src/SofaPython3/PythonEnvironment.cpp b/Plugin/src/SofaPython3/PythonEnvironment.cpp index b1f41ff2c..b79c897c0 100644 --- a/Plugin/src/SofaPython3/PythonEnvironment.cpp +++ b/Plugin/src/SofaPython3/PythonEnvironment.cpp @@ -206,7 +206,7 @@ void PythonEnvironment::Init() // Workaround: try to import scipy from the main thread this prevents a deadlock when importing // scipy from a worker thread when we use the SofaScene asynchronous loading - executePython([]{ PyRun_SimpleString("try:\n\tfrom scipy import misc, optimize\nexcept:\n\tpass\n");}); + executePython([]{ PyRun_SimpleString("try:\n\tfrom scipy import optimize\nexcept:\n\tpass\n");}); // If the script directory is not available (e.g. if the interpreter is invoked interactively // or if the script is read from standard input), path[0] is the empty string, diff --git a/Plugin/src/SofaPython3/PythonFactory.cpp b/Plugin/src/SofaPython3/PythonFactory.cpp index 3a9c678b4..9949bbe9f 100644 --- a/Plugin/src/SofaPython3/PythonFactory.cpp +++ b/Plugin/src/SofaPython3/PythonFactory.cpp @@ -503,7 +503,17 @@ bool PythonFactory::registerDefaultTypes() PythonFactory::registerType("Hexa"); PythonFactory::registerType("Penta"); - // State vectors + // Rigid + PythonFactory::registerType("Rigid3d::Coord"); + PythonFactory::registerType("Rigid3f::Coord"); + PythonFactory::registerType("Rigid3::Coord"); + + /// Quaternion + PythonFactory::registerType>("Quatd"); + PythonFactory::registerType>("Quatf"); + PythonFactory::registerType>("Quat"); + + // Rigid3 vectors PythonFactory::registerType("Rigid3d::VecCoord"); PythonFactory::registerType("Rigid3f::VecCoord"); PythonFactory::registerType("Rigid3::VecCoord"); diff --git a/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h b/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h index 1689aef20..2fe4c923a 100644 --- a/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h +++ b/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h @@ -34,7 +34,7 @@ Returns the compliance matrix projected in constraint space, built in the constr example: ------------ -constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) +constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) matrix = constraint_solver.W() )"; @@ -45,7 +45,7 @@ Returns the force resulting from the constraints example: ------------ -constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) +constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) lambda = constraint_solver.lambda_force() )"; @@ -56,7 +56,7 @@ Returns the displacement computed without any constraint example: ------------ -constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) +constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) dfree = constraint_solver.dfree() )"; diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp b/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp index 99e992655..8f02c43a6 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp +++ b/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp @@ -67,7 +67,7 @@ void bindLinearSolvers(py::module &m) c.def("A", [](CRSLinearSolver& self) -> EigenSparseMatrix { - if (CRS* matrix = self.getSystemMatrix()) + if (CRS* matrix = self.l_linearSystem->getSystemMatrix()) { return toEigen(*matrix); } @@ -76,7 +76,7 @@ void bindLinearSolvers(py::module &m) c.def("b", [](CRSLinearSolver& self) -> Vector { - if (auto* vector = self.getSystemRHVector()) + if (auto* vector = self.l_linearSystem->getRHSVector()) { return EigenVectorMap(vector->ptr(), vector->size()); } @@ -85,7 +85,7 @@ void bindLinearSolvers(py::module &m) c.def("x", [](CRSLinearSolver& self) -> Vector { - if (auto* vector = self.getSystemLHVector()) + if (auto* vector = self.l_linearSystem->getSolutionVector()) { return EigenVectorMap(vector->ptr(), vector->size()); } diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp index b0cfc4ae2..d03773972 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp @@ -17,15 +17,12 @@ ******************************************************************************* * Contact information: contact@sofa-framework.org * ******************************************************************************/ -#include -#include -#include +#include -#include -#include -#include #include +#include +#include #include @@ -39,12 +36,6 @@ using EigenSparseMatrix = Eigen::SparseMatrix; template using EigenMatrixMap = Eigen::Map >; -template -using Vector = Eigen::Matrix; - -template -using EigenVectorMap = Eigen::Map>; - template EigenSparseMatrix::Real> toEigen(sofa::linearalgebra::CompressedRowSparseMatrix& matrix) @@ -70,62 +61,104 @@ toEigen(sofa::linearalgebra::CompressedRowSparseMatrix& matrix) } } -template -void bindLinearSystems(py::module &m) +template +void bindMatrixAccessCRS(LinearSystemClass& c) { - using CRS = sofa::linearalgebra::CompressedRowSparseMatrix; - using Real = typename CRS::Real; - using CRSLinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem >; + using Real = typename TMatrix::Real; + using CRSLinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem; - const std::string typeName = CRSLinearSystem::GetClass()->className + CRSLinearSystem::GetCustomTemplateName(); - - py::class_ > c(m, typeName.c_str(), sofapython3::doc::linearsystem::linearSystemClass); - - c.def("A", [](CRSLinearSystem& self) -> EigenSparseMatrix - { - if (CRS* matrix = self.getSystemMatrix()) + const auto matrixAccess = + [](CRSLinearSystem& self) -> EigenSparseMatrix { - if (matrix->colsValue.empty()) //null matrix: contains no entries + if (TMatrix* matrix = self.getSystemMatrix()) { - return EigenSparseMatrix{matrix->rows(), matrix->cols()}; + if (matrix->colsValue.empty()) //null matrix: contains no entries + { + return EigenSparseMatrix{matrix->rows(), matrix->cols()}; + } + return toEigen(*matrix); } - return toEigen(*matrix); - } - return {}; - }, sofapython3::doc::linearsystem::linearSystem_A); + return {}; + }; - c.def("b", [](CRSLinearSystem& self) -> Vector - { - if (auto* vector = self.getRHSVector()) - { - return EigenVectorMap(vector->ptr(), vector->size()); - } - return {}; - }, sofapython3::doc::linearsystem::linearSystem_b); + c.def("A", matrixAccess, sofapython3::doc::linearsystem::linearSystem_CRS_A); + c.def("get_system_matrix", matrixAccess, sofapython3::doc::linearsystem::linearSystem_CRS_get_system_matrix); +} - c.def("x", [](CRSLinearSystem& self) -> Vector - { - if (auto* vector = self.getSolutionVector()) - { - return EigenVectorMap(vector->ptr(), vector->size()); - } - return {}; - }, sofapython3::doc::linearsystem::linearSystem_x); +template<> +void bindMatrixAccess(LinearSystemClass, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} - /// register the binding in the downcasting subsystem - PythonFactory::registerType([](sofa::core::objectmodel::Base* object) - { - return py::cast(dynamic_cast(object)); - }); +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template +using DenseMatrix = Eigen::Matrix; + +template +using EigenDenseMatrixMap = Eigen::Map>; + +template<> +void bindMatrixAccess(LinearSystemClass, sofa::linearalgebra::FullVector>& c) +{ + using CRSLinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem, sofa::linearalgebra::FullVector>; + + const auto matrixAccess = + [](CRSLinearSystem& self) -> EigenDenseMatrixMap + { + sofa::linearalgebra::FullMatrix* matrix = self.getSystemMatrix(); + const auto row = matrix ? matrix->rows() : 0; + const auto col = matrix ? matrix->cols() : 0; + return EigenDenseMatrixMap(matrix ? matrix->ptr() : nullptr, row, col); + }; + c.def("A", matrixAccess, sofapython3::doc::linearsystem::linearSystem_FullMatrix_A); + c.def("get_system_matrix", matrixAccess, sofapython3::doc::linearsystem::linearSystem_FullMatrix_get_system_matrix); } void moduleAddLinearSystem(py::module &m) { - bindLinearSystems(m); - bindLinearSystems >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + + bindLinearSystems, sofa::linearalgebra::BlockVector<6, SReal> >(m); } } diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.inl b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.inl new file mode 100644 index 000000000..570567ccd --- /dev/null +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.inl @@ -0,0 +1,101 @@ +/****************************************************************************** +* SofaPython3 plugin * +* (c) 2021 CNRS, University of Lille, INRIA * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#pragma once +#include +#include +#include +#include + +#include +#include +#include + +namespace py { using namespace pybind11; } + +namespace sofapython3 +{ + +template +using Vector = Eigen::Matrix; + +template +using EigenVectorMap = Eigen::Map>; + +template +Vector +getVector(TVector* vector) +{ + using Real = typename TVector::Real; + if (vector) + { + return EigenVectorMap(vector->ptr(), vector->size()); + } + return {}; +} + +template +Vector getRHSVector(sofa::component::linearsystem::TypedMatrixLinearSystem& linearSystem) +{ + return getVector(linearSystem.getRHSVector()); +} + +template +Vector getSolutionVector(sofa::component::linearsystem::TypedMatrixLinearSystem& linearSystem) +{ + return getVector(linearSystem.getSolutionVector()); +} + +template +using LinearSystemClass = + py::class_, + sofa::core::objectmodel::BaseObject, + sofapython3::py_shared_ptr> >; + +template +void bindMatrixAccess(LinearSystemClass& c) +{} + +template +void bindLinearSystems(py::module &m) +{ + using LinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem; + + const std::string typeName = + LinearSystem::GetClass()->className + + LinearSystem::GetCustomTemplateName(); + + LinearSystemClass c(m, typeName.c_str(), sofapython3::doc::linearsystem::linearSystemClass); + + c.def("b", getRHSVector, sofapython3::doc::linearsystem::linearSystem_b); + c.def("get_rhs_vector", getRHSVector, sofapython3::doc::linearsystem::linearSystem_b); + + c.def("x", getSolutionVector, sofapython3::doc::linearsystem::linearSystem_x); + c.def("get_solution_vector", getSolutionVector, sofapython3::doc::linearsystem::linearSystem_x); + + bindMatrixAccess(c); + + /// register the binding in the downcasting subsystem + PythonFactory::registerType([](sofa::core::objectmodel::Base* object) + { + return py::cast(dynamic_cast(object)); + }); +} + +} diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h index 6c5786e8a..421fa221f 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h @@ -32,7 +32,7 @@ Linear system. Supports only CompressedRowSparseMatrix. linear_system = root.addObject('MatrixLinearSystem', template='CompressedRowSparseMatrixd') )"; -static auto linearSystem_A = +static auto linearSystem_CRS_A = R"( Returns the global system matrix as a scipy sparse matrix @@ -42,6 +42,36 @@ linear_system = root.addObject('MatrixLinearSystem', template='CompressedRowSpar matrix = linear_system.A() )"; +static auto linearSystem_CRS_get_system_matrix = +R"( +Returns the global system matrix as a scipy sparse matrix + +example: +------------ +linear_system = root.addObject('MatrixLinearSystem', template='CompressedRowSparseMatrixd') +matrix = linear_system.get_system_matrix() +)"; + +static auto linearSystem_FullMatrix_A = +R"( +Returns the global system matrix as a numpy array matrix + +example: +------------ +linear_system = root.addObject('MatrixLinearSystem', template='FullMatrix') +matrix = linear_system.A() +)"; + +static auto linearSystem_FullMatrix_get_system_matrix = +R"( +Returns the global system matrix as a numpy array matrix + +example: +------------ +linear_system = root.addObject('MatrixLinearSystem', template='FullMatrix') +matrix = linear_system.get_system_matrix() +)"; + static auto linearSystem_b = R"( Returns the global system right hand side as a numpy array diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt b/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt index d6b64a130..fd0805488 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt @@ -2,6 +2,7 @@ project(Bindings.Modules.SofaLinearSystem) set(HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Binding_LinearSystem.h + ${CMAKE_CURRENT_SOURCE_DIR}/Binding_LinearSystem.inl ${CMAKE_CURRENT_SOURCE_DIR}/Binding_LinearSystem_doc.h ) diff --git a/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py b/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py index 00d4d00f4..037796fe3 100644 --- a/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py +++ b/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py @@ -21,7 +21,7 @@ def simulate_pendulum(self): "Sofa.Component.Topology.Container.Dynamic"]) root.addObject("FreeMotionAnimationLoop", solveVelocityConstraintFirst=True) - root.addObject("ProjectedGaussSeidelConstraintSolver", name="constraint_solver", tolerance=1e-9, maxIterations=1000) + root.addObject("BlockGaussSeidelConstraintSolver", name="constraint_solver", tolerance=1e-9, maxIterations=1000) root.addObject("StringMeshCreator", name="loader", resolution="20") root.addObject("EulerImplicitSolver") @@ -39,7 +39,7 @@ def simulate_pendulum(self): ext.addObject("DistanceMapping", name="distanceMapping", topology="@../edge_container") ext.addObject("UniformConstraint", template="Vec1d", iterative=True) - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/Modules/tests/SofaDeformable/SpringForceField.py b/bindings/Modules/tests/SofaDeformable/SpringForceField.py index 72d721b6a..052f335e5 100644 --- a/bindings/Modules/tests/SofaDeformable/SpringForceField.py +++ b/bindings/Modules/tests/SofaDeformable/SpringForceField.py @@ -38,7 +38,7 @@ class Test(unittest.TestCase): def setUp(self) -> None: self.root = Sofa.Core.Node() create_scene(self.root) - Sofa.Simulation.init(self.root) + Sofa.Simulation.initRoot(self.root) def tearDown(self) -> None: Sofa.Simulation.unload(self.root) diff --git a/bindings/Modules/tests/SofaLinearSolver/matrix_access.py b/bindings/Modules/tests/SofaLinearSolver/matrix_access.py index 5c8c54eeb..d69a0f9ce 100644 --- a/bindings/Modules/tests/SofaLinearSolver/matrix_access.py +++ b/bindings/Modules/tests/SofaLinearSolver/matrix_access.py @@ -26,7 +26,7 @@ def simulate_beam(self, linear_solver_template): root.addObject('FixedConstraint', indices="@box.indices") root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/Sofa/package/__init__.py b/bindings/Sofa/package/__init__.py index 8486b4d3b..8926e8cee 100644 --- a/bindings/Sofa/package/__init__.py +++ b/bindings/Sofa/package/__init__.py @@ -13,7 +13,7 @@ n.addChild("Node2") n.addObject("MechanicalObject", name="dofs") - Sofa.Simulation.init(n) + Sofa.Simulation.initRoot(n) Sofa.Simulation.print(n) """ diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp index e71150634..906412979 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp @@ -245,8 +245,12 @@ void BindingBase::SetDataFromArray(BaseData* data, const py::array& value) return copyScalar(data, nfo, src); else if(srcinfo.format=="f") return copyScalar(data, nfo, src); - else - throw std::runtime_error("SetAttrFromArray :: unsupported fileformat"); + else + { + std::stringstream tmp; + tmp << "SetAttrFromArray :: unsupported fileformat '" << srcinfo.format << "'" ; + throw std::runtime_error(tmp.str()); + } } } diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp index 8aed7ffa0..25f37de6a 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp @@ -45,18 +45,6 @@ using sofa::defaulttype::AbstractTypeInfo; namespace sofapython3 { -std::string getPathName(BaseData& self) -{ - Base* b= self.getOwner(); - std::string prefix = getPathTo(b); - return prefix+"."+self.getName(); -} - -std::string getLinkPath(BaseData& self) -{ - return "@"+getPathName(self); -} - bool hasChanged(BaseData& data) { if (data.isDirty()) { @@ -230,8 +218,8 @@ void moduleAddBaseData(py::module& m) data.def("getOwner", &getOwner, sofapython3::doc::baseData::getOwner); data.def("getParent", &BaseData::getParent, sofapython3::doc::baseData::getParent); data.def("typeName", [](BaseData& data){ return data.getValueTypeInfo()->name(); }, sofapython3::doc::baseData::typeName); - data.def("getPathName", getPathName, sofapython3::doc::baseData::getPathName); - data.def("getLinkPath", getLinkPath, sofapython3::doc::baseData::getLinkPath); + data.def("getPathName", &BaseData::getPathName, sofapython3::doc::baseData::getPathName); + data.def("getLinkPath", &BaseData::getLinkPath, sofapython3::doc::baseData::getLinkPath); data.def("hasChanged", hasChanged, sofapython3::doc::baseData::hasChanged); data.def("isSet", isSet, sofapython3::doc::baseData::isSet); data.def("toList", toList, sofapython3::doc::baseData::toList); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp index dec413956..92ca8954d 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp @@ -65,11 +65,13 @@ void moduleAddBaseMeshTopology(py::module& m) { c.def("getNbTriangles", &BaseMeshTopology::getNbTriangles); c.def("getNbTetrahedra", &BaseMeshTopology::getNbTetrahedra); c.def("getNbHexahedra", &BaseMeshTopology::getNbHexahedra); + c.def("getNbPrisms", &BaseMeshTopology::getNbPrisms); + c.def("getNbPyramids", &BaseMeshTopology::getNbPyramids); c.def("getNbQuads", &BaseMeshTopology::getNbQuads); c.def("getNbTetras", &BaseMeshTopology::getNbTetras); c.def("getEdge", - [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { const auto & e = self.getEdge(index); return {{e[0], e[1]}}; }, @@ -78,7 +80,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getTriangle", - [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { const auto & t = self.getTriangle(index); return {{t[0], t[1], t[2]}}; }, @@ -87,7 +89,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getQuad", - [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { const auto & q = self.getQuad(index); return {{q[0], q[1], q[2], q[3]}}; }, @@ -96,7 +98,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getTetrahedron", - [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { const auto & n = self.getTetrahedron(index); return {{n[0], n[1], n[2], n[3]}}; }, @@ -105,7 +107,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getHexahedron", - [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { const auto & n = self.getHexahedron(index); return {{n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7]}}; }, @@ -113,6 +115,24 @@ void moduleAddBaseMeshTopology(py::module& m) { "Returns the vertices of Hexahedron at index." ); + c.def("getPrism", + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + const auto & n = self.getPrism(index); + return {{n[0], n[1], n[2], n[3], n[4], n[5]}}; + }, + py::arg("index"), + "Returns the vertices of Prism at index." + ); + + c.def("getPyramid", + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + const auto & n = self.getPyramid(index); + return {{n[0], n[1], n[2], n[3], n[4]}}; + }, + py::arg("index"), + "Returns the vertices of Pyramid at index." + ); + c.def("getLocalEdgesInTetrahedron", [] (const BaseMeshTopology & self, const sofa::Index & index) -> std::array { const auto & e = self.getLocalEdgesInTetrahedron(index); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h index b85eed1f0..3908e6044 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h @@ -21,12 +21,10 @@ #pragma once #include +#include -namespace sofa::core::objectmodel { -class BaseObject; -} - -namespace sofapython3 { +namespace sofapython3 +{ pybind11::object getItem(const sofa::core::objectmodel::BaseObject & self, const std::string& path); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp index 8e87c3bf4..0f26c7656 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp @@ -68,7 +68,7 @@ void Controller_Trampoline::reinit() /// If a method named "methodName" exists in the python controller, /// methodName is called, with the Event's dict as argument -void Controller_Trampoline::callScriptMethod( +bool Controller_Trampoline::callScriptMethod( const py::object& self, Event* event, const std::string & methodName) { if(f_printLog.getValue()) @@ -81,8 +81,13 @@ void Controller_Trampoline::callScriptMethod( if( py::hasattr(self, methodName.c_str()) ) { py::object fct = self.attr(methodName.c_str()); - fct(PythonFactory::toPython(event)); + py::object result = fct(PythonFactory::toPython(event)); + if(result.is_none()) + return false; + + return py::cast(result); } + return false; } void Controller_Trampoline::handleEvent(Event* event) @@ -95,13 +100,17 @@ void Controller_Trampoline::handleEvent(Event* event) { py::object fct = self.attr(name.c_str()); if (PyCallable_Check(fct.ptr())) { - callScriptMethod(self, event, name); + bool isHandled = callScriptMethod(self, event, name); + if(isHandled) + event->setHandled(); return; } } /// Is the fallback method available. - callScriptMethod(self, event, "onEvent"); + bool isHandled = callScriptMethod(self, event, "onEvent"); + if(isHandled) + event->setHandled(); }); } diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h index dee81152f..d1bd91664 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h @@ -50,7 +50,7 @@ class Controller_Trampoline : public Controller std::string getClassName() const override; private: - void callScriptMethod(const pybind11::object& self, sofa::core::objectmodel::Event* event, + bool callScriptMethod(const pybind11::object& self, sofa::core::objectmodel::Event* event, const std::string& methodName); }; diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp index 85f2461a7..e58068f07 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp @@ -147,36 +147,39 @@ namespace sofapython3 py::object ret = _addKToMatrix(mparams, nNodes, nDofs); + if(!py::isinstance(ret)) + { + throw py::type_error("Can't read return value of AddKToMatrix. A numpy array is expected"); + } + // if ret is numpy array - if(py::isinstance(ret)) + auto r = py::cast(ret); + if (r.ndim() == 3 && r.shape(2) == 1) { - auto r = py::cast(ret); - if (r.ndim() == 3 && r.shape(2) == 1) + // read K as a plain 2D matrix + auto kMatrix = r.unchecked(); + for (size_t x = 0 ; x < size_t(kMatrix.shape(0)) ; ++x) { - // read K as a plain 2D matrix - auto kMatrix = r.unchecked(); - for (size_t x = 0 ; x < size_t(kMatrix.shape(0)) ; ++x) + for (size_t y = 0 ; y < size_t(kMatrix.shape(1)) ; ++y) { - for (size_t y = 0 ; y < size_t(kMatrix.shape(1)) ; ++y) - { - mat->add(int(offset + x), int(offset + y), kMatrix(x,y, 0)); - } + mat->add(int(offset + x), int(offset + y), kMatrix(x,y, 0)); } } - else if (r.ndim() == 2 && r.shape(1) == 3) - { - // consider ret to be a list of tuples [(i,j,[val])] - auto kMatrix = r.unchecked(); - for (auto x = 0 ; x < kMatrix.shape(0) ; ++x) - { - mat->add(int(offset + size_t(kMatrix(x,0))), int(offset + size_t(kMatrix(x,1))), kMatrix(x,2)); - } - } - else + } + else if (r.ndim() == 2 && r.shape(1) == 3) + { + // consider ret to be a list of tuples [(i,j,[val])] + auto kMatrix = r.unchecked(); + for (auto x = 0 ; x < kMatrix.shape(0) ; ++x) { - throw py::type_error("Can't read return value of AddKToMatrix. The method should return either a plain 2D matrix or a vector of tuples (i, j, val)"); + mat->add(int(offset + size_t(kMatrix(x,0))), int(offset + size_t(kMatrix(x,1))), kMatrix(x,2)); } } + else + { + throw py::type_error("Can't read return value of AddKToMatrix. The method should return either a plain 2D matrix or a vector of tuples (i, j, val)"); + } + } diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp index df26021d6..a290c1b58 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp @@ -723,5 +723,18 @@ void moduleAddNode(py::module &m) { p.def("sendEvent", &sendEvent, sofapython3::doc::sofa::core::Node::sendEvent); p.def("computeEnergy", &computeEnergy, sofapython3::doc::sofa::core::Node::computeEnergy); + p.def("__enter__", [](py::object self) + { + if(pybind11::hasattr(self, "onCtxEnter")) + self.attr("onCtxEnter")(self); + return self; + }); + + p.def("__exit__", + [](py::object self, py::object type, py::object value, py::object traceback) + { + if(pybind11::hasattr(self, "onCtxExit")) + self.attr("onCtxExit")(self, type, value, traceback); + }); } } /// namespace sofapython3 diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h index 4caeb56c6..12761f47f 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h @@ -48,7 +48,7 @@ static auto Class = # Add a mechanical component to MyNode n.addObject("MechanicalObject", name="dofs") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.print(root) The child nodes, components and parents can be accessed using generator attributes. diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp index 18da1e449..b7fd649c3 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp @@ -19,8 +19,8 @@ ******************************************************************************/ #include -#include -#include +#include +#include #include diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp index 3e4612864..f56b278ea 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp @@ -100,7 +100,7 @@ PYBIND11_MODULE(Core, core) # Add a mechanical component to MyNode n.addObject("MechanicalObject", name="dofs") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.print(root) )doc"; diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp index cc191ccf2..945744ecd 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp @@ -28,7 +28,6 @@ namespace py { using namespace pybind11; } void moduleAddSceneCheck(pybind11::module &m) { - // create a python binding for the C++ class LinearSpring from SofaDeformable py::class_> s (m, "SceneCheck", sofapython3::doc::simulation::SceneCheckClass); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h index d03d9d3f2..8d5839f05 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h @@ -37,7 +37,7 @@ static auto Class = SofaRuntime.importPlugin("SofaComponentAll") n = Sofa.Core.Node("MyNode") - Sofa.Simulation.init(n) + Sofa.Simulation.initRoot(n) Sofa.Simulation.print(n) )"; static auto print = diff --git a/bindings/Sofa/tests/Core/BaseData.py b/bindings/Sofa/tests/Core/BaseData.py index e4b1e418f..bd3cdf0f5 100644 --- a/bindings/Sofa/tests/Core/BaseData.py +++ b/bindings/Sofa/tests/Core/BaseData.py @@ -255,7 +255,7 @@ def test_DataAsContainerNumpyArray_testIsDirtyOnDoubleAccess_(self): root.addObject("PointSetTopologyContainer", points=[[0, 0, 0], [1, 0, 0]]) modifier = root.addObject("PointSetTopologyModifier") mo = root.addObject("MechanicalObject") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) modifier.addPoints(10, True) self.assertEqual(len(mo.position), 12) @@ -269,7 +269,7 @@ def test_DataAsContainerNumpyArray_testIsDirtyOnDoubleWriteAccess_(self): root.addObject("PointSetTopologyContainer", points=[[0, 0, 0], [1, 0, 0]]) modifier = root.addObject("PointSetTopologyModifier") mo = root.addObject("MechanicalObject") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) modifier.addPoints(10, True) with mo.position.writeable() as w: @@ -283,7 +283,7 @@ def test_DataAsContainerNumpyArray_(self): root = create_scene("rootNode") v = numpy.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3]]) c = root.addObject("MechanicalObject", name="t", position=v.tolist()) - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) numpy.testing.assert_array_equal(c.position.array(), v) diff --git a/bindings/Sofa/tests/Core/Controller.py b/bindings/Sofa/tests/Core/Controller.py index 2ab698434..a9c09625b 100644 --- a/bindings/Sofa/tests/Core/Controller.py +++ b/bindings/Sofa/tests/Core/Controller.py @@ -77,7 +77,7 @@ def test_events(self): self.assertTrue( hasattr(controller, "iterations") ) - Sofa.Simulation.init(node) + Sofa.Simulation.initRoot(node) for i in range(10): Sofa.Simulation.animate(node, 0.01) diff --git a/bindings/Sofa/tests/Core/Events.py b/bindings/Sofa/tests/Core/Events.py index 3e5cf83ec..2281f0280 100644 --- a/bindings/Sofa/tests/Core/Events.py +++ b/bindings/Sofa/tests/Core/Events.py @@ -42,10 +42,10 @@ def test_events(self): node.addObject("RequiredPlugin", name="Sofa.Component.AnimationLoop") node.addObject("RequiredPlugin", name="Sofa.Component.Constraint.Lagrangian.Solver") node.addObject("FreeMotionAnimationLoop", name="loop") - node.addObject("ProjectedGaussSeidelConstraintSolver", name="constraintSolver") + node.addObject("BlockGaussSeidelConstraintSolver", name="constraintSolver") controller = node.addObject( MyController() ) - Sofa.Simulation.init(node) + Sofa.Simulation.initRoot(node) for i in range(10): Sofa.Simulation.animate(node, 0.01) diff --git a/bindings/Sofa/tests/Core/ForceField.py b/bindings/Sofa/tests/Core/ForceField.py index 7a83fbe13..da429b64c 100644 --- a/bindings/Sofa/tests/Core/ForceField.py +++ b/bindings/Sofa/tests/Core/ForceField.py @@ -67,7 +67,7 @@ def test_0_explicit(self): use_iterative_solver=False) # do some steps here - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) for i in range(0, 100): Sofa.Simulation.animate(root, root.dt.value) @@ -80,7 +80,7 @@ def test_1_implicit_iterative(self): root = rssffScene(use_implicit_scheme=True, use_iterative_solver=True) # do some steps here - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) for i in range(0, 100): Sofa.Simulation.animate(root, root.dt.value) @@ -93,7 +93,7 @@ def test_2_implicit_direct(self): root = rssffScene(use_implicit_scheme=True, use_iterative_solver=False) # do some steps here - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) for i in range(0, 100): Sofa.Simulation.animate(root, root.dt.value) @@ -108,13 +108,14 @@ def simulate_beam(linear_solver_template): root.addObject('DefaultAnimationLoop') - root.addObject('RequiredPlugin', name='Sofa.Component.Topology.Container.Grid') - root.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') - root.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') - root.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') root.addObject('RequiredPlugin', name='Sofa.Component.Constraint.Projective') - root.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') + root.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') + root.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') root.addObject('RequiredPlugin', name='Sofa.Component.Mass') + root.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') + root.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') + root.addObject('RequiredPlugin', name='Sofa.Component.StateContainer') + root.addObject('RequiredPlugin', name='Sofa.Component.Topology.Container.Grid') root.addObject('EulerImplicitSolver', rayleighStiffness="0.1", rayleighMass="0.1") root.addObject('SparseLDLSolver', template=linear_solver_template) @@ -126,7 +127,7 @@ def simulate_beam(linear_solver_template): root.addObject('FixedConstraint', indices="@box.indices") root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/Sofa/tests/Core/Mass.py b/bindings/Sofa/tests/Core/Mass.py index b5ed4cdea..5de8bb5a3 100644 --- a/bindings/Sofa/tests/Core/Mass.py +++ b/bindings/Sofa/tests/Core/Mass.py @@ -34,7 +34,7 @@ def simulate_beam(linear_solver_template): root.addObject('FixedConstraint', indices="@box.indices") root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/SofaRuntime/CMakeLists.txt b/bindings/SofaRuntime/CMakeLists.txt index 196701e17..52c4d80e8 100644 --- a/bindings/SofaRuntime/CMakeLists.txt +++ b/bindings/SofaRuntime/CMakeLists.txt @@ -2,10 +2,12 @@ project(Bindings.SofaRuntime) set(SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp ) set(HEADER_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/PythonMessageHandler.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer_doc.h ) diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp index c501fdb6b..2c6636070 100644 --- a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp @@ -62,7 +62,7 @@ using sofapython3::SceneLoaderPY3; #include using sofa::helper::logging::MessageDispatcher; using sofa::helper::logging::MainPerComponentLoggingMessageHandler; -using sofa::helper::logging::MainConsoleMessageHandler; +#include #include #include @@ -155,9 +155,9 @@ PYBIND11_MODULE(SofaRuntime, m) { m.def("init", []() { MessageDispatcher::clearHandlers(); - MessageDispatcher::addHandler(&MainConsoleMessageHandler::getInstance()); + MessageDispatcher::addHandler(&MainPythonMessageHandler::getInstance()); MessageDispatcher::addHandler(&MainPerComponentLoggingMessageHandler::getInstance()); - }); + }, "redirect SOFA messages to Python's sys.stdout"); m.add_object("DataRepository", py::cast(&sofa::helper::system::DataRepository)); m.add_object("PluginRepository", py::cast(&sofa::helper::system::PluginRepository)); diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp new file mode 100644 index 000000000..3bfc9629b --- /dev/null +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp @@ -0,0 +1,94 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#include +#include +#include + +namespace sofapython3 +{ + +namespace +{ + +using namespace std::string_literals; +const std::string red { "\033[31m"s }; +const std::string green { "\033[32m"s }; +const std::string orange { "\033[38;5;214m"s }; +const std::string magenta { "\033[35m"s }; +const std::string blue { "\033[34m"s }; +const std::string reset { "\033[0m"s }; + +std::string format(const std::string& type, const std::string& color) +{ + return color + "[" + type + "] " + reset; +}; + +const std::string& getPrefixText(sofa::helper::logging::Message::Type type) +{ + static const std::string advice = format("SUGGESTION", green); + static const std::string deprecated = format("DEPRECATED", orange); + static const std::string warning = format("WARNING", orange); + static const std::string info = format("INFO", green); + static const std::string error = format("ERROR", red); + static const std::string fatal = format("FATAL", magenta); + static const std::string empty = format("EMPTY", reset); + static const std::string nothing{}; + + switch (type) + { + case sofa::helper::logging::Message::Advice : return advice; + case sofa::helper::logging::Message::Deprecated : return deprecated; + case sofa::helper::logging::Message::Warning : return warning; + case sofa::helper::logging::Message::Info : return info; + case sofa::helper::logging::Message::Error : return error; + case sofa::helper::logging::Message::Fatal : return fatal; + case sofa::helper::logging::Message::TEmpty : return empty; + + case sofa::helper::logging::Message::TypeCount: + return nothing; + } +} + +} + +namespace py { using namespace pybind11; } + +void PythonMessageHandler::process(sofa::helper::logging::Message &m) +{ + if (!m.sender().empty()) + { + py::print(getPrefixText(m.type()), format(m.sender(), blue), m.messageAsString()); + } + else + { + py::print(getPrefixText(m.type()), m.messageAsString()); + } +} + +PythonMessageHandler& MainPythonMessageHandler::getInstance() +{ + static PythonMessageHandler s_instance; + return s_instance; +} + +} // namespace sofapython3 + diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.h b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.h new file mode 100644 index 000000000..fd1636f13 --- /dev/null +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.h @@ -0,0 +1,41 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#pragma once + +#include + +namespace sofapython3 +{ + +class PythonMessageHandler : public sofa::helper::logging::MessageHandler +{ +public: + PythonMessageHandler() = default; + void process(sofa::helper::logging::Message& m) override ; +}; + +class MainPythonMessageHandler +{ +public: + static PythonMessageHandler& getInstance() ; +}; +} // namespace sofapython3 diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp index b18d88a01..c6f447228 100644 --- a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp @@ -32,23 +32,38 @@ using sofa::helper::AdvancedTimer; namespace sofapython3 { +/** + * @brief Converts AdvancedTimer records to a Python dictionary structure + * + * This function processes the timer records and builds a hierarchical Python dictionary + * that represents the timer data in a format that's easy to use in Python. + * + * @param id The timer ID to get records for + * @return A Python dictionary representing the timer records + */ py::dict getRecords(const std::string & id) { using sofa::helper::Record; using sofa::helper::system::thread::ctime_t; - using sofa::helper::system::thread::CTime; - static auto timer_freq = CTime::getTicksPerSec(); - auto getTime = [](ctime_t t) + auto getTime = [](ctime_t t, ctime_t referenceTime) { - return 1000. * t / timer_freq; + constexpr double nbMillisecPerSec = 1000.; + return nbMillisecPerSec * sofa::helper::system::thread::CTime::toSecond(t - referenceTime); }; const auto records = AdvancedTimer::getRecords(id); - std::stack tokens; - py::dict token, token_temp; - tokens.push(token); - ctime_t t0; + // Stack of dictionaries that represents the hierarchical structure of timer records + // Each element in the stack corresponds to a different level in the timer hierarchy. + std::stack hierarchyStack; + + // Current dictionary being processed at the top of the stack + // Represents the most recently created level in the timer hierarchy. + py::dict currentLevel; + + py::dict token_temp; + hierarchyStack.push(currentLevel); + std::optional referenceTime; for (const auto& r : records) { @@ -57,97 +72,67 @@ py::dict getRecords(const std::string & id) { case Record::RNONE: break; case Record::RBEGIN: // Timer begins - token = tokens.top(); - if (token.contains(r.label.c_str())) + case Record::RSTEP_BEGIN: // Step begins + currentLevel = hierarchyStack.top(); + if (currentLevel.contains(r.label.c_str())) { - if (py::list::check_(token[r.label.c_str()])) + if (py::list::check_(currentLevel[r.label.c_str()])) { token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; + py::list(currentLevel[r.label.c_str()]).append(token_temp); + currentLevel = token_temp; } - else if (py::dict::check_(token[r.label.c_str()])) + else if (py::dict::check_(currentLevel[r.label.c_str()])) { - token_temp = token[r.label.c_str()]; - token[r.label.c_str()] = py::list(); - py::list(token[r.label.c_str()]).append(token_temp); + token_temp = currentLevel[r.label.c_str()]; + currentLevel[r.label.c_str()] = py::list(); + py::list(currentLevel[r.label.c_str()]).append(token_temp); token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; + py::list(currentLevel[r.label.c_str()]).append(token_temp); + currentLevel = token_temp; } else { - msg_error("Timer::getRecords") << "Got an unexpected token of type '" << std::string(py::str(token.get_type())) << "'."; + msg_error("Timer::getRecords") << "Got an unexpected token of type '" << std::string(py::str(currentLevel.get_type())) << "'."; break; } } else { - token[r.label.c_str()] = py::dict(); - token = token[r.label.c_str()]; + // Creating a new level in the hierarchy for the current timer label + currentLevel[r.label.c_str()] = py::dict(); + // Update the current level to the one just added + currentLevel = currentLevel[r.label.c_str()]; } - t0 = r.time; - token["start_time"] = getTime(r.time - t0); - tokens.push(token); - break; - case Record::REND: // Timer ends - token = tokens.top(); - token["end_time"] = getTime(r.time - t0); - token["total_time"] = getTime(r.time - t0) - py::cast(token["start_time"]); - tokens.pop(); - break; - case Record::RSTEP_BEGIN: // Step begins - token = tokens.top(); - if (token.contains(r.label.c_str())) + if (r.type == Record::RBEGIN) { - if (py::list::check_(token[r.label.c_str()])) - { - token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; - } - else if (py::dict::check_(token[r.label.c_str()])) - { - token_temp = token[r.label.c_str()]; - token[r.label.c_str()] = py::list(); - py::list(token[r.label.c_str()]).append(token_temp); - token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; - } - else - { - msg_error("Timer::getRecords") << "Got an unexpected token of type '" << std::string(py::str(token.get_type())) << "'."; - break; - } + referenceTime = r.time; } - else + if (!referenceTime.has_value()) { - token[r.label.c_str()] = py::dict(); - token = token[r.label.c_str()]; + msg_error("Timer::getRecords") << "Reference time not set."; + break; } - token["start_time"] = getTime(r.time - t0); - tokens.push(token); + currentLevel["start_time"] = getTime(r.time, *referenceTime); + hierarchyStack.push(currentLevel); break; + case Record::REND: // Timer ends case Record::RSTEP_END: // Step ends - token = tokens.top(); - token["end_time"] = getTime(r.time - t0); - token["total_time"] = getTime(r.time - t0) - py::cast(token["start_time"]); - tokens.pop(); + currentLevel = hierarchyStack.top(); + currentLevel["end_time"] = getTime(r.time, *referenceTime); + currentLevel["total_time"] = getTime(r.time, *referenceTime) - py::cast(currentLevel["start_time"]); + hierarchyStack.pop(); break; case Record::RVAL_SET: // Sets a value - token = tokens.top(); - token[r.label.c_str()] = r.val; - break; case Record::RVAL_ADD: // Sets a value - token = tokens.top(); - token[r.label.c_str()] = r.val; + currentLevel = hierarchyStack.top(); + currentLevel[r.label.c_str()] = r.val; break; default: - token = tokens.top(); - token[r.label.c_str()] = py::list(); - token = token[r.label.c_str()]; - token["start_time"] = r.time; + currentLevel = hierarchyStack.top(); + currentLevel[r.label.c_str()] = py::list(); + currentLevel = currentLevel[r.label.c_str()]; + currentLevel["start_time"] = r.time; break; } } @@ -155,28 +140,28 @@ py::dict getRecords(const std::string & id) { // There should be two remaining records: Top level "record" + "timer starts". The "timer starts" record remains in // the stack since we normally get the records before the timer ends (ending the timer in Sofa destroys the // records...) - if (tokens.size() == 2) + if (hierarchyStack.size() == 2) { - token = tokens.top(); - tokens.pop(); + currentLevel = hierarchyStack.top(); + hierarchyStack.pop(); } - else if (tokens.size() == 1) + else if (hierarchyStack.size() == 1) { // This should not happen unless we successfully got the timer records AFTER the timer has ends, which would mean // that Sofa's advanced timer has improved, let not warn the user for that. - token = tokens.top(); + currentLevel = hierarchyStack.top(); } // Pop the last token ("records") - tokens.pop(); + hierarchyStack.pop(); // The stack should be empty by now - if (!tokens.empty()) + if (!hierarchyStack.empty()) { - msg_error("Timer::getRecords") << "Records stack leaked."; + msg_error("Timer::getRecords") << "Records stack leaked (" << hierarchyStack.size() << " elements)."; } - return token; + return currentLevel; } py::module addSubmoduleTimer(py::module &m) diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index 33991fa68..aef3b6e51 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -38,7 +38,7 @@ 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx_search.extension', - 'sphinx_tabs.tabs', + 'sphinx_design.tabs', ] import sys diff --git a/docs/sphinx/source/content/Compilation.rst b/docs/sphinx/source/content/Compilation.rst index 4cb10472c..838e51200 100644 --- a/docs/sphinx/source/content/Compilation.rst +++ b/docs/sphinx/source/content/Compilation.rst @@ -18,9 +18,9 @@ Prerequisites The compilation of SofaPython3 requires the installation of some dependencies. The following table lists all of those prerequisites. -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu .. list-table:: :widths: 15 10 30 45 @@ -45,7 +45,7 @@ prerequisites. - - .. tab:: MacOS + .. tab-item:: MacOS .. list-table:: :widths: 15 10 30 45 @@ -70,7 +70,7 @@ prerequisites. - - .. tab:: Windows + .. tab-item:: Windows .. list-table:: :widths: 15 10 30 45 diff --git a/docs/sphinx/source/content/FirstSteps.rst b/docs/sphinx/source/content/FirstSteps.rst index 3e73b5b2c..a0b9ad014 100644 --- a/docs/sphinx/source/content/FirstSteps.rst +++ b/docs/sphinx/source/content/FirstSteps.rst @@ -166,8 +166,9 @@ We first propose to add a visual grid, in order to see things more clearly. To d Now, we create a new child node, in order to add the general configuration of the scene : required plugins (here SofaPython3) and other tools (like a system of axes). -.. code-block:: python - def createScene(rootNode): +.. code-block:: python + + def createScene(rootNode): import SofaRuntime # Make sure to load all necessary libraries SofaRuntime.importPlugin("Sofa.Component.Visual") @@ -182,6 +183,7 @@ Now, we create a new child node, in order to add the general configuration of th Finally, we add the sphere itself, which consists of two parts : the mechanical representation and the visual representation of the sphere: .. code-block:: python + def createScene(rootNode): import SofaRuntime # Make sure to load all necessary libraries @@ -312,7 +314,7 @@ We first add a collision model for the scene in general, that is stating how a c # Collision pipeline rootNode.addObject('CollisionPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') @@ -435,7 +437,7 @@ Here is the entire code of the scene : # Collision pipeline rootNode.addObject('CollisionPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') diff --git a/docs/sphinx/source/content/Installation.rst b/docs/sphinx/source/content/Installation.rst index 41d76b6a1..4d1aef413 100644 --- a/docs/sphinx/source/content/Installation.rst +++ b/docs/sphinx/source/content/Installation.rst @@ -21,9 +21,9 @@ Get python installed First, make sure you have the same version of python installed on your computer as the one that is used in the binary version. -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -40,7 +40,7 @@ First, make sure you have the same version of python installed on your computer sudo apt install libopengl0 - .. tab:: MacOS + .. tab-item:: MacOS Run in a terminal: @@ -63,7 +63,7 @@ First, make sure you have the same version of python installed on your computer pip3 install numpy - .. tab:: Windows + .. tab-item:: Windows Download and install `Python 3.12 64bit `_ @@ -97,9 +97,9 @@ using python3 Before running your simulations, you must make sure to define the following environment variables: -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -108,7 +108,7 @@ Before running your simulations, you must make sure to define the following envi export SOFA_ROOT=/path/to/SOFA_install export PYTHONPATH=/path/to/SofaPython3/lib/python3/site-packages:$PYTHONPATH - .. tab:: MacOS + .. tab-item:: MacOS Run in a terminal: @@ -119,7 +119,7 @@ Before running your simulations, you must make sure to define the following envi export PATH="/usr/local/opt/python@3.12/bin/:$PATH" - .. tab:: Windows + .. tab-item:: Windows * Create a system variable **SOFA_ROOT** and set it to ```` * Create a system variable **PYTHON_ROOT** and set it to ```` diff --git a/docs/sphinx/source/content/UsingThePlugin.rst b/docs/sphinx/source/content/UsingThePlugin.rst index 659898fee..d076d7dce 100644 --- a/docs/sphinx/source/content/UsingThePlugin.rst +++ b/docs/sphinx/source/content/UsingThePlugin.rst @@ -12,9 +12,9 @@ Prerequisites If you downloaded and installed SOFA and its headers from the `SOFA website `_, make sure to have python3.10 installed on your computer. -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -31,7 +31,7 @@ If you downloaded and installed SOFA and its headers from the `SOFA website `_ @@ -98,9 +98,9 @@ Within a python3 interpreter Before running your simulations, you must make sure to define the following environment variables: -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -109,7 +109,7 @@ Before running your simulations, you must make sure to define the following envi export SOFA_ROOT=/path/to/SOFA_install export PYTHONPATH=/path/to/SofaPython3/lib/python3/site-packages:$PYTHONPATH - .. tab:: MacOS + .. tab-item:: MacOS Run in a terminal: @@ -120,7 +120,7 @@ Before running your simulations, you must make sure to define the following envi export PATH="/usr/local/opt/python@3.10/bin/:$PATH" - .. tab:: Windows + .. tab-item:: Windows * Create a system variable **SOFA_ROOT** and set it to ```` * Create a system variable **PYTHON_ROOT** and set it to ```` @@ -188,7 +188,7 @@ Within a python3 interpreter, your simulation requires more than only the ``crea createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Run as many simulation steps (here 10 steps are computed) for iteration in range(10): @@ -219,7 +219,7 @@ By structuring your scripts this way, you get the advantage to have a script loa createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") import SofaImGui @@ -362,7 +362,7 @@ We first add a collision model for the scene in general, that is stating how a c # Collision pipeline rootNode.addObject('DefaultPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') @@ -429,7 +429,7 @@ Here is the entire code of the scene : createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") Sofa.Gui.GUIManager.Init("myscene", "imgui") @@ -473,7 +473,7 @@ Here is the entire code of the scene : # Collision pipeline rootNode.addObject('DefaultPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') @@ -596,7 +596,7 @@ In the same way, Data can be modified (write access) using the ``.value`` access createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Run the simulation for 10 steps for iteration in range(10): diff --git a/docs/sphinx/source/requirements.txt b/docs/sphinx/source/requirements.txt index 20b945d12..ffbfba828 100644 --- a/docs/sphinx/source/requirements.txt +++ b/docs/sphinx/source/requirements.txt @@ -3,5 +3,5 @@ sphinxcontrib-contentui readthedocs-sphinx-search sphinx-autodoc-typehints sphinx-rtd-theme -sphinx-tabs +sphinx-design sphinx-autosummary-autocollect diff --git a/examples/.scene-tests b/examples/.scene-tests index 10310ecfa..27851cca0 100644 --- a/examples/.scene-tests +++ b/examples/.scene-tests @@ -10,3 +10,9 @@ iterations "access_stiffness_matrix.py" "3" # Ignore additional examples ignore "additional-examples/.*" + +# Ignore jax examples +ignore "jax/*" + +#Ignore meshing examples +ignore "meshing/*" diff --git a/examples/CCDIntersection.py b/examples/CCDIntersection.py new file mode 100644 index 000000000..ffe61360c --- /dev/null +++ b/examples/CCDIntersection.py @@ -0,0 +1,159 @@ + + +def main(): + # Required import for python + import Sofa + import SofaImGui + + root = Sofa.Core.Node("root") + createScene(root) + Sofa.Simulation.initRoot(root) + + for iteration in range(10): + Sofa.Simulation.animate(root, root.dt.value) + print(root.PlateMecha.Visual.BoxROI.trianglesInROI.value) + +def createScene(root_node): + root_node.name = "root" + root_node.dt = 0.1 + root_node.gravity = [-0.981, 0, 0] + + plugins = root_node.addChild('plugins') + + plugins.addObject('RequiredPlugin', name="MultiThreading") + plugins.addObject('RequiredPlugin', name="Sofa.Component.AnimationLoop") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Detection.Algorithm") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Detection.Intersection") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Geometry") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Response.Contact") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Constraint.Lagrangian.Correction") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Constraint.Lagrangian.Solver") + plugins.addObject('RequiredPlugin', name="Sofa.Component.IO.Mesh") + plugins.addObject('RequiredPlugin', name="Sofa.Component.LinearSolver.Direct") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Mapping.Linear") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Mass") + plugins.addObject('RequiredPlugin', name="Sofa.Component.ODESolver.Backward") + plugins.addObject('RequiredPlugin', name="Sofa.Component.SolidMechanics.FEM.Elastic") + plugins.addObject('RequiredPlugin', name="Sofa.Component.StateContainer") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Topology.Container.Dynamic") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Topology.Container.Grid") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Topology.Mapping") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Visual") + plugins.addObject('RequiredPlugin', name="Sofa.GL.Component.Rendering3D") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Engine.Select") + plugins.addObject('RequiredPlugin', name="Sofa.GUI.Component") + + root_node.addObject('VisualStyle', displayFlags="showVisual") + root_node.addObject('ConstraintAttachButtonSetting') + root_node.addObject('FreeMotionAnimationLoop', computeBoundingBox=False) + root_node.addObject('BlockGaussSeidelConstraintSolver', maxIterations=1000, tolerance=1.0e-6) + root_node.addObject('CollisionPipeline', name="Pipeline") + root_node.addObject('ParallelBruteForceBroadPhase', name="BroadPhase") + root_node.addObject('ParallelBVHNarrowPhase', name="NarrowPhase") + root_node.addObject('CollisionResponse', name="ContactManager", response="FrictionContactConstraint", responseParams="mu=0.4") + root_node.addObject('CCDTightInclusionIntersection', name="Intersection", continuousCollisionType="FreeMotion",maxIterations=1000, alarmDistance=0.0, contactDistance=0.0) + # root_node.addObject('NewProximityIntersection', name="Intersection", alarmDistance="0.5", contactDistance="0.0") + + + #Liver mecha + Liver = root_node.addChild('Liver') + + Liver.addObject('EulerImplicitSolver') + Liver.addObject('SparseLDLSolver', name="ldl", template="CompressedRowSparseMatrixMat3x3", parallelInverseProduct=True) + Liver.addObject('MeshGmshLoader', name="meshLoader", filename="mesh/liver.msh", scale3d=[0.2, 0.2, 0.2], rotation=[0, 180, -45], translation=[15.2, -0.15, 0.14]) + Liver.addObject('TetrahedronSetTopologyContainer', name="Container", src='@meshLoader') + Liver.addObject('TetrahedronSetTopologyModifier', name="Modifier") + Liver.addObject('TetrahedronSetGeometryAlgorithms', name="Algorithms") + Liver.addObject('MechanicalObject', name="mstate", template="Vec3d", position="@Container.position", velocity=[-5,0,0]*181) + Liver.addObject('TetrahedronFEMForceField', name="forceField", listening=True, youngModulus=1e2, poissonRatio=0.3, method="large") + Liver.addObject('MeshMatrixMass', totalMass=3) + + surface = Liver.addChild('Surface') + surface.addObject('TriangleSetTopologyContainer', name="Container") + surface.addObject('TriangleSetTopologyModifier', name="Modifier") + surface.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + surface.addObject('Tetra2TriangleTopologicalMapping', input="@../Container", output="@Container", flipNormals=False) + surface.addObject('MechanicalObject', name="dofs", rest_position="@../mstate.rest_position") + surface.addObject('PointCollisionModel', name="Collision", contactDistance=0.0001, color=[0.0, 0.93725490196078, 0.89411764705882, 1]) + surface.addObject('IdentityMapping', name="SurfaceMapping") + + visual = Liver.addChild('Visual') + visual.addObject('TriangleSetTopologyContainer', name="Container") + visual.addObject('TriangleSetTopologyModifier', name="Modifier") + visual.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + visual.addObject('Tetra2TriangleTopologicalMapping', input="@../Container", output="@Container", flipNormals=False) + visual.addObject('OglModel', name="VisualModel", src="@Container", color=[0.5, 0, 0.125, 1]) + visual.addObject('IdentityMapping', name="SurfaceMapping") + + Liver.addObject('LinearSolverConstraintCorrection', linearSolver="@ldl") + + + + #Plate topology + PlateTopo = root_node.addChild('BeamDomainFromGridTopology') + + PlateTopo.addObject('RegularGridTopology', name="HexaTop", n=[3, 10, 10], min=[-0.3, -1.5, -1.5], max=[0.3, 1.5, 1.5]) + tetra_topology = PlateTopo.addChild('TetraTopology') + + tetra_topology.addObject('TetrahedronSetTopologyContainer', name="Container", position="@HexaTop.position") + tetra_topology.addObject('TetrahedronSetTopologyModifier', name="Modifier") + tetra_topology.addObject('Hexa2TetraTopologicalMapping', input="@HexaTop", output="@Container", swapping=True) + + #Plate mecha + PlateMecha = root_node.addChild('PlateMecha') + + PlateMecha.addObject('EulerImplicitSolver') + PlateMecha.addObject('SparseLDLSolver', name="ldl", template="CompressedRowSparseMatrixMat3x3", parallelInverseProduct=True) + PlateMecha.addObject('TetrahedronSetTopologyContainer', name="Container", position="@../BeamDomainFromGridTopology/HexaTop.position", tetrahedra="@../BeamDomainFromGridTopology/TetraTopology/Container.tetrahedra") + PlateMecha.addObject('TetrahedronSetTopologyModifier', name="Modifier") + PlateMecha.addObject('TetrahedronSetGeometryAlgorithms', name="Algorithms") + PlateMecha.addObject('MechanicalObject', name="mstate", template="Vec3d", position="@Container.position", velocity=[15,0,0]*300) + PlateMecha.addObject('TetrahedronFEMForceField', name="forceField", listening=True, youngModulus=2e3, poissonRatio=0.3, method="large") + PlateMecha.addObject('MeshMatrixMass', totalMass=2) + + visual = PlateMecha.addChild('Visual') + visual.addObject('TriangleSetTopologyContainer', name="Container") + visual.addObject('TriangleSetTopologyModifier', name="Modifier") + visual.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + visual.addObject('Tetra2TriangleTopologicalMapping', input="@../Container", output="@Container", flipNormals=False) + visual.addObject('BoxROI', name="FrontCollisionTriangles", box=[0.25, 1.6, 1.6, 0.35, -1.6, -1.6]) + visual.addObject('BoxROI', name="BackCollisionTriangles", box=[-0.35, 1.6, 1.6, -0.25, -1.6, -1.6]) + + visual.addObject('OglModel', name="VisualModel", src="@Container" , color=[0.6, 1, 1, 1]) + visual.addObject('IdentityMapping', name="SurfaceMapping") + + + surface = PlateMecha.addChild('FrontSurface') + surface.addObject('TriangleSetTopologyContainer',triangles="@../Visual/FrontCollisionTriangles.trianglesInROI") + surface.addObject('TriangleSetTopologyModifier', name="Modifier") + surface.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + surface.addObject('MechanicalObject', name="dofs", rest_position="@../mstate.rest_position") + surface.addObject('TriangleCollisionModel', name="Collision",group=1, contactDistance=0.000001, color=[0.94117647058824, 0.93725490196078, 0.89411764705882, 1]) + surface.addObject('IdentityMapping', name="SurfaceMapping") + + + + surface = PlateMecha.addChild('BackCollisionEdges') + surface.addObject('TriangleSetTopologyContainer',triangles="@../Visual/BackCollisionTriangles.trianglesInROI") + surface.addObject('TriangleSetTopologyModifier', name="Modifier") + surface.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + surface.addObject('MechanicalObject', name="dofs", rest_position="@../mstate.rest_position") + surface.addObject('PointCollisionModel', name="Collision", group=1, contactDistance=0.000001, color=[0.94117647058824, 0.93725490196078, 0.89411764705882, 1]) + surface.addObject('IdentityMapping', name="SurfaceMapping") + + + PlateMecha.addObject('LinearSolverConstraintCorrection', linearSolver="@ldl") + + + + #Ground + Ground = root_node.addChild('Ground') + + Ground.addObject('TriangleSetTopologyContainer', name="FloorTopology", position="-5 -15 -15 -5 -15 15 -5 15 15 -5 15 -15", triangles="0 2 1 0 3 2") + Ground.addObject('MechanicalObject', name="FloorDOF", template="Vec3d") + Ground.addObject('TriangleCollisionModel', name="FloorCM", contactDistance="0.0001", moving="0", simulated="0", color="0.3 0.3 0.3 0.1") + visu = Ground.addChild('Visu') + visu.addObject('OglModel', name="VisualModel", src="@../FloorTopology") + +if __name__ == "__main__": + main() diff --git a/examples/CCDIntersection.py.view b/examples/CCDIntersection.py.view new file mode 100644 index 000000000..b724ddf85 --- /dev/null +++ b/examples/CCDIntersection.py.view @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/MessageHandler.py b/examples/MessageHandler.py new file mode 100644 index 000000000..43d061422 --- /dev/null +++ b/examples/MessageHandler.py @@ -0,0 +1,99 @@ +import Sofa +import Sofa.Helper + +RED = "\033[31m" +ORANGE = "\033[38;5;214m" +GREEN = "\033[32m" +RESET = "\033[0m" + + +class ExempleMessageHandler(Sofa.Helper.MessageHandler): + """A custom message handler that prints messages in the console and counts the number of errors, warnings and infos.""" + + def __init__(self): + super().__init__() + self.num_errors = 0 + self.num_warnings = 0 + self.num_infos = 0 + + def process(self, msg): + """Prints the message in the console and count the number of errors, warnings and infos.""" + if msg['type'] == "Error": + self.print_error(msg['sender'], msg['message']) + self.num_errors += 1 + elif msg['type'] == "Warning": + self.print_warning(msg['sender'], msg['message']) + self.num_warnings += 1 + elif msg['type'] == "Info": + self.print_info(msg['sender'], msg['message']) + self.num_infos += 1 + else: + print(f"{msg['type']} {msg['message']}") + + @staticmethod + def print_error(sender, message): + """Prints a string with [ERROR] in red""" + print(f"🚨{RED}[ERROR]{RESET} [👤{sender}] 📨{message}") + + @staticmethod + def print_warning(sender, message): + """Prints a string with [WARNING] in orange""" + print(f"⚠️{ORANGE}[WARNING]{RESET} [👤{sender}] 📨{message}") + + @staticmethod + def print_info(sender, message): + """Prints a string with [INFO] in green""" + print(f"ℹ️{GREEN}[INFO]{RESET} [👤{sender}] 📨{message}") + + +with ExempleMessageHandler() as msg_handler: + def createScene(root): + + root.addObject("RequiredPlugin", pluginName=[ + 'Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.Visual' + ]) + + root.addObject('VisualStyle', displayFlags="showBehaviorModels showForceFields") + + root.addObject('DefaultAnimationLoop') + root.addObject('DefaultVisualManagerLoop') + + root.addObject('EulerImplicitSolver', rayleighStiffness=0.1, rayleighMass=0.1, printLog=False) + root.addObject('SparseLDLSolver', template="CompressedRowSparseMatrixd") + + root.addObject('MechanicalObject', name="DoFs") + root.addObject('MeshMatrixMass', name="mass", totalMass=320) + root.addObject('RegularGridTopology', name="grid", + nx=4, ny=4, nz=20, xmin=-9, xmax=-6, ymin=0, ymax=3, zmin=0, zmax=19) + root.addObject('BoxROI', name="box", box=[-10, -1, -0.0001, -5, 4, 0.0001]) + root.addObject('FixedProjectiveConstraint', indices="@box.indices") + root.addObject('HexahedronFEMForceField', name="FEM", youngModulus=4000, poissonRatio=0.3, method="large") + + return root + + + def main(): + root = Sofa.Core.Node("root") + createScene(root) + Sofa.Simulation.initRoot(root) + + for iteration in range(10): + Sofa.Simulation.animate(root, root.dt.value) + + print("Simulation is done.") + + print(f"Number of errors: {msg_handler.num_errors}") + print(f"Number of warnings: {msg_handler.num_warnings}") + print(f"Number of infos: {msg_handler.num_infos}") + + + if __name__ == '__main__': + main() diff --git a/examples/RPYC/ExampleManipulatingTheNodeLocally.py b/examples/RPYC/ExampleManipulatingTheNodeLocally.py new file mode 100644 index 000000000..af7e50412 --- /dev/null +++ b/examples/RPYC/ExampleManipulatingTheNodeLocally.py @@ -0,0 +1,97 @@ +import multiprocessing +import threading +import time +import rpyc +from SOFAService import SOFAService, SOFAClient +import Sofa + +if __name__ == "__main__": + + SC = SOFAClient() + SC.start_server(port=18818) + SC.connect_client(port=18818) + + root = SC.sofa_root + + # Need to use getValue and setValue instead of the syntax `root.dt = 0.02` or `root.dt.value` to get the actual value + root.gravity.setValue([0, -9.81, 0]) + root.dt.setValue(0.02) + + # RPyC deal in a strange way with list of strings, which leads to error if you use the syntax where you pass a list of plugin name to required plugin. You need to add one required plugin per plugin for it to work, such as done in xml. + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Detection.Algorithm') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Detection.Intersection') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Geometry') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Response.Contact') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Constraint.Projective') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.IO.Mesh') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.LinearSolver.Iterative') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Mapping.Linear') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Mass') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.ODESolver.Backward') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.SolidMechanics.FEM.Elastic') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.StateContainer') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Topology.Container.Dynamic') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Visual') + root.addObject("RequiredPlugin", pluginName='Sofa.GL.Component.Rendering3D') + + root.addObject('DefaultAnimationLoop') + + root.addObject('VisualStyle', displayFlags="showCollisionModels") + root.addObject('CollisionPipeline', name="CollisionPipeline") + root.addObject('BruteForceBroadPhase', name="BroadPhase") + root.addObject('BVHNarrowPhase', name="NarrowPhase") + root.addObject('CollisionResponse', name="CollisionResponse", response="PenalityContactForceField") + root.addObject('DiscreteIntersection') + + # Don't forget that this will be launched in the remote, all files should be on its disk or else, use lambda function to capture data and access theme in this function + root.addObject('MeshOBJLoader', name="LiverSurface", filename="mesh/liver-smooth.obj") + + liver = root.addChild('Liver') + liver.addObject('EulerImplicitSolver', name="cg_odesolver", rayleighStiffness="0.1", rayleighMass="0.1") + liver.addObject('CGLinearSolver', name="linear_solver", iterations="25", tolerance="1e-09", threshold="1e-09") + liver.addObject('MeshGmshLoader', name="meshLoader", filename="mesh/liver.msh") + liver.addObject('TetrahedronSetTopologyContainer', name="topo", src="@meshLoader") + liver.addObject('MechanicalObject', name="dofs", src="@meshLoader") + liver.addObject('TetrahedronSetGeometryAlgorithms', template="Vec3d", name="GeomAlgo") + liver.addObject('DiagonalMass', name="Mass", massDensity="1.0") + liver.addObject('TetrahedralCorotationalFEMForceField', template="Vec3d", name="FEM", method="large", poissonRatio="0.3", youngModulus="3000", computeGlobalMatrix="0") + liver.addObject('FixedProjectiveConstraint', name="FixedConstraint", indices="3 39 64") + + visu = liver.addChild('Visu') + visu.addObject('OglModel', name="VisualModel", src="@../../LiverSurface") + visu.addObject('BarycentricMapping', name="VisualMapping", input="@../dofs", output="@VisualModel") + + surf = liver.addChild('Surf') + surf.addObject('SphereLoader', name="sphereLoader", filename="mesh/liver.sph") + surf.addObject('MechanicalObject', name="spheres", position="@sphereLoader.position") + surf.addObject('SphereCollisionModel', name="CollisionModel", listRadius="@sphereLoader.listRadius") + surf.addObject('BarycentricMapping', name="CollisionMapping", input="@../dofs", output="@spheres") + + SC.init_root() + + # This works only for server and client on the same machine. This tell the server that the passed data path should be copied in shared memory when accessed through the client instead of RPyC. + SC.setup_shared_memory_for_data(["Liver/dofs.position","Liver/Surf/spheres.position"]) + + asynch_step = None + currentTime = 0.0 + while currentTime<0.2: + if(asynch_step is None or asynch_step.ready): + # Time to get data from object + currentTime = SC.sofa_root.getTime() + print(currentTime) + + print(f"This comes with the socket : {SC.sofa_root.Liver.cg_odesolver.name.value}") + print(f"This comes with shared memory : {SC.sofa_root.Liver.Surf.spheres.position.value}") + SC.sofa_root.Liver.Surf.spheres.position.setValue([[0,0,0]]) + SC.sofa_root.Liver.cg_odesolver.printLog.setValue(True) + print(f"This getValue comes with the socket : {SC.sofa_root.Liver.cg_odesolver.name.getValue()}") + print(f"This getValue comes with shared memory : {SC.sofa_root.Liver.Surf.spheres.position.getValue()}") + + # Launch next step + asynch_step = SC.asynch_step() + else: + print("waiting 0.1 sec") + time.sleep(0.1) + + SC.stop_server() + diff --git a/examples/RPYC/SOFAService.py b/examples/RPYC/SOFAService.py index c201a02f8..93f8a898f 100644 --- a/examples/RPYC/SOFAService.py +++ b/examples/RPYC/SOFAService.py @@ -1,3 +1,5 @@ +import os + import rpyc import time import threading @@ -279,7 +281,7 @@ def exposed_build_scene_graph_from_method(self, createScene): """ self.exposed_sofa_root = Sofa.Core.Node("root") createScene(self.exposed_sofa_root) - Sofa.Simulation.initRoot(self.exposed_sofa_root) + self.exposed_init_root() def exposed_build_scene_graph_from_file(self, filename:str): """ @@ -294,7 +296,7 @@ def exposed_build_scene_graph_from_file(self, filename:str): self.exposed_sofa_root = Sofa.Core.Node("root") foo.createScene(self.exposed_sofa_root) - Sofa.Simulation.initRoot(self.exposed_sofa_root) + self.exposed_init_root() def exposed_setup_shared_memory_for_data(self, dataPaths:list[str], delayed=False): @@ -354,17 +356,36 @@ def getSharedMemoryNames(self): """Return list of all data paths currently shared via shared memory.""" return self.sharedPaths + def exposed_init_root(self): + """Initialize the root node.""" + if(not self.exposed_sofa_root.isInitialized()): + Sofa.Simulation.initRoot(self.exposed_sofa_root) def exposed_step_simulation(self): """ Run one step of the simulation. If shared memory hasn’t been set up yet, attempt setup now. """ + Sofa.Simulation.animate(self.exposed_sofa_root, self.exposed_sofa_root.dt.value) + if(not self.sharedMemoryIsSet): self.__internal_setup_shared_memory() + def exposed_get_cwd(self): + """ + Returns current working dir + """ + return os.getcwd() + def exposed_set_cwd(self, dir): + """ + Change the current working dir of the server + Returns old dir,current working dir + """ + oldDir = os.getcwd() + os.chdir(dir) + return oldDir, os.getcwd() ################################################# ### Multithreaded automatic execution methods @@ -382,6 +403,7 @@ def __wait_for_the_animation_to_stop(self): def __simulation_loop(self): """Continuous simulation loop run by background thread.""" + while self.animate: Sofa.Simulation.animate(self.exposed_sofa_root, self.exposed_sofa_root.dt.value) diff --git a/examples/ReadTheDocs_Example.py b/examples/ReadTheDocs_Example.py index b704045f9..5c14e3b31 100644 --- a/examples/ReadTheDocs_Example.py +++ b/examples/ReadTheDocs_Example.py @@ -47,7 +47,7 @@ def createScene(rootNode): rootNode.addObject('CollisionPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase', name="BroadPhase") rootNode.addObject('BVHNarrowPhase', name="NarrowPhase") rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') diff --git a/examples/access_compliance_matrix.py b/examples/access_compliance_matrix.py index 6108643d8..8c80d6efa 100644 --- a/examples/access_compliance_matrix.py +++ b/examples/access_compliance_matrix.py @@ -23,7 +23,7 @@ def createScene(root): ]) root.addObject("FreeMotionAnimationLoop", solveVelocityConstraintFirst=True) - constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) + constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) root.addObject("StringMeshCreator", name="loader", resolution="20") root.addObject("EulerImplicitSolver") diff --git a/examples/access_constraint_matrix.py b/examples/access_constraint_matrix.py index aa12c529a..50adf7099 100644 --- a/examples/access_constraint_matrix.py +++ b/examples/access_constraint_matrix.py @@ -22,7 +22,7 @@ def createScene(root): ]) root.addObject("FreeMotionAnimationLoop", solveVelocityConstraintFirst=True) - root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) + root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) root.addObject("StringMeshCreator", name="loader", resolution="20") root.addObject("EulerImplicitSolver") diff --git a/examples/access_contact_forces.py b/examples/access_contact_forces.py index 196ae836d..c857df9a3 100644 --- a/examples/access_contact_forces.py +++ b/examples/access_contact_forces.py @@ -44,7 +44,7 @@ def createScene(root): root.addObject('FreeMotionAnimationLoop') # Constraint solver computing the constraint/contact forces, stored in the constraint space (normal , tangential_1, tangential_2) - constraint_solver = root.addObject('ProjectedGaussSeidelConstraintSolver', maxIterations=1000, tolerance=1e-6, computeConstraintForces=True) + constraint_solver = root.addObject('BlockGaussSeidelConstraintSolver', maxIterations=1000, tolerance=1e-6, computeConstraintForces=True) root.addObject('VisualStyle', displayFlags="showCollisionModels hideVisualModels showForceFields") root.addObject('CollisionPipeline', name="collision_pipeline") diff --git a/examples/access_energy.py b/examples/access_energy.py index bd1cad9c2..07fe88e8d 100644 --- a/examples/access_energy.py +++ b/examples/access_energy.py @@ -34,7 +34,7 @@ def createScene(rootNode, dt=0.01, m=1, g=1, L=100, mu=0): rootNode.addObject('ParallelBVHNarrowPhase') rootNode.addObject('MinProximityIntersection', name='Proximity', alarmDistance='10', contactDistance='0.02') rootNode.addObject('CollisionResponse', name='Response', response='FrictionContactConstraint', responseParams=f'mu={mu}') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', maxIterations='10', multithreading='true', tolerance='1.0e-3') + rootNode.addObject('BlockGaussSeidelConstraintSolver', maxIterations='10', multithreading='true', tolerance='1.0e-3') boxTranslation = "-20 -0.9 0" rootNode.addObject('MeshOBJLoader', name='Loader-box', filename='mesh/cube.obj', translation=boxTranslation) @@ -82,7 +82,7 @@ def main(): mu = .1 root = Sofa.Core.Node("root") createScene(root, dt=1e-4, m=m, g=g, mu=mu, L=1000) - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, root.dt.value) Ks, Us = [], [] diff --git a/examples/access_mass_matrix.py b/examples/access_mass_matrix.py index b1dd1c4b6..ce37af205 100644 --- a/examples/access_mass_matrix.py +++ b/examples/access_mass_matrix.py @@ -28,15 +28,16 @@ def createScene(root): root.addObject('VisualStyle', displayFlags="showBehaviorModels showForceFields") root.addObject("RequiredPlugin", pluginName=['Sofa.Component.Constraint.Projective', - 'Sofa.Component.Engine.Select', - 'Sofa.Component.LinearSolver.Direct', - 'Sofa.Component.LinearSystem', - 'Sofa.Component.Mass', - 'Sofa.Component.ODESolver.Backward', - 'Sofa.Component.SolidMechanics.FEM.Elastic', - 'Sofa.Component.StateContainer', - 'Sofa.Component.Topology.Container.Grid', - 'Sofa.Component.Visual' + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.LinearSolver.Ordering', + 'Sofa.Component.LinearSystem', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.Visual' ]) root.addObject('DefaultAnimationLoop') @@ -57,8 +58,10 @@ def createScene(root): # This component distributes the matrix contributions to the other systems root.addObject('CompositeLinearSystem', template="CompressedRowSparseMatrixMat3x3d", name="solverSystem", linearSystems="@matrices/system @matrices/M", solverLinearSystem="@matrices/system") - # It is important to define the linear system in the linear solver: the CompositeLinearSystem - root.addObject('SparseLDLSolver', ordering='Natural', template="CompressedRowSparseMatrixMat3x3d", + + root.addObject('NaturalOrderingMethod', name='ordering') + # It is important to configure the solver to work on the CompositeLinearSystem + root.addObject('SparseLDLSolver', orderingMethod='@ordering', template="CompressedRowSparseMatrixMat3x3d", linearSystem="@solverSystem") beam1 = root.addChild('Beam1') diff --git a/examples/access_matrix.py b/examples/access_matrix.py index 3cc33523f..aeb1fa21e 100644 --- a/examples/access_matrix.py +++ b/examples/access_matrix.py @@ -1,9 +1,27 @@ # Required import for python import Sofa import SofaRuntime +from Sofa import SofaLinearSystem from Sofa import SofaLinearSolver from scipy import sparse +def createBeam(root, matrix_type): + node = root.addChild(matrix_type) + node.addObject('EulerImplicitSolver', rayleighStiffness="0.1", rayleighMass="0.1") + linear_system = node.addObject('MatrixLinearSystem', template=matrix_type) + + node.addObject('MechanicalObject', name="DoFs") + node.addObject('UniformMass', name="mass", totalMass="320") + node.addObject('RegularGridTopology', name="grid", nx="4", ny="4", nz="20", xmin="-9", xmax="-6", ymin="0", ymax="3", zmin="0", zmax="19") + node.addObject('BoxROI', name="box", box="-10 -1 -0.0001 -5 4 0.0001") + node.addObject('FixedProjectiveConstraint', indices="@box.indices") + node.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") + + node.addObject(MatrixAccessController('MatrixAccessor', name=f'matrixAccessor{matrix_type}', linear_system=linear_system)) + + return node + + # Function called when the scene graph is being created def createScene(root): @@ -15,26 +33,22 @@ def createScene(root): 'Sofa.Component.Visual' ]) - root.addObject('DefaultAnimationLoop') + root.addObject('DefaultAnimationLoop', parallelODESolving=True) root.addObject('DefaultVisualManagerLoop') - root.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') - root.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') - root.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') - root.addObject('RequiredPlugin', name='Sofa.Component.Constraint.Projective') - root.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') - - root.addObject('EulerImplicitSolver', rayleighStiffness="0.1", rayleighMass="0.1") - linear_solver = root.addObject('SparseLDLSolver', template="CompressedRowSparseMatrixMat3x3d") + plugins = root.addChild('plugins') + plugins.addObject('RequiredPlugin', name='Sofa.Component.Constraint.Projective') + plugins.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') + plugins.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') + plugins.addObject('RequiredPlugin', name='Sofa.Component.LinearSystem') + plugins.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') + plugins.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') - root.addObject('MechanicalObject', name="DoFs") - root.addObject('UniformMass', name="mass", totalMass="320") - root.addObject('RegularGridTopology', name="grid", nx="4", ny="4", nz="20", xmin="-9", xmax="-6", ymin="0", ymax="3", zmin="0", zmax="19") - root.addObject('BoxROI', name="box", box="-10 -1 -0.0001 -5 4 0.0001") - root.addObject('FixedProjectiveConstraint', indices="@box.indices") - root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") + node_crs = createBeam(root, 'CompressedRowSparseMatrixMat3x3d') + node_crs.addObject('SparseLDLSolver', template="CompressedRowSparseMatrixMat3x3d") - root.addObject(MatrixAccessController('MatrixAccessor', name='matrixAccessor', linear_solver=linear_solver)) + node_full = createBeam(root, 'FullMatrix') + node_full.addObject('CGLinearSolver', template="FullMatrix") return root @@ -43,31 +57,34 @@ class MatrixAccessController(Sofa.Core.Controller): def __init__(self, *args, **kwargs): + print('Initialize controller') Sofa.Core.Controller.__init__(self, *args, **kwargs) - self.linear_solver = kwargs.get("linear_solver") + self.linear_system = kwargs.get("linear_system") + print(f'Type linear system: {type(self.linear_system)}') def onAnimateEndEvent(self, event): - system_matrix = self.linear_solver.A() - rhs = self.linear_solver.b() - solution = self.linear_solver.x() + system_matrix = self.linear_system.get_system_matrix() + rhs = self.linear_system.get_rhs_vector() + solution = self.linear_system.get_solution_vector() print('====================================') - print('Global system matrix') + print(f'Global system matrix {self.getName()}') print('====================================') print('dtype: ' + str(system_matrix.dtype)) print('shape: ' + str(system_matrix.shape)) print('ndim: ' + str(system_matrix.ndim)) - print('nnz: ' + str(system_matrix.nnz)) + if hasattr(system_matrix, 'nnz'): #if the matrix is sparse + print('nnz: ' + str(system_matrix.nnz)) print('====================================') - print('System right hand side') + print(f'System right hand side {self.getName()}') print('====================================') print('dtype: ' + str(rhs.dtype)) print('shape: ' + str(rhs.shape)) print('ndim: ' + str(rhs.ndim)) print('====================================') - print('System solution') + print(f'System solution {self.getName()}') print('====================================') print('dtype: ' + str(solution.dtype)) print('shape: ' + str(solution.shape)) diff --git a/examples/advanced_timer.py b/examples/advanced_timer.py index 6551a6a46..dba1bccd0 100644 --- a/examples/advanced_timer.py +++ b/examples/advanced_timer.py @@ -13,17 +13,17 @@ def __init__(self, *args, **kwargs): self.use_sofa_profiler_timer = False def onAnimateBeginEvent(self, event): - if len(Timer.getRecords('Animate')): + if Timer.isEnabled('Animate'): + # The 'Animate' timer that SOFA is supposed to start is already running, we can use it directly self.use_sofa_profiler_timer = True else: + # SOFA did not start the 'Animate' timer (e.g., when batch UI and no computation time sampling): + # we need to start another one manually Timer.setEnabled("cg_timer", True) Timer.begin("cg_timer") def onAnimateEndEvent(self, event): - if self.use_sofa_profiler_timer: - records = Timer.getRecords("Animate") - else: - records = Timer.getRecords("cg_timer") + records = Timer.getRecords("Animate" if self.use_sofa_profiler_timer else "cg_timer") step_time = records['solve']['Mechanical (meca)']['total_time'] print(f"Step took {step_time:.2f} ms") @@ -43,19 +43,19 @@ def createScene(root): root.dt = 0.01 # List of required plugins - root.addObject("RequiredPlugin", pluginName=['Sofa.Component.Constraint.Projective', - 'Sofa.Component.Engine.Select', - 'Sofa.Component.LinearSolver.Iterative', - 'Sofa.Component.MechanicalLoad', - 'Sofa.Component.ODESolver.Backward', - 'Sofa.Component.SolidMechanics.FEM.Elastic', - 'Sofa.Component.StateContainer', - 'Sofa.Component.Topology.Container.Dynamic', - 'Sofa.Component.Topology.Container.Grid', - 'Sofa.Component.Visual' + root.addObject("RequiredPlugin", pluginName=[ + 'Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Iterative', + 'Sofa.Component.MechanicalLoad', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.Visual' ]) - # AnimationLoop root.addObject('DefaultAnimationLoop') @@ -63,26 +63,29 @@ def createScene(root): root.addObject('VisualStyle', displayFlags='showBehaviorModels showForceFields') # Add the python controller in the scene - root.addObject( TimerController() ) + root.addObject(TimerController()) # Create a grid topology of 10x10x60 centered on (0,0,0) root.addObject('RegularGridTopology', name='grid', min=[-5, -5, -30], max=[5, 5, 30], n=[6, 6, 16]) # Create our mechanical node - root.addChild("meca") - root.meca.addObject("StaticSolver", newton_iterations=5, printLog=False) - root.meca.addObject("CGLinearSolver", iterations=25, tolerance=1e-5, threshold=1e-5) - - root.meca.addObject('MechanicalObject', name='mo', position='@../grid.position') - root.meca.addObject('HexahedronSetTopologyContainer', name='mechanical_topology', src='@../grid') - root.meca.addObject('HexahedronFEMForceField', youngModulus=3000, poissonRatio=0) - - root.meca.addObject('BoxROI', name='base_roi', box=[-5.01, -5.01, -30.01, 30.01, 30.01, -29.99]) - root.meca.addObject('BoxROI', name='top_roi', box=[-5.01, -5.01, +29.99, 5.01, 5.01, +30.01], quad='@mechanical_topology.quads') - - root.meca.addObject('FixedProjectiveConstraint', indices='@base_roi.indices') - root.meca.addObject('QuadSetGeometryAlgorithms') - root.meca.addObject('QuadPressureForceField', pressure=[0, -30, 0], quadList='@top_roi.quadInROI', showForces=False) + with root.addChild("meca") as meca: + meca.addObject("NewtonRaphsonSolver", name="newtonSolver_springs", maxNbIterationsNewton=5, + maxNbIterationsLineSearch=1, warnWhenLineSearchFails=False, printLog=False) + meca.addObject("StaticSolver", newtonSolver="@newtonSolver_springs") + meca.addObject("CGLinearSolver", iterations=25, tolerance=1e-5, threshold=1e-5) + + meca.addObject('MechanicalObject', name='mo', position='@../grid.position') + meca.addObject('HexahedronSetTopologyContainer', name='mechanical_topology', src='@../grid') + meca.addObject('HexahedronFEMForceField', youngModulus=3000, poissonRatio=0) + + meca.addObject('BoxROI', name='base_roi', box=[-5.01, -5.01, -30.01, 30.01, 30.01, -29.99]) + meca.addObject('BoxROI', name='top_roi', box=[-5.01, -5.01, +29.99, 5.01, 5.01, +30.01], + quad='@mechanical_topology.quads') + + meca.addObject('FixedProjectiveConstraint', indices='@base_roi.indices') + meca.addObject('QuadSetGeometryAlgorithms') + meca.addObject('QuadPressureForceField', pressure=[0, -30, 0], quadList='@top_roi.quadInROI', showForces=False) # When not using runSofa, this main function will be called python diff --git a/examples/example-forcefield.py b/examples/example-forcefield.py index ce0a607d6..60c474525 100644 --- a/examples/example-forcefield.py +++ b/examples/example-forcefield.py @@ -22,10 +22,10 @@ def addForce(self, m, out_force, pos, vel): with out_force.writeableArray() as wa: wa[:] += ( (self.initpos-pos.value) * self.ks.value ) - def addDForce(self, df, dx, params): + def addDForce(self, params, df, dx): pass - #def addKToMatrix(self, a, b): + #def addKToMatrix(self, params, number_of_nodes, number_of_dofs_per_node): # print(" Python::addKToMatrix: ", a, " ", b) diff --git a/examples/jax/forcefield.py b/examples/jax/forcefield.py new file mode 100644 index 000000000..ccd827e84 --- /dev/null +++ b/examples/jax/forcefield.py @@ -0,0 +1,180 @@ +""" +Toy example of a force field leveraging autodiff with JAX. + +JAX can be installed via e.g. `pip install -U jax[cuda12]` +""" +import jax +import jax.numpy as jnp +import numpy as np + +import Sofa + + +# Some configuration for JAX: device and precision +# jax.config.update('jax_default_device', jax.devices('cpu')[0]) +jax.config.update("jax_default_device", jax.devices("gpu")[0]) # default "gpu" +jax.config.update("jax_enable_x64", True) # default False (ie use float32) + + +@jax.jit # JIT (just-in-time compilation) for better performance +def get_force(position, length, stiffness): + """ + Spring between the origin and the given position. + + position: array of shape (n_particles, n_dimensions) + length: scalar or array of shape (n_particles, 1) + stiffness: scalar or array of shape (n_particles, 1) + """ + distance = jnp.sqrt(jnp.sum(position**2, axis=1, keepdims=True)) + direction = position / distance + return - stiffness * (distance - length) * direction + + +@jax.jit # JIT (just-in-time compilation) for better performance +def get_dforce(position, length, stiffness, vector): + """ + Compute the jacobian-vector product (jvp) using autodiff + """ + def get_force_from_position(x): + return get_force(x, length, stiffness) + # Differentiate get_force() as a function of the position + return jax.jvp(get_force_from_position, (position,), (vector,))[1] + + +@jax.jit # JIT (just-in-time compilation) for better performance +def get_kmatrix(position, length, stiffness): + """ + Compute the jacobian using autodiff + + Warning: The jacobian computed this way is a dense matrix. + Check `sparsejac` if you are interested in sparse jacobian with JAX. + """ + def get_force_from_position(x): + return get_force(x, length, stiffness) + # Differentiate get_force() as a function of the position + return jax.jacrev(get_force_from_position)(position) + + +class JaxForceField(Sofa.Core.ForceFieldVec3d): + + def __init__(self, length, stiffness, *args, **kwargs): + Sofa.Core.ForceFieldVec3d.__init__(self, *args, **kwargs) + self.length = length + self.stiffness = stiffness + self.dense_to_sparse = None + + def addForce(self, mechanical_parameters, out_force, position, velocity): + with out_force.writeableArray() as wa: + wa[:] += get_force(position.value, self.length, self.stiffness) + + def addDForce(self, mechanical_parameters, df, dx): + with df.writeableArray() as wa: + wa[:] += get_dforce(self.mstate.position.value, self.length, self.stiffness, dx.value) * mechanical_parameters['kFactor'] + + # Option 1: Return the jacobian as a dense array (must have shape (n, n, 1) to be interpreted as such). + # Note: Very slow for big sparse matrices. + # def addKToMatrix(self, mechanical_parameters, n_particles, n_dimensions): + # jacobian = get_kmatrix(self.mstate.position.value, self.length, self.stiffness) + # return np.array(jacobian).reshape((n_particles*n_dimensions, n_particles*n_dimensions, 1)) + + # Option 2: Return the non-zero coefficients of the jacobian as an array with rows (i, j, value). + # Note: The extraction of the non-zero coefficients is faster with JAX on GPU. + # def addKToMatrix(self, mechanical_parameters, n_particles, n_dimensions): + # jacobian = get_kmatrix(self.mstate.position.value, self.length, self.stiffness) + # jacobian = jacobian.reshape((n_particles*n_dimensions, n_particles*n_dimensions)) + # i, j = jacobian.nonzero() + # sparse_jacobian = jnp.stack([i, j, jacobian[i, j]], axis=1) + # return np.array(sparse_jacobian) + + # Option 2 optimization: We know the sparsity of the jacobian in advance (diagonal by 3x3 blocks). + def addKToMatrix(self, mechanical_parameters, n_particles, n_dimensions): + if self.dense_to_sparse is None: + # i = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, ...] + # j = [0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 4, 5, ...] + i = jnp.repeat(jnp.arange(n_particles*n_dimensions), 3) + j = jnp.repeat(jnp.arange(n_particles*n_dimensions).reshape((-1, 3)), 3, axis=0).reshape(-1) + self.dense_to_sparse = lambda jac: jnp.stack([i, j, jac[i, j]], axis=1) + self.dense_to_sparse = jax.jit(self.dense_to_sparse) # slightly faster with jit + + jacobian = get_kmatrix(self.mstate.position.value, self.length, self.stiffness) + jacobian = jacobian.reshape((n_particles*n_dimensions, n_particles*n_dimensions)) + sparse_jacobian = self.dense_to_sparse(jacobian) + sparse_jacobian = np.array(sparse_jacobian) + # Note: with the computations optimized, the conversion below can account for + # ~90% of the time spent in this function. + return np.array(sparse_jacobian) + + +def createScene(root, method="implicit-matrix-assembly", n_particles=1_000, use_sofa=False): + root.dt = 1e-3 + root.gravity = (0, -9.8, 0) + root.box = (-5, -5, -5, 5, 5, 5) + root.addObject( + "RequiredPlugin", + pluginName=[ + 'Sofa.Component.Visual', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.LinearSolver.Iterative', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Mass', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.SolidMechanics.Spring', + ] + ) + root.addObject("DefaultAnimationLoop") + root.addObject("VisualStyle", displayFlags="showBehaviorModels showForceFields") + + physics = root.addChild("Physics") + + if method.lower() == "explicit": # Requires the implementation of 'addForce' + physics.addObject("EulerExplicitSolver", name="eulerExplicit") + elif method.lower() == "implicit-matrix-free": # Requires the implementation of 'addForce' and 'addDForce' + physics.addObject("EulerImplicitSolver", name="eulerImplicit") + physics.addObject("CGLinearSolver", template="GraphScattered", name="solver", iterations=50, tolerance=1e-5, threshold=1e-5) + elif method == "implicit-matrix-assembly": # Requires the implementation of 'addForce', 'addDForce' and 'addKToMatrix' + physics.addObject("EulerImplicitSolver", name="eulerImplicit") + physics.addObject("SparseLDLSolver", name="solver", template="CompressedRowSparseMatrixd") + + position = np.random.uniform(-1, 1, (n_particles, 3)) + velocity = np.zeros_like(position) + length = np.random.uniform(0.8, 1.2, size=(n_particles, 1)) + stiffness = 100.0 + + particles = physics.addChild("Particles") + particles.addObject("MechanicalObject", name="state", template="Vec3d", position=position, velocity=velocity, showObject=True) + particles.addObject("UniformMass", name="mass", totalMass=n_particles) + + if not use_sofa: # Use the force field implemented with JAX + particles.addObject(JaxForceField(length=length, stiffness=stiffness)) + else: # Use a SOFA equivalent for comparison + root.addObject("MechanicalObject", name="origin", template="Vec3d", position="0 0 0") + particles.addObject("SpringForceField", name="force", object1="@/origin", object2="@/Physics/Particles/state", indices1=np.zeros(n_particles, dtype=np.int32), indices2=np.arange(n_particles), length=length, stiffness=stiffness*np.ones(n_particles), damping=np.zeros(n_particles)) + + +def main(): + import argparse + import SofaRuntime + import SofaImGui + import Sofa.Gui + + parser = argparse.ArgumentParser(description="Example of a scene using a ForceField implemented with JAX") + parser.add_argument("--method", default="implicit-matrix-assembly", help="must be 'explicit', 'implicit-matrix-free' or 'implicit-matrix-assembly'") + parser.add_argument("--particles", type=int, default=1000, help="number of particles (default 1000)") + parser.add_argument("--use-sofa", action="store_true", help="use a force field from SOFA instead of the one implemented with JAX") + args = parser.parse_args() + + root=Sofa.Core.Node("root") + createScene(root, method=args.method, n_particles=args.particles, use_sofa=args.use_sofa) + Sofa.Simulation.initRoot(root) + + Sofa.Gui.GUIManager.Init("myscene", "imgui") + Sofa.Gui.GUIManager.createGUI(root, __file__) + Sofa.Gui.GUIManager.SetDimension(1600, 900) + Sofa.Gui.GUIManager.MainLoop(root) + Sofa.Gui.GUIManager.closeGUI() + + +if __name__ == "__main__": + main() diff --git a/examples/meshing/CGAL/README.txt b/examples/meshing/CGAL/README.txt new file mode 100644 index 000000000..fd036c52e --- /dev/null +++ b/examples/meshing/CGAL/README.txt @@ -0,0 +1,5 @@ +This folder presents two simple modules using CGAL bindings allowing for easy tetrahedric mesh generation from either a polyhedron of its surface or a point cloud (using a convexe hull). + +You can you this through the two mains (mesh_from_point_cloud_using_convex_hull.py and mesh_from_polyhedron.py) through command line (use -h for more information) or simply use the cgal_utils.py module if you want to build new scripts upon this work. The most important class being CGAL_Mesh_3_IO_Util heping to deal with the CGal data structure and use they results as numpy array. To see usage example, see mesh_from_polyhedron.py:CGAL_Mesh_from_polyhedron(). + +Those scripts are here to replace the components MeshGenerationFromPolyhedron and TriangularConvexHull3D from the CGALPLugin https://github.com/sofa-framework/CGALPlugin diff --git a/examples/meshing/CGAL/cgal_utils.py b/examples/meshing/CGAL/cgal_utils.py new file mode 100644 index 000000000..97b096c77 --- /dev/null +++ b/examples/meshing/CGAL/cgal_utils.py @@ -0,0 +1,259 @@ +from CGAL.CGAL_Polyhedron_3 import Polyhedron_3, Polyhedron_modifier +from CGAL.CGAL_Mesh_3 import Mesh_3_Complex_3_in_triangulation_3 +from CGAL.CGAL_Mesh_3 import Polyhedral_mesh_domain_3 +from CGAL.CGAL_Mesh_3 import Mesh_3_parameters +from CGAL.CGAL_Mesh_3 import Default_mesh_criteria +from CGAL.CGAL_Kernel import Point_3 +from CGAL import CGAL_Mesh_3 + +import numpy as np +import dataclasses +from typing import List, Optional + +from enum import Enum +from pathlib import Path + +import time + +from vtkmodules.vtkIOGeometry import ( + vtkBYUReader, + vtkOBJReader, + vtkSTLReader + ) +from vtkmodules.vtkIOLegacy import vtkPolyDataReader +from vtkmodules.vtkIOPLY import vtkPLYReader +from vtkmodules.vtkIOXML import vtkXMLPolyDataReader + +import vtk +from vtk.util import numpy_support +from vtk.numpy_interface import algorithms as algs + +timestamp = {} + +def tic(hash = 0): + global timestamp + timestamp[hash] = time.time_ns() + +def toc(hash = 0): + global timestamp + return f"{((time.time_ns() - timestamp[hash])/1000000):.3f} miliseconds" + + +def ReadPolyData(file_name): + #FROM Vtk examples https://examples.vtk.org/site/Python/Snippets/ReadPolyData/ + + valid_suffixes = ['.g', '.obj', '.stl', '.ply', '.vtk', '.vtp'] + + path = Path(file_name) + if path.suffix: + ext = path.suffix.lower() + if path.suffix not in valid_suffixes: + print(f'No reader for this file suffix: {ext}') + return None + else: + if ext == ".ply": + reader = vtkPLYReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".vtp": + reader = vtkXMLPolyDataReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".obj": + reader = vtkOBJReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".stl": + reader = vtkSTLReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".vtk": + reader = vtkPolyDataReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".g": + reader = vtkBYUReader() + reader.SetGeometryFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + + return poly_data + + +class CGAL_Mesh_3_IO_Util(object): + + points : np.array + triangles : np.array + tetras : np.array + + class Elem(Enum): + POINTS = 1 + TRIANGLES = 3 + TETRA = 4 + + def __init__(self,mesh): + self.mesh = mesh + + def extract(self, elems : list[Elem]): + + print(f"Extracting data into numpy objects...") + tic() + + for elem in elems: + vnbe = {} + match elem: + case CGAL_Mesh_3_IO_Util.Elem.TRIANGLES: + for elemf in self.mesh.facets(): + for i in range(3): + if not elemf.vertex in vnbe: + vnbe[elemf.vertex(i)] = 1 + else: + vnbe[elem.vertex(i)] += 1 + case CGAL_Mesh_3_IO_Util.Elem.TETRA: + for elemc in self.mesh.cells(): + for i in range(4): + if not elemc.vertex in vnbe: + vnbe[elemc.vertex(i)] = 1 + else: + vnbe[elemc.vertex(i)] += 1 + + + V = {} + it = 0 + for vertice in self.mesh.triangulation().finite_vertices(): + if vertice in vnbe: + V[vertice] = it + it += 1 + + if CGAL_Mesh_3_IO_Util.Elem.POINTS in elems: + self.points = np.empty((len(V),3)) + id = 0 + for key in V.keys(): + self.points[id][:] = [self.mesh.triangulation().point(key).x(), self.mesh.triangulation().point(key).y(), self.mesh.triangulation().point(key).z()] + id+=1 + + match elem: + case CGAL_Mesh_3_IO_Util.Elem.TRIANGLES: + self.triangles = np.empty((self.mesh.number_of_facets(),3), dtype=np.int32) + id = 0 + for elemt in self.mesh.facets(): + self.triangles[id][:] = [V[elemt.vertex(0)],V[elemt.vertex(1)],V[elemt.vertex(2)]] + id+= 1 + case CGAL_Mesh_3_IO_Util.Elem.TETRA: + self.tetras = np.empty((self.mesh.number_of_cells(),4), dtype=np.int32) + id = 0 + for elemc in self.mesh.cells(): + self.tetras[id][:] = [V[elemc.vertex(0)],V[elemc.vertex(1)],V[elemc.vertex(2)], V[elemc.vertex(3)]] + id += 1 + print(f"Done ! Took {toc()}") + if CGAL_Mesh_3_IO_Util.Elem.TRIANGLES in elems: + print(f"Extracted {self.points.shape[0]} points and {self.triangles.shape[0]} triangles") + if CGAL_Mesh_3_IO_Util.Elem.TETRA in elems: + print(f"Extracted {self.points.shape[0]} points and {self.tetras.shape[0]} tetras") + + + + def write_out(self, filename): + path = Path(filename) + ext = path.suffix.lower() + if ext != ".vtk" and ext != ".vtu": + print("Only vtk or vtu extension are suported") + return + + if (not "tetras" in self.__dict__) and ext == ".vtu": + print("VTU only supported for tetrahedral meshes. Use vtk instead") + return + + print(f"Saving into {filename}...") + tic() + + if "tetras" in self.__dict__: + ugrid = vtk.vtkUnstructuredGrid() + else: + ugrid = vtk.vtkPolyData() + + vtk_points = vtk.vtkPoints() + vtk_points.SetData(numpy_support.numpy_to_vtk(self.points, deep=True)) + ugrid.SetPoints(vtk_points) + + + if "triangles" in self.__dict__: + for triangle in self.triangles: + vtkTriangleObj = vtk.vtkTriangle() + vtkTriangleObj.GetPointIds().SetId(0,triangle[0]) + vtkTriangleObj.GetPointIds().SetId(1,triangle[1]) + vtkTriangleObj.GetPointIds().SetId(2,triangle[2]) + ugrid.InsertNextCell(vtkTriangleObj.GetCellType(), vtkTriangleObj.GetPointIds()) + if "tetras" in self.__dict__: + for tetra in self.tetras: + vtkTetraObj = vtk.vtkTetra() + vtkTetraObj.GetPointIds().SetId(0,tetra[0]) + vtkTetraObj.GetPointIds().SetId(1,tetra[1]) + vtkTetraObj.GetPointIds().SetId(2,tetra[2]) + vtkTetraObj.GetPointIds().SetId(3,tetra[3]) + ugrid.InsertNextCell(vtkTetraObj.GetCellType(), vtkTetraObj.GetPointIds()) + + + if "tetras" in self.__dict__: + writer = vtk.vtkUnstructuredGridWriter() + else: + writer = vtk.vtkPolyDataWriter() + writer.SetFileVersion(42) + writer.SetInputData(ugrid) + writer.SetFileName(filename) + writer.Write() + print(f"Done ! Took {toc()}") + + + + + +class CGAL_Mesh_from(object): + + class Refiner(Enum): + NONE = 0 + LLOYD = 1 + ODT = 3 + PERTURB = 4 + + @dataclasses.dataclass + class Refiner_input(): + refiner_type : Enum + + time_limit : Optional[float] = 20.0 #(ALL) to set up, in seconds, a CPU time limit after which the optimization process is stopped. This time is measured using CGAL::Real_timer. 0 means that there is no time limit. + max_iteration_number : Optional[int] = 200 #(LLOYD & ODT only) limit on the number of performed iterations. 0 means that there is no limit on the number of performed iterations. + convergence : Optional[int] = 0.0 #(LLOYD & ODT only) threshold ratio of stopping criterion based on convergence: the optimization process is stopped when at the last iteration the displacement of any vertex is less than a given fraction of the length of the shortest edge incident to that vertex. + free_bound : Optional[bool] = False #(LLOYD & ODT only) designed to reduce running time of each optimization iteration. Any vertex that has a displacement less than a given fraction of the length of its shortest incident edge, is frozen (i.e. is not relocated). The parameter freeze_bound gives the threshold ratio. If it is set to 0, freezing of vertices is disabled. + silver_bound : Optional[bool] = False #(PERTURB only) is designed to give, in degrees, a targeted lower bound on dihedral angles of mesh cells. The exudation process considers in turn all the mesh cells that have a smallest dihedral angle less than sliver_bound and tries to make them disappear by weighting their vertices. The optimization process stops when every cell in the mesh achieves this quality. The default value is 0 and means that there is no targeted bound: the exuder then runs as long as it can improve the smallest dihedral angles of the set of cells incident to some vertices. + + + IOUtil : CGAL_Mesh_3_IO_Util + + def __init__(self): + pass + + def generate(self): + pass + + + def __getattr__(self, name): + match name: + case "points": + return self.IOUtil.points + case "triangles": + return self.IOUtil.triangles + case "tetras": + return self.IOUtil.tetras + case _: + if name in self.__dict__: + return self.__dict__[name] + else: + raise AttributeError() + + + diff --git a/examples/meshing/CGAL/data/point_cloud.pcd b/examples/meshing/CGAL/data/point_cloud.pcd new file mode 100644 index 0000000000000000000000000000000000000000..f6f48c52c4e7f20ff8cb5464e1e05b376b8fed93 GIT binary patch literal 313 zcmWG@4GQ-3_fs&?Gv{*iboFrwR;W;@RH#zO%qvOFE6yybq3}0UR45v4P@TU=_h2 zjV_KMjtWVcd5J}pT+>xQ*}Zeov=0iLW6!|QV88ZznLXFkPJ5~4eYPNe>8A=i-keLe z>K2E=^13=bHYvVu?YAY$Ie_G!xPP&4d(miLS$P{QUsE&BLFlc3Lu|@)Fkc}m*p6=o zqr-ytyTSZe)#diTGGrXG-?oDJaVr?@&zc(Ax1BYx1DW^XS(QU}oxgqO@ 0.5 : + + # Make the Young's modulus related to the temperature: E = E_init / (1 + 50*Temperature) + with self.MechanicalForceField.localStiffnessFactor.writeableArray() as wa: + for i in range(576) : + wa[i] = 1.0 / ( 1.+ 50.*self.temperatures[i][0] ) + + #Enforce temperature of the node 495 (extremity of the bar) + # Heating phase until t = 4 + if time < 4.0 : + with self.ThermalObject.position.writeableArray() as wt: + wt[495][0] = 1.0 + # Cooling down phase when t > 4 + else : + with self.ThermalObject.position.writeableArray() as wt: + wt[495][0] = 0.0 + + +# Function used only if this script is called from a python environment +if __name__ == '__main__': + main() \ No newline at end of file From ed5e019a349c23cad3e0b95d054c1a073fe75c6d Mon Sep 17 00:00:00 2001 From: bakpaul Date: Mon, 2 Mar 2026 09:27:39 +0100 Subject: [PATCH 45/49] Fix constraint solvr name --- splib/simulation/headers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splib/simulation/headers.py b/splib/simulation/headers.py index 9734d3062..d3c61a21b 100644 --- a/splib/simulation/headers.py +++ b/splib/simulation/headers.py @@ -122,7 +122,7 @@ def setupLagrangianCollision(node, displayFlags = "showVisualModels",background node.addObject('CollisionResponse',name="ContactManager", response="FrictionContactConstraint", responseParams="mu="+str(frictionCoef),**kwargs) node.addObject('NewProximityIntersection' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs) - node.addObject('GenericConstraintSolver',name="ConstraintSolver", tolerance=tolerance, maxIterations=maxIterations, multithreading=parallelComputing,**kwargs) + node.addObject('BlockGaussSeidelConstraintSolver',name="ConstraintSolver", tolerance=tolerance, maxIterations=maxIterations, multithreading=parallelComputing,**kwargs) node.addObject("ConstraintAttachButtonSetting") return node From a1d91c84eb51cb2c37de4defc725fe064e2a4ace Mon Sep 17 00:00:00 2001 From: Damien Marchal Date: Thu, 30 Oct 2025 16:06:25 +0100 Subject: [PATCH 46/49] Register in the PythonFactory Rigid3::Coord (#544) Because currently only Rigid3::VecCoord was supported. --- Plugin/src/SofaPython3/PythonFactory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugin/src/SofaPython3/PythonFactory.cpp b/Plugin/src/SofaPython3/PythonFactory.cpp index 9949bbe9f..d94472e5c 100644 --- a/Plugin/src/SofaPython3/PythonFactory.cpp +++ b/Plugin/src/SofaPython3/PythonFactory.cpp @@ -508,7 +508,7 @@ bool PythonFactory::registerDefaultTypes() PythonFactory::registerType("Rigid3f::Coord"); PythonFactory::registerType("Rigid3::Coord"); - /// Quaternion + // Quaternion PythonFactory::registerType>("Quatd"); PythonFactory::registerType>("Quatf"); PythonFactory::registerType>("Quat"); From 6bc55e721c3a8e98dddad840929a33e8677b581a Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 3 Mar 2026 10:17:58 +0100 Subject: [PATCH 47/49] Squashed commit of the following: commit b0cffe74fcee427c8769a28906c9fc61a7fb1fbb Author: bakpaul Date: Tue Mar 3 10:17:06 2026 +0100 Dump ideas commit 4baade5a2aff7b03c1f93060127d96394107dd19 Author: Damien Marchal Date: Thu Oct 30 16:06:25 2025 +0100 Register in the PythonFactory Rigid3::Coord (#544) Because currently only Rigid3::VecCoord was supported. commit dd6865e94503bfa86cde9521bab4a6f628cf6472 Author: bakpaul Date: Mon Mar 2 14:26:25 2026 +0100 Make exctracted topo working' commit de007a345bdbca33e18af5951d3ab933b743b439 Author: Paul Baksic Date: Wed Oct 29 17:31:41 2025 +0100 Added S, but still need to make the free motion not crash commit d3148a50d37ccb695b5b2e71fbbed742c771980e Author: Paul Baksic Date: Wed Oct 29 12:21:41 2025 +0100 Fix plane and some splib deprecated components commit d2c120e673cc829819cbe70c045e9a20a301b442 Author: Paul Baksic Date: Tue Oct 28 18:22:41 2025 +0100 Add plane, still need to really implement function + started creating the scene --- CMakeLists.txt | 6 ++ examples/stlib/SofaScene.py | 121 +++++++++++++++++++++++++++++ splib/core/node_wrapper.py | 4 +- splib/mechanics/collision_model.py | 2 +- stlib/collision.py | 2 +- stlib/entities/__entity__.py | 11 +-- stlib/geometries/__geometry__.py | 20 ++++- stlib/geometries/extract.py | 11 ++- stlib/geometries/file.py | 22 ++++-- stlib/geometries/plane.py | 45 +++++++++++ stlib/materials/deformable.py | 4 +- 11 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 examples/stlib/SofaScene.py create mode 100644 stlib/geometries/plane.py diff --git a/CMakeLists.txt b/CMakeLists.txt index d76b3e21a..c9fac18cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,6 +203,12 @@ SP3_add_python_package( TARGET_DIRECTORY splib ) +SP3_add_python_package( + SOURCE_DIRECTORY + ${CMAKE_CURRENT_SOURCE_DIR}/stlib + TARGET_DIRECTORY + stlib +) sofa_create_package( PACKAGE_NAME ${PROJECT_NAME} diff --git a/examples/stlib/SofaScene.py b/examples/stlib/SofaScene.py new file mode 100644 index 000000000..4e60200c6 --- /dev/null +++ b/examples/stlib/SofaScene.py @@ -0,0 +1,121 @@ +from fontTools.afmLib import preferredAttributeOrder + +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.collision import Collision, CollisionParameters +from stlib.entities import Entity, EntityParameters +from stlib.visual import Visual, VisualParameters +from splib.core.enum_types import CollisionPrimitive, ElementType, ConstitutiveLaw +from splib.simulation.headers import setupLagrangianCollision, setupDefaultHeader +from splib.simulation.ode_solvers import addImplicitODE +from splib.simulation.linear_solvers import addLinearSolver +import dataclasses +import numpy as np + + +def createScene(root): + root.gravity=[0,0,9.81] + ##Solvers + # setupDefaultHeader(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1], + # parallelComputing = True) + setupLagrangianCollision(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1], + parallelComputing = True,alarmDistance=0.3, contactDistance=0.02, + frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20) + ##Environement + planes_lengthNormal = np.array([0, 1, 0]) + planes_lengthNbEdge = 1 + planes_widthNbEdge = 2 + planes_lengthSize = 30 + planes_widthSize = 70 + + plane1_collisionParams = CollisionParameters() + plane1_collisionParams.name = "UP" + plane1_collisionParams.primitives = [CollisionPrimitive.TRIANGLES] + plane1_collisionParams.kwargs = {"TriangleCollision" : {"moving" : False, "simulated" : False}} + plane1_collisionParams.geometry = PlaneParameters(np.array([15,0,1]), np.array([0,0,-1]), + planes_lengthNormal, planes_lengthNbEdge, planes_widthNbEdge, planes_lengthSize, planes_widthSize) + plane1 = root.add(Collision, parameters = plane1_collisionParams) + # TODO being able to reuse already loaded geometry of current prefab to add any new sub prefab + # We need to enable to directly pass a link to an already existing prefab in place of a prefab parameter object + plane1_visu = plane1.addChild("Visu") + plane1_visu.addObject("OglModel", name="VisualModel", src="@../Geometry/container") + + + plane2_collisionParams = CollisionParameters() + plane2_collisionParams.name = "DOWN" + plane2_collisionParams.primitives = [CollisionPrimitive.TRIANGLES] + plane2_collisionParams.kwargs = {"TriangleCollision" : {"moving" : False, "simulated" : False}} + plane2_collisionParams.geometry = PlaneParameters(np.array([15,0,-20]), np.array([0,0,1]), + planes_lengthNormal, planes_lengthNbEdge, planes_widthNbEdge, planes_lengthSize, planes_widthSize) + plane2 = root.add(Collision, parameters = plane2_collisionParams) + plane2_visu = plane2.addChild("Visu") + plane2_visu.addObject("OglModel", name="VisualModel", src="@../Geometry/container") + + + ## Real models + # VolumetricObjects = root.addChild("VolumetricObjects") + # addImplicitODE(VolumetricObjects) + # addLinearSolver(VolumetricObjects, constantSparsity=False, ) + + ### Logo + LogoNode = root.addChild("LogoNode") + addImplicitODE(LogoNode) + addLinearSolver(LogoNode, constantSparsity=False, ) + + LogoParams = EntityParameters(name = "Logo", + geometry = FileParameters(filename="mesh/SofaScene/Logo.vtk"), + material = DeformableBehaviorParameters(), + collision = CollisionParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoColli.sph")), + visual = VisualParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoVisu.obj"))) + + LogoParams.geometry.elementType = ElementType.TETRAHEDRA + LogoParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + LogoParams.material.parameters = [200, 0.4] + LogoParams.material.massDensity = 0.003261 + LogoParams.collision.primitives = [CollisionPrimitive.SPHERES] + #TODO make this flawless with spheres. Here collisions elements are not in the topology and a link is to be made between the loader and the collision object + LogoParams.collision.kwargs = {"SphereCollision" : {"radius" : "@Geometry/loader.listRadius"}} + LogoParams.visual.color = [0.7, .35, 0, 0.8] + + Logo = LogoNode.add(Entity, parameters = LogoParams) + + Logo.material.addObject("ConstantForceField", name="ConstantForceUpwards", totalForce=[0, 0, -5.0]) + Logo.material.addObject("LinearSolverConstraintCorrection", name="ConstraintCorrection", linearSolver=LogoNode.LinearSolver.linkpath, ODESolver=LogoNode.ODESolver.linkpath) + + + ### S + SNode = root.addChild("SNode") + addImplicitODE(SNode) + addLinearSolver(SNode, constantSparsity=False, ) + + SParams = EntityParameters("bob.yaml") + SParams.name = "S" + SParams.geometry = FileParameters(filename="mesh/SofaScene/S.vtk") + SParams.geometry.elementType = ElementType.TETRAHEDRA + SParams.material = DeformableBehaviorParameters() + SParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + SParams.material.parameters = [200, 0.45] + + def SAddMaterial(node): + DeformableBehaviorParameters.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) + + SParams.material.addMaterial = SAddMaterial + SParams.material.massDensity = 0.011021 + SParams.collision = CollisionParameters() + SParams.collision.primitives = [CollisionPrimitive.TRIANGLES] + # # #TODO: to fix link issues for extracted geometry, it might be better to give source geometry relative link + parameters + SParams.collision.geometry = ExtractParameters(destinationType=ElementType.TRIANGLES, sourceParameters=SParams.geometry ) + SParams.visual = VisualParameters() + SParams.visual.geometry = FileParameters(filename="mesh/SofaScene/SVisu.obj") + SParams.visual.color = [0.7, .7, 0.7, 0.8] + + S = SNode.add(Entity, parameters = SParams) + + + SDensity = 0.011021 + ODensity = SDensity + ADensity = 0.00693695 diff --git a/splib/core/node_wrapper.py b/splib/core/node_wrapper.py index 7646b9e84..a7931f088 100644 --- a/splib/core/node_wrapper.py +++ b/splib/core/node_wrapper.py @@ -25,7 +25,9 @@ def addObject(self,*args, **kwargs): parameters["name"] = kwargs["name"] if kwargs["name"] in kwargs: if isinstance(kwargs[kwargs["name"]], dict): - parameters = {**parameters, **kwargs[kwargs["name"]]} + for param in kwargs[kwargs["name"]]: + if not(isinstance(kwargs[kwargs["name"]][param],defaultValueType)): + parameters = {**parameters, param : kwargs[kwargs["name"]][param]} else: print("[Warning] You are passing a keyword arg with the same name as one obj without it being a Dict, it will not be used. ") diff --git a/splib/mechanics/collision_model.py b/splib/mechanics/collision_model.py index e1abdf457..6758ee6d4 100644 --- a/splib/mechanics/collision_model.py +++ b/splib/mechanics/collision_model.py @@ -24,7 +24,7 @@ def addCollisionModels(node, primitive : CollisionPrimitive, node.addObject("TriangleCollisionModel", name="TriangleCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) return case CollisionPrimitive.SPHERES: - node.addObject("SphereCollisionModel", name="SphereCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) + node.addObject("SphereCollisionModel", name="SphereCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) return case _: return diff --git a/stlib/collision.py b/stlib/collision.py index ebffd7b75..b8f059249 100644 --- a/stlib/collision.py +++ b/stlib/collision.py @@ -27,7 +27,7 @@ def __init__(self, parameters: CollisionParameters): def init(self): - geom = self.add(Geometry, self.parameters.geometry) + geom = self.add(Geometry, parameters = self.parameters.geometry) self.addObject("MechanicalObject", template="Vec3", position=f"@{self.parameters.geometry.name}/container.position") for primitive in self.parameters.primitives: diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index 9a332dca2..bc7c76711 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -65,16 +65,17 @@ def init(self): def addMapping(self, destinationPrefab): template = f'{self.parameters.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): 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 10b5084b3..57d0e5c76 100644 --- a/stlib/geometries/__geometry__.py +++ b/stlib/geometries/__geometry__.py @@ -4,7 +4,9 @@ from splib.topology.static import addStaticTopology from splib.core.enum_types import ElementType from splib.core.utils import DEFAULT_VALUE -from Sofa.Core import Object +from Sofa.Core import Object + +import numpy as np class Geometry(BasePrefab):... @@ -33,6 +35,10 @@ 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,11 +54,19 @@ def __init__(self, parameters: GeometryParameters): def init(self): # Generate attribute (positions, edges, triangles, quads, tetrahedra, hexahedra) from the internal data provider - if self.parameters.data is not None : + if isinstance(self.parameters.data, InternalDataProvider) : self.parameters.data.generateAttribute(self) + if self.parameters.dynamicTopology : if self.parameters.elementType is not None : - addDynamicTopology(self, container = dataclasses.asdict(self.parameters.data)) + 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 7143fb07d..87359addf 100644 --- a/stlib/geometries/extract.py +++ b/stlib/geometries/extract.py @@ -25,7 +25,7 @@ def __post_init__(self): InternalDataProvider.__init__(self) - def generateAttribute(self, parent : Geometry): + def generateAttribute(self, parent : Geometry): node = parent.addChild("ExtractedGeometry") #TODO: Specify somewhere in the doc that this should only be used for mapped topologies that extract parent topology surface @@ -34,8 +34,8 @@ def generateAttribute(self, parent : Geometry): # !!! also, on a fail, nothing is added to the graph, which makes things harder to debug # !!! also, does not work because of the function canCreate(), which checks the input (not yet created?) # this is all related - fromLink = "@../../Geometry.container" # TODO: can we do better than this? - addDynamicTopology(node, elementType=self.sourceType) + fromLink = "@../../../Geometry/container" # TODO: can we do better than this? + addDynamicTopology(node, elementType=self.destinationType, container={"position" : fromLink + ".position"}) if self.sourceType == ElementType.TETRAHEDRA: node.addObject("Tetra2TriangleTopologicalMapping", input=fromLink, output=node.container.linkpath) elif self.sourceType == ElementType.HEXAHEDRA: @@ -60,12 +60,11 @@ def generateAttribute(self, parent : Geometry): class ExtractParameters(GeometryParameters): def __init__(self, sourceParameters : GeometryParameters, - destinationType : ElementType, - dynamicTopology : bool = False, ): + destinationType : ElementType,): GeometryParameters.__init__(self, data = ExtractInternalDataProvider(destinationType = destinationType, sourceType = sourceParameters.elementType, sourceName = sourceParameters.name), - dynamicTopology = dynamicTopology, + dynamicTopology = True, elementType = destinationType) diff --git a/stlib/geometries/file.py b/stlib/geometries/file.py index b057728c1..601782f9d 100644 --- a/stlib/geometries/file.py +++ b/stlib/geometries/file.py @@ -13,14 +13,20 @@ def __post_init__(self, **kwargs): InternalDataProvider.__init__(self,**kwargs) def generateAttribute(self, parent : Geometry): - loadMesh(parent, self.filename) - - self.position = str(parent.loader.position.linkpath) - self.edges = str(parent.loader.edges.linkpath) - self.triangles = str(parent.loader.triangles.linkpath) - self.quads = str(parent.loader.quads.linkpath) - self.hexahedra = str(parent.loader.hexahedra.linkpath) - self.tetrahedra = str(parent.loader.tetras.linkpath) + loadMesh(parent, self.filename) + + if hasattr(parent.loader, 'position'): + self.position = str(parent.loader.position.linkpath) + if hasattr(parent.loader, 'edges'): + self.edges = str(parent.loader.edges.linkpath) + if hasattr(parent.loader, 'triangles'): + self.triangles = str(parent.loader.triangles.linkpath) + if hasattr(parent.loader, 'quads'): + self.quads = str(parent.loader.quads.linkpath) + if hasattr(parent.loader, 'hexahedra'): + self.hexahedra = str(parent.loader.hexahedra.linkpath) + if hasattr(parent.loader, 'tetrahedra'): + self.tetrahedra = str(parent.loader.tetrahedra.linkpath) diff --git a/stlib/geometries/plane.py b/stlib/geometries/plane.py new file mode 100644 index 000000000..2c3c637a4 --- /dev/null +++ b/stlib/geometries/plane.py @@ -0,0 +1,45 @@ +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])) + 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 + widthEdgeSize = self.widthSize / self.widthNbEdge + + self.widthNormal = np.cross(self.normal,self.lengthNormal) + bottomLeftCorner = self.center - self.lengthNormal * self.lengthNbEdge * lengthEdgeSize / 2 - self.widthNormal * self.widthNbEdge * widthEdgeSize / 2 + + self.position = np.array([[ bottomLeftCorner + j * self.widthNormal * widthEdgeSize + i * self.lengthNormal * lengthEdgeSize for j in range(self.widthNbEdge + 1) ] for i in range(self.lengthNbEdge + 1)]) + + self.triangles = np.empty((2 * self.widthNbEdge * self.lengthNbEdge, 3), dtype = int) + for i in range(self.lengthNbEdge): + for j in range(self.widthNbEdge): + self.triangles[i*self.widthNbEdge*2 + j * 2 , 0] = j + i * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 , 1] = j + (i+1) * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 , 2] = j + 1 + i * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 + 1, 0] = j + 1 + i * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 + 1, 1] = j + (i+1) * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 + 1, 2] = j + 1 + (i+1) * (self.widthNbEdge + 1) + + + + +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) diff --git a/stlib/materials/deformable.py b/stlib/materials/deformable.py index a24763df5..f899ef96b 100644 --- a/stlib/materials/deformable.py +++ b/stlib/materials/deformable.py @@ -13,7 +13,7 @@ class DeformableBehaviorParameters(MaterialParameters): elementType : ElementType = ElementType.TETRAHEDRA parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio - def __addDeformableMaterial(node): + def addDeformableMaterial(node): massKwargs = {} if node.parameters.elementType != ElementType.EDGES: #If we use the MeshMatrixMass, then the mass will need us to specify the mstate to use @@ -26,7 +26,7 @@ def __addDeformableMaterial(node): else: addLinearElasticity(node, node.parameters.elementType, node.parameters.parameters[0], node.parameters.parameters[1], topology="@../Geometry/container") - addMaterial : Optional[Callable] = __addDeformableMaterial + addMaterial : Optional[Callable] = addDeformableMaterial def createScene(root) : From 92dccd9dd4f8dbe517eb53af9b74e0be5a46eefb Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Tue, 3 Mar 2026 10:30:37 +0100 Subject: [PATCH 48/49] revert some changes --- stlib/geometries/extract.py | 5 +++-- stlib/materials/deformable.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/stlib/geometries/extract.py b/stlib/geometries/extract.py index 87359addf..5ee1b3762 100644 --- a/stlib/geometries/extract.py +++ b/stlib/geometries/extract.py @@ -60,11 +60,12 @@ def generateAttribute(self, parent : Geometry): class ExtractParameters(GeometryParameters): def __init__(self, sourceParameters : GeometryParameters, - destinationType : ElementType,): + destinationType : ElementType, + dynamicTopology : bool = False): GeometryParameters.__init__(self, data = ExtractInternalDataProvider(destinationType = destinationType, sourceType = sourceParameters.elementType, sourceName = sourceParameters.name), - dynamicTopology = True, + dynamicTopology = dynamicTopology, elementType = destinationType) diff --git a/stlib/materials/deformable.py b/stlib/materials/deformable.py index f899ef96b..a24763df5 100644 --- a/stlib/materials/deformable.py +++ b/stlib/materials/deformable.py @@ -13,7 +13,7 @@ class DeformableBehaviorParameters(MaterialParameters): elementType : ElementType = ElementType.TETRAHEDRA parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio - def addDeformableMaterial(node): + def __addDeformableMaterial(node): massKwargs = {} if node.parameters.elementType != ElementType.EDGES: #If we use the MeshMatrixMass, then the mass will need us to specify the mstate to use @@ -26,7 +26,7 @@ def addDeformableMaterial(node): else: addLinearElasticity(node, node.parameters.elementType, node.parameters.parameters[0], node.parameters.parameters[1], topology="@../Geometry/container") - addMaterial : Optional[Callable] = addDeformableMaterial + addMaterial : Optional[Callable] = __addDeformableMaterial def createScene(root) : From 9e3dda5e667bc789ae19b3f746d5d24e170d3a66 Mon Sep 17 00:00:00 2001 From: EulalieCoevoet Date: Wed, 4 Mar 2026 19:23:58 +0100 Subject: [PATCH 49/49] test modifier --- examples/stlib/EulalieScene.py | 14 ++++++ examples/stlib/SofaScene.py | 2 +- splib/core/utils.py | 22 ++++++++- splib/mechanics/mass.py | 8 ++-- stlib/__init__.py | 79 +++++++++++++++++++++++--------- stlib/collision.py | 18 +++----- stlib/core/baseEntity.py | 12 ++--- stlib/core/baseEntityModifier.py | 24 ++++++++++ stlib/core/basePrefab.py | 9 ++-- stlib/entities/__entity__.py | 62 +++++++++++++------------ stlib/geometries/__geometry__.py | 33 ++++++------- stlib/geometries/cube.py | 15 ++---- stlib/geometries/file.py | 13 +++--- stlib/materials/__material__.py | 11 ++--- stlib/materials/rigid.py | 16 +++---- stlib/modifiers/__init__.py | 1 + stlib/modifiers/__modifier__.py | 24 ++++++++++ stlib/modifiers/fixing.py | 13 ++++++ stlib/settings/__init__.py | 0 stlib/settings/simulation.py | 31 +++++++++++++ stlib/visual.py | 11 ++--- 21 files changed, 276 insertions(+), 142 deletions(-) create mode 100644 examples/stlib/EulalieScene.py create mode 100644 stlib/core/baseEntityModifier.py create mode 100644 stlib/modifiers/__init__.py create mode 100644 stlib/modifiers/__modifier__.py create mode 100644 stlib/modifiers/fixing.py create mode 100644 stlib/settings/__init__.py create mode 100644 stlib/settings/simulation.py diff --git a/examples/stlib/EulalieScene.py b/examples/stlib/EulalieScene.py new file mode 100644 index 000000000..47d0ca2f8 --- /dev/null +++ b/examples/stlib/EulalieScene.py @@ -0,0 +1,14 @@ +from stlib.entities import Entity, EntityParameters +from stlib.modifiers import SingleEntityModifier +from stlib.modifiers.fixing import FixingModifierParameters +from stlib.settings.simulation import addSimulationSettings + + +def createScene(rootnode): + + simulation = addSimulationSettings(rootnode) + + cube1 = simulation.add(Entity, parameters=EntityParameters(name = "FixedCube")) + cube2 = simulation.add(Entity, parameters=EntityParameters(name = "FallingCube")) + + simulation.create(SingleEntityModifier, parameters=FixingModifierParameters()).apply(entity=cube1) \ No newline at end of file diff --git a/examples/stlib/SofaScene.py b/examples/stlib/SofaScene.py index 4e60200c6..67f667a5a 100644 --- a/examples/stlib/SofaScene.py +++ b/examples/stlib/SofaScene.py @@ -99,7 +99,7 @@ def createScene(root): SParams.material.parameters = [200, 0.45] def SAddMaterial(node): - DeformableBehaviorParameters.addDeformableMaterial(node) + DeformableBehaviorParameters.addMaterial(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..9896a259d 100644 --- a/splib/core/utils.py +++ b/splib/core/utils.py @@ -1,5 +1,7 @@ from typing import List, Callable, Tuple, Dict from functools import wraps +import Sofa +import Sofa.Core class defaultValueType(): def __init__(self): @@ -10,14 +12,12 @@ def __init__(self): def isDefault(obj): return isinstance(obj,defaultValueType) - def getParameterSet(name : str,parameterSet : Dict) -> Dict: if name in parameterSet: if isinstance(parameterSet[name], dict): return parameterSet[name] return {} - def MapKeywordArg(objectName,*argumentMaps): def MapArg(method): @wraps(method) @@ -31,6 +31,24 @@ def wrapper(*args, **kwargs): return wrapper 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", default=False, 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", default=False, 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..883b6d698 100644 --- a/splib/mechanics/mass.py +++ b/splib/mechanics/mass.py @@ -1,17 +1,15 @@ from splib.core.node_wrapper import ReusableMethod -from splib.core.utils import defaultValueType, DEFAULT_VALUE, isDefault +from splib.core.utils import DEFAULT_VALUE, isDefault 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): + 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/__init__.py b/stlib/__init__.py index 0ad659858..3f5594daa 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1,29 +1,35 @@ -__all__ = ["core","entities","geometries","materials","collision","visual"] +__all__ = ["core","entities","geometries","materials","modifiers","collision","visual"] import Sofa.Core from stlib.core.basePrefab import BasePrefab +from stlib.core.baseEntity import BaseEntity +from stlib.core.baseEntityModifier import BaseEntityModifier -def __genericAdd(self : Sofa.Core.Node, typeName, **kwargs): - def findName(cname, names): - """Compute a working unique name in the node""" - rname = cname - for i in range(0, len(names)): - if rname not in names: - return rname - rname = cname + str(i+1) - return rname +def __findName(cname, names): + """Compute a working unique name in the node + """ + rname = cname + for i in range(0, len(names)): + if rname not in names: + return rname + rname = cname + str(i+1) + return rname - def checkName(context : Sofa.Core.Node, name): - # Check if the name already exists, if this happens, create a new one. - if name in context.children or name in context.objects: - names = {node.name.value for node in context.children} - names = names.union({object.name.value for object in context.objects}) - name = findName(name, names) - return name +def __checkName(context : Sofa.Core.Node, name): + """Check if the name already exists, if this happens, create a new one. + """ + if name in context.children or name in context.objects: + names = {node.name.value for node in context.children} + names = names.union({object.name.value for object in context.objects}) + name = __findName(name, names) + return name - # Check if a name is provided, if not, use the one of the class + +def __processParameters(self : Sofa.Core.Node, typeName, **kwargs): + """Check if a name is provided, if not, use the one of the class + """ params = kwargs.copy() if isinstance(typeName, type) and issubclass(typeName, BasePrefab): #Only for prefabs if len(params.keys()) > 1 or (len(params.keys()) == 1 and "parameters" not in params): @@ -44,25 +50,56 @@ def checkName(context : Sofa.Core.Node, name): raise RuntimeError("Invalid argument ", typeName) if isinstance(typeName, type) and issubclass(typeName, BasePrefab) and len(params.keys()) == 1: - params["parameters"].name = checkName(self, params["parameters"].name) + params["parameters"].name = __checkName(self, params["parameters"].name) else: - params["name"] = checkName(self, params["name"]) + params["name"] = __checkName(self, params["name"]) + + return params + + +def __create(self: Sofa.Core.Node, typeName, **kwargs): + + params = __processParameters(self, typeName, **kwargs) + + if isinstance(typeName, type) and issubclass(typeName, BaseEntityModifier): + node = typeName(**params) + node.creator = self + return node + if isinstance(typeName, type) and issubclass(typeName, BasePrefab): + return typeName(**params) + elif isinstance(typeName, Sofa.Core.Node): + return typeName(**params) + + return + + +def __genericAdd(self: Sofa.Core.Node, typeName, **kwargs): # Dispatch the creation to either addObject or addChild if isinstance(typeName, type) and issubclass(typeName, BasePrefab): + params = __processParameters(self, typeName, **kwargs) pref = self.addChild(typeName(**params)) pref.init() + elif isinstance(typeName, Sofa.Core.Node) and issubclass(typeName.__class__, BaseEntity): + pref = self.addChild(typeName) + pref.init() elif isinstance(typeName, Sofa.Core.Node): - pref = self.addChild(typeName(**params)) + pref = self.addChild(typeName) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): + params = __processParameters(self, typeName, **kwargs) pref = self.addObject(typeName(**params)) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.ObjectDeclaration): + params = __processParameters(self, typeName, **kwargs) pref = self.addObject(typeName.__name__, **params) elif isinstance(typeName, str): + params = __processParameters(self, typeName, **kwargs) pref = self.addObject(typeName, **params) else: raise RuntimeError("Invalid argument", typeName) + return pref + # Inject the method so it become available as if it was part of Sofa.Core.Node Sofa.Core.Node.add = __genericAdd +Sofa.Core.Node.create = __create diff --git a/stlib/collision.py b/stlib/collision.py index b8f059249..2c674137c 100644 --- a/stlib/collision.py +++ b/stlib/collision.py @@ -5,7 +5,7 @@ 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): @@ -18,7 +18,7 @@ class CollisionParameters(BaseParameters): group : Optional[int] = DEFAULT_VALUE contactDistance : Optional[float] = DEFAULT_VALUE - geometry : GeometryParameters = dataclasses.field(default_factory = lambda : GeometryParameters()) + geometry : GeometryParameters = GeometryParameters() class Collision(BasePrefab): @@ -28,7 +28,8 @@ def __init__(self, parameters: CollisionParameters): 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, @@ -36,26 +37,21 @@ def init(self): selfCollision=self.parameters.selfCollision, group=self.parameters.group, **self.parameters.kwargs) - - - @staticmethod - def getParameters(**kwargs) -> CollisionParameters: - return CollisionParameters(**kwargs) def createScene(root): - root.addObject("VisualStyle", displayFlags="showCollisionModels") + root.add("VisualStyle", displayFlags="showCollisionModels") # Create a visual from a mesh file - parameters = Collision.getParameters() + parameters = CollisionParameters() parameters.group = 1 parameters.geometry = FileParameters(filename="mesh/cube.obj") # Expert parameters # parameters.kwargs = { # "TriangleCollisionModel":{"contactStiffness": 100.0, "contactFriction": 0.5} # } - collision = root.add(Collision, parameters) + collision = root.add(Collision, parameters=parameters) # OR set the parameters post creation # collision.TriangleCollisionModel.contactStiffness = 100.0 diff --git a/stlib/core/baseEntity.py b/stlib/core/baseEntity.py index 1456436bb..f5cf7f00b 100644 --- a/stlib/core/baseEntity.py +++ b/stlib/core/baseEntity.py @@ -1,11 +1,9 @@ -import Sofa.Core -from baseParameters import BaseParameters +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters -class BaseEntity(Sofa.Core.Prefab): +class BaseEntity(BasePrefab): - parameters : BaseParameters - - def __init__(self): - Sofa.Core.Prefab.__init__(self) + def __init__(self, parameters: BaseParameters): + BasePrefab.__init__(self, parameters=parameters) diff --git a/stlib/core/baseEntityModifier.py b/stlib/core/baseEntityModifier.py new file mode 100644 index 000000000..a72a787bd --- /dev/null +++ b/stlib/core/baseEntityModifier.py @@ -0,0 +1,24 @@ +from stlib.core.basePrefab import BasePrefab +import Sofa.Core + + +class BaseEntityModifier(BasePrefab): + """ + An EntityModifier is a Prefab that modifies a set of Entities + """ + + nodeName = "Modifiers" + creator : Sofa.Core.Node = None + + def __init__(self, parameters): + BasePrefab.__init__(self, parameters) + + def apply(self, **kwargs): + if not self.creator.getChild(self.nodeName): + self.creator.modifiers = self.creator.add(Sofa.Core.Node(self.nodeName)) + + self.creator.modifiers.add(self) + self._apply(**kwargs) + + def _apply(self, **kwargs): + raise NotImplemented("To be overridden by child class") \ No newline at end of file diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py index 83d29f55d..0430175a8 100644 --- a/stlib/core/basePrefab.py +++ b/stlib/core/basePrefab.py @@ -1,13 +1,14 @@ -import copy import Sofa import Sofa.Core from stlib.core.basePrefabParameters import BasePrefabParameters class BasePrefab(Sofa.Core.Node): """ - A Prefab is a Sofa.Node that assembles a set of components and nodes + A Prefab is a Sofa.Core.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 @@ -15,9 +16,5 @@ def __init__(self, parameters: BasePrefabParameters): def init(self): raise NotImplemented("To be overridden by child class") - - def localToGlobalCoordinates(pointCloudInput, pointCloudOutput): - raise NotImplemented("Send an email to Damien, he will help you. Guaranteed :)") - diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py index bc7c76711..39771bf2a 100644 --- a/stlib/entities/__entity__.py +++ b/stlib/entities/__entity__.py @@ -1,35 +1,34 @@ from stlib.core.baseParameters import BaseParameters -from stlib.collision import CollisionParameters, Collision -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 splib.core.enum_types import StateType from stlib.core.basePrefab import BasePrefab +from stlib.geometries import Geometry +from stlib.geometries import GeometryParameters, InternalDataProvider +from stlib.geometries.file import FileParameters +from stlib.materials.rigid import RigidParameters +from stlib.materials import Material, MaterialParameters +from stlib.visual import VisualParameters, Visual +from stlib.collision import CollisionParameters, Collision +from splib.core.enum_types import ElementType + +import dataclasses +from typing import Optional + @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()) - - geometry : GeometryParameters = None - material : MaterialParameters = None + geometry : GeometryParameters = GeometryParameters(elementType = ElementType.POINTS, data = InternalDataProvider(position = [[0., 0., 0.]])) + material : MaterialParameters = RigidParameters() + visual : Optional[VisualParameters] = VisualParameters(geometry = FileParameters(filename="mesh/cube.obj")) collision : Optional[CollisionParameters] = None - visual : Optional[VisualParameters] = None - - + class Entity(BasePrefab): - # A simulated object material : Material visual : Visual collision : Collision @@ -38,20 +37,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) @@ -62,20 +57,27 @@ def init(self): self.addMapping(self.visual) - def addMapping(self, destinationPrefab): + 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: BasePrefab): template = f'{self.parameters.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 + #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): destinationPrefab.addObject("BarycentricMapping", output=destinationPrefab.linkpath, output_topology=destinationPrefab.Geometry.container.linkpath, - input=self.Material.linkpath, - input_topology=self.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..f9abb1649 100644 --- a/stlib/geometries/__geometry__.py +++ b/stlib/geometries/__geometry__.py @@ -12,9 +12,9 @@ class Geometry(BasePrefab):... @dataclasses.dataclass -class InternalDataProvider(object): +class InternalDataProvider: position : Any = None - # Topology information + edges : Any = DEFAULT_VALUE triangles : Any = DEFAULT_VALUE quads : Any = DEFAULT_VALUE @@ -29,28 +29,19 @@ def generateAttribute(self, parent : Geometry): 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): # Generate attribute (positions, edges, triangles, quads, tetrahedra, hexahedra) from the internal data provider @@ -59,14 +50,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..3444692ce 100644 --- a/stlib/geometries/cube.py +++ b/stlib/geometries/cube.py @@ -1,14 +1,9 @@ from stlib.geometries import GeometryParameters +from stlib.geometries.file import FileParameters +import dataclasses -class CubeParameters(GeometryParameters): - def __init__(self, center, edgeLength, pointPerEdge, dynamicTopology = False): - customGeom = CubeParameters.createData(center, edgeLength, pointPerEdge) - GeometryParameters.__init__(data = customGeom, dynamicTopology = dynamicTopology) +@dataclasses.dataclass +class CubeParameters(FileParameters): - @staticmethod - def createData(center, edgeLength, pointPerEdge) -> GeometryParameters.Data : - data = GeometryParameters.Data() - #Fill data - return data - \ No newline at end of file + filename : str = "mesh/cube.obj" diff --git a/stlib/geometries/file.py b/stlib/geometries/file.py index 601782f9d..87e67e5d7 100644 --- a/stlib/geometries/file.py +++ b/stlib/geometries/file.py @@ -29,12 +29,13 @@ def generateAttribute(self, parent : Geometry): self.tetrahedra = str(parent.loader.tetrahedra.linkpath) - +@dataclasses.dataclass 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 __post_init__(self): + self.data = FileInternalDataProvider(filename=self.filename) diff --git a/stlib/materials/__material__.py b/stlib/materials/__material__.py index ff88dbd43..b1cd9af24 100644 --- a/stlib/materials/__material__.py +++ b/stlib/materials/__material__.py @@ -1,5 +1,5 @@ -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, dataclasses +from splib.core.utils import DEFAULT_VALUE from splib.core.enum_types import StateType from stlib.core.basePrefab import BasePrefab @@ -9,15 +9,14 @@ class MaterialParameters(BaseParameters): name : str = "Material" - massDensity : float = DEFAULT_VALUE + massDensity : float = 1e-6 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 @@ -27,5 +26,5 @@ def __init__(self, parameters: MaterialParameters): def init(self): - self.addObject("MechanicalObject", name="States", template=str(self.parameters.stateType)) + self.addObject("MechanicalObject", template=str(self.parameters.stateType)) self.parameters.addMaterial(self) diff --git a/stlib/materials/rigid.py b/stlib/materials/rigid.py index 5668b3562..a4096036f 100644 --- a/stlib/materials/rigid.py +++ b/stlib/materials/rigid.py @@ -1,13 +1,11 @@ -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses -from stlib.geometries import GeometryParameters - +from stlib.core.baseParameters import Optional, dataclasses +from stlib.materials import MaterialParameters +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import StateType @dataclasses.dataclass -class RigidParameters(BaseParameters): - - geometry : GeometryParameters - mass : Optional[float] = None +class RigidParameters(MaterialParameters): + + stateType : StateType = StateType.RIGID - def toDict(self): - return dataclasses.asdict(self) diff --git a/stlib/modifiers/__init__.py b/stlib/modifiers/__init__.py new file mode 100644 index 000000000..186ce5b68 --- /dev/null +++ b/stlib/modifiers/__init__.py @@ -0,0 +1 @@ +from .__modifier__ import * \ No newline at end of file diff --git a/stlib/modifiers/__modifier__.py b/stlib/modifiers/__modifier__.py new file mode 100644 index 000000000..cfec85060 --- /dev/null +++ b/stlib/modifiers/__modifier__.py @@ -0,0 +1,24 @@ +from stlib.core.baseParameters import BaseParameters +from stlib.core.baseEntityModifier import BaseEntityModifier +from stlib.entities import Entity +import dataclasses + + +@dataclasses.dataclass +class ModifierParameters(BaseParameters): + + name : str = "Modifier" + creator : str = None + + def modifier(entity: Entity): + pass + + +class SingleEntityModifier(BaseEntityModifier): + + def __init__(self, parameters: ModifierParameters): + BaseEntityModifier.__init__(self, parameters) + + def _apply(self, entity: Entity): + entity.add(self) + self.parameters.modifier(entity) \ No newline at end of file diff --git a/stlib/modifiers/fixing.py b/stlib/modifiers/fixing.py new file mode 100644 index 000000000..b354c66f0 --- /dev/null +++ b/stlib/modifiers/fixing.py @@ -0,0 +1,13 @@ +from stlib.modifiers import ModifierParameters +from stlib.entities import Entity +from stlib.core.baseParameters import Optional, dataclasses +from splib.core.utils import DEFAULT_VALUE + +@dataclasses.dataclass +class FixingModifierParameters(ModifierParameters): + + name : str = "FixingModifier" + indices : Optional[list[int]] = DEFAULT_VALUE + + def modifier(self, entity: Entity): + entity.material.addObject("FixedProjectiveConstraint", indices=self.indices) \ No newline at end of file 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..d6c739808 --- /dev/null +++ b/stlib/settings/simulation.py @@ -0,0 +1,31 @@ +import Sofa.Core +from splib.core.utils import REQUIRES_COLLISIONPIPELINE, REQUIRES_LAGRANGIANCONSTRAINTSOLVER + +def addSimulationSettings(rootnode: Sofa.Core.Node): + + rootnode.add('DefaultVisualManagerLoop') + 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') + + simulation = rootnode.add(Sofa.Core.Node('Simulation')) + + simulation.add('EulerImplicitSolver') + simulation.add('SparseLDLSolver') + + if rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER) and rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER).value: + simulation.add('GenericConstraintCorrection') + + return simulation + diff --git a/stlib/visual.py b/stlib/visual.py index a0f377f7d..0a6d4ee3b 100644 --- a/stlib/visual.py +++ b/stlib/visual.py @@ -12,7 +12,7 @@ class VisualParameters(BaseParameters): 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 +25,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) \ No newline at end of file