From df6077050586eac505ff914b3d2cfaafe734f90a Mon Sep 17 00:00:00 2001 From: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:07:15 +0200 Subject: [PATCH 1/5] Use built in method numpy.set_printoptions in addObject/addChild methods to enable numpy 2 (#518) * Use set_printoptions(legacy=True) in node binfing to force usage of legacy repr for numpy objects. This is ocmpatible with numpy 1 * Patch addChild * Add RAII mechanism to make sure numpy is put back its initial state * Add [[maybe_unused]] Co-authored-by: Frederick Roy --------- Co-authored-by: Hugo Co-authored-by: Frederick Roy --- .../SofaPython3/Sofa/Core/Binding_Node.cpp | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp index c13810490..8060fc7c8 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp @@ -234,9 +234,47 @@ void setFieldsFromPythonValues(Base* self, const py::kwargs& dict) } } +class NumpyReprFixerRAII +{ +public: + NumpyReprFixerRAII() + { + using namespace pybind11::literals; + + m_numpy = py::module_::import("numpy"); + const std::string version = py::cast(m_numpy.attr("__version__")); + m_majorVersion = std::stoi(version.substr(0,1)); + if ( m_majorVersion > 1) + { + m_setPO = m_numpy.attr("set_printoptions"); + m_initialState = m_numpy.attr("get_printoptions")(); + m_setPO("legacy"_a = true); + } + } + + ~NumpyReprFixerRAII() + { + if ( m_majorVersion > 1) + { + m_setPO(**m_initialState); + } + } + +private: + py::module_ m_numpy; + int m_majorVersion; + py::object m_setPO; + py::dict m_initialState; + +}; + + /// Implement the addObject function. py::object addObjectKwargs(Node* self, const std::string& type, const py::kwargs& kwargs) { + //Instantiating this object will make sure the numpy representation is fixed during the call of this function, and comes back to its previous state after + [[maybe_unused]] const NumpyReprFixerRAII numpyReprFixer; + std::string name {}; if (kwargs.contains("name")) { @@ -291,6 +329,8 @@ py::object addObjectKwargs(Node* self, const std::string& type, const py::kwargs if(d) d->setPersistent(true); } + + return PythonFactory::toPython(object.get()); } @@ -360,6 +400,9 @@ py::object createObject(Node* self, const std::string& type, const py::kwargs& k py::object addChildKwargs(Node* self, const std::string& name, const py::kwargs& kwargs) { + //Instantiating this object will make sure the numpy representation is fixed during the call of this function, and comes back to its previous state after + [[maybe_unused]] const NumpyReprFixerRAII numpyReprFixer; + if (sofapython3::isProtectedKeyword(name)) throw py::value_error("addChild: Cannot call addChild with name " + name + ": Protected keyword"); BaseObjectDescription desc (name.c_str()); @@ -378,6 +421,7 @@ py::object addChildKwargs(Node* self, const std::string& name, const py::kwargs& d->setPersistent(true); } + return py::cast(node); } From c7c4eb9bc1e2f830f0db5db9621a4b228c98e358 Mon Sep 17 00:00:00 2001 From: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:53:50 +0200 Subject: [PATCH 2/5] Add explicit getter and setter for datas (#509) * Add explicit getter and setter * Remove mistake --- .../Sofa/Core/Binding_BaseData.cpp | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp index d00341e6b..8aed7ffa0 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp @@ -112,9 +112,13 @@ std::unique_ptr writeableArray(BaseData* self) return nullptr; } -void __setattr__(py::object self, const std::string& s, py::object value) +py::object getValue(py::object self) +{ + return PythonFactory::valueToPython_ro(py::cast(self)); +} + +void setValue(py::object self, py::object value) { - SOFA_UNUSED(s); BaseData* selfdata = py::cast(self); if(py::isinstance(value)) @@ -132,6 +136,12 @@ void __setattr__(py::object self, const std::string& s, py::object value) BindingBase::SetAttr(py::cast(selfdata->getOwner()),selfdata->getName(),value); } +void __setattr__(py::object self, const std::string& s, py::object value) +{ + SOFA_UNUSED(s); + setValue(self, value); +} + py::object __getattr__(py::object self, const std::string& s) { /// If this is data.value we returns the content value of the data field converted into @@ -139,7 +149,7 @@ py::object __getattr__(py::object self, const std::string& s) /// function. if(s == "value") { - return PythonFactory::valueToPython_ro(py::cast(self)); + return getValue(self); } if(s == "linkpath") @@ -150,6 +160,8 @@ py::object __getattr__(py::object self, const std::string& s) throw py::attribute_error("There is no attribute '"+s+"'"); } + + void setParent(BaseData* self, BaseData* parent) { self->setParent(parent); @@ -211,6 +223,8 @@ void moduleAddBaseData(py::module& m) data.def("getName", [](BaseData& b){ return b.getName(); }, sofapython3::doc::baseData::getName); data.def("setName", [](BaseData& b, const std::string& s){ b.setName(s); }, sofapython3::doc::baseData::setName); data.def("getCounter", [](BaseData& self) { return self.getCounter(); }, sofapython3::doc::baseData::getCounter); + data.def("setValue", setValue); + data.def("getValue", getValue); data.def("getHelp", &BaseData::getHelp, sofapython3::doc::baseData::getHelp); data.def("unset", [](BaseData& b){ b.unset(); }, sofapython3::doc::baseData::unset); data.def("getOwner", &getOwner, sofapython3::doc::baseData::getOwner); From 9d2a9df7272c22ec7871574e5dd88b1baf31b800 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 22 Jul 2025 16:50:23 +0200 Subject: [PATCH 3/5] [examples] Update examples using imgui as default (#521) * [examples] Update examples using imgui as default * Add example to keep the Qt GUI example --- examples/basic-addGUI.py | 15 +++----- examples/basic-useQtGUI.py | 59 ++++++++++++++++++++++++++++++ examples/emptyController.py | 12 +++++- examples/emptyDataEngine.py | 4 +- examples/emptyForceField.py | 4 +- examples/example-forcefield.py | 4 +- examples/liver-scriptcontroller.py | 8 ++-- examples/liver.py | 5 +-- examples/loadXMLfromPython.py | 6 +-- 9 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 examples/basic-useQtGUI.py diff --git a/examples/basic-addGUI.py b/examples/basic-addGUI.py index e18beda83..86de792c8 100644 --- a/examples/basic-addGUI.py +++ b/examples/basic-addGUI.py @@ -1,14 +1,10 @@ -# Required import for python -import Sofa - - # Choose in your script to activate or not the GUI USE_GUI = True def main(): + # Required import for python + import Sofa import SofaRuntime - import Sofa.Gui - # Make sure to load all SOFA libraries #Create the root node root = Sofa.Core.Node("root") @@ -20,12 +16,13 @@ def main(): for iteration in range(10): Sofa.Simulation.animate(root, root.dt.value) else: - import SofaQt + import Sofa.Gui + SofaRuntime.importPlugin("SofaImGui") # Find out the supported GUIs print ("Supported GUIs are: " + Sofa.Gui.GUIManager.ListSupportedGUI(",")) - # Launch the GUI (qt or qglviewer) - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) # Initialization of the scene will be done here diff --git a/examples/basic-useQtGUI.py b/examples/basic-useQtGUI.py new file mode 100644 index 000000000..74d2cb972 --- /dev/null +++ b/examples/basic-useQtGUI.py @@ -0,0 +1,59 @@ +# Choose in your script to activate or not the GUI +USE_GUI = True + +def main(): + # Required import for python + import Sofa + import SofaRuntime + + # Make sure to load all SOFA libraries + + #Create the root node + root = Sofa.Core.Node("root") + # Call the below 'createScene' function to create the scene graph + createScene(root) + Sofa.Simulation.initRoot(root) + + if not USE_GUI: + for iteration in range(10): + Sofa.Simulation.animate(root, root.dt.value) + else: + import Sofa.Gui + import SofaQt + + # Find out the supported GUIs + print ("Supported GUIs are: " + Sofa.Gui.GUIManager.ListSupportedGUI(",")) + # Launch the GUI (qt or qglviewer) + Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + Sofa.Gui.GUIManager.createGUI(root, __file__) + Sofa.Gui.GUIManager.SetDimension(1080, 1080) + # Initialization of the scene will be done here + Sofa.Gui.GUIManager.MainLoop(root) + Sofa.Gui.GUIManager.closeGUI() + print("GUI was closed") + + print("Simulation is done.") + + +# Function called when the scene graph is being created +def createScene(root): + + root.addObject('RequiredPlugin', name='Sofa.Component.StateContainer') + + # Scene must now include a AnimationLoop + root.addObject('DefaultAnimationLoop') + + # Add new nodes and objects in the scene + node1 = root.addChild("Node1") + node2 = root.addChild("Node2") + + node1.addObject("MechanicalObject", template="Rigid3d", position="0 0 0 0 0 0 1", showObject="1") + + node2.addObject("MechanicalObject", template="Rigid3d", position="1 1 1 0 0 0 1", showObject="1") + + return root + + +# Function used only if this script is called from a python environment +if __name__ == '__main__': + main() diff --git a/examples/emptyController.py b/examples/emptyController.py index e99d609e0..a81b1c861 100644 --- a/examples/emptyController.py +++ b/examples/emptyController.py @@ -39,6 +39,10 @@ def onKeypressedEvent(self, event): if ord(key) == 20: # right print("You pressed the Right key") + + if key == 'M': # M key of the keyboard + print("You pressed the M key") + def onKeyreleasedEvent(self, event): key = event['key'] @@ -54,6 +58,10 @@ def onKeyreleasedEvent(self, event): if ord(key) == 20: # right print("You released the Right key") + if key == 'M': # M key of the keyboard + print("You released the M key") + + def onMouseEvent(self, event): if (event['State']== 0): # mouse moving print("Mouse is moving (x,y) = "+str(event['mouseX'])+" , "+str(event['mouseY'])) @@ -96,13 +104,13 @@ def createScene(root): def main(): import SofaRuntime import Sofa.Gui - import SofaQt + SofaRuntime.importPlugin("SofaImGui") root=Sofa.Core.Node("root") createScene(root) Sofa.Simulation.initRoot(root) - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) Sofa.Gui.GUIManager.MainLoop(root) diff --git a/examples/emptyDataEngine.py b/examples/emptyDataEngine.py index e11c7579a..d777dc6c4 100644 --- a/examples/emptyDataEngine.py +++ b/examples/emptyDataEngine.py @@ -33,13 +33,13 @@ def createScene(root): def main(): import Sofa.Gui import SofaRuntime - import SofaQt + SofaRuntime.importPlugin("SofaImGui") root=Sofa.Core.Node("root") createScene(root) Sofa.Simulation.initRoot(root) - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) Sofa.Gui.GUIManager.MainLoop(root) diff --git a/examples/emptyForceField.py b/examples/emptyForceField.py index 7956dfa5e..4b66d45c9 100644 --- a/examples/emptyForceField.py +++ b/examples/emptyForceField.py @@ -58,13 +58,13 @@ def createScene(root): def main(): import SofaRuntime import Sofa.Gui - import SofaQt + SofaRuntime.importPlugin("SofaImGui") root=Sofa.Core.Node("root") createScene(root) Sofa.Simulation.initRoot(root) - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) Sofa.Gui.GUIManager.MainLoop(root) diff --git a/examples/example-forcefield.py b/examples/example-forcefield.py index b8bbfb948..fdee12c2a 100644 --- a/examples/example-forcefield.py +++ b/examples/example-forcefield.py @@ -60,13 +60,13 @@ def createScene(root): def main(): import SofaRuntime import Sofa.Gui - import SofaQt + SofaRuntime.importPlugin("SofaImGui") root=Sofa.Core.Node("root") createScene(root) Sofa.Simulation.initRoot(root) - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) Sofa.Gui.GUIManager.MainLoop(root) diff --git a/examples/liver-scriptcontroller.py b/examples/liver-scriptcontroller.py index c2aa1ab6e..24497ebe9 100644 --- a/examples/liver-scriptcontroller.py +++ b/examples/liver-scriptcontroller.py @@ -15,12 +15,11 @@ def main(): Sofa.Simulation.initRoot(root) if not USE_GUI: - import SofaQt - for iteration in range(10): Sofa.Simulation.animate(root, root.dt.value) else: - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + SofaRuntime.importPlugin("SofaImGui") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) Sofa.Gui.GUIManager.MainLoop(root) @@ -43,12 +42,13 @@ def createScene(root): 'Sofa.Component.ODESolver.Backward', 'Sofa.Component.SolidMechanics.FEM.Elastic', 'Sofa.Component.StateContainer', + 'Sofa.Component.MechanicalLoad', 'Sofa.Component.Topology.Container.Dynamic', 'Sofa.Component.Visual', 'Sofa.GL.Component.Rendering3D' ]) - root.addObject('DefaultAnimationLoop') + root.addObject('DefaultAnimationLoop', computeBoundingBox=False) root.addObject('VisualStyle', displayFlags="showCollisionModels hideVisualModels showForceFields") root.addObject('CollisionPipeline', name="CollisionPipeline") diff --git a/examples/liver.py b/examples/liver.py index 7f48be5a8..5f6c3eb40 100644 --- a/examples/liver.py +++ b/examples/liver.py @@ -18,9 +18,8 @@ def main(): for iteration in range(10): Sofa.Simulation.animate(root, root.dt.value) else: - import SofaQt - - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + SofaRuntime.importPlugin("SofaImGui") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) Sofa.Gui.GUIManager.MainLoop(root) diff --git a/examples/loadXMLfromPython.py b/examples/loadXMLfromPython.py index 35eee6ab6..b906f6928 100644 --- a/examples/loadXMLfromPython.py +++ b/examples/loadXMLfromPython.py @@ -31,7 +31,6 @@ def createScene(root): def main(): import SofaRuntime import Sofa.Gui - import SofaQt root = Sofa.Core.Node("root") createScene(root) @@ -39,8 +38,9 @@ def main(): # Find out the supported GUIs print ("Supported GUIs are: " + Sofa.Gui.GUIManager.ListSupportedGUI(",")) - # Launch the GUI (qt or qglviewer) - Sofa.Gui.GUIManager.Init("myscene", "qglviewer") + # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") + SofaRuntime.importPlugin("SofaImGui") + Sofa.Gui.GUIManager.Init("myscene", "imgui") Sofa.Gui.GUIManager.createGUI(root, __file__) Sofa.Gui.GUIManager.SetDimension(1080, 1080) # Initialization of the scene will be done here From 1e57a9652e2be88108a7e1dc5322aacee212912f Mon Sep 17 00:00:00 2001 From: Paul Baksic <30337881+bakpaul@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:27:18 +0200 Subject: [PATCH 4/5] Fix legacy version passed (#523) --- bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp index 8060fc7c8..877653cc3 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp @@ -248,7 +248,7 @@ class NumpyReprFixerRAII { m_setPO = m_numpy.attr("set_printoptions"); m_initialState = m_numpy.attr("get_printoptions")(); - m_setPO("legacy"_a = true); + m_setPO("legacy"_a = "1.25"); } } From 16723ab33ecafa6ffb4695758a2d54eb809a23dd Mon Sep 17 00:00:00 2001 From: Ryan Paul McKenna Date: Thu, 15 Jan 2026 21:19:37 +0000 Subject: [PATCH 5/5] Fix CMake 3.28 error - Python::Python undefined --- CMake/SofaPython3Tools.cmake | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMake/SofaPython3Tools.cmake b/CMake/SofaPython3Tools.cmake index 678743f65..54f711b0a 100644 --- a/CMake/SofaPython3Tools.cmake +++ b/CMake/SofaPython3Tools.cmake @@ -114,6 +114,12 @@ function(SP3_add_python_module) find_package(pybind11 CONFIG QUIET REQUIRED) + # Ensure FindPython created Python::Python (needed by python_add_library) + if(NOT TARGET Python::Python) + find_package(Python REQUIRED COMPONENTS Interpreter Development Development.Module Development.Embed) + endif() + + # We are doing manually what's usually done with pybind11_add_module(${A_TARGET} SHARED "${A_SOURCES}") # since we got some problems on MacOS using recent versions of pybind11 where the SHARED argument wasn't taken # into account