Skip to content

Commit 79a798e

Browse files
bakpaulEulalieCoevoethugtalbot
authored
[Prefabs] Add modifiers (#612)
* Start working but need a inheritable base component binding * 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 * Modifier design close to DONE. * Add attachment * Add attachment * Fix extract geometry * Clean * Revert changes made to extract * Now register in entity if node containing mstate is child of entity * Inherit from component * Fix init * Add default name * Delete examples/stlib/node_modifier.py.view * Fix footer * Apply suggestions from code review Co-authored-by: EulalieCoevoet <eulalie.coevoet@gmail.com> * Include review suggestions * Update bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseComponent.h Co-authored-by: Hugo <hugo.talbot@sofa-framework.org> --------- Co-authored-by: EulalieCoevoet <eulalie.coevoet@gmail.com> Co-authored-by: Hugo <hugo.talbot@sofa-framework.org>
1 parent 4151cfb commit 79a798e

16 files changed

Lines changed: 481 additions & 34 deletions

File tree

bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,17 @@ py::object getForceField(Node *self, unsigned int index)
619619
}
620620

621621

622+
623+
py::object getLinearSolver(Node *self, unsigned int index)
624+
{
625+
auto* ls = self->linearSolver.get(index);
626+
if (ls) {
627+
return PythonFactory::toPython(sofa::core::castToBase(ls));
628+
}
629+
return py::none();
630+
}
631+
632+
622633
py::object getMechanicalState(Node *self)
623634
{
624635
sofa::core::behavior::BaseMechanicalState* state = self->mechanicalState.get();
@@ -721,6 +732,7 @@ void moduleAddNode(py::module &m) {
721732
p.def("getMass", &getMass, sofapython3::doc::sofa::core::Node::getMass);
722733
p.def("hasODESolver", &hasODESolver, sofapython3::doc::sofa::core::Node::hasODESolver);
723734
p.def("getForceField", &getForceField, sofapython3::doc::sofa::core::Node::getForceField);
735+
p.def("getLinearSolver", &getLinearSolver, sofapython3::doc::sofa::core::Node::getLinearSolver);
724736
p.def("getMechanicalState", &getMechanicalState, sofapython3::doc::sofa::core::Node::getMechanicalState);
725737
p.def("getMechanicalMapping", &getMechanicalMapping, sofapython3::doc::sofa::core::Node::getMechanicalMapping);
726738
p.def("sendEvent", &sendEvent, sofapython3::doc::sofa::core::Node::sendEvent);

bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,13 @@ static auto getForceField =
373373
:type index: unsigned int.
374374
)";
375375

376+
static auto getLinearSolver =
377+
R"(
378+
Get the linear solver of a node, given an index.
379+
:param index: index of the linear solver
380+
:type index: unsigned int.
381+
)";
382+
376383
static auto getMechanicalState =
377384
R"(
378385
Get the mechanical state of the node.

examples/stlib/PrefabScene_beginner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from stlib.materials.deformable import Deformable
33
from stlib.geometries.cube import CubeParameters
44
from stlib.geometries.file import FileParameters
5-
from splib.simulation.headers import setupLagrangianCollision
5+
from splib.simulation.headers import setupLagrangianHeader
66
from splib.simulation.linear_solvers import addLinearSolver
77
from splib.simulation.ode_solvers import addImplicitODE
88

@@ -15,7 +15,7 @@ def addSolvers(root):
1515
def createScene(root):
1616
root.gravity = [0, 0, -9.81]
1717

18-
setupLagrangianCollision(root)
18+
setupLagrangianHeader(root)
1919
addSolvers(root)
2020

2121
rigidParams = Rigid.getParameters()

