From 3eee5af4516f9e8dfd58ee4073148620fe8c58b3 Mon Sep 17 00:00:00 2001 From: Paul Baksic Date: Thu, 19 Mar 2026 16:34:53 +0100 Subject: [PATCH 01/11] Start working but need a inheritable base component binding --- examples/stlib/SofaScene.py | 4 +- stlib/__init__.py | 2 +- stlib/node_modifiers/__init__.py | 1 + stlib/node_modifiers/__node_modifier__.py | 109 ++++++++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 stlib/node_modifiers/__init__.py create mode 100644 stlib/node_modifiers/__node_modifier__.py diff --git a/examples/stlib/SofaScene.py b/examples/stlib/SofaScene.py index 4e60200c6..d567f42d8 100644 --- a/examples/stlib/SofaScene.py +++ b/examples/stlib/SofaScene.py @@ -1,5 +1,3 @@ -from fontTools.afmLib import preferredAttributeOrder - from stlib.geometries.plane import PlaneParameters from stlib.geometries.file import FileParameters from stlib.geometries.extract import ExtractParameters @@ -99,7 +97,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/stlib/__init__.py b/stlib/__init__.py index 0ad659858..50b57cb72 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["core","entities","geometries","materials","collision","visual"] +__all__ = ["core","entities","geometries","materials","collision","visual", "node_modifiers"] import Sofa.Core from stlib.core.basePrefab import BasePrefab diff --git a/stlib/node_modifiers/__init__.py b/stlib/node_modifiers/__init__.py new file mode 100644 index 000000000..2a569875d --- /dev/null +++ b/stlib/node_modifiers/__init__.py @@ -0,0 +1 @@ +from .__node_modifier__ import * \ No newline at end of file diff --git a/stlib/node_modifiers/__node_modifier__.py b/stlib/node_modifiers/__node_modifier__.py new file mode 100644 index 000000000..f85f50a03 --- /dev/null +++ b/stlib/node_modifiers/__node_modifier__.py @@ -0,0 +1,109 @@ +from stlib.core.basePrefab import BasePrefab +from Sofa.Core import Node + +import dataclasses +from splib.core.utils import DEFAULT_VALUE + +from Sofa.Core import Node +from typing import Callable, Optional, Any + +@dataclasses.dataclass +class BaseNodeModifierParameters(object): + name : str = "NodeModifier" + kwargs : dict = dataclasses.field(default_factory=dict) + + def modifier(self, *node : Node): + pass + + + +class NodeModifier(Sofa.Core.Controller): + + parameters : BaseNodeModifierParameters + + def __init__(self, parameters : BaseNodeModifierParameters): + Sofa.Core.Controller.__init__(self, **(parameters.toDict())) + self.parameters = parameters + + + +class SingleNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, node : Node, holder : Node = None): + self.parameters.modifier(node) + self.register(holder, node) + + + +class PairNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, node1 : Node, node2 : Node, holder : Node = None): + self.parameters.modifier(node1,node2) + self.register(holder, node1, node2) + + + +class MultiNodeModifier(NodeModifier): + + def __init__(self, parameters : BaseNodeModifierParameters): + super().__init__(parameters) + + def modify(self, *nodes : Node , holder : Node = None): + self.parameters.modifier(*nodes) + self.register(holder, *nodes) + + + + +@dataclasses.dataclass +class FixConstraintParameters(SingleNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + boxROI : list[ list[ float ] ] = dataclasses.field(default_factory= [[0,0,0], [1,1,1]]) + + def modifier(self, node : Node): + splib.mechanics.fix_points.addFixation(node.Material, type = self.constraintType, boxROIs = self.boxROI) + pass + + + +@dataclasses.dataclass +class AttachmentConstraintParameters(PairNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + + def modifier(self, node1 : Node, node2 : Node): + # splib.mechanics.fix_points.addFixation(node, type = self.constraintType, boxROI = self.boxROI) + pass + + + +@dataclasses.dataclass +class SimulationSolversParameters(SingleNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + boxROI : list[ list[ float ] ] = DEFAULT_VALUE + + def modifier(self, node1 : Node): + splib.simulation.ode_solvers.addImplicitODE(node1) + splib.simulation.linear_solvers.addLinearSolver(node1, constantSparsity=False, ) + pass + + + +@dataclasses.dataclass +class SimulationSettingsParameters(SingleNodeModifierParameters): + useLagrangian : bool = False + + def modifier(self, node1 : Node): + if(self.useLagrangian): + splib.simulation.headers.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) + else: + splib.simulation.headers.setupDefaultHeader(root, displayFlags = "showVisualModels", + backgroundColor=[0.8, 0.8, 0.8, 1], + parallelComputing = True) \ No newline at end of file From 13fe54decc979509e2c8e31f4d2c55a33a737b52 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Thu, 23 Apr 2026 22:17:56 +0200 Subject: [PATCH 02/11] Node modifiers coming out. Still need to register the modified nodes inside the object dynamically (see original proposition pr) and simply make the introduced example work --- .../Sofa/Core/Binding_BaseComponent.h | 2 +- examples/stlib/node_modifier.py | 104 +++++++++++++++++ splib/Testing.py | 2 +- splib/mechanics/__init__.py | 2 +- .../{fix_points.py => attachment.py} | 18 +++ splib/simulation/headers.py | 31 ++--- stlib/__init__.py | 16 ++- stlib/node_modifiers/__node_modifier__.py | 110 ++++-------------- stlib/node_modifiers/attachments.py | 41 +++++++ stlib/node_modifiers/footers.py | 98 ++++++++++++++++ 10 files changed, 317 insertions(+), 107 deletions(-) create mode 100644 examples/stlib/node_modifier.py rename splib/mechanics/{fix_points.py => attachment.py} (58%) create mode 100644 stlib/node_modifiers/attachments.py create mode 100644 stlib/node_modifiers/footers.py diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseComponent.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseComponent.h index 9a2a3627f..b63d1c772 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseComponent.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseComponent.h @@ -1,4 +1,4 @@ -/****************************************************************************** + /****************************************************************************** * SOFA, Simulation Open-Framework Architecture * * (c) 2021 INRIA, USTL, UJF, CNRS, MGH * * * diff --git a/examples/stlib/node_modifier.py b/examples/stlib/node_modifier.py new file mode 100644 index 000000000..f558bd36a --- /dev/null +++ b/examples/stlib/node_modifier.py @@ -0,0 +1,104 @@ +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 + + +from stlib.node_modifiers import NodeModifier +from stlib.node_modifiers.footers import SimulationSolversParameters, SimulationSettingsParameters +from stlib.node_modifiers.attachments import FixConstraintParameters, AttachmentConstraintParameters + +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") + + + ### Logo + LogoNode = root.addChild("LogoNode") + + LogoNode.add(NodeModifier, on = [LogoNode], parameters = SimulationSolversParameters(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]) + + + + 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, -2.0]) + + diff --git a/splib/Testing.py b/splib/Testing.py index 146b4096b..6ffc38892 100644 --- a/splib/Testing.py +++ b/splib/Testing.py @@ -4,7 +4,7 @@ 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.mechanics.attachment import * from splib.topology.loader import * from splib.core.node_wrapper import * diff --git a/splib/mechanics/__init__.py b/splib/mechanics/__init__.py index 3761021a2..5e84c5580 100644 --- a/splib/mechanics/__init__.py +++ b/splib/mechanics/__init__.py @@ -1 +1 @@ -__all__ = ["linear_elasticity","hyperelasticity","fix_points","collision_model","mass"] \ No newline at end of file +__all__ = ["linear_elasticity","hyperelasticity","attachment","collision_model","mass"] \ No newline at end of file diff --git a/splib/mechanics/fix_points.py b/splib/mechanics/attachment.py similarity index 58% rename from splib/mechanics/fix_points.py rename to splib/mechanics/attachment.py index ddbbd6308..2e17341fb 100644 --- a/splib/mechanics/fix_points.py +++ b/splib/mechanics/attachment.py @@ -28,3 +28,21 @@ def addFixation(node,type:ConstraintType,boxROIs=DEFAULT_VALUE, sphereROIs=DEFAU case _: print('Contraint type is either ConstraintType.PROJECTIVE, ConstraintType.WEAK or ConstraintType.LAGRANGIAN') return + + + +@ReusableMethod +def attachObjects(node, type:ConstraintType, object1, object2, indices1, indices2, stiffness=DEFAULT_VALUE, damping=DEFAULT_VALUE, **kwargs): + match type: + case ConstraintType.WEAK: + node.addObject("SpringForceField", name="attachment", object1=object1, object2=object2, indices1=indices1, indices2=indices2, stiffness=stiffness, damping=damping, lengths="0", **kwargs) + return + case ConstraintType.PROJECTIVE: + node.addObject("AttachProjectiveConstraint", name="attachment", object1=object1, object2=object2, indices1=indices1, indices2=indices2, **kwargs) + return + case ConstraintType.LAGRANGIAN: + node.addObject("BilateralLagrangianConstraint", name="attachment", object1=object1, object2=object2, first_point=indices1, second_point=indices2, **kwargs) + return + case _: + print('Contraint type is either ConstraintType.PROJECTIVE, ConstraintType.WEAK or ConstraintType.LAGRANGIAN') + return diff --git a/splib/simulation/headers.py b/splib/simulation/headers.py index d3c61a21b..3923aec85 100644 --- a/splib/simulation/headers.py +++ b/splib/simulation/headers.py @@ -67,16 +67,16 @@ def setupPenalityCollisionHeader(node, displayFlags = "showVisualModels",backgr node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase", **kwargs) if(stick): - node.addObject('CollisionResponse',name="ContactManager", response="BarycentricStickContact",**kwargs) + node.addObject('CollisionResponse',name="ContactManager", response="StickContactForceField",**kwargs) else: - node.addObject('CollisionResponse',name="ContactManager", response="BarycentricPenalityContact",**kwargs) + node.addObject('CollisionResponse',name="ContactManager", response="PenalityContactForceField",**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): +def setupLagrangianCollision(node, enableCollision = True, 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) @@ -107,21 +107,24 @@ def setupLagrangianCollision(node, displayFlags = "showVisualModels",background if(parallelComputing): parallelPrefix="Parallel" - node.addObject('CollisionPipeline', name="collisionPipeline", - **kwargs) + if enableCollision: + node.addObject('CollisionPipeline', name="collisionPipeline", + **kwargs) - node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase", - **kwargs) + node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase", + **kwargs) - node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase", - **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) - 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('BlockGaussSeidelConstraintSolver',name="ConstraintSolver", tolerance=tolerance, maxIterations=maxIterations, multithreading=parallelComputing,**kwargs) node.addObject("ConstraintAttachButtonSetting") diff --git a/stlib/__init__.py b/stlib/__init__.py index 50b57cb72..498c29262 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -2,6 +2,7 @@ import Sofa.Core from stlib.core.basePrefab import BasePrefab +from stlib.node_modifiers import NodeModifier def __genericAdd(self : Sofa.Core.Node, typeName, **kwargs): def findName(cname, names): @@ -25,9 +26,13 @@ def checkName(context : Sofa.Core.Node, name): # 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): - raise RuntimeError("Invalid argument, a prefab takes only the \"parameters\" kwargs as input") + if isinstance(typeName, type) : + if 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 issubclass(typeName, NodeModifier): + if len(params.keys()) > 2 or (len(params.keys()) == 2 and (("parameters" not in params) or ("on" not in params ))): + raise RuntimeError("Invalid argument, a node modifier takes two parameters: the node modifier parameters structure as \"parameters\" kwarg and the \"on\" kwarg as a list of nodes on which to apply the modifier") elif "name" not in params : #This doesn't apply to prefab if isinstance(typeName, str): @@ -43,7 +48,7 @@ def checkName(context : Sofa.Core.Node, name): else: raise RuntimeError("Invalid argument ", typeName) - if isinstance(typeName, type) and issubclass(typeName, BasePrefab) and len(params.keys()) == 1: + if isinstance(typeName, type) and ((issubclass(typeName, BasePrefab) and len(params.keys()) == 1) or (issubclass(typeName, NodeModifier) and len(params.keys()) == 2)): params["parameters"].name = checkName(self, params["parameters"].name) else: params["name"] = checkName(self, params["name"]) @@ -52,6 +57,9 @@ def checkName(context : Sofa.Core.Node, name): if isinstance(typeName, type) and issubclass(typeName, BasePrefab): pref = self.addChild(typeName(**params)) pref.init() + elif isinstance(typeName, type) and issubclass(typeName, NodeModifier): + pref = self.addObject(typeName(params["parameters"])) + pref.apply(self, params["on"]) elif isinstance(typeName, Sofa.Core.Node): pref = self.addChild(typeName(**params)) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): diff --git a/stlib/node_modifiers/__node_modifier__.py b/stlib/node_modifiers/__node_modifier__.py index f85f50a03..8634b3d9c 100644 --- a/stlib/node_modifiers/__node_modifier__.py +++ b/stlib/node_modifiers/__node_modifier__.py @@ -1,109 +1,47 @@ -from stlib.core.basePrefab import BasePrefab +from numpy.f2py.auxfuncs import throw_error + +import Sofa from Sofa.Core import Node +from typing import final +from warnings import warn import dataclasses -from splib.core.utils import DEFAULT_VALUE -from Sofa.Core import Node -from typing import Callable, Optional, Any @dataclasses.dataclass class BaseNodeModifierParameters(object): name : str = "NodeModifier" kwargs : dict = dataclasses.field(default_factory=dict) - def modifier(self, *node : Node): - pass - - - -class NodeModifier(Sofa.Core.Controller): - - parameters : BaseNodeModifierParameters - - def __init__(self, parameters : BaseNodeModifierParameters): - Sofa.Core.Controller.__init__(self, **(parameters.toDict())) - self.parameters = parameters - - - -class SingleNodeModifier(NodeModifier): - - def __init__(self, parameters : BaseNodeModifierParameters): - super().__init__(parameters) - - def modify(self, node : Node, holder : Node = None): - self.parameters.modifier(node) - self.register(holder, node) - - - -class PairNodeModifier(NodeModifier): - - def __init__(self, parameters : BaseNodeModifierParameters): - super().__init__(parameters) - - def modify(self, node1 : Node, node2 : Node, holder : Node = None): - self.parameters.modifier(node1,node2) - self.register(holder, node1, node2) - - - -class MultiNodeModifier(NodeModifier): - - def __init__(self, parameters : BaseNodeModifierParameters): - super().__init__(parameters) - - def modify(self, *nodes : Node , holder : Node = None): - self.parameters.modifier(*nodes) - self.register(holder, *nodes) - + _numberOfAffectedNodes : int = -1 - - -@dataclasses.dataclass -class FixConstraintParameters(SingleNodeModifierParameters): - constraintType : ConstraintType = ConstraintType.PROJECTIVE - boxROI : list[ list[ float ] ] = dataclasses.field(default_factory= [[0,0,0], [1,1,1]]) - - def modifier(self, node : Node): - splib.mechanics.fix_points.addFixation(node.Material, type = self.constraintType, boxROIs = self.boxROI) + def doModify(self, owner, node : list[Node]): pass + @final + def _modifier(self, owner, node : list[Node]): + if self._numberOfAffectedNodes == -1: + warn(f"Parameter type {type(self)} doesn't set the attribute __numberOfAffectedNodes. No check on the number of nodes given as input will be made.") + elif len(node) != self._numberOfAffectedNodes: + raise AttributeError(f"This modifier required {self._numberOfAffectedNodes} nodes to be applied.") + return self.doModify(owner, node) + def toDict(self): + return dataclasses.asdict(self) -@dataclasses.dataclass -class AttachmentConstraintParameters(PairNodeModifierParameters): - constraintType : ConstraintType = ConstraintType.PROJECTIVE - def modifier(self, node1 : Node, node2 : Node): - # splib.mechanics.fix_points.addFixation(node, type = self.constraintType, boxROI = self.boxROI) - pass -@dataclasses.dataclass -class SimulationSolversParameters(SingleNodeModifierParameters): - constraintType : ConstraintType = ConstraintType.PROJECTIVE - boxROI : list[ list[ float ] ] = DEFAULT_VALUE +#TODO use Component when available +class NodeModifier(Sofa.Core.Controller): - def modifier(self, node1 : Node): - splib.simulation.ode_solvers.addImplicitODE(node1) - splib.simulation.linear_solvers.addLinearSolver(node1, constantSparsity=False, ) - pass + parameters : BaseNodeModifierParameters + def __init__(self, parameters : BaseNodeModifierParameters): + Sofa.Core.Controller.__init__(self, **(parameters.toDict())) + self.parameters = parameters + def apply(self, owner, nodes : list[Node] ): + self.parameters._modifier(owner, nodes) -@dataclasses.dataclass -class SimulationSettingsParameters(SingleNodeModifierParameters): - useLagrangian : bool = False - - def modifier(self, node1 : Node): - if(self.useLagrangian): - splib.simulation.headers.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) - else: - splib.simulation.headers.setupDefaultHeader(root, displayFlags = "showVisualModels", - backgroundColor=[0.8, 0.8, 0.8, 1], - parallelComputing = True) \ No newline at end of file diff --git a/stlib/node_modifiers/attachments.py b/stlib/node_modifiers/attachments.py new file mode 100644 index 000000000..e88eb24d2 --- /dev/null +++ b/stlib/node_modifiers/attachments.py @@ -0,0 +1,41 @@ +import dataclasses +import splib +from stlib.node_modifiers import BaseNodeModifierParameters +from splib.core.enum_types import ConstraintType +from splib.core.utils import DEFAULT_VALUE + +from typing import override +from Sofa.Core import Node + +@dataclasses.dataclass +class FixConstraintParameters(BaseNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.PROJECTIVE + boxROIs : list[ list[ float ] ] = DEFAULT_VALUE + sphereROIs : list[ list[ float ] ] = DEFAULT_VALUE + indices : list[ int ] = DEFAULT_VALUE + fixAll : bool = DEFAULT_VALUE + + _numberOfAffectedNodes = 1 + + @override + def doModify(self, owner, node : list[Node]): + splib.mechanics.attachment.addFixation(node.Material, type = self.constraintType, boxROIs = self.boxROIs, sphereROIs=self.sphereROIs, indices=self.indices, fixAll=self.fixAll) + pass + + + +@dataclasses.dataclass +class AttachmentConstraintParameters(BaseNodeModifierParameters): + constraintType : ConstraintType = ConstraintType.WEAK + indices1 : list[int] = dataclasses.field(default_factory= [0]) + indices2 : list[int] = dataclasses.field(default_factory= [0]) + stiffness : float = DEFAULT_VALUE + damping : float = DEFAULT_VALUE + _numberOfAffectedNodes = 2 + + @override + def doModify(self, owner, node : list[Node]): + splib.mechanics.attachment.attachObjects(owner, type = self.constraintType, object1=node[0], object2=node[1], indices1=self.indices1, indices2=self.indices2, stiffness=self.stiffness, damping=self.damping, **self.kwargs) + pass + + diff --git a/stlib/node_modifiers/footers.py b/stlib/node_modifiers/footers.py new file mode 100644 index 000000000..768d2bc97 --- /dev/null +++ b/stlib/node_modifiers/footers.py @@ -0,0 +1,98 @@ +import dataclasses +from stlib.node_modifiers import BaseNodeModifierParameters +from splib.core.enum_types import ConstraintType +from splib.core.utils import DEFAULT_VALUE +from Sofa.Core import Node +from typing import override + +import splib + + +@dataclasses.dataclass +class SimulationSolversParameters(BaseNodeModifierParameters): + staticODE : bool = False + iterative : bool = False + iterations : int = DEFAULT_VALUE + tolerance : float = DEFAULT_VALUE + threshold : float = DEFAULT_VALUE + template : str = DEFAULT_VALUE + constantSparsity : bool = False + parallelInverseProduct : bool = True + + _numberOfAffectedNodes = 1 + + @override + def doModify(self, owner, node : list[Node]): + splib.simulation.ode_solvers.addImplicitODE(node[0], static=self.staticODE, **self.kwargs) + splib.simulation.linear_solvers.addLinearSolver(node[0], iterative=self.iterative, + iterations=self.iterations, + tolerance=self.tolerance, + threshold=self.threshold, + template=self.template, + constantSparsity=self.constantSparsity, + parallelInverseProduct= self.parallelInverseProduct, + **self.kwargs) + + + +@dataclasses.dataclass +class SimulationSettingsParameters(BaseNodeModifierParameters): + enableCollisionDetection : bool = False + useLagrangian : bool = False + displayFlags : str = "showVisualModels" + backgroundColor : list[float] = dataclasses.field(default_factory= [0.8,0.8,0.8,1]) + + #Collision detection specific + parallelComputing : bool = True + alarmDistance : float = DEFAULT_VALUE + contactDistance : float = DEFAULT_VALUE + stick : bool = False + + #Lagrangian specific + frictionCoef : float = DEFAULT_VALUE + tolerance : float = DEFAULT_VALUE + maxIterations : int = DEFAULT_VALUE + + _numberOfAffectedNodes = 1 + + @staticmethod + def _addConstraintCorrectionToMechanicalNodes(node, constraintCorrectionType : str = "LinearSolverConstraintCorrection"): + #Might be weak if we only have mapped mass or forcefield. It might be better to rethink this + for child in node.children(): + if child.getMass is not None or child.getForceField(0) is not None: + child.addObject(constraintCorrectionType) + else: + SimulationSettingsParameters._addConstraintCorrectionToMechanicalNodes(child, constraintCorrectionType) + + def modifier(self, owner, node : list[Node]): + if(self.useLagrangian): + splib.simulation.headers.setupLagrangianCollision(node[0], + enableCollision=self.enableCollisionDetection, + displayFlags = self.displayFlags, + backgroundColor = self.backgroundColor, + parallelComputing = self.parallelComputing, + alarmDistance=self.alarmDistance, + contactDistance=self.contactDistance, + frictionCoef=self.frictionCoef, + tolerance=self.tolerance, + maxIterations=self.maxIterations, + stick = self.stick, + **self.kwargs) + SimulationSettingsParameters._addConstraintCorrectionToMechanicalNodes(node[0], "LinearSolverConstraintCorrection") + + + elif self.enableCollisionDetection: + splib.simulation.headers.setupPenalityCollisionHeader(node[0], + displayFlags = self.displayFlags, + backgroundColor = self.backgroundColor, + parallelComputing = self.parallelComputing, + alarmDistance=self.alarmDistance, + contactDistance=self.contactDistance, + stick = self.stick, + **self.kwargs) + else: + splib.simulation.headers.setupDefaultHeader(node[0], + displayFlags = self.displayFlags, + backgroundColor = self.backgroundColor, + parallelComputing = self.parallelComputing, + **self.kwargs) \ No newline at end of file From 58a5e2e9ae21df9cd3d5834eaa99655903d65504 Mon Sep 17 00:00:00 2001 From: Paul Baksic Date: Fri, 24 Apr 2026 16:00:23 +0200 Subject: [PATCH 03/11] Modifier design close to DONE. --- .../SofaPython3/Sofa/Core/Binding_Node.cpp | 12 +++ .../SofaPython3/Sofa/Core/Binding_Node_doc.h | 7 ++ examples/stlib/node_modifier.py | 84 +++++++++---------- examples/stlib/node_modifier.py.view | 17 ++++ stlib/__init__.py | 7 +- stlib/node_modifiers/__node_modifier__.py | 67 ++++++++++++--- stlib/node_modifiers/attachments.py | 17 ++-- stlib/node_modifiers/footers.py | 56 +++++++++---- 8 files changed, 180 insertions(+), 87 deletions(-) create mode 100644 examples/stlib/node_modifier.py.view diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp index 85d0de2bf..3d0ec8e3a 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp @@ -619,6 +619,17 @@ py::object getForceField(Node *self, unsigned int index) } + +py::object getLinearSolver(Node *self, unsigned int index) +{ + auto* ls = self->linearSolver.get(index); + if (ls) { + return PythonFactory::toPython(sofa::core::castToBase(ls)); + } + return py::none(); +} + + py::object getMechanicalState(Node *self) { sofa::core::behavior::BaseMechanicalState* state = self->mechanicalState.get(); @@ -721,6 +732,7 @@ void moduleAddNode(py::module &m) { p.def("getMass", &getMass, sofapython3::doc::sofa::core::Node::getMass); p.def("hasODESolver", &hasODESolver, sofapython3::doc::sofa::core::Node::hasODESolver); p.def("getForceField", &getForceField, sofapython3::doc::sofa::core::Node::getForceField); + p.def("getLinearSolver", &getLinearSolver, sofapython3::doc::sofa::core::Node::getLinearSolver); p.def("getMechanicalState", &getMechanicalState, sofapython3::doc::sofa::core::Node::getMechanicalState); p.def("getMechanicalMapping", &getMechanicalMapping, sofapython3::doc::sofa::core::Node::getMechanicalMapping); p.def("sendEvent", &sendEvent, sofapython3::doc::sofa::core::Node::sendEvent); 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 56ca7411a..ec4fdc45b 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h @@ -373,6 +373,13 @@ static auto getForceField = :type index: unsigned int. )"; +static auto getLinearSolver = + R"( + Get the linear solver of a node, given an index. + :param index: index of the linear solver + :type index: unsigned int. + )"; + static auto getMechanicalState = R"( Get the mechanical state of the node. diff --git a/examples/stlib/node_modifier.py b/examples/stlib/node_modifier.py index f558bd36a..5f32e05b9 100644 --- a/examples/stlib/node_modifier.py +++ b/examples/stlib/node_modifier.py @@ -19,12 +19,8 @@ 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 @@ -45,44 +41,10 @@ def createScene(root): 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") - - ### Logo - LogoNode = root.addChild("LogoNode") - - LogoNode.add(NodeModifier, on = [LogoNode], parameters = SimulationSolversParameters(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] + ModelsNode = root.addChild("ModelsNode") - Logo = LogoNode.add(Entity, parameters = LogoParams) - - Logo.material.addObject("ConstantForceField", name="ConstantForceUpwards", totalForce=[0, 0, -5.0]) - - - - LogoParams = EntityParameters(name = "Logo", + LogoParams = EntityParameters(name = "Logo1", geometry = FileParameters(filename="mesh/SofaScene/Logo.vtk"), material = DeformableBehaviorParameters(), collision = CollisionParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoColli.sph")), @@ -97,8 +59,38 @@ def createScene(root): 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, -2.0]) - + Logo = ModelsNode.add(Entity, parameters = LogoParams) + + + + ### S + 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] + 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 = ModelsNode.add(Entity, parameters = SParams) + + #TODO make the name automatically match the modifier type if none is given + #TODO why this doesn't add itself to the modelsnode "modified by" data ??? + root.add(NodeModifier, on = [ModelsNode], parameters = SimulationSolversParameters(name = "SimulationSolvers", + constantSparsity=False)) + + root.add(NodeModifier, on = [root], parameters = SimulationSettingsParameters(name = "SimulationSettings", + enableCollisionDetection = True, + useLagrangian = True, + parallelComputing = True, + alarmDistance=0.3, contactDistance=0.02, + frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20)) diff --git a/examples/stlib/node_modifier.py.view b/examples/stlib/node_modifier.py.view new file mode 100644 index 000000000..0c583fcab --- /dev/null +++ b/examples/stlib/node_modifier.py.view @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/stlib/__init__.py b/stlib/__init__.py index 498c29262..e0af3b84d 100644 --- a/stlib/__init__.py +++ b/stlib/__init__.py @@ -58,8 +58,11 @@ def checkName(context : Sofa.Core.Node, name): pref = self.addChild(typeName(**params)) pref.init() elif isinstance(typeName, type) and issubclass(typeName, NodeModifier): - pref = self.addObject(typeName(params["parameters"])) - pref.apply(self, params["on"]) + if "Modifiers" not in self.children: + self.addChild("Modifiers") + pref = self.Modifiers.addObject(typeName(params["parameters"])) + touchedNodes = pref.apply(self, params["on"]) + pref.register(self, touchedNodes) elif isinstance(typeName, Sofa.Core.Node): pref = self.addChild(typeName(**params)) elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): diff --git a/stlib/node_modifiers/__node_modifier__.py b/stlib/node_modifiers/__node_modifier__.py index 8634b3d9c..12c48c427 100644 --- a/stlib/node_modifiers/__node_modifier__.py +++ b/stlib/node_modifiers/__node_modifier__.py @@ -7,24 +7,32 @@ from warnings import warn import dataclasses +from functools import wraps + +def AffectedNodes(nbOfNodes): + def _AffectedNodes(method): + @wraps(method) + def wrapper(*args, **kwargs): + + if(len(args) < 3): + raise ValueError("No list of node was given as argument to the node modifier") + + if isinstance(args[2], list) and (len(args[2]) != nbOfNodes): + raise ValueError(f"Number of node to modify mismatch : {nbOfNodes} where expected, only {len(args[1])} where given.\n" + "The affected nodes are passed through the call to add when adding the modifier to the graph as the keywork argument \"on\" and must be in a form o list.") + return method(*args, **kwargs) + return wrapper + return _AffectedNodes @dataclasses.dataclass class BaseNodeModifierParameters(object): name : str = "NodeModifier" kwargs : dict = dataclasses.field(default_factory=dict) - _numberOfAffectedNodes : int = -1 - - def doModify(self, owner, node : list[Node]): - pass + @AffectedNodes(0) + def modify(self, owner, node : list[Node]) -> list[Node] : + return [] - @final - def _modifier(self, owner, node : list[Node]): - if self._numberOfAffectedNodes == -1: - warn(f"Parameter type {type(self)} doesn't set the attribute __numberOfAffectedNodes. No check on the number of nodes given as input will be made.") - elif len(node) != self._numberOfAffectedNodes: - raise AttributeError(f"This modifier required {self._numberOfAffectedNodes} nodes to be applied.") - return self.doModify(owner, node) def toDict(self): return dataclasses.asdict(self) @@ -42,6 +50,39 @@ def __init__(self, parameters : BaseNodeModifierParameters): Sofa.Core.Controller.__init__(self, **(parameters.toDict())) self.parameters = parameters - def apply(self, owner, nodes : list[Node] ): - self.parameters._modifier(owner, nodes) + def register( self, owner, nodes : list[Node]) : + if len(nodes) == 0: + raise ValueError(f"No node seems to have been modified by the node modifier {self.parameters.name}. Make sure the \"modify\" method of the used parameter type returns the list of nodes modified by this modifier.") + + #Remove last / if exists (this is only the case for root + holderPath = str(owner.getLinkPath()) + if holderPath[-1] == "/": + holderPath = holderPath[:-1] + + #Find Relative paths to nodes on which it is applied + add myself to data + for node in nodes: + targetPath = str(node.getLinkPath()) + relPath = targetPath.removeprefix(holderPath) + relPath = "@" + relPath.count('/')*"../" + "Modifiers/" + self.name.value + + + if node.getData("modifiedBy") is None: + node.addData("modifiedBy", type="vector",value=[], default=[], help="Modifiers that modified this Node", group="STLIB") + + node.modifiedBy = node.modifiedBy.value + [relPath] + + #Add targets to self data + for node in nodes: + targetPath = str(node.getLinkPath()) + relPath = targetPath.removeprefix(holderPath) + relPath = "@.." + relPath + + if self.getData("modified") is None: + self.addData("modified", type="vector",value=[], default=[], help="Nodes modified by this modifier", group="STLIB") + + self.modified = self.modified.value + [relPath] + + + def apply(self, owner, nodes : list[Node] ) -> list[Node]: + return self.parameters.modify(owner, nodes) diff --git a/stlib/node_modifiers/attachments.py b/stlib/node_modifiers/attachments.py index e88eb24d2..bc6d0f425 100644 --- a/stlib/node_modifiers/attachments.py +++ b/stlib/node_modifiers/attachments.py @@ -1,6 +1,6 @@ import dataclasses import splib -from stlib.node_modifiers import BaseNodeModifierParameters +from stlib.node_modifiers import BaseNodeModifierParameters, AffectedNodes from splib.core.enum_types import ConstraintType from splib.core.utils import DEFAULT_VALUE @@ -15,12 +15,11 @@ class FixConstraintParameters(BaseNodeModifierParameters): indices : list[ int ] = DEFAULT_VALUE fixAll : bool = DEFAULT_VALUE - _numberOfAffectedNodes = 1 - @override - def doModify(self, owner, node : list[Node]): - splib.mechanics.attachment.addFixation(node.Material, type = self.constraintType, boxROIs = self.boxROIs, sphereROIs=self.sphereROIs, indices=self.indices, fixAll=self.fixAll) - pass + @AffectedNodes(1) + def modify(self, owner, nodes : list[Node]) -> list[Node]: + splib.mechanics.attachment.addFixation(nodes[0].Material, type = self.constraintType, boxROIs = self.boxROIs, sphereROIs=self.sphereROIs, indices=self.indices, fixAll=self.fixAll) + return nodes @@ -31,11 +30,11 @@ class AttachmentConstraintParameters(BaseNodeModifierParameters): indices2 : list[int] = dataclasses.field(default_factory= [0]) stiffness : float = DEFAULT_VALUE damping : float = DEFAULT_VALUE - _numberOfAffectedNodes = 2 @override - def doModify(self, owner, node : list[Node]): + @AffectedNodes(2) + def modify(self, owner, node : list[Node]) -> list[Node]: splib.mechanics.attachment.attachObjects(owner, type = self.constraintType, object1=node[0], object2=node[1], indices1=self.indices1, indices2=self.indices2, stiffness=self.stiffness, damping=self.damping, **self.kwargs) - pass + return node + [owner] diff --git a/stlib/node_modifiers/footers.py b/stlib/node_modifiers/footers.py index 768d2bc97..ce7a13871 100644 --- a/stlib/node_modifiers/footers.py +++ b/stlib/node_modifiers/footers.py @@ -1,5 +1,5 @@ import dataclasses -from stlib.node_modifiers import BaseNodeModifierParameters +from stlib.node_modifiers import BaseNodeModifierParameters, AffectedNodes from splib.core.enum_types import ConstraintType from splib.core.utils import DEFAULT_VALUE from Sofa.Core import Node @@ -19,10 +19,9 @@ class SimulationSolversParameters(BaseNodeModifierParameters): constantSparsity : bool = False parallelInverseProduct : bool = True - _numberOfAffectedNodes = 1 - @override - def doModify(self, owner, node : list[Node]): + @AffectedNodes(1) + def modify(self, owner, node : list[Node]) -> list[Node]: splib.simulation.ode_solvers.addImplicitODE(node[0], static=self.staticODE, **self.kwargs) splib.simulation.linear_solvers.addLinearSolver(node[0], iterative=self.iterative, iterations=self.iterations, @@ -32,6 +31,7 @@ def doModify(self, owner, node : list[Node]): constantSparsity=self.constantSparsity, parallelInverseProduct= self.parallelInverseProduct, **self.kwargs) + return [owner] @@ -39,8 +39,8 @@ def doModify(self, owner, node : list[Node]): class SimulationSettingsParameters(BaseNodeModifierParameters): enableCollisionDetection : bool = False useLagrangian : bool = False - displayFlags : str = "showVisualModels" - backgroundColor : list[float] = dataclasses.field(default_factory= [0.8,0.8,0.8,1]) + displayFlags : list[str] = dataclasses.field(default_factory=lambda: ["showVisualModels"]) + backgroundColor : list[float] = dataclasses.field(default_factory=lambda: [0.8,0.8,0.8,1]) #Collision detection specific parallelComputing : bool = True @@ -53,18 +53,29 @@ class SimulationSettingsParameters(BaseNodeModifierParameters): tolerance : float = DEFAULT_VALUE maxIterations : int = DEFAULT_VALUE - _numberOfAffectedNodes = 1 - @staticmethod - def _addConstraintCorrectionToMechanicalNodes(node, constraintCorrectionType : str = "LinearSolverConstraintCorrection"): - #Might be weak if we only have mapped mass or forcefield. It might be better to rethink this - for child in node.children(): - if child.getMass is not None or child.getForceField(0) is not None: - child.addObject(constraintCorrectionType) + def _addConstraintCorrectionToMechanicalNodes(node, constraintCorrectionType : str = "LinearSolverConstraintCorrection", linearSolverPath=None): + #TODO This only adds LinearSolvers to nodes that have mass of forcefield. Might be weak if we have mapped mass or forcefield. It might be better to rethink this + modified = [] + for child in node.children: + nodeLinearSolver = child.getLinearSolver(0) + if nodeLinearSolver is not None : + linearSolverPath = nodeLinearSolver.getLinkPath() + + if (child.getMass() is not None) or (child.getForceField(0) is not None): + print(f"Adding constraint corr to node {child.getLinkPath()}") + print(f"{child.getMass} {child.getForceField(0) }") + if(linearSolverPath is None): + raise ValueError(f"No linear solver found before {child.getLinkPath()}. Please add one to fix this.") + child.addObject(constraintCorrectionType, linearSolver=linearSolverPath) + modified += [child] else: - SimulationSettingsParameters._addConstraintCorrectionToMechanicalNodes(child, constraintCorrectionType) + modified += SimulationSettingsParameters._addConstraintCorrectionToMechanicalNodes(child, constraintCorrectionType, linearSolverPath) + return modified - def modifier(self, owner, node : list[Node]): + @override + @AffectedNodes(1) + def modify(self, owner, node : list[Node]) -> list[Node]: if(self.useLagrangian): splib.simulation.headers.setupLagrangianCollision(node[0], enableCollision=self.enableCollisionDetection, @@ -78,9 +89,17 @@ def modifier(self, owner, node : list[Node]): maxIterations=self.maxIterations, stick = self.stick, **self.kwargs) - SimulationSettingsParameters._addConstraintCorrectionToMechanicalNodes(node[0], "LinearSolverConstraintCorrection") + nodeLinearSolver = node[0].getLinearSolver(0) + if nodeLinearSolver is not None : + linearSolverPath = nodeLinearSolver.getLinkPath() + else: + linearSolverPath = None + + touchedNodes = SimulationSettingsParameters._addConstraintCorrectionToMechanicalNodes(node[0], "LinearSolverConstraintCorrection", linearSolverPath = linearSolverPath) + return [owner] + touchedNodes + elif self.enableCollisionDetection: splib.simulation.headers.setupPenalityCollisionHeader(node[0], displayFlags = self.displayFlags, @@ -90,9 +109,12 @@ def modifier(self, owner, node : list[Node]): contactDistance=self.contactDistance, stick = self.stick, **self.kwargs) + return [owner] + else: splib.simulation.headers.setupDefaultHeader(node[0], displayFlags = self.displayFlags, backgroundColor = self.backgroundColor, parallelComputing = self.parallelComputing, - **self.kwargs) \ No newline at end of file + **self.kwargs) + return [owner] \ No newline at end of file From 48f2e2d2c6965bea8348b153b6e524fdeb57069d Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 19 May 2026 11:22:55 +0200 Subject: [PATCH 04/11] Add attachment --- examples/stlib/node_modifier.py | 5 ++++- splib/mechanics/attachment.py | 4 ++-- stlib/node_modifiers/attachments.py | 7 ++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/stlib/node_modifier.py b/examples/stlib/node_modifier.py index 5f32e05b9..53c7a1896 100644 --- a/examples/stlib/node_modifier.py +++ b/examples/stlib/node_modifier.py @@ -64,7 +64,7 @@ def createScene(root): ### S - SParams = EntityParameters("bob.yaml") + SParams = EntityParameters() SParams.name = "S" SParams.geometry = FileParameters(filename="mesh/SofaScene/S.vtk") SParams.geometry.elementType = ElementType.TETRAHEDRA @@ -94,3 +94,6 @@ def createScene(root): alarmDistance=0.3, contactDistance=0.02, frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20)) + Logo.add(NodeModifier, on = [Logo], parameters = FixConstraintParameters(name = "FixConstraintParameters", boxROIs=[[-1, -2, -13, 3, 2, -7]])) + # ModelsNode.add(AttachmentConstraintParameters, on = [S, Logo], parameters = AttachmentConstraintParameters(name = "AttachmentConstraintParameters", indices1=[], indices2=[], stiffness=10, length=0.2)) + diff --git a/splib/mechanics/attachment.py b/splib/mechanics/attachment.py index 2e17341fb..57eda03eb 100644 --- a/splib/mechanics/attachment.py +++ b/splib/mechanics/attachment.py @@ -32,10 +32,10 @@ def addFixation(node,type:ConstraintType,boxROIs=DEFAULT_VALUE, sphereROIs=DEFAU @ReusableMethod -def attachObjects(node, type:ConstraintType, object1, object2, indices1, indices2, stiffness=DEFAULT_VALUE, damping=DEFAULT_VALUE, **kwargs): +def attachObjects(node, type:ConstraintType, object1, object2, indices1, indices2, stiffness=DEFAULT_VALUE, damping=DEFAULT_VALUE, lengths=DEFAULT_VALUE, **kwargs): match type: case ConstraintType.WEAK: - node.addObject("SpringForceField", name="attachment", object1=object1, object2=object2, indices1=indices1, indices2=indices2, stiffness=stiffness, damping=damping, lengths="0", **kwargs) + node.addObject("SpringForceField", name="attachment", object1=object1, object2=object2, indices1=indices1, indices2=indices2, stiffness=stiffness, damping=damping, lengths=lengths, **kwargs) return case ConstraintType.PROJECTIVE: node.addObject("AttachProjectiveConstraint", name="attachment", object1=object1, object2=object2, indices1=indices1, indices2=indices2, **kwargs) diff --git a/stlib/node_modifiers/attachments.py b/stlib/node_modifiers/attachments.py index bc6d0f425..43cbedb3c 100644 --- a/stlib/node_modifiers/attachments.py +++ b/stlib/node_modifiers/attachments.py @@ -1,5 +1,5 @@ import dataclasses -import splib +import splib.mechanics.attachment as splib_att from stlib.node_modifiers import BaseNodeModifierParameters, AffectedNodes from splib.core.enum_types import ConstraintType from splib.core.utils import DEFAULT_VALUE @@ -18,7 +18,7 @@ class FixConstraintParameters(BaseNodeModifierParameters): @override @AffectedNodes(1) def modify(self, owner, nodes : list[Node]) -> list[Node]: - splib.mechanics.attachment.addFixation(nodes[0].Material, type = self.constraintType, boxROIs = self.boxROIs, sphereROIs=self.sphereROIs, indices=self.indices, fixAll=self.fixAll) + splib_att.addFixation(nodes[0].Material, type = self.constraintType, boxROIs = self.boxROIs, sphereROIs=self.sphereROIs, indices=self.indices, fixAll=self.fixAll) return nodes @@ -30,11 +30,12 @@ class AttachmentConstraintParameters(BaseNodeModifierParameters): indices2 : list[int] = dataclasses.field(default_factory= [0]) stiffness : float = DEFAULT_VALUE damping : float = DEFAULT_VALUE + length : float = DEFAULT_VALUE @override @AffectedNodes(2) def modify(self, owner, node : list[Node]) -> list[Node]: - splib.mechanics.attachment.attachObjects(owner, type = self.constraintType, object1=node[0], object2=node[1], indices1=self.indices1, indices2=self.indices2, stiffness=self.stiffness, damping=self.damping, **self.kwargs) + splib_att.attachObjects(owner, type = self.constraintType, object1=node[0], object2=node[1], indices1=self.indices1, indices2=self.indices2, stiffness=self.stiffness, damping=self.damping, length=self.length, **self.kwargs) return node + [owner] From 32f4ad70644b49883cfa7ef02ef1b91692baef61 Mon Sep 17 00:00:00 2001 From: Paul Baksic Date: Tue, 19 May 2026 11:55:01 +0200 Subject: [PATCH 05/11] Add attachment --- examples/stlib/node_modifier.py | 6 ++++-- splib/simulation/headers.py | 8 ++++---- splib/simulation/ode_solvers.py | 2 +- stlib/node_modifiers/attachments.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/stlib/node_modifier.py b/examples/stlib/node_modifier.py index 53c7a1896..3c5e8698c 100644 --- a/examples/stlib/node_modifier.py +++ b/examples/stlib/node_modifier.py @@ -32,7 +32,7 @@ def createScene(root): 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]), + plane1_collisionParams.geometry = PlaneParameters(np.array([15,0,5]), 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 @@ -88,6 +88,7 @@ def createScene(root): constantSparsity=False)) root.add(NodeModifier, on = [root], parameters = SimulationSettingsParameters(name = "SimulationSettings", + displayFlags = ["showVisualModels", "showInteractionForceFields"], enableCollisionDetection = True, useLagrangian = True, parallelComputing = True, @@ -95,5 +96,6 @@ def createScene(root): frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20)) Logo.add(NodeModifier, on = [Logo], parameters = FixConstraintParameters(name = "FixConstraintParameters", boxROIs=[[-1, -2, -13, 3, 2, -7]])) - # ModelsNode.add(AttachmentConstraintParameters, on = [S, Logo], parameters = AttachmentConstraintParameters(name = "AttachmentConstraintParameters", indices1=[], indices2=[], stiffness=10, length=0.2)) + ModelsNode.add(NodeModifier, on = [S, Logo], parameters = AttachmentConstraintParameters(name = "AttachmentConstraintParameters", indices1=[26,20,119,121], indices2=[722,732,574,573], stiffness=0.5, damping=0.0, + length=[((9.43-9.35)**2 + (-.44-0.48)**2 + (-6.01+6.56)**2)**(0.5) ])) diff --git a/splib/simulation/headers.py b/splib/simulation/headers.py index 3923aec85..9c16116c3 100644 --- a/splib/simulation/headers.py +++ b/splib/simulation/headers.py @@ -45,13 +45,13 @@ def setupPenalityCollisionHeader(node, displayFlags = "showVisualModels",backgr 'Sofa.Component.Engine.Select', 'Sofa.Component.LinearSolver.Direct', 'Sofa.Component.Mass', - 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.IntegrationSchemes.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.IntegrationSchemes.Forward', 'Sofa.Component.Topology.Container.Dynamic', 'Sofa.Component.Visual', ], @@ -85,13 +85,13 @@ def setupLagrangianCollision(node, enableCollision = True, displayFlags = "show 'Sofa.Component.Engine.Select', 'Sofa.Component.LinearSolver.Direct', 'Sofa.Component.Mass', - 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.IntegrationSchemes.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.IntegrationSchemes.Forward', 'Sofa.Component.Topology.Container.Dynamic', 'Sofa.Component.Visual', ], diff --git a/splib/simulation/ode_solvers.py b/splib/simulation/ode_solvers.py index eed57bab8..56075523e 100644 --- a/splib/simulation/ode_solvers.py +++ b/splib/simulation/ode_solvers.py @@ -3,7 +3,7 @@ @ReusableMethod def addImplicitODE(node,static=False,**kwargs): if( not(static) ): - node.addObject("EulerImplicitSolver",name="ODESolver",**kwargs) + node.addObject("EulerImplicitIntegrationScheme",name="ODESolver",**kwargs) else: node.addObject("StaticSolver",name="ODESolver",**kwargs) diff --git a/stlib/node_modifiers/attachments.py b/stlib/node_modifiers/attachments.py index 43cbedb3c..620eb542f 100644 --- a/stlib/node_modifiers/attachments.py +++ b/stlib/node_modifiers/attachments.py @@ -35,7 +35,7 @@ class AttachmentConstraintParameters(BaseNodeModifierParameters): @override @AffectedNodes(2) def modify(self, owner, node : list[Node]) -> list[Node]: - splib_att.attachObjects(owner, type = self.constraintType, object1=node[0], object2=node[1], indices1=self.indices1, indices2=self.indices2, stiffness=self.stiffness, damping=self.damping, length=self.length, **self.kwargs) + splib_att.attachObjects(owner, type = self.constraintType, object1=node[0].Material.States.linkpath, object2=node[1].Material.States.linkpath, indices1=self.indices1, indices2=self.indices2, stiffness=self.stiffness, damping=self.damping, length=self.length, **self.kwargs) return node + [owner] From 98d9c7bfe182f535d557fd2f220588707e20b286 Mon Sep 17 00:00:00 2001 From: Paul Baksic Date: Tue, 19 May 2026 13:15:12 +0200 Subject: [PATCH 06/11] Fix extract geometry --- examples/stlib/node_modifier.py | 7 +-- stlib/geometries/__geometry__.py | 10 ++++- stlib/geometries/extract.py | 75 ++++++++++++-------------------- 3 files changed, 41 insertions(+), 51 deletions(-) diff --git a/examples/stlib/node_modifier.py b/examples/stlib/node_modifier.py index 3c5e8698c..3e13c6594 100644 --- a/examples/stlib/node_modifier.py +++ b/examples/stlib/node_modifier.py @@ -19,7 +19,7 @@ def createScene(root): root.gravity=[0,0,9.81] - + root.dt=0.01 ##Environement planes_lengthNormal = np.array([0, 1, 0]) @@ -75,7 +75,8 @@ def createScene(root): 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 ) + ## TODO: not working with static container because the init order is always wrong so that the triangle vector is always empty when initializing the container + SParams.collision.geometry = ExtractParameters(destinationType=ElementType.TRIANGLES, sourceParameters=SParams.geometry,dynamicTopology=True) SParams.visual = VisualParameters() SParams.visual.geometry = FileParameters(filename="mesh/SofaScene/SVisu.obj") SParams.visual.color = [0.7, .7, 0.7, 0.8] @@ -91,7 +92,7 @@ def createScene(root): displayFlags = ["showVisualModels", "showInteractionForceFields"], enableCollisionDetection = True, useLagrangian = True, - parallelComputing = True, + parallelComputing = False, alarmDistance=0.3, contactDistance=0.02, frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20)) diff --git a/stlib/geometries/__geometry__.py b/stlib/geometries/__geometry__.py index 57d0e5c76..33da09cd8 100644 --- a/stlib/geometries/__geometry__.py +++ b/stlib/geometries/__geometry__.py @@ -1,5 +1,5 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any, Callable from splib.topology.dynamic import addDynamicTopology from splib.topology.static import addStaticTopology from splib.core.enum_types import ElementType @@ -35,6 +35,10 @@ class GeometryParameters(BaseParameters): dynamicTopology : bool = False + + def postInitExec(self, node): + pass + def Data(self): return InternalDataProvider() @@ -79,4 +83,6 @@ def init(self): "quads": self.parameters.data.quads, "tetrahedra": self.parameters.data.tetrahedra, "hexahedra": self.parameters.data.hexahedra - }) \ No newline at end of file + }) + + self.parameters.postInitExec(self) \ No newline at end of file diff --git a/stlib/geometries/extract.py b/stlib/geometries/extract.py index 5ee1b3762..aec65afa7 100644 --- a/stlib/geometries/extract.py +++ b/stlib/geometries/extract.py @@ -6,66 +6,49 @@ import Sofa from Sofa.Core import Node +from functools import partial class ExtractInternalDataProvider(InternalDataProvider): - destinationType : ElementType - sourceType : ElementType - sourceName : str - def __init__(self, destinationType : ElementType, sourceType : ElementType, sourceName : str): - self.destinationType = destinationType - self.sourceType = sourceType - self.sourceName = sourceName - - def __post_init__(self): - 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 __init__(self): + super().__init__() def generateAttribute(self, parent : Geometry): - node = parent.addChild("ExtractedGeometry") + self.position = parent.parents[0].parents[0].getChild("Geometry").container.position.linkpath + - #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.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.destinationType, container={"position" : fromLink + ".position"}) - 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.") +def extractGeometry(sourceType : ElementType, parent : Geometry): + #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.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 = parent.parents[0].parents[0].getChild("Geometry").container.linkpath # TODO: can we do better than this? + if sourceType == ElementType.TETRAHEDRA: + parent.addObject("Tetra2TriangleTopologicalMapping", input=fromLink, output=parent.container.linkpath) + elif sourceType == ElementType.HEXAHEDRA: + parent.addObject("Hexa2QuadTopologicalMapping", input=fromLink, output=parent.container.linkpath) + else: + Sofa.msg_error("[stlib/geometry/exctrat.py]", "Element type: " + str(sourceType) + " not supported.") - 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 class ExtractParameters(GeometryParameters): - def __init__(self, - sourceParameters : GeometryParameters, + def __init__(self, + sourceParameters : GeometryParameters, destinationType : ElementType, dynamicTopology : bool = False): GeometryParameters.__init__(self, - data = ExtractInternalDataProvider(destinationType = destinationType, - sourceType = sourceParameters.elementType, - sourceName = sourceParameters.name), + data = ExtractInternalDataProvider(), dynamicTopology = dynamicTopology, elementType = destinationType) - + + self.postInitExec = partial(extractGeometry, sourceParameters.elementType) + + if(not (sourceParameters.elementType == ElementType.TETRAHEDRA and destinationType == ElementType.TRIANGLES) + and not (sourceParameters.elementType == ElementType.HEXAHEDRA and destinationType == ElementType.QUADS) ): + raise ValueError("Only configuration possible are 'Tetrahedra to Triangles' and 'Hexahedra to Quads'") + From f1aa203a61e59dc1142542619fc0260799f88ce1 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 19 May 2026 15:05:10 +0200 Subject: [PATCH 07/11] Clean --- splib/simulation/headers.py | 8 ++++---- splib/simulation/ode_solvers.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/splib/simulation/headers.py b/splib/simulation/headers.py index 9c16116c3..3923aec85 100644 --- a/splib/simulation/headers.py +++ b/splib/simulation/headers.py @@ -45,13 +45,13 @@ def setupPenalityCollisionHeader(node, displayFlags = "showVisualModels",backgr 'Sofa.Component.Engine.Select', 'Sofa.Component.LinearSolver.Direct', 'Sofa.Component.Mass', - 'Sofa.Component.IntegrationSchemes.Backward', + '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.IntegrationSchemes.Forward', + 'Sofa.Component.ODESolver.Forward', 'Sofa.Component.Topology.Container.Dynamic', 'Sofa.Component.Visual', ], @@ -85,13 +85,13 @@ def setupLagrangianCollision(node, enableCollision = True, displayFlags = "show 'Sofa.Component.Engine.Select', 'Sofa.Component.LinearSolver.Direct', 'Sofa.Component.Mass', - 'Sofa.Component.IntegrationSchemes.Backward', + '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.IntegrationSchemes.Forward', + 'Sofa.Component.ODESolver.Forward', 'Sofa.Component.Topology.Container.Dynamic', 'Sofa.Component.Visual', ], diff --git a/splib/simulation/ode_solvers.py b/splib/simulation/ode_solvers.py index 56075523e..eed57bab8 100644 --- a/splib/simulation/ode_solvers.py +++ b/splib/simulation/ode_solvers.py @@ -3,7 +3,7 @@ @ReusableMethod def addImplicitODE(node,static=False,**kwargs): if( not(static) ): - node.addObject("EulerImplicitIntegrationScheme",name="ODESolver",**kwargs) + node.addObject("EulerImplicitSolver",name="ODESolver",**kwargs) else: node.addObject("StaticSolver",name="ODESolver",**kwargs) From 910168d7b6978fab7028488b5acfc2c81a2bd3d9 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 19 May 2026 16:50:26 +0200 Subject: [PATCH 08/11] Revert changes made to extract --- stlib/geometries/__geometry__.py | 10 +--- stlib/geometries/extract.py | 80 +++++++++++++++++++------------- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/stlib/geometries/__geometry__.py b/stlib/geometries/__geometry__.py index 33da09cd8..57d0e5c76 100644 --- a/stlib/geometries/__geometry__.py +++ b/stlib/geometries/__geometry__.py @@ -1,5 +1,5 @@ from stlib.core.basePrefab import BasePrefab -from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any, Callable +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 @@ -35,10 +35,6 @@ class GeometryParameters(BaseParameters): dynamicTopology : bool = False - - def postInitExec(self, node): - pass - def Data(self): return InternalDataProvider() @@ -83,6 +79,4 @@ def init(self): "quads": self.parameters.data.quads, "tetrahedra": self.parameters.data.tetrahedra, "hexahedra": self.parameters.data.hexahedra - }) - - self.parameters.postInitExec(self) \ No newline at end of file + }) \ No newline at end of file diff --git a/stlib/geometries/extract.py b/stlib/geometries/extract.py index aec65afa7..87359addf 100644 --- a/stlib/geometries/extract.py +++ b/stlib/geometries/extract.py @@ -6,49 +6,65 @@ import Sofa from Sofa.Core import Node -from functools import partial class ExtractInternalDataProvider(InternalDataProvider): + destinationType : ElementType + sourceType : ElementType + sourceName : str - def __init__(self): - super().__init__() + def __init__(self, destinationType : ElementType, sourceType : ElementType, sourceName : str): + self.destinationType = destinationType + self.sourceType = sourceType + self.sourceName = sourceName - def generateAttribute(self, parent : Geometry): - self.position = parent.parents[0].parents[0].getChild("Geometry").container.position.linkpath + def __post_init__(self): + if(not (self.sourceType == ElementType.TETRAHEDRA and self.destinationType == ElementType.TRIANGLES) + and not (self.sourceType == ElementType.HEXAHEDRA and self.destinationType == ElementType.QUADS) ): + raise ValueError("Only configuration possible are 'Tetrahedra to Triangles' and 'Hexahedra to Quads'") + InternalDataProvider.__init__(self) + + def generateAttribute(self, parent : Geometry): + node = parent.addChild("ExtractedGeometry") -def extractGeometry(sourceType : ElementType, parent : Geometry): - #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.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 = parent.parents[0].parents[0].getChild("Geometry").container.linkpath # TODO: can we do better than this? - if sourceType == ElementType.TETRAHEDRA: - parent.addObject("Tetra2TriangleTopologicalMapping", input=fromLink, output=parent.container.linkpath) - elif sourceType == ElementType.HEXAHEDRA: - parent.addObject("Hexa2QuadTopologicalMapping", input=fromLink, output=parent.container.linkpath) - else: - Sofa.msg_error("[stlib/geometry/exctrat.py]", "Element type: " + str(sourceType) + " not supported.") + #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.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.destinationType, container={"position" : fromLink + ".position"}) + 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 = 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 class ExtractParameters(GeometryParameters): - def __init__(self, - sourceParameters : GeometryParameters, - destinationType : ElementType, - dynamicTopology : bool = False): + def __init__(self, + sourceParameters : GeometryParameters, + destinationType : ElementType,): GeometryParameters.__init__(self, - data = ExtractInternalDataProvider(), - dynamicTopology = dynamicTopology, + data = ExtractInternalDataProvider(destinationType = destinationType, + sourceType = sourceParameters.elementType, + sourceName = sourceParameters.name), + dynamicTopology = True, elementType = destinationType) - - self.postInitExec = partial(extractGeometry, sourceParameters.elementType) - - if(not (sourceParameters.elementType == ElementType.TETRAHEDRA and destinationType == ElementType.TRIANGLES) - and not (sourceParameters.elementType == ElementType.HEXAHEDRA and destinationType == ElementType.QUADS) ): - raise ValueError("Only configuration possible are 'Tetrahedra to Triangles' and 'Hexahedra to Quads'") - + From 7a51738ef845297fbf990779663b1680c9b59add Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 19 May 2026 17:07:57 +0200 Subject: [PATCH 09/11] Now register in entity if node containing mstate is child of entity --- examples/stlib/node_modifier.py | 3 +-- stlib/node_modifiers/footers.py | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/stlib/node_modifier.py b/examples/stlib/node_modifier.py index 3e13c6594..88a513c3c 100644 --- a/examples/stlib/node_modifier.py +++ b/examples/stlib/node_modifier.py @@ -76,7 +76,7 @@ def createScene(root): SParams.collision.primitives = [CollisionPrimitive.TRIANGLES] # # #TODO: to fix link issues for extracted geometry, it might be better to give source geometry relative link + parameters ## TODO: not working with static container because the init order is always wrong so that the triangle vector is always empty when initializing the container - SParams.collision.geometry = ExtractParameters(destinationType=ElementType.TRIANGLES, sourceParameters=SParams.geometry,dynamicTopology=True) + 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] @@ -84,7 +84,6 @@ def createScene(root): S = ModelsNode.add(Entity, parameters = SParams) #TODO make the name automatically match the modifier type if none is given - #TODO why this doesn't add itself to the modelsnode "modified by" data ??? root.add(NodeModifier, on = [ModelsNode], parameters = SimulationSolversParameters(name = "SimulationSolvers", constantSparsity=False)) diff --git a/stlib/node_modifiers/footers.py b/stlib/node_modifiers/footers.py index ce7a13871..84042811e 100644 --- a/stlib/node_modifiers/footers.py +++ b/stlib/node_modifiers/footers.py @@ -1,4 +1,6 @@ import dataclasses + +from stlib.entities import EntityParameters, Entity from stlib.node_modifiers import BaseNodeModifierParameters, AffectedNodes from splib.core.enum_types import ConstraintType from splib.core.utils import DEFAULT_VALUE @@ -68,7 +70,13 @@ def _addConstraintCorrectionToMechanicalNodes(node, constraintCorrectionType : s if(linearSolverPath is None): raise ValueError(f"No linear solver found before {child.getLinkPath()}. Please add one to fix this.") child.addObject(constraintCorrectionType, linearSolver=linearSolverPath) - modified += [child] + + if(isinstance(node, Entity)): + modified += [node] + else: + modified += [child] + + break else: modified += SimulationSettingsParameters._addConstraintCorrectionToMechanicalNodes(child, constraintCorrectionType, linearSolverPath) return modified From 9c9e7310e003be3e7efa1dfb69806be4292e8013 Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 19 May 2026 17:18:07 +0200 Subject: [PATCH 10/11] Inherit from component --- stlib/node_modifiers/__node_modifier__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlib/node_modifiers/__node_modifier__.py b/stlib/node_modifiers/__node_modifier__.py index 12c48c427..b1b52ff5a 100644 --- a/stlib/node_modifiers/__node_modifier__.py +++ b/stlib/node_modifiers/__node_modifier__.py @@ -42,7 +42,7 @@ def toDict(self): #TODO use Component when available -class NodeModifier(Sofa.Core.Controller): +class NodeModifier(Sofa.Core.Component): parameters : BaseNodeModifierParameters From 13702562fbd99e280a3f73fa4665194ff452238d Mon Sep 17 00:00:00 2001 From: bakpaul Date: Tue, 19 May 2026 17:23:18 +0200 Subject: [PATCH 11/11] Fix init --- stlib/node_modifiers/__node_modifier__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlib/node_modifiers/__node_modifier__.py b/stlib/node_modifiers/__node_modifier__.py index b1b52ff5a..73216e26d 100644 --- a/stlib/node_modifiers/__node_modifier__.py +++ b/stlib/node_modifiers/__node_modifier__.py @@ -47,7 +47,7 @@ class NodeModifier(Sofa.Core.Component): parameters : BaseNodeModifierParameters def __init__(self, parameters : BaseNodeModifierParameters): - Sofa.Core.Controller.__init__(self, **(parameters.toDict())) + Sofa.Core.Component.__init__(self, **(parameters.toDict())) self.parameters = parameters def register( self, owner, nodes : list[Node]) :