From 98396b987c8c27480728a3884390cc47b4255991 Mon Sep 17 00:00:00 2001 From: Damien Marchal Date: Wed, 4 Mar 2026 14:44:55 +0100 Subject: [PATCH 1/5] Fix wrong name for getInstanciationSourfeFileName --- bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp index 906412979..c7894df94 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp @@ -477,7 +477,7 @@ void moduleAddBase(py::module &m) base.def("getDefinitionSourceFilePos", &Base::getDefinitionSourceFilePos, sofapython3::doc::base::getDefinitionSourceFilePos); base.def("getDefinitionSourceFileName", &Base::getDefinitionSourceFileName, sofapython3::doc::base::getDefinitionSourceFileName); base.def("getInstanciationSourceFilePos", &Base::getInstanciationSourceFilePos, sofapython3::doc::base::getInstanciationSourceFilePos); - base.def("getInstanciationFileName", &Base::getInstanciationSourceFileName, sofapython3::doc::base::getInstanciationSourceFilePos); + base.def("getInstanciationSourceFileName", &Base::getInstanciationSourceFileName, sofapython3::doc::base::getInstanciationSourceFileName); base.def("setDefinitionSourceFilePos", &Base::setDefinitionSourceFilePos, sofapython3::doc::base::setDefinitionSourceFilePos); base.def("setDefinitionSourceFileName", &Base::setDefinitionSourceFileName, sofapython3::doc::base::setDefinitionSourceFileName); From eb72bcbae1eae9f6ee4a9adfae834b5bd3a07ee2 Mon Sep 17 00:00:00 2001 From: Damien Marchal Date: Wed, 4 Mar 2026 14:45:38 +0100 Subject: [PATCH 2/5] Add a getOwner method returning a node. (getContext) --- .../Sofa/Core/Binding_BaseObject.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.cpp index 6740de72b..2b25bf39a 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.cpp @@ -56,6 +56,7 @@ #include #include +#include /// Makes an alias for the pybind11 namespace to increase readability. namespace py { using namespace pybind11; } @@ -99,11 +100,20 @@ py::list getSlaves(BaseObject &self) return slaveList; } -py::object getContext(const BaseObject &self) +py::object getContext(BaseObject &self) { - const sofa::core::objectmodel::BaseContext* context = self.getContext(); + sofa::core::objectmodel::BaseContext* context = self.getContext(); if (context){ - return PythonFactory::toPython(const_cast(context)); + return PythonFactory::toPython(context); + } + return py::none(); +} + +py::object getOwner(BaseObject &self) +{ + sofa::simulation::Node* node = dynamic_cast(self.getContext()); + if (node){ + return PythonFactory::toPython(node); } return py::none(); } @@ -218,6 +228,7 @@ void moduleAddBaseObject(py::module& m) p.def("getLinkPath", [](const BaseObject &self){ return std::string("@") + self.getPathName(); }, sofapython3::doc::baseObject::getLink); p.def("getSlaves", getSlaves, sofapython3::doc::baseObject::getSlaves); p.def("getContext", getContext, sofapython3::doc::baseObject::getContext); + p.def("getOwner", getOwner); p.def("getMaster", getMaster, sofapython3::doc::baseObject::getMaster); p.def("addSlave", &BaseObject::addSlave, sofapython3::doc::baseObject::addSlave); p.def("storeResetState", &BaseObject::storeResetState, sofapython3::doc::baseObject::storeResetState); From b70cc7e54d26102c2fc986c2b63906ca8b001004 Mon Sep 17 00:00:00 2001 From: Damien Marchal Date: Wed, 4 Mar 2026 14:51:21 +0100 Subject: [PATCH 3/5] Add an sketch for Modifier component in SOFA --- examples/modifier-exp/test1.py | 212 +++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 examples/modifier-exp/test1.py diff --git a/examples/modifier-exp/test1.py b/examples/modifier-exp/test1.py new file mode 100644 index 000000000..c978cc9d6 --- /dev/null +++ b/examples/modifier-exp/test1.py @@ -0,0 +1,212 @@ +import Sofa + +## Pyggy patch ############################################################## +class Parameters(object): + name : str = "" + def __init__(self, **kwargs): + print(f"parameters {kwargs}") + for k,v in kwargs.items(): + setattr(self, k, v) + + def toDict(self): + return {"name":self.name} + +class ModifierParameters(Parameters): + def __init__(self, **kwargs): + Parameters.__init__(self, **kwargs) + + def toDict(self): + return Parameters.toDict(self) + +def getpythonCallingPoint(depth=0): + import inspect + d = inspect.stack()[depth] + return (d.filename, d.positions.lineno) + +class Modifier(Sofa.Core.Controller): + def __init__(self, parameters : ModifierParameters = ModifierParameters()): + Sofa.Core.Controller.__init__(self, **parameters.toDict()) + + def getModifiedNode(self): + return self.getOwner().parents[0] + +Sofa.Core.Modifier = Modifier + +def addModifier(self, type, parameters:Parameters, **kwargs) -> Modifier: + print("Add modifier ") + if "Modifiers" not in self.children: + self.addChild("Modifiers") + o = self.Modifiers.addObject(type(parameters=parameters, **kwargs)) + o.apply() + return o +Sofa.Core.Node.addModifier = addModifier + +def add(self, type, parameters:Parameters) -> Sofa.Core.Base: + print("Add object") + if issubclass(type, Sofa.Core.Node): + return self.addChild(type(parameters)) +Sofa.Core.Node.add = add + +############################################################################# +class PrefabParameters(Parameters): + def __init__(self, **kwargs): + Parameters.__init__(self, **kwargs) + + def toDict(self): + return Parameters.toDict(self) + +class Prefab(Sofa.Core.Node): + def __init__(self, parameters): + Sofa.Core.Node.__init__(self, name=parameters.name) + +class MyEntityParameters(PrefabParameters): + def __init__(self, **kwargs): + PrefabParameters.__init__(self, **kwargs) + + def toDict(self): + return PrefabParameters.toDict(self) + +class MyEntity(Prefab): + def __init__(self, parameters): + Prefab.__init__(self, PrefabParameters(name=parameters.name)) + + self.addChild("Material") + self.addChild("Geometry") + self.addChild("Collision") + self.addChild("Visual") + self.addChild("Modifiers") + + self.Geometry.addObject("MeshOBJLoader", filename="mesh/cube.obj", name="loader") + + self.Material.addObject("MechanicalObject", name="state", template="Vec3", position=self.Geometry.loader.position.linkpath) + self.Material.addObject("UniformMass", name="mass", totalMass=1.0) + + self.Visual.addObject("OglModel", src=self.Geometry.loader.linkpath) + + +class FixedPointBoundaryConditionParameters(ModifierParameters): + def __init__(self, **kwargs): + ModifierParameters.__init__(self, **kwargs) + + def toDict(self): + return ModifierParameters.toDict(self) + +class FixedPointBoundaryCondition(Modifier): + def __init__(self, parameters : FixedPointBoundaryConditionParameters = FixedPointBoundaryConditionParameters()): + Modifier.__init__(self, parameters=parameters) + + def apply(self): + self.getModifiedNode().Material.addObject("UnilateralInteractionConstraint") + +class BilateralInteractionModifierParameters(ModifierParameters): + object1: Sofa.Core.Object = None + object2: Sofa.Core.Object = None + + def __init__(self, **kwargs): + ModifierParameters.__init__(self, **kwargs) + + def toDict(self): + return ModifierParameters.toDict(self) | {"object1":self.object1,"object2":self.object2} + +class BilateralInteractionModifier(Modifier): + def __init__(self, parameters : BilateralInteractionModifierParameters = BilateralInteractionModifierParameters()): + Modifier.__init__(self, parameters=parameters) + self.object1 = parameters.object1 + self.object2 = parameters.object2 + + h = getpythonCallingPoint(3) + self.setInstanciationSourceFileName(h[0]) + self.setInstanciationSourceFilePos(h[1]) + + def apply(self): + a = self.object1.Material.addObject("UnilateralInteractionConstraint") + b = self.object2.Material.addObject("UnilateralInteractionConstraint") + + a.setInstanciationSourceFileName(self.getInstanciationSourceFileName()) + a.setInstanciationSourceFilePos(self.getInstanciationSourceFilePos()) + + b.setInstanciationSourceFileName(self.getInstanciationSourceFileName()) + b.setInstanciationSourceFilePos(self.getInstanciationSourceFilePos()) + +class SolversFromSceneModifierParameters(ModifierParameters): + root : Sofa.Core.Node = None + + def __init__(self, **kwargs): + ModifierParameters.__init__(self, **kwargs) + + def toDict(self): + return ModifierParameters.toDict(self) | {"root":self.root} + +def find_component(cond, node): + c = [component for component in node.objects if cond(component)] + for child in node.children: + c += find_component(cond, child) + return c + +class SolverFromSceneModifier(Modifier): + def __init__(self, parameters : SolversFromSceneModifierParameters = SolversFromSceneModifierParameters()): + Modifier.__init__(self, parameters=parameters) + self.root = parameters.root + + h = getpythonCallingPoint(3) + self.setInstanciationSourceFileName(h[0]) + self.setInstanciationSourceFilePos(h[1]) + + def apply(self): + # Traverse scene to deduce stuff + constraints = find_component(lambda x: x.getClassName() == "Lagrangian", self.root) + constraints = [constraint.name.value for constraint in constraints] + + print("I'm modifing ", constraints) + if "UnilateralLagrangianConstraint" in constraints: + self.root.addObject("GenericConstraintCorrection") + +class HeaderToSceneModifierParameters(ModifierParameters): + root : Sofa.Core.Node = None + + def __init__(self, **kwargs): + ModifierParameters.__init__(self, **kwargs) + + def toDict(self): + return ModifierParameters.toDict(self) | {"root":self.root} + +class HeaderToSceneModifier(Modifier): + def __init__(self, parameters : SolversFromSceneModifierParameters = SolversFromSceneModifierParameters()): + Modifier.__init__(self, parameters=parameters) + self.root = parameters.root + + def apply(self): + # Traverse scene to deduce stuff + + def is_mechanical(object): + return object.getClassName() == "MechanicalObject" + + mechanicals = find_component(is_mechanical, self.root) + for mechanical in mechanicals: + mechanical.getOwner().addObject("GenericConstraintCorrection") + +def createScene(root): + # Add a modifier in Example1/prefab, it modify the node it is applied to + # The use case is to enrich an object + root.addChild("Example1") + root.Example1.add(MyEntity, parameters=MyEntityParameters(name="prefab")) + root.Example1.prefab.addModifier(FixedPointBoundaryCondition, parameters=FixedPointBoundaryConditionParameters(name="boundary1", stiffness=30.0)) + + # Add a modifier in Example2, it modify a specific pair of node + # The use case is to explicit an action that involve several possibly disconnected in the graph object + # Personnally I'm not sure I want to express BoundaryCondition as Modifiers. + root.addChild("Example2") + root.Example2.add(MyEntity, parameters=MyEntityParameters(name="prefab1")) + root.Example2.add(MyEntity, parameters=MyEntityParameters(name="prefab2")) + root.Example2.addModifier(BilateralInteractionModifier, parameters=BilateralInteractionModifierParameters(name="constraint", + object1=root.Example2.prefab1, + object2=root.Example2.prefab2)) + + # Add a modifier in the root node, this modifier deduce something from the scene and modify anywhere in the scene. + root.addModifier(SolverFromSceneModifier, parameters=SolversFromSceneModifierParameters(name="bottom-up-fix", + root=root)) + + # Add a modifier in the root node, this modifier deduce something from the scene and modify top down the scene. + root.addModifier(HeaderToSceneModifier, parameters=HeaderToSceneModifierParameters(name="top-down-fix", + root=root)) + From 8ddee69baaeb026b41ba4c3edf26c208e6b97a1f Mon Sep 17 00:00:00 2001 From: Damien Marchal Date: Wed, 4 Mar 2026 15:16:16 +0100 Subject: [PATCH 4/5] Add example on how to enforce what is required and what is not. --- examples/modifier-exp/test1.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/examples/modifier-exp/test1.py b/examples/modifier-exp/test1.py index c978cc9d6..4f9c96313 100644 --- a/examples/modifier-exp/test1.py +++ b/examples/modifier-exp/test1.py @@ -4,7 +4,7 @@ class Parameters(object): name : str = "" def __init__(self, **kwargs): - print(f"parameters {kwargs}") + print(f"Params {kwargs}") for k,v in kwargs.items(): setattr(self, k, v) @@ -104,16 +104,23 @@ class BilateralInteractionModifierParameters(ModifierParameters): def __init__(self, **kwargs): ModifierParameters.__init__(self, **kwargs) - + def toDict(self): return ModifierParameters.toDict(self) | {"object1":self.object1,"object2":self.object2} class BilateralInteractionModifier(Modifier): def __init__(self, parameters : BilateralInteractionModifierParameters = BilateralInteractionModifierParameters()): Modifier.__init__(self, parameters=parameters) + self.object1 = parameters.object1 self.object2 = parameters.object2 + if self.object1 is None: + raise Exception("It is mandatory to have 'object1'") + + if self.object2 is None: + raise Exception("It is mandatory to have 'object2'") + h = getpythonCallingPoint(3) self.setInstanciationSourceFileName(h[0]) self.setInstanciationSourceFilePos(h[1]) @@ -147,6 +154,9 @@ class SolverFromSceneModifier(Modifier): def __init__(self, parameters : SolversFromSceneModifierParameters = SolversFromSceneModifierParameters()): Modifier.__init__(self, parameters=parameters) self.root = parameters.root + + if self.root is None: + raise Exception("It is mandatory to have 'root' parameter for this modifier") h = getpythonCallingPoint(3) self.setInstanciationSourceFileName(h[0]) @@ -157,7 +167,6 @@ def apply(self): constraints = find_component(lambda x: x.getClassName() == "Lagrangian", self.root) constraints = [constraint.name.value for constraint in constraints] - print("I'm modifing ", constraints) if "UnilateralLagrangianConstraint" in constraints: self.root.addObject("GenericConstraintCorrection") @@ -173,8 +182,12 @@ def toDict(self): class HeaderToSceneModifier(Modifier): def __init__(self, parameters : SolversFromSceneModifierParameters = SolversFromSceneModifierParameters()): Modifier.__init__(self, parameters=parameters) - self.root = parameters.root + self.root = parameters.root + + if self.root is None: + raise Exception("It is mandatory to have 'root' parameter for this modifier") + def apply(self): # Traverse scene to deduce stuff @@ -202,6 +215,17 @@ def createScene(root): object1=root.Example2.prefab1, object2=root.Example2.prefab2)) + # The same but with the parameters written before calling the modifier + root.addChild("Example2bis") + root.Example2bis.add(MyEntity, parameters=MyEntityParameters(name="prefab1")) + root.Example2bis.add(MyEntity, parameters=MyEntityParameters(name="prefab2")) + parameters = BilateralInteractionModifierParameters() + parameters.name="constraint" + parameters.object1=root.Example2bis.prefab1 + parameters.object2=root.Example2bis.prefab2 + root.Example2bis.addModifier(BilateralInteractionModifier, parameters=parameters) + + # Add a modifier in the root node, this modifier deduce something from the scene and modify anywhere in the scene. root.addModifier(SolverFromSceneModifier, parameters=SolversFromSceneModifierParameters(name="bottom-up-fix", root=root)) From 033d982e2648afc79d92635d95c9843ee45d358e Mon Sep 17 00:00:00 2001 From: Damien Marchal Date: Wed, 4 Mar 2026 15:25:25 +0100 Subject: [PATCH 5/5] Improve naming --- examples/modifier-exp/test1.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/examples/modifier-exp/test1.py b/examples/modifier-exp/test1.py index 4f9c96313..2e62d0deb 100644 --- a/examples/modifier-exp/test1.py +++ b/examples/modifier-exp/test1.py @@ -4,7 +4,6 @@ class Parameters(object): name : str = "" def __init__(self, **kwargs): - print(f"Params {kwargs}") for k,v in kwargs.items(): setattr(self, k, v) @@ -33,7 +32,6 @@ def getModifiedNode(self): Sofa.Core.Modifier = Modifier def addModifier(self, type, parameters:Parameters, **kwargs) -> Modifier: - print("Add modifier ") if "Modifiers" not in self.children: self.addChild("Modifiers") o = self.Modifiers.addObject(type(parameters=parameters, **kwargs)) @@ -42,7 +40,6 @@ def addModifier(self, type, parameters:Parameters, **kwargs) -> Modifier: Sofa.Core.Node.addModifier = addModifier def add(self, type, parameters:Parameters) -> Sofa.Core.Base: - print("Add object") if issubclass(type, Sofa.Core.Node): return self.addChild(type(parameters)) Sofa.Core.Node.add = add @@ -135,7 +132,7 @@ def apply(self): b.setInstanciationSourceFileName(self.getInstanciationSourceFileName()) b.setInstanciationSourceFilePos(self.getInstanciationSourceFilePos()) -class SolversFromSceneModifierParameters(ModifierParameters): +class HeaderFromSceneModifierParameters(ModifierParameters): root : Sofa.Core.Node = None def __init__(self, **kwargs): @@ -150,8 +147,8 @@ def find_component(cond, node): c += find_component(cond, child) return c -class SolverFromSceneModifier(Modifier): - def __init__(self, parameters : SolversFromSceneModifierParameters = SolversFromSceneModifierParameters()): +class HeaderFromSceneModifier(Modifier): + def __init__(self, parameters : HeaderFromSceneModifierParameters = HeaderFromSceneModifierParameters()): Modifier.__init__(self, parameters=parameters) self.root = parameters.root @@ -164,11 +161,11 @@ def __init__(self, parameters : SolversFromSceneModifierParameters = SolversFrom def apply(self): # Traverse scene to deduce stuff - constraints = find_component(lambda x: x.getClassName() == "Lagrangian", self.root) - constraints = [constraint.name.value for constraint in constraints] - + constraints = find_component(lambda x: True, self.root) + constraints = [constraint.getClassName() for constraint in constraints] + print(f"Lagrangian {constraints}") if "UnilateralLagrangianConstraint" in constraints: - self.root.addObject("GenericConstraintCorrection") + self.root.addObject("FreeMotionAnimationLoop") class HeaderToSceneModifierParameters(ModifierParameters): root : Sofa.Core.Node = None @@ -180,7 +177,7 @@ def toDict(self): return ModifierParameters.toDict(self) | {"root":self.root} class HeaderToSceneModifier(Modifier): - def __init__(self, parameters : SolversFromSceneModifierParameters = SolversFromSceneModifierParameters()): + def __init__(self, parameters : HeaderFromSceneModifierParameters = HeaderFromSceneModifierParameters()): Modifier.__init__(self, parameters=parameters) self.root = parameters.root @@ -227,7 +224,7 @@ def createScene(root): # Add a modifier in the root node, this modifier deduce something from the scene and modify anywhere in the scene. - root.addModifier(SolverFromSceneModifier, parameters=SolversFromSceneModifierParameters(name="bottom-up-fix", + root.addModifier(HeaderFromSceneModifier, parameters=HeaderFromSceneModifierParameters(name="bottom-up-fix", root=root)) # Add a modifier in the root node, this modifier deduce something from the scene and modify top down the scene.