examples/stlib/SofaScene.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from fontTools.afmLib import preferredAttributeOrder
2-
31
from stlib.geometries.plane import PlaneParameters
42
from stlib.geometries.file import FileParameters
53
from stlib.geometries.extract import ExtractParameters
@@ -8,7 +6,7 @@
86
from stlib.entities import Entity, EntityParameters
97
from stlib.visual import Visual, VisualParameters
108
from splib.core.enum_types import CollisionPrimitive, ElementType, ConstitutiveLaw
11-
from splib.simulation.headers import setupLagrangianCollision, setupDefaultHeader
9+
from splib.simulation.headers import setupLagrangianHeader, setupDefaultHeader
1210
from splib.simulation.ode_solvers import addImplicitODE
1311
from splib.simulation.linear_solvers import addLinearSolver
1412
import dataclasses
@@ -20,7 +18,7 @@ def createScene(root):
2018
##Solvers
2119
# setupDefaultHeader(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1],
2220
# parallelComputing = True)
23-
setupLagrangianCollision(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1],
21+
setupLagrangianHeader(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1],
2422
parallelComputing = True,alarmDistance=0.3, contactDistance=0.02,
2523
frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20)
2624
##Environement
@@ -99,7 +97,7 @@ def createScene(root):
9997
SParams.material.parameters = [200, 0.45]
10098

10199
def SAddMaterial(node):
102-
DeformableBehaviorParameters.addDeformableMaterial(node)
100+
DeformableBehaviorParameters.addMaterial(node)
103101
#TODO deal with that is a more smooth way in the material directly
104102
node.addObject("LinearSolverConstraintCorrection", name="ConstraintCorrection", linearSolver=SNode.LinearSolver.linkpath, ODESolver=SNode.ODESolver.linkpath)
105103

examples/stlib/node_modifier.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from stlib.geometries.plane import PlaneParameters
2+
from stlib.geometries.file import FileParameters
3+
from stlib.geometries.extract import ExtractParameters
4+
from stlib.materials.deformable import DeformableBehaviorParameters
5+
from stlib.collision import Collision, CollisionParameters
6+
from stlib.entities import Entity, EntityParameters
7+
from stlib.visual import Visual, VisualParameters
8+
from splib.core.enum_types import CollisionPrimitive, ElementType, ConstitutiveLaw
9+
from splib.simulation.headers import setupLagrangianHeader, setupDefaultHeader
10+
from splib.simulation.ode_solvers import addImplicitODE
11+
from splib.simulation.linear_solvers import addLinearSolver
12+
import dataclasses
13+
import numpy as np
14+
15+
16+
from stlib.node_modifiers import NodeModifier
17+
from stlib.node_modifiers.footers import SimulationSolversParameters, SimulationSettingsParameters
18+
from stlib.node_modifiers.attachments import FixConstraintParameters, AttachmentConstraintParameters
19+
20+
def createScene(root):
21+
root.gravity=[0,0,9.81]
22+
root.dt=0.01
23+
24+
##Environement
25+
plane1_collisionParams = CollisionParameters()
26+
plane1_collisionParams.name = "UP"
27+
plane1_collisionParams.primitives = [CollisionPrimitive.TRIANGLES]
28+
plane1_collisionParams.kwargs = {"TriangleCollision" : {"moving" : False, "simulated" : False}}
29+
plane1_collisionParams.geometry = PlaneParameters(center=np.array([15,0,5]),
30+
normal=np.array([0,0,-1]),
31+
lengthNormal=np.array([0, 1, 0]),
32+
lengthNbEdge=1,
33+
widthNbEdge=2,
34+
lengthSize=30,
35+
widthSize=70)
36+
plane1 = root.add(Collision, parameters = plane1_collisionParams)
37+
# TODO being able to reuse already loaded geometry of current prefab to add any new sub prefab
38+
# We need to enable to directly pass a link to an already existing prefab in place of a prefab parameter object
39+
plane1_visu = plane1.addChild("Visu")
40+
plane1_visu.addObject("OglModel", name="VisualModel", src="@../Geometry/container")
41+
42+
43+
### Logo
44+
ModelsNode = root.addChild("ModelsNode")
45+
46+
LogoParams = EntityParameters(name = "Logo1",
47+
geometry = FileParameters(filename="mesh/SofaScene/Logo.vtk"),
48+
material = DeformableBehaviorParameters(),
49+
collision = CollisionParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoColli.sph")),
50+
visual = VisualParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoVisu.obj")))
51+
52+
LogoParams.geometry.elementType = ElementType.TETRAHEDRA
53+
LogoParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC
54+
LogoParams.material.parameters = [200, 0.4]
55+
LogoParams.material.massDensity = 0.003261
56+
LogoParams.collision.primitives = [CollisionPrimitive.SPHERES]
57+
#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
58+
LogoParams.collision.kwargs = {"SphereCollision" : {"radius" : "@Geometry/loader.listRadius"}}
59+
LogoParams.visual.color = [0.7, .35, 0, 0.8]
60+
61+
Logo = ModelsNode.add(Entity, parameters = LogoParams)
62+
63+
64+
65+
### S
66+
SParams = EntityParameters()
67+
SParams.name = "S"
68+
SParams.geometry = FileParameters(filename="mesh/SofaScene/S.vtk")
69+
SParams.geometry.elementType = ElementType.TETRAHEDRA
70+
SParams.material = DeformableBehaviorParameters()
71+
SParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC
72+
SParams.material.parameters = [200, 0.45]
73+
SParams.material.massDensity = 0.011021
74+
SParams.collision = CollisionParameters()
75+
SParams.collision.primitives = [CollisionPrimitive.TRIANGLES]
76+
# # #TODO: to fix link issues for extracted geometry, it might be better to give source geometry relative link + parameters
77+
## 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
78+
SParams.collision.geometry = ExtractParameters(destinationType=ElementType.TRIANGLES, sourceParameters=SParams.geometry)
79+
SParams.visual = VisualParameters()
80+
SParams.visual.geometry = FileParameters(filename="mesh/SofaScene/SVisu.obj")
81+
SParams.visual.color = [0.7, .7, 0.7, 0.8]
82+
83+
S = ModelsNode.add(Entity, parameters = SParams)
84+
85+
#TODO make the name automatically match the modifier type if none is given
86+
root.add(NodeModifier, on = [ModelsNode], parameters = SimulationSolversParameters(constantSparsity=False))
87+
88+
root.add(NodeModifier, on = [root], parameters = SimulationSettingsParameters(displayFlags = ["showVisualModels", "showInteractionForceFields"],
89+
enableCollisionDetection = True,
90+
useLagrangian = True,
91+
parallelComputing = False,
92+
alarmDistance=0.3, contactDistance=0.02,
93+
frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20))
94+
95+
Logo.add(NodeModifier, on = [Logo], parameters = FixConstraintParameters( boxROIs=[[-1, -2, -13, 3, 2, -7]]))
96+
Logo.add(NodeModifier, on = [Logo], parameters = FixConstraintParameters( boxROIs=[[-100, -2, -13, -300, 2, -7]]))
97+
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,
98+
length=[((9.43-9.35)**2 + (-.44-0.48)**2 + (-6.01+6.56)**2)**(0.5) ]))
99+

splib/Testing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from splib.simulation.linear_solvers import *
55
from splib.mechanics.linear_elasticity import *
66
from splib.mechanics.mass import *
7-
from splib.mechanics.fix_points import *
7+
from splib.mechanics.attachment import *
88
from splib.topology.loader import *
99
from splib.core.node_wrapper import *
1010

@@ -16,7 +16,7 @@ def createScene(rootNode):
1616
rootNode.dt = 0.03
1717
rootNode.gravity = [0,-9.81,0]
1818

19-
setupLagrangianCollision(rootNode,requiredPlugins={"pluginName":['Sofa.Component.Constraint.Projective',
19+
setupLagrangianHeader(rootNode,requiredPlugins={"pluginName":['Sofa.Component.Constraint.Projective',
2020
'Sofa.Component.Engine.Select',
2121
'Sofa.Component.LinearSolver.Direct',
2222
'Sofa.Component.Mass',

splib/mechanics/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__all__ = ["linear_elasticity","hyperelasticity","fix_points","collision_model","mass"]
1+
__all__ = ["linear_elasticity","hyperelasticity","attachment","collision_model","mass"]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,21 @@ def addFixation(node,type:ConstraintType,boxROIs=DEFAULT_VALUE, sphereROIs=DEFAU
2828
case _:
2929
print('Contraint type is either ConstraintType.PROJECTIVE, ConstraintType.WEAK or ConstraintType.LAGRANGIAN')
3030
return
31+
32+
33+
34+
@ReusableMethod
35+
def attachObjects(node, type:ConstraintType, object1, object2, indices1, indices2, stiffness=DEFAULT_VALUE, damping=DEFAULT_VALUE, lengths=DEFAULT_VALUE, **kwargs):
36+
match type:
37+
case ConstraintType.WEAK:
38+
node.addObject("SpringForceField", name="attachment", object1=object1, object2=object2, indices1=indices1, indices2=indices2, stiffness=stiffness, damping=damping, lengths=lengths, **kwargs)
39+
return
40+
case ConstraintType.PROJECTIVE:
41+
node.addObject("AttachProjectiveConstraint", name="attachment", object1=object1, object2=object2, indices1=indices1, indices2=indices2, **kwargs)
42+
return
43+
case ConstraintType.LAGRANGIAN:
44+
node.addObject("BilateralLagrangianConstraint", name="attachment", object1=object1, object2=object2, first_point=indices1, second_point=indices2, **kwargs)
45+
return
46+
case _:
47+
print('Contraint type is either ConstraintType.PROJECTIVE, ConstraintType.WEAK or ConstraintType.LAGRANGIAN')
48+
return

splib/simulation/headers.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def setupDefaultHeader(node, displayFlags = "showVisualModels", backgroundColor=
3737

3838

3939
@ReusableMethod
40-
def setupPenalityCollisionHeader(node, displayFlags = "showVisualModels",backgroundColor=[1,1,1,1], stick=False, parallelComputing=False, alarmDistance=DEFAULT_VALUE, contactDistance=DEFAULT_VALUE, **kwargs):
40+
def setupPenalityHeader(node, displayFlags = "showVisualModels",backgroundColor=[1,1,1,1], stick=False, parallelComputing=False, alarmDistance=DEFAULT_VALUE, contactDistance=DEFAULT_VALUE, **kwargs):
4141
node.addObject('VisualStyle', displayFlags=displayFlags)
4242
node.addObject('BackgroundSetting', color=backgroundColor)
4343

@@ -67,16 +67,16 @@ def setupPenalityCollisionHeader(node, displayFlags = "showVisualModels",backgr
6767
node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase", **kwargs)
6868

6969
if(stick):
70-
node.addObject('CollisionResponse',name="ContactManager", response="BarycentricStickContact",**kwargs)
70+
node.addObject('CollisionResponse',name="ContactManager", response="StickContactForceField",**kwargs)
7171
else:
72-
node.addObject('CollisionResponse',name="ContactManager", response="BarycentricPenalityContact",**kwargs)
72+
node.addObject('CollisionResponse',name="ContactManager", response="PenalityContactForceField",**kwargs)
7373
node.addObject('LocalMinDistance' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs)
7474

7575
return node
7676

7777

7878
@ReusableMethod
79-
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):
79+
def setupLagrangianHeader(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):
8080
node.addObject('VisualStyle', displayFlags=displayFlags)
8181
node.addObject('BackgroundSetting', color=backgroundColor)
8282

@@ -107,21 +107,24 @@ def setupLagrangianCollision(node, displayFlags = "showVisualModels",background
107107
if(parallelComputing):
108108
parallelPrefix="Parallel"
109109

110-
node.addObject('CollisionPipeline', name="collisionPipeline",
111-
**kwargs)
110+
if enableCollision:
111+
node.addObject('CollisionPipeline', name="collisionPipeline",
112+
**kwargs)
112113

113-
node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase",
114-
**kwargs)
114+
node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase",
115+
**kwargs)
115116

116-
node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase",
117-
**kwargs)
117+
node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase",
118+
**kwargs)
119+
120+
if(stick):
121+
node.addObject('CollisionResponse',name="ContactManager", response="StickContactConstraint", responseParams="tol="+str(tolerance),**kwargs)
122+
else:
123+
node.addObject('CollisionResponse',name="ContactManager", response="FrictionContactConstraint", responseParams="mu="+str(frictionCoef),**kwargs)
124+
125+
node.addObject('NewProximityIntersection' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs)
118126

119-
if(stick):
120-
node.addObject('CollisionResponse',name="ContactManager", response="StickContactConstraint", responseParams="tol="+str(tolerance),**kwargs)
121-
else:
122-
node.addObject('CollisionResponse',name="ContactManager", response="FrictionContactConstraint", responseParams="mu="+str(frictionCoef),**kwargs)
123127

124-
node.addObject('NewProximityIntersection' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs)
125128
node.addObject('BlockGaussSeidelConstraintSolver',name="ConstraintSolver", tolerance=tolerance, maxIterations=maxIterations, multithreading=parallelComputing,**kwargs)
126129
node.addObject("ConstraintAttachButtonSetting")
127130

stlib/__init__.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
__all__ = ["core","entities","geometries","materials","collision","visual"]
1+
__all__ = ["core","entities","geometries","materials","collision","visual", "node_modifiers"]
22

33
import Sofa.Core
44
from stlib.core.basePrefab import BasePrefab
5+
from stlib.node_modifiers import NodeModifier
56

67
def __genericAdd(self : Sofa.Core.Node, typeName, **kwargs):
78
def findName(cname, names):
@@ -25,9 +26,13 @@ def checkName(context : Sofa.Core.Node, name):
2526

2627
# Check if a name is provided, if not, use the one of the class
2728
params = kwargs.copy()
28-
if isinstance(typeName, type) and issubclass(typeName, BasePrefab): #Only for prefabs
29-
if len(params.keys()) > 1 or (len(params.keys()) == 1 and "parameters" not in params):
30-
raise RuntimeError("Invalid argument, a prefab takes only the \"parameters\" kwargs as input")
29+
if isinstance(typeName, type) :
30+
if issubclass(typeName, BasePrefab): #Only for prefabs
31+
if len(params.keys()) > 1 or (len(params.keys()) == 1 and "parameters" not in params):
32+
raise RuntimeError("Invalid argument, a prefab takes only the \"parameters\" kwargs as input")
33+
elif issubclass(typeName, NodeModifier):
34+
if len(params.keys()) > 2 or (len(params.keys()) == 2 and (("parameters" not in params) or ("on" not in params ))):
35+
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")
3136

3237
elif "name" not in params : #This doesn't apply to prefab
3338
if isinstance(typeName, str):
@@ -43,7 +48,7 @@ def checkName(context : Sofa.Core.Node, name):
4348
else:
4449
raise RuntimeError("Invalid argument ", typeName)
4550

46-
if isinstance(typeName, type) and issubclass(typeName, BasePrefab) and len(params.keys()) == 1:
51+
if isinstance(typeName, type) and ((issubclass(typeName, BasePrefab) and len(params.keys()) == 1) or (issubclass(typeName, NodeModifier) and len(params.keys()) == 2)):
4752
params["parameters"].name = checkName(self, params["parameters"].name)
4853
else:
4954
params["name"] = checkName(self, params["name"])
@@ -53,6 +58,12 @@ def checkName(context : Sofa.Core.Node, name):
5358
pref = self.addChild(typeName(**params))
5459
pref.init()
5560
pref.postInit()
61+
elif isinstance(typeName, type) and issubclass(typeName, NodeModifier):
62+
if "Modifiers" not in self.children:
63+
self.addChild("Modifiers")
64+
pref = self.Modifiers.addObject(typeName(params["parameters"]))
65+
touchedNodes = pref.apply(self, params["on"])
66+
pref.register(self, touchedNodes)
5667
elif isinstance(typeName, Sofa.Core.Node):
5768
pref = self.addChild(typeName(**params))
5869
elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object):

0 commit comments

Comments
 (0)