diff --git a/CMakeLists.txt b/CMakeLists.txt index d76b3e21a..c9fac18cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,6 +203,12 @@ SP3_add_python_package( TARGET_DIRECTORY splib ) +SP3_add_python_package( + SOURCE_DIRECTORY + ${CMAKE_CURRENT_SOURCE_DIR}/stlib + TARGET_DIRECTORY + stlib +) sofa_create_package( PACKAGE_NAME ${PROJECT_NAME} diff --git a/Plugin/src/SofaPython3/DataHelper.cpp b/Plugin/src/SofaPython3/DataHelper.cpp index 2f4ab166d..df6f59866 100644 --- a/Plugin/src/SofaPython3/DataHelper.cpp +++ b/Plugin/src/SofaPython3/DataHelper.cpp @@ -95,15 +95,7 @@ std::ostream& operator<<(std::ostream& out, const py::buffer_info& p) std::string getPathTo(Base* b) { - BaseNode* node = dynamic_cast(b); - if(node) - return node->getPathName(); - BaseObject* object = dynamic_cast(b); - if(object) - return object->getPathName(); - - assert(true && "Only Base & BaseObject are supported"); - return ""; + return b->getPathName(); } const char* getFormat(const AbstractTypeInfo& nfo) diff --git a/Plugin/src/SofaPython3/PythonEnvironment.cpp b/Plugin/src/SofaPython3/PythonEnvironment.cpp index b1f41ff2c..b79c897c0 100644 --- a/Plugin/src/SofaPython3/PythonEnvironment.cpp +++ b/Plugin/src/SofaPython3/PythonEnvironment.cpp @@ -206,7 +206,7 @@ void PythonEnvironment::Init() // Workaround: try to import scipy from the main thread this prevents a deadlock when importing // scipy from a worker thread when we use the SofaScene asynchronous loading - executePython([]{ PyRun_SimpleString("try:\n\tfrom scipy import misc, optimize\nexcept:\n\tpass\n");}); + executePython([]{ PyRun_SimpleString("try:\n\tfrom scipy import optimize\nexcept:\n\tpass\n");}); // If the script directory is not available (e.g. if the interpreter is invoked interactively // or if the script is read from standard input), path[0] is the empty string, diff --git a/Plugin/src/SofaPython3/PythonFactory.cpp b/Plugin/src/SofaPython3/PythonFactory.cpp index 3a9c678b4..d94472e5c 100644 --- a/Plugin/src/SofaPython3/PythonFactory.cpp +++ b/Plugin/src/SofaPython3/PythonFactory.cpp @@ -503,7 +503,17 @@ bool PythonFactory::registerDefaultTypes() PythonFactory::registerType("Hexa"); PythonFactory::registerType("Penta"); - // State vectors + // Rigid + PythonFactory::registerType("Rigid3d::Coord"); + PythonFactory::registerType("Rigid3f::Coord"); + PythonFactory::registerType("Rigid3::Coord"); + + // Quaternion + PythonFactory::registerType>("Quatd"); + PythonFactory::registerType>("Quatf"); + PythonFactory::registerType>("Quat"); + + // Rigid3 vectors PythonFactory::registerType("Rigid3d::VecCoord"); PythonFactory::registerType("Rigid3f::VecCoord"); PythonFactory::registerType("Rigid3::VecCoord"); diff --git a/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h b/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h index 1689aef20..2fe4c923a 100644 --- a/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h +++ b/bindings/Modules/src/SofaPython3/SofaConstraintSolver/Binding_ConstraintSolver_doc.h @@ -34,7 +34,7 @@ Returns the compliance matrix projected in constraint space, built in the constr example: ------------ -constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) +constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) matrix = constraint_solver.W() )"; @@ -45,7 +45,7 @@ Returns the force resulting from the constraints example: ------------ -constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) +constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) lambda = constraint_solver.lambda_force() )"; @@ -56,7 +56,7 @@ Returns the displacement computed without any constraint example: ------------ -constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) +constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) dfree = constraint_solver.dfree() )"; diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp b/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp index 99e992655..8f02c43a6 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp +++ b/bindings/Modules/src/SofaPython3/SofaLinearSolver/Binding_LinearSolver.cpp @@ -67,7 +67,7 @@ void bindLinearSolvers(py::module &m) c.def("A", [](CRSLinearSolver& self) -> EigenSparseMatrix { - if (CRS* matrix = self.getSystemMatrix()) + if (CRS* matrix = self.l_linearSystem->getSystemMatrix()) { return toEigen(*matrix); } @@ -76,7 +76,7 @@ void bindLinearSolvers(py::module &m) c.def("b", [](CRSLinearSolver& self) -> Vector { - if (auto* vector = self.getSystemRHVector()) + if (auto* vector = self.l_linearSystem->getRHSVector()) { return EigenVectorMap(vector->ptr(), vector->size()); } @@ -85,7 +85,7 @@ void bindLinearSolvers(py::module &m) c.def("x", [](CRSLinearSolver& self) -> Vector { - if (auto* vector = self.getSystemLHVector()) + if (auto* vector = self.l_linearSystem->getSolutionVector()) { return EigenVectorMap(vector->ptr(), vector->size()); } diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp index b0cfc4ae2..d03773972 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.cpp @@ -17,15 +17,12 @@ ******************************************************************************* * Contact information: contact@sofa-framework.org * ******************************************************************************/ -#include -#include -#include +#include -#include -#include -#include #include +#include +#include #include @@ -39,12 +36,6 @@ using EigenSparseMatrix = Eigen::SparseMatrix; template using EigenMatrixMap = Eigen::Map >; -template -using Vector = Eigen::Matrix; - -template -using EigenVectorMap = Eigen::Map>; - template EigenSparseMatrix::Real> toEigen(sofa::linearalgebra::CompressedRowSparseMatrix& matrix) @@ -70,62 +61,104 @@ toEigen(sofa::linearalgebra::CompressedRowSparseMatrix& matrix) } } -template -void bindLinearSystems(py::module &m) +template +void bindMatrixAccessCRS(LinearSystemClass& c) { - using CRS = sofa::linearalgebra::CompressedRowSparseMatrix; - using Real = typename CRS::Real; - using CRSLinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem >; + using Real = typename TMatrix::Real; + using CRSLinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem; - const std::string typeName = CRSLinearSystem::GetClass()->className + CRSLinearSystem::GetCustomTemplateName(); - - py::class_ > c(m, typeName.c_str(), sofapython3::doc::linearsystem::linearSystemClass); - - c.def("A", [](CRSLinearSystem& self) -> EigenSparseMatrix - { - if (CRS* matrix = self.getSystemMatrix()) + const auto matrixAccess = + [](CRSLinearSystem& self) -> EigenSparseMatrix { - if (matrix->colsValue.empty()) //null matrix: contains no entries + if (TMatrix* matrix = self.getSystemMatrix()) { - return EigenSparseMatrix{matrix->rows(), matrix->cols()}; + if (matrix->colsValue.empty()) //null matrix: contains no entries + { + return EigenSparseMatrix{matrix->rows(), matrix->cols()}; + } + return toEigen(*matrix); } - return toEigen(*matrix); - } - return {}; - }, sofapython3::doc::linearsystem::linearSystem_A); + return {}; + }; - c.def("b", [](CRSLinearSystem& self) -> Vector - { - if (auto* vector = self.getRHSVector()) - { - return EigenVectorMap(vector->ptr(), vector->size()); - } - return {}; - }, sofapython3::doc::linearsystem::linearSystem_b); + c.def("A", matrixAccess, sofapython3::doc::linearsystem::linearSystem_CRS_A); + c.def("get_system_matrix", matrixAccess, sofapython3::doc::linearsystem::linearSystem_CRS_get_system_matrix); +} - c.def("x", [](CRSLinearSystem& self) -> Vector - { - if (auto* vector = self.getSolutionVector()) - { - return EigenVectorMap(vector->ptr(), vector->size()); - } - return {}; - }, sofapython3::doc::linearsystem::linearSystem_x); +template<> +void bindMatrixAccess(LinearSystemClass, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} - /// register the binding in the downcasting subsystem - PythonFactory::registerType([](sofa::core::objectmodel::Base* object) - { - return py::cast(dynamic_cast(object)); - }); +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template<> +void bindMatrixAccess(LinearSystemClass>, sofa::linearalgebra::FullVector>& c) +{ + bindMatrixAccessCRS(c); +} + +template +using DenseMatrix = Eigen::Matrix; + +template +using EigenDenseMatrixMap = Eigen::Map>; + +template<> +void bindMatrixAccess(LinearSystemClass, sofa::linearalgebra::FullVector>& c) +{ + using CRSLinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem, sofa::linearalgebra::FullVector>; + + const auto matrixAccess = + [](CRSLinearSystem& self) -> EigenDenseMatrixMap + { + sofa::linearalgebra::FullMatrix* matrix = self.getSystemMatrix(); + const auto row = matrix ? matrix->rows() : 0; + const auto col = matrix ? matrix->cols() : 0; + return EigenDenseMatrixMap(matrix ? matrix->ptr() : nullptr, row, col); + }; + c.def("A", matrixAccess, sofapython3::doc::linearsystem::linearSystem_FullMatrix_A); + c.def("get_system_matrix", matrixAccess, sofapython3::doc::linearsystem::linearSystem_FullMatrix_get_system_matrix); } void moduleAddLinearSystem(py::module &m) { - bindLinearSystems(m); - bindLinearSystems >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems >, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + bindLinearSystems, sofa::linearalgebra::FullVector >(m); + + bindLinearSystems, sofa::linearalgebra::BlockVector<6, SReal> >(m); } } diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.inl b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.inl new file mode 100644 index 000000000..570567ccd --- /dev/null +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem.inl @@ -0,0 +1,101 @@ +/****************************************************************************** +* SofaPython3 plugin * +* (c) 2021 CNRS, University of Lille, INRIA * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#pragma once +#include +#include +#include +#include + +#include +#include +#include + +namespace py { using namespace pybind11; } + +namespace sofapython3 +{ + +template +using Vector = Eigen::Matrix; + +template +using EigenVectorMap = Eigen::Map>; + +template +Vector +getVector(TVector* vector) +{ + using Real = typename TVector::Real; + if (vector) + { + return EigenVectorMap(vector->ptr(), vector->size()); + } + return {}; +} + +template +Vector getRHSVector(sofa::component::linearsystem::TypedMatrixLinearSystem& linearSystem) +{ + return getVector(linearSystem.getRHSVector()); +} + +template +Vector getSolutionVector(sofa::component::linearsystem::TypedMatrixLinearSystem& linearSystem) +{ + return getVector(linearSystem.getSolutionVector()); +} + +template +using LinearSystemClass = + py::class_, + sofa::core::objectmodel::BaseObject, + sofapython3::py_shared_ptr> >; + +template +void bindMatrixAccess(LinearSystemClass& c) +{} + +template +void bindLinearSystems(py::module &m) +{ + using LinearSystem = sofa::component::linearsystem::TypedMatrixLinearSystem; + + const std::string typeName = + LinearSystem::GetClass()->className + + LinearSystem::GetCustomTemplateName(); + + LinearSystemClass c(m, typeName.c_str(), sofapython3::doc::linearsystem::linearSystemClass); + + c.def("b", getRHSVector, sofapython3::doc::linearsystem::linearSystem_b); + c.def("get_rhs_vector", getRHSVector, sofapython3::doc::linearsystem::linearSystem_b); + + c.def("x", getSolutionVector, sofapython3::doc::linearsystem::linearSystem_x); + c.def("get_solution_vector", getSolutionVector, sofapython3::doc::linearsystem::linearSystem_x); + + bindMatrixAccess(c); + + /// register the binding in the downcasting subsystem + PythonFactory::registerType([](sofa::core::objectmodel::Base* object) + { + return py::cast(dynamic_cast(object)); + }); +} + +} diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h index 6c5786e8a..421fa221f 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/Binding_LinearSystem_doc.h @@ -32,7 +32,7 @@ Linear system. Supports only CompressedRowSparseMatrix. linear_system = root.addObject('MatrixLinearSystem', template='CompressedRowSparseMatrixd') )"; -static auto linearSystem_A = +static auto linearSystem_CRS_A = R"( Returns the global system matrix as a scipy sparse matrix @@ -42,6 +42,36 @@ linear_system = root.addObject('MatrixLinearSystem', template='CompressedRowSpar matrix = linear_system.A() )"; +static auto linearSystem_CRS_get_system_matrix = +R"( +Returns the global system matrix as a scipy sparse matrix + +example: +------------ +linear_system = root.addObject('MatrixLinearSystem', template='CompressedRowSparseMatrixd') +matrix = linear_system.get_system_matrix() +)"; + +static auto linearSystem_FullMatrix_A = +R"( +Returns the global system matrix as a numpy array matrix + +example: +------------ +linear_system = root.addObject('MatrixLinearSystem', template='FullMatrix') +matrix = linear_system.A() +)"; + +static auto linearSystem_FullMatrix_get_system_matrix = +R"( +Returns the global system matrix as a numpy array matrix + +example: +------------ +linear_system = root.addObject('MatrixLinearSystem', template='FullMatrix') +matrix = linear_system.get_system_matrix() +)"; + static auto linearSystem_b = R"( Returns the global system right hand side as a numpy array diff --git a/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt b/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt index d6b64a130..fd0805488 100644 --- a/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt +++ b/bindings/Modules/src/SofaPython3/SofaLinearSystem/CMakeLists.txt @@ -2,6 +2,7 @@ project(Bindings.Modules.SofaLinearSystem) set(HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Binding_LinearSystem.h + ${CMAKE_CURRENT_SOURCE_DIR}/Binding_LinearSystem.inl ${CMAKE_CURRENT_SOURCE_DIR}/Binding_LinearSystem_doc.h ) diff --git a/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py b/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py index 00d4d00f4..037796fe3 100644 --- a/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py +++ b/bindings/Modules/tests/SofaConstraintSolver/matrix_access.py @@ -21,7 +21,7 @@ def simulate_pendulum(self): "Sofa.Component.Topology.Container.Dynamic"]) root.addObject("FreeMotionAnimationLoop", solveVelocityConstraintFirst=True) - root.addObject("ProjectedGaussSeidelConstraintSolver", name="constraint_solver", tolerance=1e-9, maxIterations=1000) + root.addObject("BlockGaussSeidelConstraintSolver", name="constraint_solver", tolerance=1e-9, maxIterations=1000) root.addObject("StringMeshCreator", name="loader", resolution="20") root.addObject("EulerImplicitSolver") @@ -39,7 +39,7 @@ def simulate_pendulum(self): ext.addObject("DistanceMapping", name="distanceMapping", topology="@../edge_container") ext.addObject("UniformConstraint", template="Vec1d", iterative=True) - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/Modules/tests/SofaDeformable/SpringForceField.py b/bindings/Modules/tests/SofaDeformable/SpringForceField.py index 72d721b6a..052f335e5 100644 --- a/bindings/Modules/tests/SofaDeformable/SpringForceField.py +++ b/bindings/Modules/tests/SofaDeformable/SpringForceField.py @@ -38,7 +38,7 @@ class Test(unittest.TestCase): def setUp(self) -> None: self.root = Sofa.Core.Node() create_scene(self.root) - Sofa.Simulation.init(self.root) + Sofa.Simulation.initRoot(self.root) def tearDown(self) -> None: Sofa.Simulation.unload(self.root) diff --git a/bindings/Modules/tests/SofaLinearSolver/matrix_access.py b/bindings/Modules/tests/SofaLinearSolver/matrix_access.py index 5c8c54eeb..d69a0f9ce 100644 --- a/bindings/Modules/tests/SofaLinearSolver/matrix_access.py +++ b/bindings/Modules/tests/SofaLinearSolver/matrix_access.py @@ -26,7 +26,7 @@ def simulate_beam(self, linear_solver_template): root.addObject('FixedConstraint', indices="@box.indices") root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/Sofa/package/__init__.py b/bindings/Sofa/package/__init__.py index 8486b4d3b..8926e8cee 100644 --- a/bindings/Sofa/package/__init__.py +++ b/bindings/Sofa/package/__init__.py @@ -13,7 +13,7 @@ n.addChild("Node2") n.addObject("MechanicalObject", name="dofs") - Sofa.Simulation.init(n) + Sofa.Simulation.initRoot(n) Sofa.Simulation.print(n) """ diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp index e71150634..906412979 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp @@ -245,8 +245,12 @@ void BindingBase::SetDataFromArray(BaseData* data, const py::array& value) return copyScalar(data, nfo, src); else if(srcinfo.format=="f") return copyScalar(data, nfo, src); - else - throw std::runtime_error("SetAttrFromArray :: unsupported fileformat"); + else + { + std::stringstream tmp; + tmp << "SetAttrFromArray :: unsupported fileformat '" << srcinfo.format << "'" ; + throw std::runtime_error(tmp.str()); + } } } diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp index 8aed7ffa0..25f37de6a 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseData.cpp @@ -45,18 +45,6 @@ using sofa::defaulttype::AbstractTypeInfo; namespace sofapython3 { -std::string getPathName(BaseData& self) -{ - Base* b= self.getOwner(); - std::string prefix = getPathTo(b); - return prefix+"."+self.getName(); -} - -std::string getLinkPath(BaseData& self) -{ - return "@"+getPathName(self); -} - bool hasChanged(BaseData& data) { if (data.isDirty()) { @@ -230,8 +218,8 @@ void moduleAddBaseData(py::module& m) data.def("getOwner", &getOwner, sofapython3::doc::baseData::getOwner); data.def("getParent", &BaseData::getParent, sofapython3::doc::baseData::getParent); data.def("typeName", [](BaseData& data){ return data.getValueTypeInfo()->name(); }, sofapython3::doc::baseData::typeName); - data.def("getPathName", getPathName, sofapython3::doc::baseData::getPathName); - data.def("getLinkPath", getLinkPath, sofapython3::doc::baseData::getLinkPath); + data.def("getPathName", &BaseData::getPathName, sofapython3::doc::baseData::getPathName); + data.def("getLinkPath", &BaseData::getLinkPath, sofapython3::doc::baseData::getLinkPath); data.def("hasChanged", hasChanged, sofapython3::doc::baseData::hasChanged); data.def("isSet", isSet, sofapython3::doc::baseData::isSet); data.def("toList", toList, sofapython3::doc::baseData::toList); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp index dec413956..92ca8954d 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseMeshTopology.cpp @@ -65,11 +65,13 @@ void moduleAddBaseMeshTopology(py::module& m) { c.def("getNbTriangles", &BaseMeshTopology::getNbTriangles); c.def("getNbTetrahedra", &BaseMeshTopology::getNbTetrahedra); c.def("getNbHexahedra", &BaseMeshTopology::getNbHexahedra); + c.def("getNbPrisms", &BaseMeshTopology::getNbPrisms); + c.def("getNbPyramids", &BaseMeshTopology::getNbPyramids); c.def("getNbQuads", &BaseMeshTopology::getNbQuads); c.def("getNbTetras", &BaseMeshTopology::getNbTetras); c.def("getEdge", - [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { const auto & e = self.getEdge(index); return {{e[0], e[1]}}; }, @@ -78,7 +80,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getTriangle", - [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { const auto & t = self.getTriangle(index); return {{t[0], t[1], t[2]}}; }, @@ -87,7 +89,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getQuad", - [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology &self, const sofa::Index & index) -> std::array { const auto & q = self.getQuad(index); return {{q[0], q[1], q[2], q[3]}}; }, @@ -96,7 +98,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getTetrahedron", - [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { const auto & n = self.getTetrahedron(index); return {{n[0], n[1], n[2], n[3]}}; }, @@ -105,7 +107,7 @@ void moduleAddBaseMeshTopology(py::module& m) { ); c.def("getHexahedron", - [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { const auto & n = self.getHexahedron(index); return {{n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7]}}; }, @@ -113,6 +115,24 @@ void moduleAddBaseMeshTopology(py::module& m) { "Returns the vertices of Hexahedron at index." ); + c.def("getPrism", + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + const auto & n = self.getPrism(index); + return {{n[0], n[1], n[2], n[3], n[4], n[5]}}; + }, + py::arg("index"), + "Returns the vertices of Prism at index." + ); + + c.def("getPyramid", + [] (BaseMeshTopology & self, const sofa::Index & index) -> std::array { + const auto & n = self.getPyramid(index); + return {{n[0], n[1], n[2], n[3], n[4]}}; + }, + py::arg("index"), + "Returns the vertices of Pyramid at index." + ); + c.def("getLocalEdgesInTetrahedron", [] (const BaseMeshTopology & self, const sofa::Index & index) -> std::array { const auto & e = self.getLocalEdgesInTetrahedron(index); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h index b85eed1f0..3908e6044 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_BaseObject.h @@ -21,12 +21,10 @@ #pragma once #include +#include -namespace sofa::core::objectmodel { -class BaseObject; -} - -namespace sofapython3 { +namespace sofapython3 +{ pybind11::object getItem(const sofa::core::objectmodel::BaseObject & self, const std::string& path); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp index 8e87c3bf4..0f26c7656 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp @@ -68,7 +68,7 @@ void Controller_Trampoline::reinit() /// If a method named "methodName" exists in the python controller, /// methodName is called, with the Event's dict as argument -void Controller_Trampoline::callScriptMethod( +bool Controller_Trampoline::callScriptMethod( const py::object& self, Event* event, const std::string & methodName) { if(f_printLog.getValue()) @@ -81,8 +81,13 @@ void Controller_Trampoline::callScriptMethod( if( py::hasattr(self, methodName.c_str()) ) { py::object fct = self.attr(methodName.c_str()); - fct(PythonFactory::toPython(event)); + py::object result = fct(PythonFactory::toPython(event)); + if(result.is_none()) + return false; + + return py::cast(result); } + return false; } void Controller_Trampoline::handleEvent(Event* event) @@ -95,13 +100,17 @@ void Controller_Trampoline::handleEvent(Event* event) { py::object fct = self.attr(name.c_str()); if (PyCallable_Check(fct.ptr())) { - callScriptMethod(self, event, name); + bool isHandled = callScriptMethod(self, event, name); + if(isHandled) + event->setHandled(); return; } } /// Is the fallback method available. - callScriptMethod(self, event, "onEvent"); + bool isHandled = callScriptMethod(self, event, "onEvent"); + if(isHandled) + event->setHandled(); }); } diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h index dee81152f..d1bd91664 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h @@ -50,7 +50,7 @@ class Controller_Trampoline : public Controller std::string getClassName() const override; private: - void callScriptMethod(const pybind11::object& self, sofa::core::objectmodel::Event* event, + bool callScriptMethod(const pybind11::object& self, sofa::core::objectmodel::Event* event, const std::string& methodName); }; diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp index 85f2461a7..e58068f07 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_ForceField.cpp @@ -147,36 +147,39 @@ namespace sofapython3 py::object ret = _addKToMatrix(mparams, nNodes, nDofs); + if(!py::isinstance(ret)) + { + throw py::type_error("Can't read return value of AddKToMatrix. A numpy array is expected"); + } + // if ret is numpy array - if(py::isinstance(ret)) + auto r = py::cast(ret); + if (r.ndim() == 3 && r.shape(2) == 1) { - auto r = py::cast(ret); - if (r.ndim() == 3 && r.shape(2) == 1) + // read K as a plain 2D matrix + auto kMatrix = r.unchecked(); + for (size_t x = 0 ; x < size_t(kMatrix.shape(0)) ; ++x) { - // read K as a plain 2D matrix - auto kMatrix = r.unchecked(); - for (size_t x = 0 ; x < size_t(kMatrix.shape(0)) ; ++x) + for (size_t y = 0 ; y < size_t(kMatrix.shape(1)) ; ++y) { - for (size_t y = 0 ; y < size_t(kMatrix.shape(1)) ; ++y) - { - mat->add(int(offset + x), int(offset + y), kMatrix(x,y, 0)); - } + mat->add(int(offset + x), int(offset + y), kMatrix(x,y, 0)); } } - else if (r.ndim() == 2 && r.shape(1) == 3) - { - // consider ret to be a list of tuples [(i,j,[val])] - auto kMatrix = r.unchecked(); - for (auto x = 0 ; x < kMatrix.shape(0) ; ++x) - { - mat->add(int(offset + size_t(kMatrix(x,0))), int(offset + size_t(kMatrix(x,1))), kMatrix(x,2)); - } - } - else + } + else if (r.ndim() == 2 && r.shape(1) == 3) + { + // consider ret to be a list of tuples [(i,j,[val])] + auto kMatrix = r.unchecked(); + for (auto x = 0 ; x < kMatrix.shape(0) ; ++x) { - throw py::type_error("Can't read return value of AddKToMatrix. The method should return either a plain 2D matrix or a vector of tuples (i, j, val)"); + mat->add(int(offset + size_t(kMatrix(x,0))), int(offset + size_t(kMatrix(x,1))), kMatrix(x,2)); } } + else + { + throw py::type_error("Can't read return value of AddKToMatrix. The method should return either a plain 2D matrix or a vector of tuples (i, j, val)"); + } + } diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp index df26021d6..a290c1b58 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp @@ -723,5 +723,18 @@ void moduleAddNode(py::module &m) { p.def("sendEvent", &sendEvent, sofapython3::doc::sofa::core::Node::sendEvent); p.def("computeEnergy", &computeEnergy, sofapython3::doc::sofa::core::Node::computeEnergy); + p.def("__enter__", [](py::object self) + { + if(pybind11::hasattr(self, "onCtxEnter")) + self.attr("onCtxEnter")(self); + return self; + }); + + p.def("__exit__", + [](py::object self, py::object type, py::object value, py::object traceback) + { + if(pybind11::hasattr(self, "onCtxExit")) + self.attr("onCtxExit")(self, type, value, traceback); + }); } } /// namespace sofapython3 diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h index 4caeb56c6..12761f47f 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node_doc.h @@ -48,7 +48,7 @@ static auto Class = # Add a mechanical component to MyNode n.addObject("MechanicalObject", name="dofs") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.print(root) The child nodes, components and parents can be accessed using generator attributes. diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp index 18da1e449..b7fd649c3 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_TaskScheduler.cpp @@ -19,8 +19,8 @@ ******************************************************************************/ #include -#include -#include +#include +#include #include diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp index 3e4612864..f56b278ea 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Submodule_Core.cpp @@ -100,7 +100,7 @@ PYBIND11_MODULE(Core, core) # Add a mechanical component to MyNode n.addObject("MechanicalObject", name="dofs") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.print(root) )doc"; diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp index cc191ccf2..945744ecd 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Binding_SceneCheck.cpp @@ -28,7 +28,6 @@ namespace py { using namespace pybind11; } void moduleAddSceneCheck(pybind11::module &m) { - // create a python binding for the C++ class LinearSpring from SofaDeformable py::class_> s (m, "SceneCheck", sofapython3::doc::simulation::SceneCheckClass); diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h index d03d9d3f2..8d5839f05 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Simulation/Submodule_Simulation_doc.h @@ -37,7 +37,7 @@ static auto Class = SofaRuntime.importPlugin("SofaComponentAll") n = Sofa.Core.Node("MyNode") - Sofa.Simulation.init(n) + Sofa.Simulation.initRoot(n) Sofa.Simulation.print(n) )"; static auto print = diff --git a/bindings/Sofa/tests/Core/BaseData.py b/bindings/Sofa/tests/Core/BaseData.py index e4b1e418f..bd3cdf0f5 100644 --- a/bindings/Sofa/tests/Core/BaseData.py +++ b/bindings/Sofa/tests/Core/BaseData.py @@ -255,7 +255,7 @@ def test_DataAsContainerNumpyArray_testIsDirtyOnDoubleAccess_(self): root.addObject("PointSetTopologyContainer", points=[[0, 0, 0], [1, 0, 0]]) modifier = root.addObject("PointSetTopologyModifier") mo = root.addObject("MechanicalObject") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) modifier.addPoints(10, True) self.assertEqual(len(mo.position), 12) @@ -269,7 +269,7 @@ def test_DataAsContainerNumpyArray_testIsDirtyOnDoubleWriteAccess_(self): root.addObject("PointSetTopologyContainer", points=[[0, 0, 0], [1, 0, 0]]) modifier = root.addObject("PointSetTopologyModifier") mo = root.addObject("MechanicalObject") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) modifier.addPoints(10, True) with mo.position.writeable() as w: @@ -283,7 +283,7 @@ def test_DataAsContainerNumpyArray_(self): root = create_scene("rootNode") v = numpy.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3]]) c = root.addObject("MechanicalObject", name="t", position=v.tolist()) - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) numpy.testing.assert_array_equal(c.position.array(), v) diff --git a/bindings/Sofa/tests/Core/Controller.py b/bindings/Sofa/tests/Core/Controller.py index 2ab698434..a9c09625b 100644 --- a/bindings/Sofa/tests/Core/Controller.py +++ b/bindings/Sofa/tests/Core/Controller.py @@ -77,7 +77,7 @@ def test_events(self): self.assertTrue( hasattr(controller, "iterations") ) - Sofa.Simulation.init(node) + Sofa.Simulation.initRoot(node) for i in range(10): Sofa.Simulation.animate(node, 0.01) diff --git a/bindings/Sofa/tests/Core/Events.py b/bindings/Sofa/tests/Core/Events.py index 3e5cf83ec..2281f0280 100644 --- a/bindings/Sofa/tests/Core/Events.py +++ b/bindings/Sofa/tests/Core/Events.py @@ -42,10 +42,10 @@ def test_events(self): node.addObject("RequiredPlugin", name="Sofa.Component.AnimationLoop") node.addObject("RequiredPlugin", name="Sofa.Component.Constraint.Lagrangian.Solver") node.addObject("FreeMotionAnimationLoop", name="loop") - node.addObject("ProjectedGaussSeidelConstraintSolver", name="constraintSolver") + node.addObject("BlockGaussSeidelConstraintSolver", name="constraintSolver") controller = node.addObject( MyController() ) - Sofa.Simulation.init(node) + Sofa.Simulation.initRoot(node) for i in range(10): Sofa.Simulation.animate(node, 0.01) diff --git a/bindings/Sofa/tests/Core/ForceField.py b/bindings/Sofa/tests/Core/ForceField.py index 7a83fbe13..da429b64c 100644 --- a/bindings/Sofa/tests/Core/ForceField.py +++ b/bindings/Sofa/tests/Core/ForceField.py @@ -67,7 +67,7 @@ def test_0_explicit(self): use_iterative_solver=False) # do some steps here - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) for i in range(0, 100): Sofa.Simulation.animate(root, root.dt.value) @@ -80,7 +80,7 @@ def test_1_implicit_iterative(self): root = rssffScene(use_implicit_scheme=True, use_iterative_solver=True) # do some steps here - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) for i in range(0, 100): Sofa.Simulation.animate(root, root.dt.value) @@ -93,7 +93,7 @@ def test_2_implicit_direct(self): root = rssffScene(use_implicit_scheme=True, use_iterative_solver=False) # do some steps here - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) for i in range(0, 100): Sofa.Simulation.animate(root, root.dt.value) @@ -108,13 +108,14 @@ def simulate_beam(linear_solver_template): root.addObject('DefaultAnimationLoop') - root.addObject('RequiredPlugin', name='Sofa.Component.Topology.Container.Grid') - root.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') - root.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') - root.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') root.addObject('RequiredPlugin', name='Sofa.Component.Constraint.Projective') - root.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') + root.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') + root.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') root.addObject('RequiredPlugin', name='Sofa.Component.Mass') + root.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') + root.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') + root.addObject('RequiredPlugin', name='Sofa.Component.StateContainer') + root.addObject('RequiredPlugin', name='Sofa.Component.Topology.Container.Grid') root.addObject('EulerImplicitSolver', rayleighStiffness="0.1", rayleighMass="0.1") root.addObject('SparseLDLSolver', template=linear_solver_template) @@ -126,7 +127,7 @@ def simulate_beam(linear_solver_template): root.addObject('FixedConstraint', indices="@box.indices") root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/Sofa/tests/Core/Mass.py b/bindings/Sofa/tests/Core/Mass.py index b5ed4cdea..5de8bb5a3 100644 --- a/bindings/Sofa/tests/Core/Mass.py +++ b/bindings/Sofa/tests/Core/Mass.py @@ -34,7 +34,7 @@ def simulate_beam(linear_solver_template): root.addObject('FixedConstraint', indices="@box.indices") root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, 0.0001) return root diff --git a/bindings/SofaRuntime/CMakeLists.txt b/bindings/SofaRuntime/CMakeLists.txt index 196701e17..52c4d80e8 100644 --- a/bindings/SofaRuntime/CMakeLists.txt +++ b/bindings/SofaRuntime/CMakeLists.txt @@ -2,10 +2,12 @@ project(Bindings.SofaRuntime) set(SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp ) set(HEADER_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/PythonMessageHandler.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer_doc.h ) diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp index c501fdb6b..2c6636070 100644 --- a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Module_SofaRuntime.cpp @@ -62,7 +62,7 @@ using sofapython3::SceneLoaderPY3; #include using sofa::helper::logging::MessageDispatcher; using sofa::helper::logging::MainPerComponentLoggingMessageHandler; -using sofa::helper::logging::MainConsoleMessageHandler; +#include #include #include @@ -155,9 +155,9 @@ PYBIND11_MODULE(SofaRuntime, m) { m.def("init", []() { MessageDispatcher::clearHandlers(); - MessageDispatcher::addHandler(&MainConsoleMessageHandler::getInstance()); + MessageDispatcher::addHandler(&MainPythonMessageHandler::getInstance()); MessageDispatcher::addHandler(&MainPerComponentLoggingMessageHandler::getInstance()); - }); + }, "redirect SOFA messages to Python's sys.stdout"); m.add_object("DataRepository", py::cast(&sofa::helper::system::DataRepository)); m.add_object("PluginRepository", py::cast(&sofa::helper::system::PluginRepository)); diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp new file mode 100644 index 000000000..3bfc9629b --- /dev/null +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.cpp @@ -0,0 +1,94 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#include +#include +#include + +namespace sofapython3 +{ + +namespace +{ + +using namespace std::string_literals; +const std::string red { "\033[31m"s }; +const std::string green { "\033[32m"s }; +const std::string orange { "\033[38;5;214m"s }; +const std::string magenta { "\033[35m"s }; +const std::string blue { "\033[34m"s }; +const std::string reset { "\033[0m"s }; + +std::string format(const std::string& type, const std::string& color) +{ + return color + "[" + type + "] " + reset; +}; + +const std::string& getPrefixText(sofa::helper::logging::Message::Type type) +{ + static const std::string advice = format("SUGGESTION", green); + static const std::string deprecated = format("DEPRECATED", orange); + static const std::string warning = format("WARNING", orange); + static const std::string info = format("INFO", green); + static const std::string error = format("ERROR", red); + static const std::string fatal = format("FATAL", magenta); + static const std::string empty = format("EMPTY", reset); + static const std::string nothing{}; + + switch (type) + { + case sofa::helper::logging::Message::Advice : return advice; + case sofa::helper::logging::Message::Deprecated : return deprecated; + case sofa::helper::logging::Message::Warning : return warning; + case sofa::helper::logging::Message::Info : return info; + case sofa::helper::logging::Message::Error : return error; + case sofa::helper::logging::Message::Fatal : return fatal; + case sofa::helper::logging::Message::TEmpty : return empty; + + case sofa::helper::logging::Message::TypeCount: + return nothing; + } +} + +} + +namespace py { using namespace pybind11; } + +void PythonMessageHandler::process(sofa::helper::logging::Message &m) +{ + if (!m.sender().empty()) + { + py::print(getPrefixText(m.type()), format(m.sender(), blue), m.messageAsString()); + } + else + { + py::print(getPrefixText(m.type()), m.messageAsString()); + } +} + +PythonMessageHandler& MainPythonMessageHandler::getInstance() +{ + static PythonMessageHandler s_instance; + return s_instance; +} + +} // namespace sofapython3 + diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.h b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.h new file mode 100644 index 000000000..fd1636f13 --- /dev/null +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/PythonMessageHandler.h @@ -0,0 +1,41 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#pragma once + +#include + +namespace sofapython3 +{ + +class PythonMessageHandler : public sofa::helper::logging::MessageHandler +{ +public: + PythonMessageHandler() = default; + void process(sofa::helper::logging::Message& m) override ; +}; + +class MainPythonMessageHandler +{ +public: + static PythonMessageHandler& getInstance() ; +}; +} // namespace sofapython3 diff --git a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp index b18d88a01..c6f447228 100644 --- a/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp +++ b/bindings/SofaRuntime/src/SofaPython3/SofaRuntime/Timer/Submodule_Timer.cpp @@ -32,23 +32,38 @@ using sofa::helper::AdvancedTimer; namespace sofapython3 { +/** + * @brief Converts AdvancedTimer records to a Python dictionary structure + * + * This function processes the timer records and builds a hierarchical Python dictionary + * that represents the timer data in a format that's easy to use in Python. + * + * @param id The timer ID to get records for + * @return A Python dictionary representing the timer records + */ py::dict getRecords(const std::string & id) { using sofa::helper::Record; using sofa::helper::system::thread::ctime_t; - using sofa::helper::system::thread::CTime; - static auto timer_freq = CTime::getTicksPerSec(); - auto getTime = [](ctime_t t) + auto getTime = [](ctime_t t, ctime_t referenceTime) { - return 1000. * t / timer_freq; + constexpr double nbMillisecPerSec = 1000.; + return nbMillisecPerSec * sofa::helper::system::thread::CTime::toSecond(t - referenceTime); }; const auto records = AdvancedTimer::getRecords(id); - std::stack tokens; - py::dict token, token_temp; - tokens.push(token); - ctime_t t0; + // Stack of dictionaries that represents the hierarchical structure of timer records + // Each element in the stack corresponds to a different level in the timer hierarchy. + std::stack hierarchyStack; + + // Current dictionary being processed at the top of the stack + // Represents the most recently created level in the timer hierarchy. + py::dict currentLevel; + + py::dict token_temp; + hierarchyStack.push(currentLevel); + std::optional referenceTime; for (const auto& r : records) { @@ -57,97 +72,67 @@ py::dict getRecords(const std::string & id) { case Record::RNONE: break; case Record::RBEGIN: // Timer begins - token = tokens.top(); - if (token.contains(r.label.c_str())) + case Record::RSTEP_BEGIN: // Step begins + currentLevel = hierarchyStack.top(); + if (currentLevel.contains(r.label.c_str())) { - if (py::list::check_(token[r.label.c_str()])) + if (py::list::check_(currentLevel[r.label.c_str()])) { token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; + py::list(currentLevel[r.label.c_str()]).append(token_temp); + currentLevel = token_temp; } - else if (py::dict::check_(token[r.label.c_str()])) + else if (py::dict::check_(currentLevel[r.label.c_str()])) { - token_temp = token[r.label.c_str()]; - token[r.label.c_str()] = py::list(); - py::list(token[r.label.c_str()]).append(token_temp); + token_temp = currentLevel[r.label.c_str()]; + currentLevel[r.label.c_str()] = py::list(); + py::list(currentLevel[r.label.c_str()]).append(token_temp); token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; + py::list(currentLevel[r.label.c_str()]).append(token_temp); + currentLevel = token_temp; } else { - msg_error("Timer::getRecords") << "Got an unexpected token of type '" << std::string(py::str(token.get_type())) << "'."; + msg_error("Timer::getRecords") << "Got an unexpected token of type '" << std::string(py::str(currentLevel.get_type())) << "'."; break; } } else { - token[r.label.c_str()] = py::dict(); - token = token[r.label.c_str()]; + // Creating a new level in the hierarchy for the current timer label + currentLevel[r.label.c_str()] = py::dict(); + // Update the current level to the one just added + currentLevel = currentLevel[r.label.c_str()]; } - t0 = r.time; - token["start_time"] = getTime(r.time - t0); - tokens.push(token); - break; - case Record::REND: // Timer ends - token = tokens.top(); - token["end_time"] = getTime(r.time - t0); - token["total_time"] = getTime(r.time - t0) - py::cast(token["start_time"]); - tokens.pop(); - break; - case Record::RSTEP_BEGIN: // Step begins - token = tokens.top(); - if (token.contains(r.label.c_str())) + if (r.type == Record::RBEGIN) { - if (py::list::check_(token[r.label.c_str()])) - { - token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; - } - else if (py::dict::check_(token[r.label.c_str()])) - { - token_temp = token[r.label.c_str()]; - token[r.label.c_str()] = py::list(); - py::list(token[r.label.c_str()]).append(token_temp); - token_temp = py::dict(); - py::list(token[r.label.c_str()]).append(token_temp); - token = token_temp; - } - else - { - msg_error("Timer::getRecords") << "Got an unexpected token of type '" << std::string(py::str(token.get_type())) << "'."; - break; - } + referenceTime = r.time; } - else + if (!referenceTime.has_value()) { - token[r.label.c_str()] = py::dict(); - token = token[r.label.c_str()]; + msg_error("Timer::getRecords") << "Reference time not set."; + break; } - token["start_time"] = getTime(r.time - t0); - tokens.push(token); + currentLevel["start_time"] = getTime(r.time, *referenceTime); + hierarchyStack.push(currentLevel); break; + case Record::REND: // Timer ends case Record::RSTEP_END: // Step ends - token = tokens.top(); - token["end_time"] = getTime(r.time - t0); - token["total_time"] = getTime(r.time - t0) - py::cast(token["start_time"]); - tokens.pop(); + currentLevel = hierarchyStack.top(); + currentLevel["end_time"] = getTime(r.time, *referenceTime); + currentLevel["total_time"] = getTime(r.time, *referenceTime) - py::cast(currentLevel["start_time"]); + hierarchyStack.pop(); break; case Record::RVAL_SET: // Sets a value - token = tokens.top(); - token[r.label.c_str()] = r.val; - break; case Record::RVAL_ADD: // Sets a value - token = tokens.top(); - token[r.label.c_str()] = r.val; + currentLevel = hierarchyStack.top(); + currentLevel[r.label.c_str()] = r.val; break; default: - token = tokens.top(); - token[r.label.c_str()] = py::list(); - token = token[r.label.c_str()]; - token["start_time"] = r.time; + currentLevel = hierarchyStack.top(); + currentLevel[r.label.c_str()] = py::list(); + currentLevel = currentLevel[r.label.c_str()]; + currentLevel["start_time"] = r.time; break; } } @@ -155,28 +140,28 @@ py::dict getRecords(const std::string & id) { // There should be two remaining records: Top level "record" + "timer starts". The "timer starts" record remains in // the stack since we normally get the records before the timer ends (ending the timer in Sofa destroys the // records...) - if (tokens.size() == 2) + if (hierarchyStack.size() == 2) { - token = tokens.top(); - tokens.pop(); + currentLevel = hierarchyStack.top(); + hierarchyStack.pop(); } - else if (tokens.size() == 1) + else if (hierarchyStack.size() == 1) { // This should not happen unless we successfully got the timer records AFTER the timer has ends, which would mean // that Sofa's advanced timer has improved, let not warn the user for that. - token = tokens.top(); + currentLevel = hierarchyStack.top(); } // Pop the last token ("records") - tokens.pop(); + hierarchyStack.pop(); // The stack should be empty by now - if (!tokens.empty()) + if (!hierarchyStack.empty()) { - msg_error("Timer::getRecords") << "Records stack leaked."; + msg_error("Timer::getRecords") << "Records stack leaked (" << hierarchyStack.size() << " elements)."; } - return token; + return currentLevel; } py::module addSubmoduleTimer(py::module &m) diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index 33991fa68..aef3b6e51 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -38,7 +38,7 @@ 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx_search.extension', - 'sphinx_tabs.tabs', + 'sphinx_design.tabs', ] import sys diff --git a/docs/sphinx/source/content/Compilation.rst b/docs/sphinx/source/content/Compilation.rst index 4cb10472c..838e51200 100644 --- a/docs/sphinx/source/content/Compilation.rst +++ b/docs/sphinx/source/content/Compilation.rst @@ -18,9 +18,9 @@ Prerequisites The compilation of SofaPython3 requires the installation of some dependencies. The following table lists all of those prerequisites. -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu .. list-table:: :widths: 15 10 30 45 @@ -45,7 +45,7 @@ prerequisites. - - .. tab:: MacOS + .. tab-item:: MacOS .. list-table:: :widths: 15 10 30 45 @@ -70,7 +70,7 @@ prerequisites. - - .. tab:: Windows + .. tab-item:: Windows .. list-table:: :widths: 15 10 30 45 diff --git a/docs/sphinx/source/content/FirstSteps.rst b/docs/sphinx/source/content/FirstSteps.rst index 3e73b5b2c..a0b9ad014 100644 --- a/docs/sphinx/source/content/FirstSteps.rst +++ b/docs/sphinx/source/content/FirstSteps.rst @@ -166,8 +166,9 @@ We first propose to add a visual grid, in order to see things more clearly. To d Now, we create a new child node, in order to add the general configuration of the scene : required plugins (here SofaPython3) and other tools (like a system of axes). -.. code-block:: python - def createScene(rootNode): +.. code-block:: python + + def createScene(rootNode): import SofaRuntime # Make sure to load all necessary libraries SofaRuntime.importPlugin("Sofa.Component.Visual") @@ -182,6 +183,7 @@ Now, we create a new child node, in order to add the general configuration of th Finally, we add the sphere itself, which consists of two parts : the mechanical representation and the visual representation of the sphere: .. code-block:: python + def createScene(rootNode): import SofaRuntime # Make sure to load all necessary libraries @@ -312,7 +314,7 @@ We first add a collision model for the scene in general, that is stating how a c # Collision pipeline rootNode.addObject('CollisionPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') @@ -435,7 +437,7 @@ Here is the entire code of the scene : # Collision pipeline rootNode.addObject('CollisionPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') diff --git a/docs/sphinx/source/content/Installation.rst b/docs/sphinx/source/content/Installation.rst index 41d76b6a1..4d1aef413 100644 --- a/docs/sphinx/source/content/Installation.rst +++ b/docs/sphinx/source/content/Installation.rst @@ -21,9 +21,9 @@ Get python installed First, make sure you have the same version of python installed on your computer as the one that is used in the binary version. -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -40,7 +40,7 @@ First, make sure you have the same version of python installed on your computer sudo apt install libopengl0 - .. tab:: MacOS + .. tab-item:: MacOS Run in a terminal: @@ -63,7 +63,7 @@ First, make sure you have the same version of python installed on your computer pip3 install numpy - .. tab:: Windows + .. tab-item:: Windows Download and install `Python 3.12 64bit `_ @@ -97,9 +97,9 @@ using python3 Before running your simulations, you must make sure to define the following environment variables: -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -108,7 +108,7 @@ Before running your simulations, you must make sure to define the following envi export SOFA_ROOT=/path/to/SOFA_install export PYTHONPATH=/path/to/SofaPython3/lib/python3/site-packages:$PYTHONPATH - .. tab:: MacOS + .. tab-item:: MacOS Run in a terminal: @@ -119,7 +119,7 @@ Before running your simulations, you must make sure to define the following envi export PATH="/usr/local/opt/python@3.12/bin/:$PATH" - .. tab:: Windows + .. tab-item:: Windows * Create a system variable **SOFA_ROOT** and set it to ```` * Create a system variable **PYTHON_ROOT** and set it to ```` diff --git a/docs/sphinx/source/content/UsingThePlugin.rst b/docs/sphinx/source/content/UsingThePlugin.rst index 659898fee..d076d7dce 100644 --- a/docs/sphinx/source/content/UsingThePlugin.rst +++ b/docs/sphinx/source/content/UsingThePlugin.rst @@ -12,9 +12,9 @@ Prerequisites If you downloaded and installed SOFA and its headers from the `SOFA website `_, make sure to have python3.10 installed on your computer. -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -31,7 +31,7 @@ If you downloaded and installed SOFA and its headers from the `SOFA website `_ @@ -98,9 +98,9 @@ Within a python3 interpreter Before running your simulations, you must make sure to define the following environment variables: -.. tabs:: +.. tab-set:: - .. tab:: Ubuntu + .. tab-item:: Ubuntu Run in a terminal: @@ -109,7 +109,7 @@ Before running your simulations, you must make sure to define the following envi export SOFA_ROOT=/path/to/SOFA_install export PYTHONPATH=/path/to/SofaPython3/lib/python3/site-packages:$PYTHONPATH - .. tab:: MacOS + .. tab-item:: MacOS Run in a terminal: @@ -120,7 +120,7 @@ Before running your simulations, you must make sure to define the following envi export PATH="/usr/local/opt/python@3.10/bin/:$PATH" - .. tab:: Windows + .. tab-item:: Windows * Create a system variable **SOFA_ROOT** and set it to ```` * Create a system variable **PYTHON_ROOT** and set it to ```` @@ -188,7 +188,7 @@ Within a python3 interpreter, your simulation requires more than only the ``crea createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Run as many simulation steps (here 10 steps are computed) for iteration in range(10): @@ -219,7 +219,7 @@ By structuring your scripts this way, you get the advantage to have a script loa createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") import SofaImGui @@ -362,7 +362,7 @@ We first add a collision model for the scene in general, that is stating how a c # Collision pipeline rootNode.addObject('DefaultPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') @@ -429,7 +429,7 @@ Here is the entire code of the scene : createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") Sofa.Gui.GUIManager.Init("myscene", "imgui") @@ -473,7 +473,7 @@ Here is the entire code of the scene : # Collision pipeline rootNode.addObject('DefaultPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase') rootNode.addObject('BVHNarrowPhase') rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') @@ -596,7 +596,7 @@ In the same way, Data can be modified (write access) using the ``.value`` access createScene(root) # Once defined, initialization of the scene graph - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) # Run the simulation for 10 steps for iteration in range(10): diff --git a/docs/sphinx/source/requirements.txt b/docs/sphinx/source/requirements.txt index 20b945d12..ffbfba828 100644 --- a/docs/sphinx/source/requirements.txt +++ b/docs/sphinx/source/requirements.txt @@ -3,5 +3,5 @@ sphinxcontrib-contentui readthedocs-sphinx-search sphinx-autodoc-typehints sphinx-rtd-theme -sphinx-tabs +sphinx-design sphinx-autosummary-autocollect diff --git a/examples/.scene-tests b/examples/.scene-tests index 10310ecfa..27851cca0 100644 --- a/examples/.scene-tests +++ b/examples/.scene-tests @@ -10,3 +10,9 @@ iterations "access_stiffness_matrix.py" "3" # Ignore additional examples ignore "additional-examples/.*" + +# Ignore jax examples +ignore "jax/*" + +#Ignore meshing examples +ignore "meshing/*" diff --git a/examples/CCDIntersection.py b/examples/CCDIntersection.py new file mode 100644 index 000000000..ffe61360c --- /dev/null +++ b/examples/CCDIntersection.py @@ -0,0 +1,159 @@ + + +def main(): + # Required import for python + import Sofa + import SofaImGui + + root = Sofa.Core.Node("root") + createScene(root) + Sofa.Simulation.initRoot(root) + + for iteration in range(10): + Sofa.Simulation.animate(root, root.dt.value) + print(root.PlateMecha.Visual.BoxROI.trianglesInROI.value) + +def createScene(root_node): + root_node.name = "root" + root_node.dt = 0.1 + root_node.gravity = [-0.981, 0, 0] + + plugins = root_node.addChild('plugins') + + plugins.addObject('RequiredPlugin', name="MultiThreading") + plugins.addObject('RequiredPlugin', name="Sofa.Component.AnimationLoop") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Detection.Algorithm") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Detection.Intersection") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Geometry") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Collision.Response.Contact") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Constraint.Lagrangian.Correction") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Constraint.Lagrangian.Solver") + plugins.addObject('RequiredPlugin', name="Sofa.Component.IO.Mesh") + plugins.addObject('RequiredPlugin', name="Sofa.Component.LinearSolver.Direct") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Mapping.Linear") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Mass") + plugins.addObject('RequiredPlugin', name="Sofa.Component.ODESolver.Backward") + plugins.addObject('RequiredPlugin', name="Sofa.Component.SolidMechanics.FEM.Elastic") + plugins.addObject('RequiredPlugin', name="Sofa.Component.StateContainer") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Topology.Container.Dynamic") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Topology.Container.Grid") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Topology.Mapping") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Visual") + plugins.addObject('RequiredPlugin', name="Sofa.GL.Component.Rendering3D") + plugins.addObject('RequiredPlugin', name="Sofa.Component.Engine.Select") + plugins.addObject('RequiredPlugin', name="Sofa.GUI.Component") + + root_node.addObject('VisualStyle', displayFlags="showVisual") + root_node.addObject('ConstraintAttachButtonSetting') + root_node.addObject('FreeMotionAnimationLoop', computeBoundingBox=False) + root_node.addObject('BlockGaussSeidelConstraintSolver', maxIterations=1000, tolerance=1.0e-6) + root_node.addObject('CollisionPipeline', name="Pipeline") + root_node.addObject('ParallelBruteForceBroadPhase', name="BroadPhase") + root_node.addObject('ParallelBVHNarrowPhase', name="NarrowPhase") + root_node.addObject('CollisionResponse', name="ContactManager", response="FrictionContactConstraint", responseParams="mu=0.4") + root_node.addObject('CCDTightInclusionIntersection', name="Intersection", continuousCollisionType="FreeMotion",maxIterations=1000, alarmDistance=0.0, contactDistance=0.0) + # root_node.addObject('NewProximityIntersection', name="Intersection", alarmDistance="0.5", contactDistance="0.0") + + + #Liver mecha + Liver = root_node.addChild('Liver') + + Liver.addObject('EulerImplicitSolver') + Liver.addObject('SparseLDLSolver', name="ldl", template="CompressedRowSparseMatrixMat3x3", parallelInverseProduct=True) + Liver.addObject('MeshGmshLoader', name="meshLoader", filename="mesh/liver.msh", scale3d=[0.2, 0.2, 0.2], rotation=[0, 180, -45], translation=[15.2, -0.15, 0.14]) + Liver.addObject('TetrahedronSetTopologyContainer', name="Container", src='@meshLoader') + Liver.addObject('TetrahedronSetTopologyModifier', name="Modifier") + Liver.addObject('TetrahedronSetGeometryAlgorithms', name="Algorithms") + Liver.addObject('MechanicalObject', name="mstate", template="Vec3d", position="@Container.position", velocity=[-5,0,0]*181) + Liver.addObject('TetrahedronFEMForceField', name="forceField", listening=True, youngModulus=1e2, poissonRatio=0.3, method="large") + Liver.addObject('MeshMatrixMass', totalMass=3) + + surface = Liver.addChild('Surface') + surface.addObject('TriangleSetTopologyContainer', name="Container") + surface.addObject('TriangleSetTopologyModifier', name="Modifier") + surface.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + surface.addObject('Tetra2TriangleTopologicalMapping', input="@../Container", output="@Container", flipNormals=False) + surface.addObject('MechanicalObject', name="dofs", rest_position="@../mstate.rest_position") + surface.addObject('PointCollisionModel', name="Collision", contactDistance=0.0001, color=[0.0, 0.93725490196078, 0.89411764705882, 1]) + surface.addObject('IdentityMapping', name="SurfaceMapping") + + visual = Liver.addChild('Visual') + visual.addObject('TriangleSetTopologyContainer', name="Container") + visual.addObject('TriangleSetTopologyModifier', name="Modifier") + visual.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + visual.addObject('Tetra2TriangleTopologicalMapping', input="@../Container", output="@Container", flipNormals=False) + visual.addObject('OglModel', name="VisualModel", src="@Container", color=[0.5, 0, 0.125, 1]) + visual.addObject('IdentityMapping', name="SurfaceMapping") + + Liver.addObject('LinearSolverConstraintCorrection', linearSolver="@ldl") + + + + #Plate topology + PlateTopo = root_node.addChild('BeamDomainFromGridTopology') + + PlateTopo.addObject('RegularGridTopology', name="HexaTop", n=[3, 10, 10], min=[-0.3, -1.5, -1.5], max=[0.3, 1.5, 1.5]) + tetra_topology = PlateTopo.addChild('TetraTopology') + + tetra_topology.addObject('TetrahedronSetTopologyContainer', name="Container", position="@HexaTop.position") + tetra_topology.addObject('TetrahedronSetTopologyModifier', name="Modifier") + tetra_topology.addObject('Hexa2TetraTopologicalMapping', input="@HexaTop", output="@Container", swapping=True) + + #Plate mecha + PlateMecha = root_node.addChild('PlateMecha') + + PlateMecha.addObject('EulerImplicitSolver') + PlateMecha.addObject('SparseLDLSolver', name="ldl", template="CompressedRowSparseMatrixMat3x3", parallelInverseProduct=True) + PlateMecha.addObject('TetrahedronSetTopologyContainer', name="Container", position="@../BeamDomainFromGridTopology/HexaTop.position", tetrahedra="@../BeamDomainFromGridTopology/TetraTopology/Container.tetrahedra") + PlateMecha.addObject('TetrahedronSetTopologyModifier', name="Modifier") + PlateMecha.addObject('TetrahedronSetGeometryAlgorithms', name="Algorithms") + PlateMecha.addObject('MechanicalObject', name="mstate", template="Vec3d", position="@Container.position", velocity=[15,0,0]*300) + PlateMecha.addObject('TetrahedronFEMForceField', name="forceField", listening=True, youngModulus=2e3, poissonRatio=0.3, method="large") + PlateMecha.addObject('MeshMatrixMass', totalMass=2) + + visual = PlateMecha.addChild('Visual') + visual.addObject('TriangleSetTopologyContainer', name="Container") + visual.addObject('TriangleSetTopologyModifier', name="Modifier") + visual.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + visual.addObject('Tetra2TriangleTopologicalMapping', input="@../Container", output="@Container", flipNormals=False) + visual.addObject('BoxROI', name="FrontCollisionTriangles", box=[0.25, 1.6, 1.6, 0.35, -1.6, -1.6]) + visual.addObject('BoxROI', name="BackCollisionTriangles", box=[-0.35, 1.6, 1.6, -0.25, -1.6, -1.6]) + + visual.addObject('OglModel', name="VisualModel", src="@Container" , color=[0.6, 1, 1, 1]) + visual.addObject('IdentityMapping', name="SurfaceMapping") + + + surface = PlateMecha.addChild('FrontSurface') + surface.addObject('TriangleSetTopologyContainer',triangles="@../Visual/FrontCollisionTriangles.trianglesInROI") + surface.addObject('TriangleSetTopologyModifier', name="Modifier") + surface.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + surface.addObject('MechanicalObject', name="dofs", rest_position="@../mstate.rest_position") + surface.addObject('TriangleCollisionModel', name="Collision",group=1, contactDistance=0.000001, color=[0.94117647058824, 0.93725490196078, 0.89411764705882, 1]) + surface.addObject('IdentityMapping', name="SurfaceMapping") + + + + surface = PlateMecha.addChild('BackCollisionEdges') + surface.addObject('TriangleSetTopologyContainer',triangles="@../Visual/BackCollisionTriangles.trianglesInROI") + surface.addObject('TriangleSetTopologyModifier', name="Modifier") + surface.addObject('TriangleSetGeometryAlgorithms', name="Algorithms") + surface.addObject('MechanicalObject', name="dofs", rest_position="@../mstate.rest_position") + surface.addObject('PointCollisionModel', name="Collision", group=1, contactDistance=0.000001, color=[0.94117647058824, 0.93725490196078, 0.89411764705882, 1]) + surface.addObject('IdentityMapping', name="SurfaceMapping") + + + PlateMecha.addObject('LinearSolverConstraintCorrection', linearSolver="@ldl") + + + + #Ground + Ground = root_node.addChild('Ground') + + Ground.addObject('TriangleSetTopologyContainer', name="FloorTopology", position="-5 -15 -15 -5 -15 15 -5 15 15 -5 15 -15", triangles="0 2 1 0 3 2") + Ground.addObject('MechanicalObject', name="FloorDOF", template="Vec3d") + Ground.addObject('TriangleCollisionModel', name="FloorCM", contactDistance="0.0001", moving="0", simulated="0", color="0.3 0.3 0.3 0.1") + visu = Ground.addChild('Visu') + visu.addObject('OglModel', name="VisualModel", src="@../FloorTopology") + +if __name__ == "__main__": + main() diff --git a/examples/CCDIntersection.py.view b/examples/CCDIntersection.py.view new file mode 100644 index 000000000..b724ddf85 --- /dev/null +++ b/examples/CCDIntersection.py.view @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/MessageHandler.py b/examples/MessageHandler.py new file mode 100644 index 000000000..43d061422 --- /dev/null +++ b/examples/MessageHandler.py @@ -0,0 +1,99 @@ +import Sofa +import Sofa.Helper + +RED = "\033[31m" +ORANGE = "\033[38;5;214m" +GREEN = "\033[32m" +RESET = "\033[0m" + + +class ExempleMessageHandler(Sofa.Helper.MessageHandler): + """A custom message handler that prints messages in the console and counts the number of errors, warnings and infos.""" + + def __init__(self): + super().__init__() + self.num_errors = 0 + self.num_warnings = 0 + self.num_infos = 0 + + def process(self, msg): + """Prints the message in the console and count the number of errors, warnings and infos.""" + if msg['type'] == "Error": + self.print_error(msg['sender'], msg['message']) + self.num_errors += 1 + elif msg['type'] == "Warning": + self.print_warning(msg['sender'], msg['message']) + self.num_warnings += 1 + elif msg['type'] == "Info": + self.print_info(msg['sender'], msg['message']) + self.num_infos += 1 + else: + print(f"{msg['type']} {msg['message']}") + + @staticmethod + def print_error(sender, message): + """Prints a string with [ERROR] in red""" + print(f"🚨{RED}[ERROR]{RESET} [👤{sender}] 📨{message}") + + @staticmethod + def print_warning(sender, message): + """Prints a string with [WARNING] in orange""" + print(f"⚠️{ORANGE}[WARNING]{RESET} [👤{sender}] 📨{message}") + + @staticmethod + def print_info(sender, message): + """Prints a string with [INFO] in green""" + print(f"ℹ️{GREEN}[INFO]{RESET} [👤{sender}] 📨{message}") + + +with ExempleMessageHandler() as msg_handler: + def createScene(root): + + root.addObject("RequiredPlugin", pluginName=[ + 'Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.Visual' + ]) + + root.addObject('VisualStyle', displayFlags="showBehaviorModels showForceFields") + + root.addObject('DefaultAnimationLoop') + root.addObject('DefaultVisualManagerLoop') + + root.addObject('EulerImplicitSolver', rayleighStiffness=0.1, rayleighMass=0.1, printLog=False) + root.addObject('SparseLDLSolver', template="CompressedRowSparseMatrixd") + + root.addObject('MechanicalObject', name="DoFs") + root.addObject('MeshMatrixMass', name="mass", totalMass=320) + root.addObject('RegularGridTopology', name="grid", + nx=4, ny=4, nz=20, xmin=-9, xmax=-6, ymin=0, ymax=3, zmin=0, zmax=19) + root.addObject('BoxROI', name="box", box=[-10, -1, -0.0001, -5, 4, 0.0001]) + root.addObject('FixedProjectiveConstraint', indices="@box.indices") + root.addObject('HexahedronFEMForceField', name="FEM", youngModulus=4000, poissonRatio=0.3, method="large") + + return root + + + def main(): + root = Sofa.Core.Node("root") + createScene(root) + Sofa.Simulation.initRoot(root) + + for iteration in range(10): + Sofa.Simulation.animate(root, root.dt.value) + + print("Simulation is done.") + + print(f"Number of errors: {msg_handler.num_errors}") + print(f"Number of warnings: {msg_handler.num_warnings}") + print(f"Number of infos: {msg_handler.num_infos}") + + + if __name__ == '__main__': + main() diff --git a/examples/RPYC/ExampleManipulatingTheNodeLocally.py b/examples/RPYC/ExampleManipulatingTheNodeLocally.py new file mode 100644 index 000000000..af7e50412 --- /dev/null +++ b/examples/RPYC/ExampleManipulatingTheNodeLocally.py @@ -0,0 +1,97 @@ +import multiprocessing +import threading +import time +import rpyc +from SOFAService import SOFAService, SOFAClient +import Sofa + +if __name__ == "__main__": + + SC = SOFAClient() + SC.start_server(port=18818) + SC.connect_client(port=18818) + + root = SC.sofa_root + + # Need to use getValue and setValue instead of the syntax `root.dt = 0.02` or `root.dt.value` to get the actual value + root.gravity.setValue([0, -9.81, 0]) + root.dt.setValue(0.02) + + # RPyC deal in a strange way with list of strings, which leads to error if you use the syntax where you pass a list of plugin name to required plugin. You need to add one required plugin per plugin for it to work, such as done in xml. + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Detection.Algorithm') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Detection.Intersection') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Geometry') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Collision.Response.Contact') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Constraint.Projective') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.IO.Mesh') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.LinearSolver.Iterative') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Mapping.Linear') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Mass') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.ODESolver.Backward') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.SolidMechanics.FEM.Elastic') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.StateContainer') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Topology.Container.Dynamic') + root.addObject("RequiredPlugin", pluginName='Sofa.Component.Visual') + root.addObject("RequiredPlugin", pluginName='Sofa.GL.Component.Rendering3D') + + root.addObject('DefaultAnimationLoop') + + root.addObject('VisualStyle', displayFlags="showCollisionModels") + root.addObject('CollisionPipeline', name="CollisionPipeline") + root.addObject('BruteForceBroadPhase', name="BroadPhase") + root.addObject('BVHNarrowPhase', name="NarrowPhase") + root.addObject('CollisionResponse', name="CollisionResponse", response="PenalityContactForceField") + root.addObject('DiscreteIntersection') + + # Don't forget that this will be launched in the remote, all files should be on its disk or else, use lambda function to capture data and access theme in this function + root.addObject('MeshOBJLoader', name="LiverSurface", filename="mesh/liver-smooth.obj") + + liver = root.addChild('Liver') + liver.addObject('EulerImplicitSolver', name="cg_odesolver", rayleighStiffness="0.1", rayleighMass="0.1") + liver.addObject('CGLinearSolver', name="linear_solver", iterations="25", tolerance="1e-09", threshold="1e-09") + liver.addObject('MeshGmshLoader', name="meshLoader", filename="mesh/liver.msh") + liver.addObject('TetrahedronSetTopologyContainer', name="topo", src="@meshLoader") + liver.addObject('MechanicalObject', name="dofs", src="@meshLoader") + liver.addObject('TetrahedronSetGeometryAlgorithms', template="Vec3d", name="GeomAlgo") + liver.addObject('DiagonalMass', name="Mass", massDensity="1.0") + liver.addObject('TetrahedralCorotationalFEMForceField', template="Vec3d", name="FEM", method="large", poissonRatio="0.3", youngModulus="3000", computeGlobalMatrix="0") + liver.addObject('FixedProjectiveConstraint', name="FixedConstraint", indices="3 39 64") + + visu = liver.addChild('Visu') + visu.addObject('OglModel', name="VisualModel", src="@../../LiverSurface") + visu.addObject('BarycentricMapping', name="VisualMapping", input="@../dofs", output="@VisualModel") + + surf = liver.addChild('Surf') + surf.addObject('SphereLoader', name="sphereLoader", filename="mesh/liver.sph") + surf.addObject('MechanicalObject', name="spheres", position="@sphereLoader.position") + surf.addObject('SphereCollisionModel', name="CollisionModel", listRadius="@sphereLoader.listRadius") + surf.addObject('BarycentricMapping', name="CollisionMapping", input="@../dofs", output="@spheres") + + SC.init_root() + + # This works only for server and client on the same machine. This tell the server that the passed data path should be copied in shared memory when accessed through the client instead of RPyC. + SC.setup_shared_memory_for_data(["Liver/dofs.position","Liver/Surf/spheres.position"]) + + asynch_step = None + currentTime = 0.0 + while currentTime<0.2: + if(asynch_step is None or asynch_step.ready): + # Time to get data from object + currentTime = SC.sofa_root.getTime() + print(currentTime) + + print(f"This comes with the socket : {SC.sofa_root.Liver.cg_odesolver.name.value}") + print(f"This comes with shared memory : {SC.sofa_root.Liver.Surf.spheres.position.value}") + SC.sofa_root.Liver.Surf.spheres.position.setValue([[0,0,0]]) + SC.sofa_root.Liver.cg_odesolver.printLog.setValue(True) + print(f"This getValue comes with the socket : {SC.sofa_root.Liver.cg_odesolver.name.getValue()}") + print(f"This getValue comes with shared memory : {SC.sofa_root.Liver.Surf.spheres.position.getValue()}") + + # Launch next step + asynch_step = SC.asynch_step() + else: + print("waiting 0.1 sec") + time.sleep(0.1) + + SC.stop_server() + diff --git a/examples/RPYC/SOFAService.py b/examples/RPYC/SOFAService.py index c201a02f8..93f8a898f 100644 --- a/examples/RPYC/SOFAService.py +++ b/examples/RPYC/SOFAService.py @@ -1,3 +1,5 @@ +import os + import rpyc import time import threading @@ -279,7 +281,7 @@ def exposed_build_scene_graph_from_method(self, createScene): """ self.exposed_sofa_root = Sofa.Core.Node("root") createScene(self.exposed_sofa_root) - Sofa.Simulation.initRoot(self.exposed_sofa_root) + self.exposed_init_root() def exposed_build_scene_graph_from_file(self, filename:str): """ @@ -294,7 +296,7 @@ def exposed_build_scene_graph_from_file(self, filename:str): self.exposed_sofa_root = Sofa.Core.Node("root") foo.createScene(self.exposed_sofa_root) - Sofa.Simulation.initRoot(self.exposed_sofa_root) + self.exposed_init_root() def exposed_setup_shared_memory_for_data(self, dataPaths:list[str], delayed=False): @@ -354,17 +356,36 @@ def getSharedMemoryNames(self): """Return list of all data paths currently shared via shared memory.""" return self.sharedPaths + def exposed_init_root(self): + """Initialize the root node.""" + if(not self.exposed_sofa_root.isInitialized()): + Sofa.Simulation.initRoot(self.exposed_sofa_root) def exposed_step_simulation(self): """ Run one step of the simulation. If shared memory hasn’t been set up yet, attempt setup now. """ + Sofa.Simulation.animate(self.exposed_sofa_root, self.exposed_sofa_root.dt.value) + if(not self.sharedMemoryIsSet): self.__internal_setup_shared_memory() + def exposed_get_cwd(self): + """ + Returns current working dir + """ + return os.getcwd() + def exposed_set_cwd(self, dir): + """ + Change the current working dir of the server + Returns old dir,current working dir + """ + oldDir = os.getcwd() + os.chdir(dir) + return oldDir, os.getcwd() ################################################# ### Multithreaded automatic execution methods @@ -382,6 +403,7 @@ def __wait_for_the_animation_to_stop(self): def __simulation_loop(self): """Continuous simulation loop run by background thread.""" + while self.animate: Sofa.Simulation.animate(self.exposed_sofa_root, self.exposed_sofa_root.dt.value) diff --git a/examples/ReadTheDocs_Example.py b/examples/ReadTheDocs_Example.py index b704045f9..5c14e3b31 100644 --- a/examples/ReadTheDocs_Example.py +++ b/examples/ReadTheDocs_Example.py @@ -47,7 +47,7 @@ def createScene(rootNode): rootNode.addObject('CollisionPipeline') rootNode.addObject('FreeMotionAnimationLoop') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") + rootNode.addObject('BlockGaussSeidelConstraintSolver', tolerance="1e-6", maxIterations="1000") rootNode.addObject('BruteForceBroadPhase', name="BroadPhase") rootNode.addObject('BVHNarrowPhase', name="NarrowPhase") rootNode.addObject('RuleBasedContactManager', responseParams="mu="+str(0.0), name='Response', response='FrictionContactConstraint') diff --git a/examples/access_compliance_matrix.py b/examples/access_compliance_matrix.py index 6108643d8..8c80d6efa 100644 --- a/examples/access_compliance_matrix.py +++ b/examples/access_compliance_matrix.py @@ -23,7 +23,7 @@ def createScene(root): ]) root.addObject("FreeMotionAnimationLoop", solveVelocityConstraintFirst=True) - constraint_solver = root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) + constraint_solver = root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) root.addObject("StringMeshCreator", name="loader", resolution="20") root.addObject("EulerImplicitSolver") diff --git a/examples/access_constraint_matrix.py b/examples/access_constraint_matrix.py index aa12c529a..50adf7099 100644 --- a/examples/access_constraint_matrix.py +++ b/examples/access_constraint_matrix.py @@ -22,7 +22,7 @@ def createScene(root): ]) root.addObject("FreeMotionAnimationLoop", solveVelocityConstraintFirst=True) - root.addObject("ProjectedGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) + root.addObject("BlockGaussSeidelConstraintSolver", tolerance=1e-9, maxIterations=1000) root.addObject("StringMeshCreator", name="loader", resolution="20") root.addObject("EulerImplicitSolver") diff --git a/examples/access_contact_forces.py b/examples/access_contact_forces.py index 196ae836d..c857df9a3 100644 --- a/examples/access_contact_forces.py +++ b/examples/access_contact_forces.py @@ -44,7 +44,7 @@ def createScene(root): root.addObject('FreeMotionAnimationLoop') # Constraint solver computing the constraint/contact forces, stored in the constraint space (normal , tangential_1, tangential_2) - constraint_solver = root.addObject('ProjectedGaussSeidelConstraintSolver', maxIterations=1000, tolerance=1e-6, computeConstraintForces=True) + constraint_solver = root.addObject('BlockGaussSeidelConstraintSolver', maxIterations=1000, tolerance=1e-6, computeConstraintForces=True) root.addObject('VisualStyle', displayFlags="showCollisionModels hideVisualModels showForceFields") root.addObject('CollisionPipeline', name="collision_pipeline") diff --git a/examples/access_energy.py b/examples/access_energy.py index bd1cad9c2..07fe88e8d 100644 --- a/examples/access_energy.py +++ b/examples/access_energy.py @@ -34,7 +34,7 @@ def createScene(rootNode, dt=0.01, m=1, g=1, L=100, mu=0): rootNode.addObject('ParallelBVHNarrowPhase') rootNode.addObject('MinProximityIntersection', name='Proximity', alarmDistance='10', contactDistance='0.02') rootNode.addObject('CollisionResponse', name='Response', response='FrictionContactConstraint', responseParams=f'mu={mu}') - rootNode.addObject('ProjectedGaussSeidelConstraintSolver', maxIterations='10', multithreading='true', tolerance='1.0e-3') + rootNode.addObject('BlockGaussSeidelConstraintSolver', maxIterations='10', multithreading='true', tolerance='1.0e-3') boxTranslation = "-20 -0.9 0" rootNode.addObject('MeshOBJLoader', name='Loader-box', filename='mesh/cube.obj', translation=boxTranslation) @@ -82,7 +82,7 @@ def main(): mu = .1 root = Sofa.Core.Node("root") createScene(root, dt=1e-4, m=m, g=g, mu=mu, L=1000) - Sofa.Simulation.init(root) + Sofa.Simulation.initRoot(root) Sofa.Simulation.animate(root, root.dt.value) Ks, Us = [], [] diff --git a/examples/access_mass_matrix.py b/examples/access_mass_matrix.py index b1dd1c4b6..ce37af205 100644 --- a/examples/access_mass_matrix.py +++ b/examples/access_mass_matrix.py @@ -28,15 +28,16 @@ def createScene(root): root.addObject('VisualStyle', displayFlags="showBehaviorModels showForceFields") root.addObject("RequiredPlugin", pluginName=['Sofa.Component.Constraint.Projective', - 'Sofa.Component.Engine.Select', - 'Sofa.Component.LinearSolver.Direct', - 'Sofa.Component.LinearSystem', - 'Sofa.Component.Mass', - 'Sofa.Component.ODESolver.Backward', - 'Sofa.Component.SolidMechanics.FEM.Elastic', - 'Sofa.Component.StateContainer', - 'Sofa.Component.Topology.Container.Grid', - 'Sofa.Component.Visual' + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.LinearSolver.Ordering', + 'Sofa.Component.LinearSystem', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.Visual' ]) root.addObject('DefaultAnimationLoop') @@ -57,8 +58,10 @@ def createScene(root): # This component distributes the matrix contributions to the other systems root.addObject('CompositeLinearSystem', template="CompressedRowSparseMatrixMat3x3d", name="solverSystem", linearSystems="@matrices/system @matrices/M", solverLinearSystem="@matrices/system") - # It is important to define the linear system in the linear solver: the CompositeLinearSystem - root.addObject('SparseLDLSolver', ordering='Natural', template="CompressedRowSparseMatrixMat3x3d", + + root.addObject('NaturalOrderingMethod', name='ordering') + # It is important to configure the solver to work on the CompositeLinearSystem + root.addObject('SparseLDLSolver', orderingMethod='@ordering', template="CompressedRowSparseMatrixMat3x3d", linearSystem="@solverSystem") beam1 = root.addChild('Beam1') diff --git a/examples/access_matrix.py b/examples/access_matrix.py index 3cc33523f..aeb1fa21e 100644 --- a/examples/access_matrix.py +++ b/examples/access_matrix.py @@ -1,9 +1,27 @@ # Required import for python import Sofa import SofaRuntime +from Sofa import SofaLinearSystem from Sofa import SofaLinearSolver from scipy import sparse +def createBeam(root, matrix_type): + node = root.addChild(matrix_type) + node.addObject('EulerImplicitSolver', rayleighStiffness="0.1", rayleighMass="0.1") + linear_system = node.addObject('MatrixLinearSystem', template=matrix_type) + + node.addObject('MechanicalObject', name="DoFs") + node.addObject('UniformMass', name="mass", totalMass="320") + node.addObject('RegularGridTopology', name="grid", nx="4", ny="4", nz="20", xmin="-9", xmax="-6", ymin="0", ymax="3", zmin="0", zmax="19") + node.addObject('BoxROI', name="box", box="-10 -1 -0.0001 -5 4 0.0001") + node.addObject('FixedProjectiveConstraint', indices="@box.indices") + node.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") + + node.addObject(MatrixAccessController('MatrixAccessor', name=f'matrixAccessor{matrix_type}', linear_system=linear_system)) + + return node + + # Function called when the scene graph is being created def createScene(root): @@ -15,26 +33,22 @@ def createScene(root): 'Sofa.Component.Visual' ]) - root.addObject('DefaultAnimationLoop') + root.addObject('DefaultAnimationLoop', parallelODESolving=True) root.addObject('DefaultVisualManagerLoop') - root.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') - root.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') - root.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') - root.addObject('RequiredPlugin', name='Sofa.Component.Constraint.Projective') - root.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') - - root.addObject('EulerImplicitSolver', rayleighStiffness="0.1", rayleighMass="0.1") - linear_solver = root.addObject('SparseLDLSolver', template="CompressedRowSparseMatrixMat3x3d") + plugins = root.addChild('plugins') + plugins.addObject('RequiredPlugin', name='Sofa.Component.Constraint.Projective') + plugins.addObject('RequiredPlugin', name='Sofa.Component.Engine.Select') + plugins.addObject('RequiredPlugin', name='Sofa.Component.LinearSolver.Direct') + plugins.addObject('RequiredPlugin', name='Sofa.Component.LinearSystem') + plugins.addObject('RequiredPlugin', name='Sofa.Component.ODESolver.Backward') + plugins.addObject('RequiredPlugin', name='Sofa.Component.SolidMechanics.FEM.Elastic') - root.addObject('MechanicalObject', name="DoFs") - root.addObject('UniformMass', name="mass", totalMass="320") - root.addObject('RegularGridTopology', name="grid", nx="4", ny="4", nz="20", xmin="-9", xmax="-6", ymin="0", ymax="3", zmin="0", zmax="19") - root.addObject('BoxROI', name="box", box="-10 -1 -0.0001 -5 4 0.0001") - root.addObject('FixedProjectiveConstraint', indices="@box.indices") - root.addObject('HexahedronFEMForceField', name="FEM", youngModulus="4000", poissonRatio="0.3", method="large") + node_crs = createBeam(root, 'CompressedRowSparseMatrixMat3x3d') + node_crs.addObject('SparseLDLSolver', template="CompressedRowSparseMatrixMat3x3d") - root.addObject(MatrixAccessController('MatrixAccessor', name='matrixAccessor', linear_solver=linear_solver)) + node_full = createBeam(root, 'FullMatrix') + node_full.addObject('CGLinearSolver', template="FullMatrix") return root @@ -43,31 +57,34 @@ class MatrixAccessController(Sofa.Core.Controller): def __init__(self, *args, **kwargs): + print('Initialize controller') Sofa.Core.Controller.__init__(self, *args, **kwargs) - self.linear_solver = kwargs.get("linear_solver") + self.linear_system = kwargs.get("linear_system") + print(f'Type linear system: {type(self.linear_system)}') def onAnimateEndEvent(self, event): - system_matrix = self.linear_solver.A() - rhs = self.linear_solver.b() - solution = self.linear_solver.x() + system_matrix = self.linear_system.get_system_matrix() + rhs = self.linear_system.get_rhs_vector() + solution = self.linear_system.get_solution_vector() print('====================================') - print('Global system matrix') + print(f'Global system matrix {self.getName()}') print('====================================') print('dtype: ' + str(system_matrix.dtype)) print('shape: ' + str(system_matrix.shape)) print('ndim: ' + str(system_matrix.ndim)) - print('nnz: ' + str(system_matrix.nnz)) + if hasattr(system_matrix, 'nnz'): #if the matrix is sparse + print('nnz: ' + str(system_matrix.nnz)) print('====================================') - print('System right hand side') + print(f'System right hand side {self.getName()}') print('====================================') print('dtype: ' + str(rhs.dtype)) print('shape: ' + str(rhs.shape)) print('ndim: ' + str(rhs.ndim)) print('====================================') - print('System solution') + print(f'System solution {self.getName()}') print('====================================') print('dtype: ' + str(solution.dtype)) print('shape: ' + str(solution.shape)) diff --git a/examples/advanced_timer.py b/examples/advanced_timer.py index 6551a6a46..dba1bccd0 100644 --- a/examples/advanced_timer.py +++ b/examples/advanced_timer.py @@ -13,17 +13,17 @@ def __init__(self, *args, **kwargs): self.use_sofa_profiler_timer = False def onAnimateBeginEvent(self, event): - if len(Timer.getRecords('Animate')): + if Timer.isEnabled('Animate'): + # The 'Animate' timer that SOFA is supposed to start is already running, we can use it directly self.use_sofa_profiler_timer = True else: + # SOFA did not start the 'Animate' timer (e.g., when batch UI and no computation time sampling): + # we need to start another one manually Timer.setEnabled("cg_timer", True) Timer.begin("cg_timer") def onAnimateEndEvent(self, event): - if self.use_sofa_profiler_timer: - records = Timer.getRecords("Animate") - else: - records = Timer.getRecords("cg_timer") + records = Timer.getRecords("Animate" if self.use_sofa_profiler_timer else "cg_timer") step_time = records['solve']['Mechanical (meca)']['total_time'] print(f"Step took {step_time:.2f} ms") @@ -43,19 +43,19 @@ def createScene(root): root.dt = 0.01 # List of required plugins - root.addObject("RequiredPlugin", pluginName=['Sofa.Component.Constraint.Projective', - 'Sofa.Component.Engine.Select', - 'Sofa.Component.LinearSolver.Iterative', - 'Sofa.Component.MechanicalLoad', - 'Sofa.Component.ODESolver.Backward', - 'Sofa.Component.SolidMechanics.FEM.Elastic', - 'Sofa.Component.StateContainer', - 'Sofa.Component.Topology.Container.Dynamic', - 'Sofa.Component.Topology.Container.Grid', - 'Sofa.Component.Visual' + root.addObject("RequiredPlugin", pluginName=[ + 'Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Iterative', + 'Sofa.Component.MechanicalLoad', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.Visual' ]) - # AnimationLoop root.addObject('DefaultAnimationLoop') @@ -63,26 +63,29 @@ def createScene(root): root.addObject('VisualStyle', displayFlags='showBehaviorModels showForceFields') # Add the python controller in the scene - root.addObject( TimerController() ) + root.addObject(TimerController()) # Create a grid topology of 10x10x60 centered on (0,0,0) root.addObject('RegularGridTopology', name='grid', min=[-5, -5, -30], max=[5, 5, 30], n=[6, 6, 16]) # Create our mechanical node - root.addChild("meca") - root.meca.addObject("StaticSolver", newton_iterations=5, printLog=False) - root.meca.addObject("CGLinearSolver", iterations=25, tolerance=1e-5, threshold=1e-5) - - root.meca.addObject('MechanicalObject', name='mo', position='@../grid.position') - root.meca.addObject('HexahedronSetTopologyContainer', name='mechanical_topology', src='@../grid') - root.meca.addObject('HexahedronFEMForceField', youngModulus=3000, poissonRatio=0) - - root.meca.addObject('BoxROI', name='base_roi', box=[-5.01, -5.01, -30.01, 30.01, 30.01, -29.99]) - root.meca.addObject('BoxROI', name='top_roi', box=[-5.01, -5.01, +29.99, 5.01, 5.01, +30.01], quad='@mechanical_topology.quads') - - root.meca.addObject('FixedProjectiveConstraint', indices='@base_roi.indices') - root.meca.addObject('QuadSetGeometryAlgorithms') - root.meca.addObject('QuadPressureForceField', pressure=[0, -30, 0], quadList='@top_roi.quadInROI', showForces=False) + with root.addChild("meca") as meca: + meca.addObject("NewtonRaphsonSolver", name="newtonSolver_springs", maxNbIterationsNewton=5, + maxNbIterationsLineSearch=1, warnWhenLineSearchFails=False, printLog=False) + meca.addObject("StaticSolver", newtonSolver="@newtonSolver_springs") + meca.addObject("CGLinearSolver", iterations=25, tolerance=1e-5, threshold=1e-5) + + meca.addObject('MechanicalObject', name='mo', position='@../grid.position') + meca.addObject('HexahedronSetTopologyContainer', name='mechanical_topology', src='@../grid') + meca.addObject('HexahedronFEMForceField', youngModulus=3000, poissonRatio=0) + + meca.addObject('BoxROI', name='base_roi', box=[-5.01, -5.01, -30.01, 30.01, 30.01, -29.99]) + meca.addObject('BoxROI', name='top_roi', box=[-5.01, -5.01, +29.99, 5.01, 5.01, +30.01], + quad='@mechanical_topology.quads') + + meca.addObject('FixedProjectiveConstraint', indices='@base_roi.indices') + meca.addObject('QuadSetGeometryAlgorithms') + meca.addObject('QuadPressureForceField', pressure=[0, -30, 0], quadList='@top_roi.quadInROI', showForces=False) # When not using runSofa, this main function will be called python diff --git a/examples/example-forcefield.py b/examples/example-forcefield.py index ce0a607d6..60c474525 100644 --- a/examples/example-forcefield.py +++ b/examples/example-forcefield.py @@ -22,10 +22,10 @@ def addForce(self, m, out_force, pos, vel): with out_force.writeableArray() as wa: wa[:] += ( (self.initpos-pos.value) * self.ks.value ) - def addDForce(self, df, dx, params): + def addDForce(self, params, df, dx): pass - #def addKToMatrix(self, a, b): + #def addKToMatrix(self, params, number_of_nodes, number_of_dofs_per_node): # print(" Python::addKToMatrix: ", a, " ", b) diff --git a/examples/jax/forcefield.py b/examples/jax/forcefield.py new file mode 100644 index 000000000..ccd827e84 --- /dev/null +++ b/examples/jax/forcefield.py @@ -0,0 +1,180 @@ +""" +Toy example of a force field leveraging autodiff with JAX. + +JAX can be installed via e.g. `pip install -U jax[cuda12]` +""" +import jax +import jax.numpy as jnp +import numpy as np + +import Sofa + + +# Some configuration for JAX: device and precision +# jax.config.update('jax_default_device', jax.devices('cpu')[0]) +jax.config.update("jax_default_device", jax.devices("gpu")[0]) # default "gpu" +jax.config.update("jax_enable_x64", True) # default False (ie use float32) + + +@jax.jit # JIT (just-in-time compilation) for better performance +def get_force(position, length, stiffness): + """ + Spring between the origin and the given position. + + position: array of shape (n_particles, n_dimensions) + length: scalar or array of shape (n_particles, 1) + stiffness: scalar or array of shape (n_particles, 1) + """ + distance = jnp.sqrt(jnp.sum(position**2, axis=1, keepdims=True)) + direction = position / distance + return - stiffness * (distance - length) * direction + + +@jax.jit # JIT (just-in-time compilation) for better performance +def get_dforce(position, length, stiffness, vector): + """ + Compute the jacobian-vector product (jvp) using autodiff + """ + def get_force_from_position(x): + return get_force(x, length, stiffness) + # Differentiate get_force() as a function of the position + return jax.jvp(get_force_from_position, (position,), (vector,))[1] + + +@jax.jit # JIT (just-in-time compilation) for better performance +def get_kmatrix(position, length, stiffness): + """ + Compute the jacobian using autodiff + + Warning: The jacobian computed this way is a dense matrix. + Check `sparsejac` if you are interested in sparse jacobian with JAX. + """ + def get_force_from_position(x): + return get_force(x, length, stiffness) + # Differentiate get_force() as a function of the position + return jax.jacrev(get_force_from_position)(position) + + +class JaxForceField(Sofa.Core.ForceFieldVec3d): + + def __init__(self, length, stiffness, *args, **kwargs): + Sofa.Core.ForceFieldVec3d.__init__(self, *args, **kwargs) + self.length = length + self.stiffness = stiffness + self.dense_to_sparse = None + + def addForce(self, mechanical_parameters, out_force, position, velocity): + with out_force.writeableArray() as wa: + wa[:] += get_force(position.value, self.length, self.stiffness) + + def addDForce(self, mechanical_parameters, df, dx): + with df.writeableArray() as wa: + wa[:] += get_dforce(self.mstate.position.value, self.length, self.stiffness, dx.value) * mechanical_parameters['kFactor'] + + # Option 1: Return the jacobian as a dense array (must have shape (n, n, 1) to be interpreted as such). + # Note: Very slow for big sparse matrices. + # def addKToMatrix(self, mechanical_parameters, n_particles, n_dimensions): + # jacobian = get_kmatrix(self.mstate.position.value, self.length, self.stiffness) + # return np.array(jacobian).reshape((n_particles*n_dimensions, n_particles*n_dimensions, 1)) + + # Option 2: Return the non-zero coefficients of the jacobian as an array with rows (i, j, value). + # Note: The extraction of the non-zero coefficients is faster with JAX on GPU. + # def addKToMatrix(self, mechanical_parameters, n_particles, n_dimensions): + # jacobian = get_kmatrix(self.mstate.position.value, self.length, self.stiffness) + # jacobian = jacobian.reshape((n_particles*n_dimensions, n_particles*n_dimensions)) + # i, j = jacobian.nonzero() + # sparse_jacobian = jnp.stack([i, j, jacobian[i, j]], axis=1) + # return np.array(sparse_jacobian) + + # Option 2 optimization: We know the sparsity of the jacobian in advance (diagonal by 3x3 blocks). + def addKToMatrix(self, mechanical_parameters, n_particles, n_dimensions): + if self.dense_to_sparse is None: + # i = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, ...] + # j = [0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 4, 5, ...] + i = jnp.repeat(jnp.arange(n_particles*n_dimensions), 3) + j = jnp.repeat(jnp.arange(n_particles*n_dimensions).reshape((-1, 3)), 3, axis=0).reshape(-1) + self.dense_to_sparse = lambda jac: jnp.stack([i, j, jac[i, j]], axis=1) + self.dense_to_sparse = jax.jit(self.dense_to_sparse) # slightly faster with jit + + jacobian = get_kmatrix(self.mstate.position.value, self.length, self.stiffness) + jacobian = jacobian.reshape((n_particles*n_dimensions, n_particles*n_dimensions)) + sparse_jacobian = self.dense_to_sparse(jacobian) + sparse_jacobian = np.array(sparse_jacobian) + # Note: with the computations optimized, the conversion below can account for + # ~90% of the time spent in this function. + return np.array(sparse_jacobian) + + +def createScene(root, method="implicit-matrix-assembly", n_particles=1_000, use_sofa=False): + root.dt = 1e-3 + root.gravity = (0, -9.8, 0) + root.box = (-5, -5, -5, 5, 5, 5) + root.addObject( + "RequiredPlugin", + pluginName=[ + 'Sofa.Component.Visual', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.LinearSolver.Iterative', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Mass', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.SolidMechanics.Spring', + ] + ) + root.addObject("DefaultAnimationLoop") + root.addObject("VisualStyle", displayFlags="showBehaviorModels showForceFields") + + physics = root.addChild("Physics") + + if method.lower() == "explicit": # Requires the implementation of 'addForce' + physics.addObject("EulerExplicitSolver", name="eulerExplicit") + elif method.lower() == "implicit-matrix-free": # Requires the implementation of 'addForce' and 'addDForce' + physics.addObject("EulerImplicitSolver", name="eulerImplicit") + physics.addObject("CGLinearSolver", template="GraphScattered", name="solver", iterations=50, tolerance=1e-5, threshold=1e-5) + elif method == "implicit-matrix-assembly": # Requires the implementation of 'addForce', 'addDForce' and 'addKToMatrix' + physics.addObject("EulerImplicitSolver", name="eulerImplicit") + physics.addObject("SparseLDLSolver", name="solver", template="CompressedRowSparseMatrixd") + + position = np.random.uniform(-1, 1, (n_particles, 3)) + velocity = np.zeros_like(position) + length = np.random.uniform(0.8, 1.2, size=(n_particles, 1)) + stiffness = 100.0 + + particles = physics.addChild("Particles") + particles.addObject("MechanicalObject", name="state", template="Vec3d", position=position, velocity=velocity, showObject=True) + particles.addObject("UniformMass", name="mass", totalMass=n_particles) + + if not use_sofa: # Use the force field implemented with JAX + particles.addObject(JaxForceField(length=length, stiffness=stiffness)) + else: # Use a SOFA equivalent for comparison + root.addObject("MechanicalObject", name="origin", template="Vec3d", position="0 0 0") + particles.addObject("SpringForceField", name="force", object1="@/origin", object2="@/Physics/Particles/state", indices1=np.zeros(n_particles, dtype=np.int32), indices2=np.arange(n_particles), length=length, stiffness=stiffness*np.ones(n_particles), damping=np.zeros(n_particles)) + + +def main(): + import argparse + import SofaRuntime + import SofaImGui + import Sofa.Gui + + parser = argparse.ArgumentParser(description="Example of a scene using a ForceField implemented with JAX") + parser.add_argument("--method", default="implicit-matrix-assembly", help="must be 'explicit', 'implicit-matrix-free' or 'implicit-matrix-assembly'") + parser.add_argument("--particles", type=int, default=1000, help="number of particles (default 1000)") + parser.add_argument("--use-sofa", action="store_true", help="use a force field from SOFA instead of the one implemented with JAX") + args = parser.parse_args() + + root=Sofa.Core.Node("root") + createScene(root, method=args.method, n_particles=args.particles, use_sofa=args.use_sofa) + Sofa.Simulation.initRoot(root) + + Sofa.Gui.GUIManager.Init("myscene", "imgui") + Sofa.Gui.GUIManager.createGUI(root, __file__) + Sofa.Gui.GUIManager.SetDimension(1600, 900) + Sofa.Gui.GUIManager.MainLoop(root) + Sofa.Gui.GUIManager.closeGUI() + + +if __name__ == "__main__": + main() diff --git a/examples/meshing/CGAL/README.txt b/examples/meshing/CGAL/README.txt new file mode 100644 index 000000000..fd036c52e --- /dev/null +++ b/examples/meshing/CGAL/README.txt @@ -0,0 +1,5 @@ +This folder presents two simple modules using CGAL bindings allowing for easy tetrahedric mesh generation from either a polyhedron of its surface or a point cloud (using a convexe hull). + +You can you this through the two mains (mesh_from_point_cloud_using_convex_hull.py and mesh_from_polyhedron.py) through command line (use -h for more information) or simply use the cgal_utils.py module if you want to build new scripts upon this work. The most important class being CGAL_Mesh_3_IO_Util heping to deal with the CGal data structure and use they results as numpy array. To see usage example, see mesh_from_polyhedron.py:CGAL_Mesh_from_polyhedron(). + +Those scripts are here to replace the components MeshGenerationFromPolyhedron and TriangularConvexHull3D from the CGALPLugin https://github.com/sofa-framework/CGALPlugin diff --git a/examples/meshing/CGAL/cgal_utils.py b/examples/meshing/CGAL/cgal_utils.py new file mode 100644 index 000000000..97b096c77 --- /dev/null +++ b/examples/meshing/CGAL/cgal_utils.py @@ -0,0 +1,259 @@ +from CGAL.CGAL_Polyhedron_3 import Polyhedron_3, Polyhedron_modifier +from CGAL.CGAL_Mesh_3 import Mesh_3_Complex_3_in_triangulation_3 +from CGAL.CGAL_Mesh_3 import Polyhedral_mesh_domain_3 +from CGAL.CGAL_Mesh_3 import Mesh_3_parameters +from CGAL.CGAL_Mesh_3 import Default_mesh_criteria +from CGAL.CGAL_Kernel import Point_3 +from CGAL import CGAL_Mesh_3 + +import numpy as np +import dataclasses +from typing import List, Optional + +from enum import Enum +from pathlib import Path + +import time + +from vtkmodules.vtkIOGeometry import ( + vtkBYUReader, + vtkOBJReader, + vtkSTLReader + ) +from vtkmodules.vtkIOLegacy import vtkPolyDataReader +from vtkmodules.vtkIOPLY import vtkPLYReader +from vtkmodules.vtkIOXML import vtkXMLPolyDataReader + +import vtk +from vtk.util import numpy_support +from vtk.numpy_interface import algorithms as algs + +timestamp = {} + +def tic(hash = 0): + global timestamp + timestamp[hash] = time.time_ns() + +def toc(hash = 0): + global timestamp + return f"{((time.time_ns() - timestamp[hash])/1000000):.3f} miliseconds" + + +def ReadPolyData(file_name): + #FROM Vtk examples https://examples.vtk.org/site/Python/Snippets/ReadPolyData/ + + valid_suffixes = ['.g', '.obj', '.stl', '.ply', '.vtk', '.vtp'] + + path = Path(file_name) + if path.suffix: + ext = path.suffix.lower() + if path.suffix not in valid_suffixes: + print(f'No reader for this file suffix: {ext}') + return None + else: + if ext == ".ply": + reader = vtkPLYReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".vtp": + reader = vtkXMLPolyDataReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".obj": + reader = vtkOBJReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".stl": + reader = vtkSTLReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".vtk": + reader = vtkPolyDataReader() + reader.SetFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + elif ext == ".g": + reader = vtkBYUReader() + reader.SetGeometryFileName(file_name) + reader.Update() + poly_data = reader.GetOutput() + + return poly_data + + +class CGAL_Mesh_3_IO_Util(object): + + points : np.array + triangles : np.array + tetras : np.array + + class Elem(Enum): + POINTS = 1 + TRIANGLES = 3 + TETRA = 4 + + def __init__(self,mesh): + self.mesh = mesh + + def extract(self, elems : list[Elem]): + + print(f"Extracting data into numpy objects...") + tic() + + for elem in elems: + vnbe = {} + match elem: + case CGAL_Mesh_3_IO_Util.Elem.TRIANGLES: + for elemf in self.mesh.facets(): + for i in range(3): + if not elemf.vertex in vnbe: + vnbe[elemf.vertex(i)] = 1 + else: + vnbe[elem.vertex(i)] += 1 + case CGAL_Mesh_3_IO_Util.Elem.TETRA: + for elemc in self.mesh.cells(): + for i in range(4): + if not elemc.vertex in vnbe: + vnbe[elemc.vertex(i)] = 1 + else: + vnbe[elemc.vertex(i)] += 1 + + + V = {} + it = 0 + for vertice in self.mesh.triangulation().finite_vertices(): + if vertice in vnbe: + V[vertice] = it + it += 1 + + if CGAL_Mesh_3_IO_Util.Elem.POINTS in elems: + self.points = np.empty((len(V),3)) + id = 0 + for key in V.keys(): + self.points[id][:] = [self.mesh.triangulation().point(key).x(), self.mesh.triangulation().point(key).y(), self.mesh.triangulation().point(key).z()] + id+=1 + + match elem: + case CGAL_Mesh_3_IO_Util.Elem.TRIANGLES: + self.triangles = np.empty((self.mesh.number_of_facets(),3), dtype=np.int32) + id = 0 + for elemt in self.mesh.facets(): + self.triangles[id][:] = [V[elemt.vertex(0)],V[elemt.vertex(1)],V[elemt.vertex(2)]] + id+= 1 + case CGAL_Mesh_3_IO_Util.Elem.TETRA: + self.tetras = np.empty((self.mesh.number_of_cells(),4), dtype=np.int32) + id = 0 + for elemc in self.mesh.cells(): + self.tetras[id][:] = [V[elemc.vertex(0)],V[elemc.vertex(1)],V[elemc.vertex(2)], V[elemc.vertex(3)]] + id += 1 + print(f"Done ! Took {toc()}") + if CGAL_Mesh_3_IO_Util.Elem.TRIANGLES in elems: + print(f"Extracted {self.points.shape[0]} points and {self.triangles.shape[0]} triangles") + if CGAL_Mesh_3_IO_Util.Elem.TETRA in elems: + print(f"Extracted {self.points.shape[0]} points and {self.tetras.shape[0]} tetras") + + + + def write_out(self, filename): + path = Path(filename) + ext = path.suffix.lower() + if ext != ".vtk" and ext != ".vtu": + print("Only vtk or vtu extension are suported") + return + + if (not "tetras" in self.__dict__) and ext == ".vtu": + print("VTU only supported for tetrahedral meshes. Use vtk instead") + return + + print(f"Saving into {filename}...") + tic() + + if "tetras" in self.__dict__: + ugrid = vtk.vtkUnstructuredGrid() + else: + ugrid = vtk.vtkPolyData() + + vtk_points = vtk.vtkPoints() + vtk_points.SetData(numpy_support.numpy_to_vtk(self.points, deep=True)) + ugrid.SetPoints(vtk_points) + + + if "triangles" in self.__dict__: + for triangle in self.triangles: + vtkTriangleObj = vtk.vtkTriangle() + vtkTriangleObj.GetPointIds().SetId(0,triangle[0]) + vtkTriangleObj.GetPointIds().SetId(1,triangle[1]) + vtkTriangleObj.GetPointIds().SetId(2,triangle[2]) + ugrid.InsertNextCell(vtkTriangleObj.GetCellType(), vtkTriangleObj.GetPointIds()) + if "tetras" in self.__dict__: + for tetra in self.tetras: + vtkTetraObj = vtk.vtkTetra() + vtkTetraObj.GetPointIds().SetId(0,tetra[0]) + vtkTetraObj.GetPointIds().SetId(1,tetra[1]) + vtkTetraObj.GetPointIds().SetId(2,tetra[2]) + vtkTetraObj.GetPointIds().SetId(3,tetra[3]) + ugrid.InsertNextCell(vtkTetraObj.GetCellType(), vtkTetraObj.GetPointIds()) + + + if "tetras" in self.__dict__: + writer = vtk.vtkUnstructuredGridWriter() + else: + writer = vtk.vtkPolyDataWriter() + writer.SetFileVersion(42) + writer.SetInputData(ugrid) + writer.SetFileName(filename) + writer.Write() + print(f"Done ! Took {toc()}") + + + + + +class CGAL_Mesh_from(object): + + class Refiner(Enum): + NONE = 0 + LLOYD = 1 + ODT = 3 + PERTURB = 4 + + @dataclasses.dataclass + class Refiner_input(): + refiner_type : Enum + + time_limit : Optional[float] = 20.0 #(ALL) to set up, in seconds, a CPU time limit after which the optimization process is stopped. This time is measured using CGAL::Real_timer. 0 means that there is no time limit. + max_iteration_number : Optional[int] = 200 #(LLOYD & ODT only) limit on the number of performed iterations. 0 means that there is no limit on the number of performed iterations. + convergence : Optional[int] = 0.0 #(LLOYD & ODT only) threshold ratio of stopping criterion based on convergence: the optimization process is stopped when at the last iteration the displacement of any vertex is less than a given fraction of the length of the shortest edge incident to that vertex. + free_bound : Optional[bool] = False #(LLOYD & ODT only) designed to reduce running time of each optimization iteration. Any vertex that has a displacement less than a given fraction of the length of its shortest incident edge, is frozen (i.e. is not relocated). The parameter freeze_bound gives the threshold ratio. If it is set to 0, freezing of vertices is disabled. + silver_bound : Optional[bool] = False #(PERTURB only) is designed to give, in degrees, a targeted lower bound on dihedral angles of mesh cells. The exudation process considers in turn all the mesh cells that have a smallest dihedral angle less than sliver_bound and tries to make them disappear by weighting their vertices. The optimization process stops when every cell in the mesh achieves this quality. The default value is 0 and means that there is no targeted bound: the exuder then runs as long as it can improve the smallest dihedral angles of the set of cells incident to some vertices. + + + IOUtil : CGAL_Mesh_3_IO_Util + + def __init__(self): + pass + + def generate(self): + pass + + + def __getattr__(self, name): + match name: + case "points": + return self.IOUtil.points + case "triangles": + return self.IOUtil.triangles + case "tetras": + return self.IOUtil.tetras + case _: + if name in self.__dict__: + return self.__dict__[name] + else: + raise AttributeError() + + + diff --git a/examples/meshing/CGAL/data/point_cloud.pcd b/examples/meshing/CGAL/data/point_cloud.pcd new file mode 100644 index 000000000..f6f48c52c Binary files /dev/null and b/examples/meshing/CGAL/data/point_cloud.pcd differ diff --git a/examples/meshing/CGAL/data/torus.obj b/examples/meshing/CGAL/data/torus.obj new file mode 100644 index 000000000..9ac9ddbfe --- /dev/null +++ b/examples/meshing/CGAL/data/torus.obj @@ -0,0 +1,3263 @@ +# This file uses centimeters as units for non-parametric coordinates. + +v 1.434962 -0.000000 -0.156434 +v 1.381741 -0.000000 -0.309017 +v 1.294498 -0.000000 -0.453991 +v 1.175380 -0.000000 -0.587785 +v 1.027319 -0.000000 -0.707107 +v 0.853963 -0.000000 -0.809017 +v 0.659580 -0.000000 -0.891007 +v 0.448955 -0.000000 -0.951057 +v 0.227276 -0.000000 -0.987689 +v 0.000001 -0.000000 -1.000001 +v -0.221675 -0.000000 -0.987689 +v -0.437893 -0.000000 -0.951057 +v -0.643328 -0.000000 -0.891007 +v -0.832923 -0.000000 -0.809018 +v -1.002008 -0.000000 -0.707107 +v -1.146420 -0.000000 -0.587786 +v -1.262604 -0.000000 -0.453991 +v -1.347697 -0.000000 -0.309017 +v -1.399607 -0.000000 -0.156435 +v -1.417053 -0.000000 -0.000000 +v -1.399607 -0.000000 0.156434 +v -1.347698 -0.000000 0.309017 +v -1.262604 -0.000000 0.453991 +v -1.146420 -0.000000 0.587785 +v -1.002008 -0.000000 0.707107 +v -0.832923 -0.000000 0.809017 +v -0.643329 -0.000000 0.891007 +v -0.437893 -0.000000 0.951057 +v -0.221676 -0.000000 0.987688 +v 0.000000 -0.000000 1.000000 +v 0.227275 -0.000000 0.987688 +v 0.448955 -0.000000 0.951057 +v 0.659579 -0.000000 0.891007 +v 0.853962 -0.000000 0.809017 +v 1.027318 -0.000000 0.707107 +v 1.175378 -0.000000 0.587785 +v 1.294497 -0.000000 0.453991 +v 1.381740 -0.000000 0.309017 +v 1.434960 -0.000000 0.156434 +v 1.452847 -0.000000 -0.000000 +v 1.464707 0.154509 -0.160262 +v 1.415555 0.154509 -0.316579 +v 1.326176 0.154509 -0.465101 +v 1.204143 0.154509 -0.602170 +v 1.052460 0.154509 -0.724411 +v 0.874861 0.154509 -0.828815 +v 0.675721 0.154509 -0.912811 +v 0.459942 0.154509 -0.974331 +v 0.232838 0.154509 -1.011859 +v 0.000001 0.154509 -1.024472 +v -0.227100 0.154509 -1.011859 +v -0.448609 0.154509 -0.974331 +v -0.659072 0.154509 -0.912812 +v -0.853306 0.154509 -0.828816 +v -1.026529 0.154509 -0.724411 +v -1.174475 0.154509 -0.602170 +v -1.293502 0.154509 -0.465101 +v -1.380678 0.154509 -0.316580 +v -1.428912 0.154509 -0.160263 +v -1.441525 0.154509 -0.000000 +v -1.428912 0.154509 0.160263 +v -1.380678 0.154509 0.316579 +v -1.293502 0.154509 0.465100 +v -1.174475 0.154509 0.602169 +v -1.026529 0.154509 0.724411 +v -0.853306 0.154509 0.828815 +v -0.659072 0.154509 0.912811 +v -0.448609 0.154509 0.974331 +v -0.227101 0.154509 1.011859 +v 0.000000 0.154509 1.024472 +v 0.232837 0.154509 1.011859 +v 0.459941 0.154509 0.974331 +v 0.675720 0.154509 0.912811 +v 0.874860 0.154509 0.828815 +v 1.052459 0.154509 0.724411 +v 1.204142 0.154509 0.602169 +v 1.326175 0.154509 0.465100 +v 1.415554 0.154509 0.316579 +v 1.464706 0.154509 0.160263 +v 1.477319 0.154509 -0.000000 +v 1.534852 0.293893 -0.171372 +v 1.494722 0.293893 -0.338525 +v 1.418111 0.293893 -0.497343 +v 1.287618 0.293893 -0.643914 +v 1.125419 0.293893 -0.774630 +v 0.935509 0.293893 -0.886272 +v 0.722564 0.293893 -0.976090 +v 0.491827 0.293893 -1.041875 +v 0.248979 0.293893 -1.082005 +v 0.000001 0.293893 -1.095492 +v -0.242844 0.293893 -1.082005 +v -0.479708 0.293893 -1.041875 +v -0.704761 0.293893 -0.976091 +v -0.912460 0.293893 -0.886272 +v -1.097691 0.293893 -0.774630 +v -1.255893 0.293893 -0.643914 +v -1.383171 0.293893 -0.497343 +v -1.458928 0.293893 -0.338526 +v -1.499057 0.293893 -0.171373 +v -1.512545 0.293893 -0.000000 +v -1.499057 0.293893 0.171372 +v -1.458928 0.293893 0.338525 +v -1.383171 0.293893 0.497343 +v -1.255893 0.293893 0.643914 +v -1.097691 0.293893 0.774630 +v -0.912460 0.293893 0.886271 +v -0.704761 0.293893 0.976090 +v -0.479708 0.293893 1.041874 +v -0.242844 0.293893 1.082004 +v 0.000000 0.293893 1.095492 +v 0.248978 0.293893 1.082004 +v 0.491826 0.293893 1.041874 +v 0.722563 0.293893 0.976090 +v 0.935508 0.293893 0.886271 +v 1.125418 0.293893 0.774629 +v 1.287617 0.293893 0.643914 +v 1.418110 0.293893 0.497343 +v 1.494722 0.293893 0.338526 +v 1.534851 0.293893 0.171373 +v 1.548339 0.293893 -0.000000 +v 1.644107 0.404509 -0.188676 +v 1.599925 0.404509 -0.372708 +v 1.527498 0.404509 -0.547561 +v 1.417634 0.404509 -0.708932 +v 1.239057 0.404509 -0.852847 +v 1.029971 0.404509 -0.975762 +v 0.795524 0.404509 -1.074650 +v 0.541488 0.404509 -1.147077 +v 0.274119 0.404509 -1.191259 +v 0.000001 0.404509 -1.206108 +v -0.267364 0.404509 -1.191259 +v -0.528146 0.404509 -1.147077 +v -0.775923 0.404509 -1.074650 +v -1.004594 0.404509 -0.975762 +v -1.208529 0.404509 -0.852847 +v -1.382706 0.404509 -0.708933 +v -1.491703 0.404509 -0.547562 +v -1.564130 0.404509 -0.372708 +v -1.608311 0.404509 -0.188677 +v -1.623161 0.404509 -0.000000 +v -1.608311 0.404509 0.188677 +v -1.564130 0.404509 0.372708 +v -1.491703 0.404509 0.547561 +v -1.382706 0.404509 0.708932 +v -1.208529 0.404509 0.852847 +v -1.004594 0.404509 0.975762 +v -0.775923 0.404509 1.074650 +v -0.528147 0.404509 1.147076 +v -0.267365 0.404509 1.191258 +v 0.000000 0.404509 1.206107 +v 0.274119 0.404509 1.191258 +v 0.541487 0.404509 1.147076 +v 0.795523 0.404509 1.074650 +v 1.029970 0.404509 0.975761 +v 1.239056 0.404509 0.852847 +v 1.417632 0.404509 0.708932 +v 1.527497 0.404509 0.547561 +v 1.599924 0.404509 0.372708 +v 1.644106 0.404509 0.188677 +v 1.658955 0.404509 -0.000000 +v 1.781775 0.475528 -0.210481 +v 1.732487 0.475528 -0.415780 +v 1.651690 0.475528 -0.610840 +v 1.541374 0.475528 -0.790860 +v 1.382249 0.475528 -0.951407 +v 1.149000 0.475528 -1.088526 +v 0.887459 0.475528 -1.198842 +v 0.604066 0.475528 -1.279639 +v 0.305798 0.475528 -1.328927 +v 0.000001 0.475528 -1.345492 +v -0.298263 0.475528 -1.328927 +v -0.589182 0.475528 -1.279639 +v -0.865593 0.475528 -1.198843 +v -1.120691 0.475528 -1.088526 +v -1.348193 0.475528 -0.951407 +v -1.505579 0.475528 -0.790861 +v -1.615895 0.475528 -0.610841 +v -1.696692 0.475528 -0.415780 +v -1.745980 0.475528 -0.210482 +v -1.762545 0.475528 -0.000000 +v -1.745980 0.475528 0.210481 +v -1.696692 0.475528 0.415780 +v -1.615895 0.475528 0.610840 +v -1.505579 0.475528 0.790860 +v -1.348193 0.475528 0.951406 +v -1.120691 0.475528 1.088526 +v -0.865593 0.475528 1.198842 +v -0.589182 0.475528 1.279639 +v -0.298263 0.475528 1.328926 +v 0.000000 0.475528 1.345492 +v 0.305797 0.475528 1.328926 +v 0.604065 0.475528 1.279639 +v 0.887458 0.475528 1.198842 +v 1.148999 0.475528 1.088526 +v 1.382248 0.475528 0.951406 +v 1.541373 0.475528 0.790860 +v 1.651689 0.475528 0.610840 +v 1.732486 0.475528 0.415780 +v 1.781774 0.475528 0.210481 +v 1.798339 0.475528 -0.000000 +v 1.934381 0.500000 -0.234651 +v 1.879433 0.500000 -0.463525 +v 1.789358 0.500000 -0.680986 +v 1.666374 0.500000 -0.881678 +v 1.513509 0.500000 -1.060661 +v 1.280945 0.500000 -1.213526 +v 0.989370 0.500000 -1.336510 +v 0.673433 0.500000 -1.426586 +v 0.340914 0.500000 -1.481533 +v 0.000001 0.500000 -1.500001 +v -0.332513 0.500000 -1.481533 +v -0.656840 0.500000 -1.426586 +v -0.964993 0.500000 -1.336511 +v -1.249384 0.500000 -1.213526 +v -1.477713 0.500000 -1.060661 +v -1.630579 0.500000 -0.881679 +v -1.753563 0.500000 -0.680986 +v -1.843638 0.500000 -0.463526 +v -1.898586 0.500000 -0.234652 +v -1.917053 0.500000 -0.000000 +v -1.898586 0.500000 0.234652 +v -1.843638 0.500000 0.463525 +v -1.753563 0.500000 0.680986 +v -1.630579 0.500000 0.881678 +v -1.477713 0.500000 1.060660 +v -1.249384 0.500000 1.213526 +v -0.964993 0.500000 1.336510 +v -0.656840 0.500000 1.426585 +v -0.332514 0.500000 1.481533 +v 0.000000 0.500000 1.500000 +v 0.340913 0.500000 1.481533 +v 0.673432 0.500000 1.426585 +v 0.989368 0.500000 1.336510 +v 1.280943 0.500000 1.213526 +v 1.513507 0.500000 1.060660 +v 1.666373 0.500000 0.881678 +v 1.789357 0.500000 0.680986 +v 1.879432 0.500000 0.463526 +v 1.934380 0.500000 0.234652 +v 1.952847 0.500000 -0.000000 +v 2.086987 0.475528 -0.258822 +v 2.026380 0.475528 -0.511271 +v 1.927027 0.475528 -0.751131 +v 1.791374 0.475528 -0.972496 +v 1.622763 0.475528 -1.169915 +v 1.412889 0.475528 -1.338526 +v 1.091280 0.475528 -1.474179 +v 0.742800 0.475528 -1.573532 +v 0.376030 0.475528 -1.634140 +v 0.000001 0.475528 -1.654509 +v -0.366764 0.475528 -1.634140 +v -0.724498 0.475528 -1.573532 +v -1.064392 0.475528 -1.474179 +v -1.378078 0.475528 -1.338526 +v -1.586967 0.475528 -1.169915 +v -1.755579 0.475528 -0.972496 +v -1.891231 0.475528 -0.751132 +v -1.990584 0.475528 -0.511272 +v -2.051192 0.475528 -0.258823 +v -2.071562 0.475528 -0.000000 +v -2.051192 0.475528 0.258822 +v -1.990585 0.475528 0.511271 +v -1.891231 0.475528 0.751131 +v -1.755579 0.475528 0.972496 +v -1.586967 0.475528 1.169914 +v -1.378078 0.475528 1.338526 +v -1.064393 0.475528 1.474178 +v -0.724498 0.475528 1.573531 +v -0.366765 0.475528 1.634139 +v 0.000000 0.475528 1.654509 +v 0.376029 0.475528 1.634139 +v 0.742799 0.475528 1.573531 +v 1.091279 0.475528 1.474178 +v 1.412888 0.475528 1.338526 +v 1.622761 0.475528 1.169914 +v 1.791373 0.475528 0.972496 +v 1.927025 0.475528 0.751131 +v 2.026378 0.475528 0.511271 +v 2.086986 0.475528 0.258822 +v 2.107356 0.475528 -0.000000 +v 2.224656 0.404509 -0.280626 +v 2.158942 0.404509 -0.554343 +v 2.051219 0.404509 -0.814410 +v 1.904138 0.404509 -1.054424 +v 1.721322 0.404509 -1.268474 +v 1.507272 0.404509 -1.451290 +v 1.183215 0.404509 -1.598371 +v 0.805378 0.404509 -1.706094 +v 0.407709 0.404509 -1.771808 +v 0.000001 0.404509 -1.793894 +v -0.397662 0.404509 -1.771808 +v -0.785533 0.404509 -1.706094 +v -1.154062 0.404509 -1.598371 +v -1.471477 0.404509 -1.451291 +v -1.685527 0.404509 -1.268475 +v -1.868343 0.404509 -1.054425 +v -2.015423 0.404509 -0.814411 +v -2.123147 0.404509 -0.554344 +v -2.188860 0.404509 -0.280627 +v -2.210946 0.404509 -0.000000 +v -2.188860 0.404509 0.280626 +v -2.123147 0.404509 0.554343 +v -2.015424 0.404509 0.814410 +v -1.868343 0.404509 1.054424 +v -1.685527 0.404509 1.268474 +v -1.471477 0.404509 1.451290 +v -1.154063 0.404509 1.598370 +v -0.785534 0.404509 1.706094 +v -0.397663 0.404509 1.771807 +v 0.000000 0.404509 1.793893 +v 0.407708 0.404509 1.771807 +v 0.805376 0.404509 1.706093 +v 1.183214 0.404509 1.598370 +v 1.507271 0.404509 1.451290 +v 1.721321 0.404509 1.268474 +v 1.904137 0.404509 1.054424 +v 2.051217 0.404509 0.814410 +v 2.158941 0.404509 0.554343 +v 2.224654 0.404509 0.280627 +v 2.246740 0.404509 -0.000000 +v 2.333910 0.293893 -0.297930 +v 2.264144 0.293893 -0.588525 +v 2.149778 0.293893 -0.864629 +v 1.993629 0.293893 -1.119442 +v 1.799540 0.293893 -1.346691 +v 1.572291 0.293893 -1.540781 +v 1.256175 0.293893 -1.696930 +v 0.855039 0.293893 -1.811296 +v 0.432849 0.293893 -1.881062 +v 0.000001 0.293893 -1.904510 +v -0.422183 0.293893 -1.881062 +v -0.833971 0.293893 -1.811296 +v -1.225224 0.293893 -1.696931 +v -1.536495 0.293893 -1.540781 +v -1.763744 0.293893 -1.346692 +v -1.957833 0.293893 -1.119443 +v -2.113983 0.293893 -0.864630 +v -2.228349 0.293893 -0.588526 +v -2.298114 0.293893 -0.297931 +v -2.321562 0.293893 -0.000000 +v -2.298115 0.293893 0.297931 +v -2.228349 0.293893 0.588525 +v -2.113983 0.293893 0.864629 +v -1.957833 0.293893 1.119442 +v -1.763744 0.293893 1.346691 +v -1.536495 0.293893 1.540780 +v -1.225225 0.293893 1.696930 +v -0.833972 0.293893 1.811296 +v -0.422184 0.293893 1.881061 +v 0.000000 0.293893 1.904509 +v 0.432848 0.293893 1.881061 +v 0.855038 0.293893 1.811296 +v 1.256174 0.293893 1.696930 +v 1.572289 0.293893 1.540780 +v 1.799538 0.293893 1.346691 +v 1.993627 0.293893 1.119442 +v 2.149777 0.293893 0.864629 +v 2.264143 0.293893 0.588526 +v 2.333908 0.293893 0.297931 +v 2.357356 0.293893 -0.000000 +v 2.404055 0.154509 -0.309040 +v 2.331688 0.154509 -0.610472 +v 2.213058 0.154509 -0.896871 +v 2.051085 0.154509 -1.161187 +v 1.849758 0.154509 -1.396910 +v 1.614035 0.154509 -1.598237 +v 1.303019 0.154509 -1.760209 +v 0.886924 0.154509 -1.878840 +v 0.448990 0.154509 -1.951207 +v 0.000001 0.154509 -1.975530 +v -0.437926 0.154509 -1.951208 +v -0.865070 0.154509 -1.878840 +v -1.270914 0.154509 -1.760210 +v -1.578240 0.154509 -1.598237 +v -1.813963 0.154509 -1.396911 +v -2.015289 0.154509 -1.161187 +v -2.177262 0.154509 -0.896872 +v -2.295892 0.154509 -0.610472 +v -2.368260 0.154509 -0.309041 +v -2.392582 0.154509 -0.000000 +v -2.368260 0.154509 0.309041 +v -2.295893 0.154509 0.610472 +v -2.177262 0.154509 0.896871 +v -2.015290 0.154509 1.161187 +v -1.813963 0.154509 1.396910 +v -1.578240 0.154509 1.598236 +v -1.270914 0.154509 1.760209 +v -0.865071 0.154509 1.878839 +v -0.437927 0.154509 1.951207 +v 0.000000 0.154509 1.975529 +v 0.448989 0.154509 1.951206 +v 0.886922 0.154509 1.878839 +v 1.303017 0.154509 1.760209 +v 1.614034 0.154509 1.598236 +v 1.849757 0.154509 1.396910 +v 2.051083 0.154509 1.161187 +v 2.213056 0.154509 0.896871 +v 2.331686 0.154509 0.610472 +v 2.404054 0.154509 0.309041 +v 2.428376 0.154509 -0.000000 +v 2.428226 -0.000000 -0.312869 +v 2.354962 -0.000000 -0.618034 +v 2.234862 -0.000000 -0.907981 +v 2.070883 -0.000000 -1.175571 +v 1.867062 -0.000000 -1.414214 +v 1.628419 -0.000000 -1.618035 +v 1.319160 -0.000000 -1.782014 +v 0.897911 -0.000000 -1.902114 +v 0.454552 -0.000000 -1.975378 +v 0.000001 -0.000000 -2.000001 +v -0.443351 -0.000000 -1.975378 +v -0.875786 -0.000000 -1.902114 +v -1.286657 -0.000000 -1.782014 +v -1.592623 -0.000000 -1.618035 +v -1.831267 -0.000000 -1.414215 +v -2.035087 -0.000000 -1.175571 +v -2.199066 -0.000000 -0.907982 +v -2.319166 -0.000000 -0.618035 +v -2.392430 -0.000000 -0.312869 +v -2.417053 -0.000000 -0.000000 +v -2.392430 -0.000000 0.312869 +v -2.319167 -0.000000 0.618034 +v -2.199067 -0.000000 0.907981 +v -2.035087 -0.000000 1.175571 +v -1.831267 -0.000000 1.414214 +v -1.592624 -0.000000 1.618034 +v -1.286657 -0.000000 1.782013 +v -0.875787 -0.000000 1.902113 +v -0.443352 -0.000000 1.975377 +v 0.000000 -0.000000 2.000000 +v 0.454551 -0.000000 1.975377 +v 0.897909 -0.000000 1.902113 +v 1.319158 -0.000000 1.782013 +v 1.628418 -0.000000 1.618034 +v 1.867061 -0.000000 1.414214 +v 2.070881 -0.000000 1.175571 +v 2.234860 -0.000000 0.907981 +v 2.354960 -0.000000 0.618034 +v 2.428224 -0.000000 0.312869 +v 2.452847 -0.000000 -0.000000 +v 2.404055 -0.154509 -0.309040 +v 2.331688 -0.154509 -0.610472 +v 2.213058 -0.154509 -0.896871 +v 2.051085 -0.154509 -1.161187 +v 1.849758 -0.154509 -1.396910 +v 1.614035 -0.154509 -1.598237 +v 1.303019 -0.154509 -1.760209 +v 0.886924 -0.154509 -1.878840 +v 0.448990 -0.154509 -1.951207 +v 0.000001 -0.154509 -1.975530 +v -0.437926 -0.154509 -1.951208 +v -0.865070 -0.154509 -1.878840 +v -1.270914 -0.154509 -1.760210 +v -1.578240 -0.154509 -1.598237 +v -1.813963 -0.154509 -1.396911 +v -2.015289 -0.154509 -1.161187 +v -2.177262 -0.154509 -0.896872 +v -2.295892 -0.154509 -0.610472 +v -2.368260 -0.154509 -0.309041 +v -2.392582 -0.154509 -0.000000 +v -2.368260 -0.154509 0.309041 +v -2.295893 -0.154509 0.610472 +v -2.177262 -0.154509 0.896871 +v -2.015290 -0.154509 1.161187 +v -1.813963 -0.154509 1.396910 +v -1.578240 -0.154509 1.598236 +v -1.270914 -0.154509 1.760209 +v -0.865071 -0.154509 1.878839 +v -0.437927 -0.154509 1.951207 +v 0.000000 -0.154509 1.975529 +v 0.448989 -0.154509 1.951206 +v 0.886922 -0.154509 1.878839 +v 1.303017 -0.154509 1.760209 +v 1.614034 -0.154509 1.598236 +v 1.849757 -0.154509 1.396910 +v 2.051083 -0.154509 1.161187 +v 2.213056 -0.154509 0.896871 +v 2.331686 -0.154509 0.610472 +v 2.404054 -0.154509 0.309041 +v 2.428376 -0.154509 -0.000000 +v 2.333910 -0.293893 -0.297930 +v 2.264144 -0.293893 -0.588525 +v 2.149778 -0.293893 -0.864629 +v 1.993629 -0.293893 -1.119442 +v 1.799540 -0.293893 -1.346691 +v 1.572291 -0.293893 -1.540781 +v 1.256175 -0.293893 -1.696930 +v 0.855039 -0.293893 -1.811296 +v 0.432849 -0.293893 -1.881062 +v 0.000001 -0.293893 -1.904510 +v -0.422183 -0.293893 -1.881062 +v -0.833971 -0.293893 -1.811296 +v -1.225224 -0.293893 -1.696931 +v -1.536495 -0.293893 -1.540781 +v -1.763744 -0.293893 -1.346692 +v -1.957833 -0.293893 -1.119443 +v -2.113983 -0.293893 -0.864630 +v -2.228349 -0.293893 -0.588526 +v -2.298114 -0.293893 -0.297931 +v -2.321562 -0.293893 -0.000000 +v -2.298115 -0.293893 0.297931 +v -2.228349 -0.293893 0.588525 +v -2.113983 -0.293893 0.864629 +v -1.957833 -0.293893 1.119442 +v -1.763744 -0.293893 1.346691 +v -1.536495 -0.293893 1.540780 +v -1.225225 -0.293893 1.696930 +v -0.833972 -0.293893 1.811296 +v -0.422184 -0.293893 1.881061 +v 0.000000 -0.293893 1.904509 +v 0.432848 -0.293893 1.881061 +v 0.855038 -0.293893 1.811296 +v 1.256174 -0.293893 1.696930 +v 1.572289 -0.293893 1.540780 +v 1.799538 -0.293893 1.346691 +v 1.993627 -0.293893 1.119442 +v 2.149777 -0.293893 0.864629 +v 2.264143 -0.293893 0.588526 +v 2.333908 -0.293893 0.297931 +v 2.357356 -0.293893 -0.000000 +v 2.224656 -0.404509 -0.280626 +v 2.158942 -0.404509 -0.554343 +v 2.051219 -0.404509 -0.814410 +v 1.904138 -0.404509 -1.054424 +v 1.721322 -0.404509 -1.268474 +v 1.507272 -0.404509 -1.451290 +v 1.183215 -0.404509 -1.598371 +v 0.805378 -0.404509 -1.706094 +v 0.407709 -0.404509 -1.771808 +v 0.000001 -0.404509 -1.793894 +v -0.397662 -0.404509 -1.771808 +v -0.785533 -0.404509 -1.706095 +v -1.154062 -0.404509 -1.598371 +v -1.471477 -0.404509 -1.451291 +v -1.685527 -0.404509 -1.268475 +v -1.868343 -0.404509 -1.054425 +v -2.015424 -0.404509 -0.814411 +v -2.123147 -0.404509 -0.554344 +v -2.188860 -0.404509 -0.280627 +v -2.210946 -0.404509 -0.000000 +v -2.188860 -0.404509 0.280626 +v -2.123147 -0.404509 0.554343 +v -2.015424 -0.404509 0.814410 +v -1.868343 -0.404509 1.054424 +v -1.685527 -0.404509 1.268474 +v -1.471477 -0.404509 1.451290 +v -1.154063 -0.404509 1.598370 +v -0.785534 -0.404509 1.706094 +v -0.397663 -0.404509 1.771807 +v 0.000000 -0.404509 1.793893 +v 0.407708 -0.404509 1.771807 +v 0.805376 -0.404509 1.706094 +v 1.183214 -0.404509 1.598370 +v 1.507271 -0.404509 1.451290 +v 1.721321 -0.404509 1.268474 +v 1.904137 -0.404509 1.054424 +v 2.051218 -0.404509 0.814410 +v 2.158941 -0.404509 0.554343 +v 2.224654 -0.404509 0.280627 +v 2.246740 -0.404509 -0.000000 +v 2.086987 -0.475528 -0.258822 +v 2.026380 -0.475528 -0.511271 +v 1.927027 -0.475528 -0.751131 +v 1.791374 -0.475528 -0.972496 +v 1.622763 -0.475528 -1.169915 +v 1.412889 -0.475528 -1.338526 +v 1.091280 -0.475528 -1.474179 +v 0.742800 -0.475528 -1.573532 +v 0.376030 -0.475528 -1.634140 +v 0.000001 -0.475528 -1.654510 +v -0.366764 -0.475528 -1.634140 +v -0.724498 -0.475528 -1.573532 +v -1.064392 -0.475528 -1.474179 +v -1.378078 -0.475528 -1.338526 +v -1.586967 -0.475528 -1.169915 +v -1.755579 -0.475528 -0.972497 +v -1.891231 -0.475528 -0.751132 +v -1.990585 -0.475528 -0.511272 +v -2.051192 -0.475528 -0.258823 +v -2.071562 -0.475528 -0.000000 +v -2.051192 -0.475528 0.258822 +v -1.990585 -0.475528 0.511271 +v -1.891231 -0.475528 0.751131 +v -1.755579 -0.475528 0.972496 +v -1.586967 -0.475528 1.169914 +v -1.378078 -0.475528 1.338526 +v -1.064393 -0.475528 1.474178 +v -0.724499 -0.475528 1.573531 +v -0.366765 -0.475528 1.634139 +v 0.000000 -0.475528 1.654509 +v 0.376029 -0.475528 1.634139 +v 0.742799 -0.475528 1.573531 +v 1.091279 -0.475528 1.474178 +v 1.412888 -0.475528 1.338526 +v 1.622762 -0.475528 1.169914 +v 1.791373 -0.475528 0.972496 +v 1.927025 -0.475528 0.751131 +v 2.026379 -0.475528 0.511271 +v 2.086986 -0.475528 0.258822 +v 2.107356 -0.475528 -0.000000 +v 1.934381 -0.500000 -0.234651 +v 1.879433 -0.500000 -0.463525 +v 1.789358 -0.500000 -0.680986 +v 1.666374 -0.500000 -0.881678 +v 1.513509 -0.500000 -1.060661 +v 1.280945 -0.500000 -1.213526 +v 0.989370 -0.500000 -1.336510 +v 0.673433 -0.500000 -1.426586 +v 0.340914 -0.500000 -1.481533 +v 0.000001 -0.500000 -1.500001 +v -0.332513 -0.500000 -1.481533 +v -0.656840 -0.500000 -1.426586 +v -0.964993 -0.500000 -1.336511 +v -1.249384 -0.500000 -1.213526 +v -1.477713 -0.500000 -1.060661 +v -1.630579 -0.500000 -0.881679 +v -1.753563 -0.500000 -0.680986 +v -1.843638 -0.500000 -0.463526 +v -1.898586 -0.500000 -0.234652 +v -1.917053 -0.500000 -0.000000 +v -1.898586 -0.500000 0.234652 +v -1.843638 -0.500000 0.463525 +v -1.753563 -0.500000 0.680986 +v -1.630579 -0.500000 0.881678 +v -1.477713 -0.500000 1.060660 +v -1.249384 -0.500000 1.213526 +v -0.964993 -0.500000 1.336510 +v -0.656840 -0.500000 1.426585 +v -0.332514 -0.500000 1.481533 +v 0.000000 -0.500000 1.500000 +v 0.340913 -0.500000 1.481533 +v 0.673432 -0.500000 1.426585 +v 0.989368 -0.500000 1.336510 +v 1.280943 -0.500000 1.213526 +v 1.513507 -0.500000 1.060660 +v 1.666373 -0.500000 0.881678 +v 1.789357 -0.500000 0.680986 +v 1.879432 -0.500000 0.463526 +v 1.934380 -0.500000 0.234652 +v 1.952847 -0.500000 -0.000000 +v 1.781775 -0.475528 -0.210481 +v 1.732487 -0.475528 -0.415780 +v 1.651690 -0.475528 -0.610840 +v 1.541374 -0.475528 -0.790860 +v 1.382249 -0.475528 -0.951406 +v 1.149000 -0.475528 -1.088526 +v 0.887459 -0.475528 -1.198842 +v 0.604066 -0.475528 -1.279639 +v 0.305798 -0.475528 -1.328927 +v 0.000001 -0.475528 -1.345492 +v -0.298262 -0.475528 -1.328927 +v -0.589181 -0.475528 -1.279639 +v -0.865593 -0.475528 -1.198842 +v -1.120690 -0.475528 -1.088526 +v -1.348193 -0.475528 -0.951407 +v -1.505579 -0.475528 -0.790861 +v -1.615895 -0.475528 -0.610841 +v -1.696692 -0.475528 -0.415780 +v -1.745979 -0.475528 -0.210482 +v -1.762545 -0.475528 -0.000000 +v -1.745980 -0.475528 0.210481 +v -1.696692 -0.475528 0.415780 +v -1.615895 -0.475528 0.610840 +v -1.505579 -0.475528 0.790860 +v -1.348193 -0.475528 0.951406 +v -1.120691 -0.475528 1.088526 +v -0.865593 -0.475528 1.198842 +v -0.589182 -0.475528 1.279639 +v -0.298263 -0.475528 1.328926 +v 0.000000 -0.475528 1.345492 +v 0.305797 -0.475528 1.328926 +v 0.604065 -0.475528 1.279639 +v 0.887458 -0.475528 1.198842 +v 1.148999 -0.475528 1.088526 +v 1.382248 -0.475528 0.951406 +v 1.541373 -0.475528 0.790860 +v 1.651689 -0.475528 0.610840 +v 1.732486 -0.475528 0.415780 +v 1.781774 -0.475528 0.210481 +v 1.798339 -0.475528 -0.000000 +v 1.644107 -0.404509 -0.188676 +v 1.599925 -0.404509 -0.372708 +v 1.527498 -0.404509 -0.547561 +v 1.417634 -0.404509 -0.708932 +v 1.239057 -0.404509 -0.852847 +v 1.029971 -0.404509 -0.975762 +v 0.795524 -0.404509 -1.074650 +v 0.541488 -0.404509 -1.147077 +v 0.274119 -0.404509 -1.191259 +v 0.000001 -0.404509 -1.206108 +v -0.267364 -0.404509 -1.191259 +v -0.528146 -0.404509 -1.147077 +v -0.775923 -0.404509 -1.074650 +v -1.004594 -0.404509 -0.975762 +v -1.208529 -0.404509 -0.852847 +v -1.382705 -0.404509 -0.708933 +v -1.491703 -0.404509 -0.547562 +v -1.564129 -0.404509 -0.372708 +v -1.608311 -0.404509 -0.188677 +v -1.623160 -0.404509 -0.000000 +v -1.608311 -0.404509 0.188677 +v -1.564129 -0.404509 0.372708 +v -1.491703 -0.404509 0.547561 +v -1.382706 -0.404509 0.708932 +v -1.208529 -0.404509 0.852847 +v -1.004594 -0.404509 0.975761 +v -0.775923 -0.404509 1.074650 +v -0.528146 -0.404509 1.147076 +v -0.267365 -0.404509 1.191258 +v 0.000000 -0.404509 1.206107 +v 0.274119 -0.404509 1.191258 +v 0.541487 -0.404509 1.147076 +v 0.795523 -0.404509 1.074650 +v 1.029970 -0.404509 0.975761 +v 1.239056 -0.404509 0.852847 +v 1.417632 -0.404509 0.708932 +v 1.527497 -0.404509 0.547561 +v 1.599924 -0.404509 0.372708 +v 1.644105 -0.404509 0.188677 +v 1.658955 -0.404509 -0.000000 +v 1.534852 -0.293893 -0.171372 +v 1.494722 -0.293893 -0.338525 +v 1.418111 -0.293893 -0.497343 +v 1.287618 -0.293893 -0.643914 +v 1.125419 -0.293893 -0.774630 +v 0.935509 -0.293893 -0.886271 +v 0.722564 -0.293893 -0.976090 +v 0.491827 -0.293893 -1.041875 +v 0.248979 -0.293893 -1.082005 +v 0.000001 -0.293893 -1.095492 +v -0.242844 -0.293893 -1.082005 +v -0.479708 -0.293893 -1.041875 +v -0.704761 -0.293893 -0.976091 +v -0.912460 -0.293893 -0.886272 +v -1.097691 -0.293893 -0.774630 +v -1.255893 -0.293893 -0.643914 +v -1.383171 -0.293893 -0.497343 +v -1.458927 -0.293893 -0.338526 +v -1.499057 -0.293893 -0.171373 +v -1.512545 -0.293893 -0.000000 +v -1.499057 -0.293893 0.171372 +v -1.458927 -0.293893 0.338525 +v -1.383171 -0.293893 0.497343 +v -1.255893 -0.293893 0.643914 +v -1.097691 -0.293893 0.774629 +v -0.912460 -0.293893 0.886271 +v -0.704761 -0.293893 0.976090 +v -0.479708 -0.293893 1.041874 +v -0.242844 -0.293893 1.082004 +v 0.000000 -0.293893 1.095491 +v 0.248978 -0.293893 1.082004 +v 0.491826 -0.293893 1.041874 +v 0.722563 -0.293893 0.976090 +v 0.935508 -0.293893 0.886271 +v 1.125418 -0.293893 0.774629 +v 1.287617 -0.293893 0.643914 +v 1.418110 -0.293893 0.497343 +v 1.494722 -0.293893 0.338525 +v 1.534851 -0.293893 0.171373 +v 1.548339 -0.293893 -0.000000 +v 1.464707 -0.154509 -0.160262 +v 1.415555 -0.154509 -0.316579 +v 1.326176 -0.154509 -0.465100 +v 1.204143 -0.154509 -0.602169 +v 1.052459 -0.154509 -0.724411 +v 0.874861 -0.154509 -0.828815 +v 0.675721 -0.154509 -0.912811 +v 0.459942 -0.154509 -0.974331 +v 0.232838 -0.154509 -1.011859 +v 0.000001 -0.154509 -1.024472 +v -0.227100 -0.154509 -1.011859 +v -0.448609 -0.154509 -0.974331 +v -0.659072 -0.154509 -0.912811 +v -0.853306 -0.154509 -0.828815 +v -1.026528 -0.154509 -0.724411 +v -1.174475 -0.154509 -0.602170 +v -1.293501 -0.154509 -0.465101 +v -1.380678 -0.154509 -0.316579 +v -1.428912 -0.154509 -0.160263 +v -1.441525 -0.154509 -0.000000 +v -1.428912 -0.154509 0.160263 +v -1.380678 -0.154509 0.316579 +v -1.293501 -0.154509 0.465100 +v -1.174475 -0.154509 0.602169 +v -1.026529 -0.154509 0.724411 +v -0.853306 -0.154509 0.828815 +v -0.659072 -0.154509 0.912811 +v -0.448609 -0.154509 0.974330 +v -0.227101 -0.154509 1.011859 +v 0.000000 -0.154509 1.024472 +v 0.232837 -0.154509 1.011859 +v 0.459941 -0.154509 0.974330 +v 0.675720 -0.154509 0.912811 +v 0.874860 -0.154509 0.828815 +v 1.052458 -0.154509 0.724411 +v 1.204142 -0.154509 0.602169 +v 1.326175 -0.154509 0.465100 +v 1.415553 -0.154509 0.316579 +v 1.464706 -0.154509 0.160263 +v 1.477319 -0.154509 -0.000000 +vt 0.000000 1.000000 +vt 0.025000 1.000000 +vt 0.050000 1.000000 +vt 0.075000 1.000000 +vt 0.100000 1.000000 +vt 0.125000 1.000000 +vt 0.150000 1.000000 +vt 0.175000 1.000000 +vt 0.200000 1.000000 +vt 0.225000 1.000000 +vt 0.250000 1.000000 +vt 0.275000 1.000000 +vt 0.300000 1.000000 +vt 0.325000 1.000000 +vt 0.350000 1.000000 +vt 0.375000 1.000000 +vt 0.400000 1.000000 +vt 0.425000 1.000000 +vt 0.450000 1.000000 +vt 0.475000 1.000000 +vt 0.500000 1.000000 +vt 0.525000 1.000000 +vt 0.550000 1.000000 +vt 0.575000 1.000000 +vt 0.600000 1.000000 +vt 0.625000 1.000000 +vt 0.650000 1.000000 +vt 0.675000 1.000000 +vt 0.700000 1.000000 +vt 0.725000 1.000000 +vt 0.750000 1.000000 +vt 0.775000 1.000000 +vt 0.800000 1.000000 +vt 0.825000 1.000000 +vt 0.850000 1.000000 +vt 0.875000 1.000000 +vt 0.900000 1.000000 +vt 0.925000 1.000000 +vt 0.950000 1.000000 +vt 0.975000 1.000000 +vt 1.000000 1.000000 +vt 0.000000 0.950000 +vt 0.025000 0.950000 +vt 0.050000 0.950000 +vt 0.075000 0.950000 +vt 0.100000 0.950000 +vt 0.125000 0.950000 +vt 0.150000 0.950000 +vt 0.175000 0.950000 +vt 0.200000 0.950000 +vt 0.225000 0.950000 +vt 0.250000 0.950000 +vt 0.275000 0.950000 +vt 0.300000 0.950000 +vt 0.325000 0.950000 +vt 0.350000 0.950000 +vt 0.375000 0.950000 +vt 0.400000 0.950000 +vt 0.425000 0.950000 +vt 0.450000 0.950000 +vt 0.475000 0.950000 +vt 0.500000 0.950000 +vt 0.525000 0.950000 +vt 0.550000 0.950000 +vt 0.575000 0.950000 +vt 0.600000 0.950000 +vt 0.625000 0.950000 +vt 0.650000 0.950000 +vt 0.675000 0.950000 +vt 0.700000 0.950000 +vt 0.725000 0.950000 +vt 0.750000 0.950000 +vt 0.775000 0.950000 +vt 0.800000 0.950000 +vt 0.825000 0.950000 +vt 0.850000 0.950000 +vt 0.875000 0.950000 +vt 0.900000 0.950000 +vt 0.925000 0.950000 +vt 0.950000 0.950000 +vt 0.975000 0.950000 +vt 1.000000 0.950000 +vt 0.000000 0.900000 +vt 0.025000 0.900000 +vt 0.050000 0.900000 +vt 0.075000 0.900000 +vt 0.100000 0.900000 +vt 0.125000 0.900000 +vt 0.150000 0.900000 +vt 0.175000 0.900000 +vt 0.200000 0.900000 +vt 0.225000 0.900000 +vt 0.250000 0.900000 +vt 0.275000 0.900000 +vt 0.300000 0.900000 +vt 0.325000 0.900000 +vt 0.350000 0.900000 +vt 0.375000 0.900000 +vt 0.400000 0.900000 +vt 0.425000 0.900000 +vt 0.450000 0.900000 +vt 0.475000 0.900000 +vt 0.500000 0.900000 +vt 0.525000 0.900000 +vt 0.550000 0.900000 +vt 0.575000 0.900000 +vt 0.600000 0.900000 +vt 0.625000 0.900000 +vt 0.650000 0.900000 +vt 0.675000 0.900000 +vt 0.700000 0.900000 +vt 0.725000 0.900000 +vt 0.750000 0.900000 +vt 0.775000 0.900000 +vt 0.800000 0.900000 +vt 0.825000 0.900000 +vt 0.850000 0.900000 +vt 0.875000 0.900000 +vt 0.900000 0.900000 +vt 0.925000 0.900000 +vt 0.950000 0.900000 +vt 0.975000 0.900000 +vt 1.000000 0.900000 +vt 0.000000 0.850000 +vt 0.025000 0.850000 +vt 0.050000 0.850000 +vt 0.075000 0.850000 +vt 0.100000 0.850000 +vt 0.125000 0.850000 +vt 0.150000 0.850000 +vt 0.175000 0.850000 +vt 0.200000 0.850000 +vt 0.225000 0.850000 +vt 0.250000 0.850000 +vt 0.275000 0.850000 +vt 0.300000 0.850000 +vt 0.325000 0.850000 +vt 0.350000 0.850000 +vt 0.375000 0.850000 +vt 0.400000 0.850000 +vt 0.425000 0.850000 +vt 0.450000 0.850000 +vt 0.475000 0.850000 +vt 0.500000 0.850000 +vt 0.525000 0.850000 +vt 0.550000 0.850000 +vt 0.575000 0.850000 +vt 0.600000 0.850000 +vt 0.625000 0.850000 +vt 0.650000 0.850000 +vt 0.675000 0.850000 +vt 0.700000 0.850000 +vt 0.725000 0.850000 +vt 0.750000 0.850000 +vt 0.775000 0.850000 +vt 0.800000 0.850000 +vt 0.825000 0.850000 +vt 0.850000 0.850000 +vt 0.875000 0.850000 +vt 0.900000 0.850000 +vt 0.925000 0.850000 +vt 0.950000 0.850000 +vt 0.975000 0.850000 +vt 1.000000 0.850000 +vt 0.000000 0.800000 +vt 0.025000 0.800000 +vt 0.050000 0.800000 +vt 0.075000 0.800000 +vt 0.100000 0.800000 +vt 0.125000 0.800000 +vt 0.150000 0.800000 +vt 0.175000 0.800000 +vt 0.200000 0.800000 +vt 0.225000 0.800000 +vt 0.250000 0.800000 +vt 0.275000 0.800000 +vt 0.300000 0.800000 +vt 0.325000 0.800000 +vt 0.350000 0.800000 +vt 0.375000 0.800000 +vt 0.400000 0.800000 +vt 0.425000 0.800000 +vt 0.450000 0.800000 +vt 0.475000 0.800000 +vt 0.500000 0.800000 +vt 0.525000 0.800000 +vt 0.550000 0.800000 +vt 0.575000 0.800000 +vt 0.600000 0.800000 +vt 0.625000 0.800000 +vt 0.650000 0.800000 +vt 0.675000 0.800000 +vt 0.700000 0.800000 +vt 0.725000 0.800000 +vt 0.750000 0.800000 +vt 0.775000 0.800000 +vt 0.800000 0.800000 +vt 0.825000 0.800000 +vt 0.850000 0.800000 +vt 0.875000 0.800000 +vt 0.900000 0.800000 +vt 0.925000 0.800000 +vt 0.950000 0.800000 +vt 0.975000 0.800000 +vt 1.000000 0.800000 +vt 0.000000 0.750000 +vt 0.025000 0.750000 +vt 0.050000 0.750000 +vt 0.075000 0.750000 +vt 0.100000 0.750000 +vt 0.125000 0.750000 +vt 0.150000 0.750000 +vt 0.175000 0.750000 +vt 0.200000 0.750000 +vt 0.225000 0.750000 +vt 0.250000 0.750000 +vt 0.275000 0.750000 +vt 0.300000 0.750000 +vt 0.325000 0.750000 +vt 0.350000 0.750000 +vt 0.375000 0.750000 +vt 0.400000 0.750000 +vt 0.425000 0.750000 +vt 0.450000 0.750000 +vt 0.475000 0.750000 +vt 0.500000 0.750000 +vt 0.525000 0.750000 +vt 0.550000 0.750000 +vt 0.575000 0.750000 +vt 0.600000 0.750000 +vt 0.625000 0.750000 +vt 0.650000 0.750000 +vt 0.675000 0.750000 +vt 0.700000 0.750000 +vt 0.725000 0.750000 +vt 0.750000 0.750000 +vt 0.775000 0.750000 +vt 0.800000 0.750000 +vt 0.825000 0.750000 +vt 0.850000 0.750000 +vt 0.875000 0.750000 +vt 0.900000 0.750000 +vt 0.925000 0.750000 +vt 0.950000 0.750000 +vt 0.975000 0.750000 +vt 1.000000 0.750000 +vt 0.000000 0.700000 +vt 0.025000 0.700000 +vt 0.050000 0.700000 +vt 0.075000 0.700000 +vt 0.100000 0.700000 +vt 0.125000 0.700000 +vt 0.150000 0.700000 +vt 0.175000 0.700000 +vt 0.200000 0.700000 +vt 0.225000 0.700000 +vt 0.250000 0.700000 +vt 0.275000 0.700000 +vt 0.300000 0.700000 +vt 0.325000 0.700000 +vt 0.350000 0.700000 +vt 0.375000 0.700000 +vt 0.400000 0.700000 +vt 0.425000 0.700000 +vt 0.450000 0.700000 +vt 0.475000 0.700000 +vt 0.500000 0.700000 +vt 0.525000 0.700000 +vt 0.550000 0.700000 +vt 0.575000 0.700000 +vt 0.600000 0.700000 +vt 0.625000 0.700000 +vt 0.650000 0.700000 +vt 0.675000 0.700000 +vt 0.700000 0.700000 +vt 0.725000 0.700000 +vt 0.750000 0.700000 +vt 0.775000 0.700000 +vt 0.800000 0.700000 +vt 0.825000 0.700000 +vt 0.850000 0.700000 +vt 0.875000 0.700000 +vt 0.900000 0.700000 +vt 0.925000 0.700000 +vt 0.950000 0.700000 +vt 0.975000 0.700000 +vt 1.000000 0.700000 +vt 0.000000 0.650000 +vt 0.025000 0.650000 +vt 0.050000 0.650000 +vt 0.075000 0.650000 +vt 0.100000 0.650000 +vt 0.125000 0.650000 +vt 0.150000 0.650000 +vt 0.175000 0.650000 +vt 0.200000 0.650000 +vt 0.225000 0.650000 +vt 0.250000 0.650000 +vt 0.275000 0.650000 +vt 0.300000 0.650000 +vt 0.325000 0.650000 +vt 0.350000 0.650000 +vt 0.375000 0.650000 +vt 0.400000 0.650000 +vt 0.425000 0.650000 +vt 0.450000 0.650000 +vt 0.475000 0.650000 +vt 0.500000 0.650000 +vt 0.525000 0.650000 +vt 0.550000 0.650000 +vt 0.575000 0.650000 +vt 0.600000 0.650000 +vt 0.625000 0.650000 +vt 0.650000 0.650000 +vt 0.675000 0.650000 +vt 0.700000 0.650000 +vt 0.725000 0.650000 +vt 0.750000 0.650000 +vt 0.775000 0.650000 +vt 0.800000 0.650000 +vt 0.825000 0.650000 +vt 0.850000 0.650000 +vt 0.875000 0.650000 +vt 0.900000 0.650000 +vt 0.925000 0.650000 +vt 0.950000 0.650000 +vt 0.975000 0.650000 +vt 1.000000 0.650000 +vt 0.000000 0.600000 +vt 0.025000 0.600000 +vt 0.050000 0.600000 +vt 0.075000 0.600000 +vt 0.100000 0.600000 +vt 0.125000 0.600000 +vt 0.150000 0.600000 +vt 0.175000 0.600000 +vt 0.200000 0.600000 +vt 0.225000 0.600000 +vt 0.250000 0.600000 +vt 0.275000 0.600000 +vt 0.300000 0.600000 +vt 0.325000 0.600000 +vt 0.350000 0.600000 +vt 0.375000 0.600000 +vt 0.400000 0.600000 +vt 0.425000 0.600000 +vt 0.450000 0.600000 +vt 0.475000 0.600000 +vt 0.500000 0.600000 +vt 0.525000 0.600000 +vt 0.550000 0.600000 +vt 0.575000 0.600000 +vt 0.600000 0.600000 +vt 0.625000 0.600000 +vt 0.650000 0.600000 +vt 0.675000 0.600000 +vt 0.700000 0.600000 +vt 0.725000 0.600000 +vt 0.750000 0.600000 +vt 0.775000 0.600000 +vt 0.800000 0.600000 +vt 0.825000 0.600000 +vt 0.850000 0.600000 +vt 0.875000 0.600000 +vt 0.900000 0.600000 +vt 0.925000 0.600000 +vt 0.950000 0.600000 +vt 0.975000 0.600000 +vt 1.000000 0.600000 +vt 0.000000 0.550000 +vt 0.025000 0.550000 +vt 0.050000 0.550000 +vt 0.075000 0.550000 +vt 0.100000 0.550000 +vt 0.125000 0.550000 +vt 0.150000 0.550000 +vt 0.175000 0.550000 +vt 0.200000 0.550000 +vt 0.225000 0.550000 +vt 0.250000 0.550000 +vt 0.275000 0.550000 +vt 0.300000 0.550000 +vt 0.325000 0.550000 +vt 0.350000 0.550000 +vt 0.375000 0.550000 +vt 0.400000 0.550000 +vt 0.425000 0.550000 +vt 0.450000 0.550000 +vt 0.475000 0.550000 +vt 0.500000 0.550000 +vt 0.525000 0.550000 +vt 0.550000 0.550000 +vt 0.575000 0.550000 +vt 0.600000 0.550000 +vt 0.625000 0.550000 +vt 0.650000 0.550000 +vt 0.675000 0.550000 +vt 0.700000 0.550000 +vt 0.725000 0.550000 +vt 0.750000 0.550000 +vt 0.775000 0.550000 +vt 0.800000 0.550000 +vt 0.825000 0.550000 +vt 0.850000 0.550000 +vt 0.875000 0.550000 +vt 0.900000 0.550000 +vt 0.925000 0.550000 +vt 0.950000 0.550000 +vt 0.975000 0.550000 +vt 1.000000 0.550000 +vt 0.000000 0.500000 +vt 0.025000 0.500000 +vt 0.050000 0.500000 +vt 0.075000 0.500000 +vt 0.100000 0.500000 +vt 0.125000 0.500000 +vt 0.150000 0.500000 +vt 0.175000 0.500000 +vt 0.200000 0.500000 +vt 0.225000 0.500000 +vt 0.250000 0.500000 +vt 0.275000 0.500000 +vt 0.300000 0.500000 +vt 0.325000 0.500000 +vt 0.350000 0.500000 +vt 0.375000 0.500000 +vt 0.400000 0.500000 +vt 0.425000 0.500000 +vt 0.450000 0.500000 +vt 0.475000 0.500000 +vt 0.500000 0.500000 +vt 0.525000 0.500000 +vt 0.550000 0.500000 +vt 0.575000 0.500000 +vt 0.600000 0.500000 +vt 0.625000 0.500000 +vt 0.650000 0.500000 +vt 0.675000 0.500000 +vt 0.700000 0.500000 +vt 0.725000 0.500000 +vt 0.750000 0.500000 +vt 0.775000 0.500000 +vt 0.800000 0.500000 +vt 0.825000 0.500000 +vt 0.850000 0.500000 +vt 0.875000 0.500000 +vt 0.900000 0.500000 +vt 0.925000 0.500000 +vt 0.950000 0.500000 +vt 0.975000 0.500000 +vt 1.000000 0.500000 +vt 0.000000 0.450000 +vt 0.025000 0.450000 +vt 0.050000 0.450000 +vt 0.075000 0.450000 +vt 0.100000 0.450000 +vt 0.125000 0.450000 +vt 0.150000 0.450000 +vt 0.175000 0.450000 +vt 0.200000 0.450000 +vt 0.225000 0.450000 +vt 0.250000 0.450000 +vt 0.275000 0.450000 +vt 0.300000 0.450000 +vt 0.325000 0.450000 +vt 0.350000 0.450000 +vt 0.375000 0.450000 +vt 0.400000 0.450000 +vt 0.425000 0.450000 +vt 0.450000 0.450000 +vt 0.475000 0.450000 +vt 0.500000 0.450000 +vt 0.525000 0.450000 +vt 0.550000 0.450000 +vt 0.575000 0.450000 +vt 0.600000 0.450000 +vt 0.625000 0.450000 +vt 0.650000 0.450000 +vt 0.675000 0.450000 +vt 0.700000 0.450000 +vt 0.725000 0.450000 +vt 0.750000 0.450000 +vt 0.775000 0.450000 +vt 0.800000 0.450000 +vt 0.825000 0.450000 +vt 0.850000 0.450000 +vt 0.875000 0.450000 +vt 0.900000 0.450000 +vt 0.925000 0.450000 +vt 0.950000 0.450000 +vt 0.975000 0.450000 +vt 1.000000 0.450000 +vt 0.000000 0.400000 +vt 0.025000 0.400000 +vt 0.050000 0.400000 +vt 0.075000 0.400000 +vt 0.100000 0.400000 +vt 0.125000 0.400000 +vt 0.150000 0.400000 +vt 0.175000 0.400000 +vt 0.200000 0.400000 +vt 0.225000 0.400000 +vt 0.250000 0.400000 +vt 0.275000 0.400000 +vt 0.300000 0.400000 +vt 0.325000 0.400000 +vt 0.350000 0.400000 +vt 0.375000 0.400000 +vt 0.400000 0.400000 +vt 0.425000 0.400000 +vt 0.450000 0.400000 +vt 0.475000 0.400000 +vt 0.500000 0.400000 +vt 0.525000 0.400000 +vt 0.550000 0.400000 +vt 0.575000 0.400000 +vt 0.600000 0.400000 +vt 0.625000 0.400000 +vt 0.650000 0.400000 +vt 0.675000 0.400000 +vt 0.700000 0.400000 +vt 0.725000 0.400000 +vt 0.750000 0.400000 +vt 0.775000 0.400000 +vt 0.800000 0.400000 +vt 0.825000 0.400000 +vt 0.850000 0.400000 +vt 0.875000 0.400000 +vt 0.900000 0.400000 +vt 0.925000 0.400000 +vt 0.950000 0.400000 +vt 0.975000 0.400000 +vt 1.000000 0.400000 +vt 0.000000 0.350000 +vt 0.025000 0.350000 +vt 0.050000 0.350000 +vt 0.075000 0.350000 +vt 0.100000 0.350000 +vt 0.125000 0.350000 +vt 0.150000 0.350000 +vt 0.175000 0.350000 +vt 0.200000 0.350000 +vt 0.225000 0.350000 +vt 0.250000 0.350000 +vt 0.275000 0.350000 +vt 0.300000 0.350000 +vt 0.325000 0.350000 +vt 0.350000 0.350000 +vt 0.375000 0.350000 +vt 0.400000 0.350000 +vt 0.425000 0.350000 +vt 0.450000 0.350000 +vt 0.475000 0.350000 +vt 0.500000 0.350000 +vt 0.525000 0.350000 +vt 0.550000 0.350000 +vt 0.575000 0.350000 +vt 0.600000 0.350000 +vt 0.625000 0.350000 +vt 0.650000 0.350000 +vt 0.675000 0.350000 +vt 0.700000 0.350000 +vt 0.725000 0.350000 +vt 0.750000 0.350000 +vt 0.775000 0.350000 +vt 0.800000 0.350000 +vt 0.825000 0.350000 +vt 0.850000 0.350000 +vt 0.875000 0.350000 +vt 0.900000 0.350000 +vt 0.925000 0.350000 +vt 0.950000 0.350000 +vt 0.975000 0.350000 +vt 1.000000 0.350000 +vt 0.000000 0.300000 +vt 0.025000 0.300000 +vt 0.050000 0.300000 +vt 0.075000 0.300000 +vt 0.100000 0.300000 +vt 0.125000 0.300000 +vt 0.150000 0.300000 +vt 0.175000 0.300000 +vt 0.200000 0.300000 +vt 0.225000 0.300000 +vt 0.250000 0.300000 +vt 0.275000 0.300000 +vt 0.300000 0.300000 +vt 0.325000 0.300000 +vt 0.350000 0.300000 +vt 0.375000 0.300000 +vt 0.400000 0.300000 +vt 0.425000 0.300000 +vt 0.450000 0.300000 +vt 0.475000 0.300000 +vt 0.500000 0.300000 +vt 0.525000 0.300000 +vt 0.550000 0.300000 +vt 0.575000 0.300000 +vt 0.600000 0.300000 +vt 0.625000 0.300000 +vt 0.650000 0.300000 +vt 0.675000 0.300000 +vt 0.700000 0.300000 +vt 0.725000 0.300000 +vt 0.750000 0.300000 +vt 0.775000 0.300000 +vt 0.800000 0.300000 +vt 0.825000 0.300000 +vt 0.850000 0.300000 +vt 0.875000 0.300000 +vt 0.900000 0.300000 +vt 0.925000 0.300000 +vt 0.950000 0.300000 +vt 0.975000 0.300000 +vt 1.000000 0.300000 +vt 0.000000 0.250000 +vt 0.025000 0.250000 +vt 0.050000 0.250000 +vt 0.075000 0.250000 +vt 0.100000 0.250000 +vt 0.125000 0.250000 +vt 0.150000 0.250000 +vt 0.175000 0.250000 +vt 0.200000 0.250000 +vt 0.225000 0.250000 +vt 0.250000 0.250000 +vt 0.275000 0.250000 +vt 0.300000 0.250000 +vt 0.325000 0.250000 +vt 0.350000 0.250000 +vt 0.375000 0.250000 +vt 0.400000 0.250000 +vt 0.425000 0.250000 +vt 0.450000 0.250000 +vt 0.475000 0.250000 +vt 0.500000 0.250000 +vt 0.525000 0.250000 +vt 0.550000 0.250000 +vt 0.575000 0.250000 +vt 0.600000 0.250000 +vt 0.625000 0.250000 +vt 0.650000 0.250000 +vt 0.675000 0.250000 +vt 0.700000 0.250000 +vt 0.725000 0.250000 +vt 0.750000 0.250000 +vt 0.775000 0.250000 +vt 0.800000 0.250000 +vt 0.825000 0.250000 +vt 0.850000 0.250000 +vt 0.875000 0.250000 +vt 0.900000 0.250000 +vt 0.925000 0.250000 +vt 0.950000 0.250000 +vt 0.975000 0.250000 +vt 1.000000 0.250000 +vt 0.000000 0.200000 +vt 0.025000 0.200000 +vt 0.050000 0.200000 +vt 0.075000 0.200000 +vt 0.100000 0.200000 +vt 0.125000 0.200000 +vt 0.150000 0.200000 +vt 0.175000 0.200000 +vt 0.200000 0.200000 +vt 0.225000 0.200000 +vt 0.250000 0.200000 +vt 0.275000 0.200000 +vt 0.300000 0.200000 +vt 0.325000 0.200000 +vt 0.350000 0.200000 +vt 0.375000 0.200000 +vt 0.400000 0.200000 +vt 0.425000 0.200000 +vt 0.450000 0.200000 +vt 0.475000 0.200000 +vt 0.500000 0.200000 +vt 0.525000 0.200000 +vt 0.550000 0.200000 +vt 0.575000 0.200000 +vt 0.600000 0.200000 +vt 0.625000 0.200000 +vt 0.650000 0.200000 +vt 0.675000 0.200000 +vt 0.700000 0.200000 +vt 0.725000 0.200000 +vt 0.750000 0.200000 +vt 0.775000 0.200000 +vt 0.800000 0.200000 +vt 0.825000 0.200000 +vt 0.850000 0.200000 +vt 0.875000 0.200000 +vt 0.900000 0.200000 +vt 0.925000 0.200000 +vt 0.950000 0.200000 +vt 0.975000 0.200000 +vt 1.000000 0.200000 +vt 0.000000 0.150000 +vt 0.025000 0.150000 +vt 0.050000 0.150000 +vt 0.075000 0.150000 +vt 0.100000 0.150000 +vt 0.125000 0.150000 +vt 0.150000 0.150000 +vt 0.175000 0.150000 +vt 0.200000 0.150000 +vt 0.225000 0.150000 +vt 0.250000 0.150000 +vt 0.275000 0.150000 +vt 0.300000 0.150000 +vt 0.325000 0.150000 +vt 0.350000 0.150000 +vt 0.375000 0.150000 +vt 0.400000 0.150000 +vt 0.425000 0.150000 +vt 0.450000 0.150000 +vt 0.475000 0.150000 +vt 0.500000 0.150000 +vt 0.525000 0.150000 +vt 0.550000 0.150000 +vt 0.575000 0.150000 +vt 0.600000 0.150000 +vt 0.625000 0.150000 +vt 0.650000 0.150000 +vt 0.675000 0.150000 +vt 0.700000 0.150000 +vt 0.725000 0.150000 +vt 0.750000 0.150000 +vt 0.775000 0.150000 +vt 0.800000 0.150000 +vt 0.825000 0.150000 +vt 0.850000 0.150000 +vt 0.875000 0.150000 +vt 0.900000 0.150000 +vt 0.925000 0.150000 +vt 0.950000 0.150000 +vt 0.975000 0.150000 +vt 1.000000 0.150000 +vt 0.000000 0.100000 +vt 0.025000 0.100000 +vt 0.050000 0.100000 +vt 0.075000 0.100000 +vt 0.100000 0.100000 +vt 0.125000 0.100000 +vt 0.150000 0.100000 +vt 0.175000 0.100000 +vt 0.200000 0.100000 +vt 0.225000 0.100000 +vt 0.250000 0.100000 +vt 0.275000 0.100000 +vt 0.300000 0.100000 +vt 0.325000 0.100000 +vt 0.350000 0.100000 +vt 0.375000 0.100000 +vt 0.400000 0.100000 +vt 0.425000 0.100000 +vt 0.450000 0.100000 +vt 0.475000 0.100000 +vt 0.500000 0.100000 +vt 0.525000 0.100000 +vt 0.550000 0.100000 +vt 0.575000 0.100000 +vt 0.600000 0.100000 +vt 0.625000 0.100000 +vt 0.650000 0.100000 +vt 0.675000 0.100000 +vt 0.700000 0.100000 +vt 0.725000 0.100000 +vt 0.750000 0.100000 +vt 0.775000 0.100000 +vt 0.800000 0.100000 +vt 0.825000 0.100000 +vt 0.850000 0.100000 +vt 0.875000 0.100000 +vt 0.900000 0.100000 +vt 0.925000 0.100000 +vt 0.950000 0.100000 +vt 0.975000 0.100000 +vt 1.000000 0.100000 +vt 0.000000 0.050000 +vt 0.025000 0.050000 +vt 0.050000 0.050000 +vt 0.075000 0.050000 +vt 0.100000 0.050000 +vt 0.125000 0.050000 +vt 0.150000 0.050000 +vt 0.175000 0.050000 +vt 0.200000 0.050000 +vt 0.225000 0.050000 +vt 0.250000 0.050000 +vt 0.275000 0.050000 +vt 0.300000 0.050000 +vt 0.325000 0.050000 +vt 0.350000 0.050000 +vt 0.375000 0.050000 +vt 0.400000 0.050000 +vt 0.425000 0.050000 +vt 0.450000 0.050000 +vt 0.475000 0.050000 +vt 0.500000 0.050000 +vt 0.525000 0.050000 +vt 0.550000 0.050000 +vt 0.575000 0.050000 +vt 0.600000 0.050000 +vt 0.625000 0.050000 +vt 0.650000 0.050000 +vt 0.675000 0.050000 +vt 0.700000 0.050000 +vt 0.725000 0.050000 +vt 0.750000 0.050000 +vt 0.775000 0.050000 +vt 0.800000 0.050000 +vt 0.825000 0.050000 +vt 0.850000 0.050000 +vt 0.875000 0.050000 +vt 0.900000 0.050000 +vt 0.925000 0.050000 +vt 0.950000 0.050000 +vt 0.975000 0.050000 +vt 1.000000 0.050000 +vt 0.000000 -0.000000 +vt 0.025000 -0.000000 +vt 0.050000 -0.000000 +vt 0.075000 -0.000000 +vt 0.100000 -0.000000 +vt 0.125000 -0.000000 +vt 0.150000 -0.000000 +vt 0.175000 -0.000000 +vt 0.200000 -0.000000 +vt 0.225000 -0.000000 +vt 0.250000 -0.000000 +vt 0.275000 -0.000000 +vt 0.300000 -0.000000 +vt 0.325000 -0.000000 +vt 0.350000 -0.000000 +vt 0.375000 -0.000000 +vt 0.400000 -0.000000 +vt 0.425000 -0.000000 +vt 0.450000 -0.000000 +vt 0.475000 -0.000000 +vt 0.500000 -0.000000 +vt 0.525000 -0.000000 +vt 0.550000 -0.000000 +vt 0.575000 -0.000000 +vt 0.600000 -0.000000 +vt 0.625000 -0.000000 +vt 0.650000 -0.000000 +vt 0.675000 -0.000000 +vt 0.700000 -0.000000 +vt 0.725000 -0.000000 +vt 0.750000 -0.000000 +vt 0.775000 -0.000000 +vt 0.800000 -0.000000 +vt 0.825000 -0.000000 +vt 0.850000 -0.000000 +vt 0.875000 -0.000000 +vt 0.900000 -0.000000 +vt 0.925000 -0.000000 +vt 0.950000 -0.000000 +vt 0.975000 -0.000000 +vt 1.000000 -0.000000 +f 41/42 42/43 1/1 +f 2/2 1/1 42/43 +f 42/43 43/44 2/2 +f 3/3 2/2 43/44 +f 43/44 44/45 3/3 +f 4/4 3/3 44/45 +f 44/45 45/46 4/4 +f 5/5 4/4 45/46 +f 45/46 46/47 5/5 +f 6/6 5/5 46/47 +f 46/47 47/48 6/6 +f 7/7 6/6 47/48 +f 47/48 48/49 7/7 +f 8/8 7/7 48/49 +f 48/49 49/50 8/8 +f 9/9 8/8 49/50 +f 49/50 50/51 9/9 +f 10/10 9/9 50/51 +f 50/51 51/52 10/10 +f 11/11 10/10 51/52 +f 52/53 12/12 51/52 +f 12/12 11/11 51/52 +f 53/54 13/13 52/53 +f 13/13 12/12 52/53 +f 54/55 14/14 53/54 +f 14/14 13/13 53/54 +f 55/56 15/15 54/55 +f 15/15 14/14 54/55 +f 56/57 16/16 55/56 +f 16/16 15/15 55/56 +f 57/58 17/17 56/57 +f 17/17 16/16 56/57 +f 58/59 18/18 57/58 +f 18/18 17/17 57/58 +f 59/60 19/19 58/59 +f 19/19 18/18 58/59 +f 59/60 60/61 19/19 +f 20/20 19/19 60/61 +f 60/61 61/62 20/20 +f 21/21 20/20 61/62 +f 61/62 62/63 21/21 +f 22/22 21/21 62/63 +f 62/63 63/64 22/22 +f 23/23 22/22 63/64 +f 63/64 64/65 23/23 +f 24/24 23/23 64/65 +f 64/65 65/66 24/24 +f 25/25 24/24 65/66 +f 65/66 66/67 25/25 +f 26/26 25/25 66/67 +f 66/67 67/68 26/26 +f 27/27 26/26 67/68 +f 67/68 68/69 27/27 +f 28/28 27/27 68/69 +f 68/69 69/70 28/28 +f 29/29 28/28 69/70 +f 69/70 70/71 29/29 +f 30/30 29/29 70/71 +f 70/71 71/72 30/30 +f 31/31 30/30 71/72 +f 72/73 32/32 71/72 +f 32/32 31/31 71/72 +f 73/74 33/33 72/73 +f 33/33 32/32 72/73 +f 74/75 34/34 73/74 +f 34/34 33/33 73/74 +f 75/76 35/35 74/75 +f 35/35 34/34 74/75 +f 76/77 36/36 75/76 +f 36/36 35/35 75/76 +f 77/78 37/37 76/77 +f 37/37 36/36 76/77 +f 78/79 38/38 77/78 +f 38/38 37/37 77/78 +f 79/80 39/39 78/79 +f 39/39 38/38 78/79 +f 79/80 80/81 39/39 +f 40/40 39/39 80/81 +f 80/81 41/82 40/40 +f 1/41 40/40 41/82 +f 82/84 42/43 81/83 +f 42/43 41/42 81/83 +f 43/44 42/43 83/85 +f 42/43 82/84 83/85 +f 83/85 84/86 43/44 +f 44/45 43/44 84/86 +f 84/86 85/87 44/45 +f 45/46 44/45 85/87 +f 85/87 86/88 45/46 +f 46/47 45/46 86/88 +f 86/88 87/89 46/47 +f 47/48 46/47 87/89 +f 87/89 88/90 47/48 +f 48/49 47/48 88/90 +f 88/90 89/91 48/49 +f 49/50 48/49 89/91 +f 89/91 90/92 49/50 +f 50/51 49/50 90/92 +f 91/93 51/52 90/92 +f 51/52 50/51 90/92 +f 92/94 52/53 91/93 +f 52/53 51/52 91/93 +f 93/95 53/54 92/94 +f 53/54 52/53 92/94 +f 94/96 54/55 93/95 +f 54/55 53/54 93/95 +f 95/97 55/56 94/96 +f 55/56 54/55 94/96 +f 96/98 56/57 95/97 +f 56/57 55/56 95/97 +f 97/99 57/58 96/98 +f 57/58 56/57 96/98 +f 57/58 97/99 58/59 +f 58/59 97/99 98/100 +f 98/100 99/101 58/59 +f 59/60 58/59 99/101 +f 99/101 100/102 59/60 +f 60/61 59/60 100/102 +f 100/102 101/103 60/61 +f 61/62 60/61 101/103 +f 102/104 62/63 101/103 +f 62/63 61/62 101/103 +f 63/64 62/63 103/105 +f 62/63 102/104 103/105 +f 103/105 104/106 63/64 +f 64/65 63/64 104/106 +f 104/106 105/107 64/65 +f 65/66 64/65 105/107 +f 105/107 106/108 65/66 +f 66/67 65/66 106/108 +f 106/108 107/109 66/67 +f 67/68 66/67 107/109 +f 107/109 108/110 67/68 +f 68/69 67/68 108/110 +f 108/110 109/111 68/69 +f 69/70 68/69 109/111 +f 109/111 110/112 69/70 +f 70/71 69/70 110/112 +f 111/113 71/72 110/112 +f 71/72 70/71 110/112 +f 112/114 72/73 111/113 +f 72/73 71/72 111/113 +f 113/115 73/74 112/114 +f 73/74 72/73 112/114 +f 114/116 74/75 113/115 +f 74/75 73/74 113/115 +f 115/117 75/76 114/116 +f 75/76 74/75 114/116 +f 116/118 76/77 115/117 +f 76/77 75/76 115/117 +f 117/119 77/78 116/118 +f 77/78 76/77 116/118 +f 77/78 117/119 78/79 +f 78/79 117/119 118/120 +f 118/120 119/121 78/79 +f 79/80 78/79 119/121 +f 119/121 120/122 79/80 +f 80/81 79/80 120/122 +f 120/122 81/123 80/81 +f 41/82 80/81 81/123 +f 121/124 122/125 81/83 +f 82/84 81/83 122/125 +f 123/126 83/85 122/125 +f 83/85 82/84 122/125 +f 84/86 83/85 124/127 +f 83/85 123/126 124/127 +f 124/127 125/128 84/86 +f 85/87 84/86 125/128 +f 125/128 126/129 85/87 +f 86/88 85/87 126/129 +f 126/129 127/130 86/88 +f 87/89 86/88 127/130 +f 127/130 128/131 87/89 +f 88/90 87/89 128/131 +f 128/131 129/132 88/90 +f 89/91 88/90 129/132 +f 129/132 130/133 89/91 +f 90/92 89/91 130/133 +f 131/134 91/93 130/133 +f 91/93 90/92 130/133 +f 132/135 92/94 131/134 +f 92/94 91/93 131/134 +f 133/136 93/95 132/135 +f 93/95 92/94 132/135 +f 134/137 94/96 133/136 +f 94/96 93/95 133/136 +f 135/138 95/97 134/137 +f 95/97 94/96 134/137 +f 136/139 96/98 135/138 +f 96/98 95/97 135/138 +f 96/98 136/139 97/99 +f 97/99 136/139 137/140 +f 137/140 138/141 97/99 +f 98/100 97/99 138/141 +f 138/141 139/142 98/100 +f 99/101 98/100 139/142 +f 139/142 140/143 99/101 +f 100/102 99/101 140/143 +f 140/143 141/144 100/102 +f 101/103 100/102 141/144 +f 141/144 142/145 101/103 +f 102/104 101/103 142/145 +f 143/146 103/105 142/145 +f 103/105 102/104 142/145 +f 104/106 103/105 144/147 +f 103/105 143/146 144/147 +f 144/147 145/148 104/106 +f 105/107 104/106 145/148 +f 145/148 146/149 105/107 +f 106/108 105/107 146/149 +f 146/149 147/150 106/108 +f 107/109 106/108 147/150 +f 147/150 148/151 107/109 +f 108/110 107/109 148/151 +f 148/151 149/152 108/110 +f 109/111 108/110 149/152 +f 149/152 150/153 109/111 +f 110/112 109/111 150/153 +f 151/154 111/113 150/153 +f 111/113 110/112 150/153 +f 152/155 112/114 151/154 +f 112/114 111/113 151/154 +f 153/156 113/115 152/155 +f 113/115 112/114 152/155 +f 154/157 114/116 153/156 +f 114/116 113/115 153/156 +f 155/158 115/117 154/157 +f 115/117 114/116 154/157 +f 156/159 116/118 155/158 +f 116/118 115/117 155/158 +f 116/118 156/159 117/119 +f 117/119 156/159 157/160 +f 157/160 158/161 117/119 +f 118/120 117/119 158/161 +f 158/161 159/162 118/120 +f 119/121 118/120 159/162 +f 159/162 160/163 119/121 +f 120/122 119/121 160/163 +f 160/163 121/164 120/122 +f 81/123 120/122 121/164 +f 161/165 162/166 121/124 +f 122/125 121/124 162/166 +f 163/167 123/126 162/166 +f 123/126 122/125 162/166 +f 164/168 124/127 163/167 +f 124/127 123/126 163/167 +f 125/128 124/127 165/169 +f 124/127 164/168 165/169 +f 165/169 166/170 125/128 +f 126/129 125/128 166/170 +f 166/170 167/171 126/129 +f 127/130 126/129 167/171 +f 167/171 168/172 127/130 +f 128/131 127/130 168/172 +f 168/172 169/173 128/131 +f 129/132 128/131 169/173 +f 169/173 170/174 129/132 +f 130/133 129/132 170/174 +f 171/175 131/134 170/174 +f 131/134 130/133 170/174 +f 172/176 132/135 171/175 +f 132/135 131/134 171/175 +f 173/177 133/136 172/176 +f 133/136 132/135 172/176 +f 174/178 134/137 173/177 +f 134/137 133/136 173/177 +f 175/179 135/138 174/178 +f 135/138 134/137 174/178 +f 135/138 175/179 136/139 +f 136/139 175/179 176/180 +f 176/180 177/181 136/139 +f 137/140 136/139 177/181 +f 177/181 178/182 137/140 +f 138/141 137/140 178/182 +f 178/182 179/183 138/141 +f 139/142 138/141 179/183 +f 179/183 180/184 139/142 +f 140/143 139/142 180/184 +f 180/184 181/185 140/143 +f 141/144 140/143 181/185 +f 181/185 182/186 141/144 +f 142/145 141/144 182/186 +f 183/187 143/146 182/186 +f 143/146 142/145 182/186 +f 184/188 144/147 183/187 +f 144/147 143/146 183/187 +f 145/148 144/147 185/189 +f 144/147 184/188 185/189 +f 185/189 186/190 145/148 +f 146/149 145/148 186/190 +f 186/190 187/191 146/149 +f 147/150 146/149 187/191 +f 187/191 188/192 147/150 +f 148/151 147/150 188/192 +f 188/192 189/193 148/151 +f 149/152 148/151 189/193 +f 189/193 190/194 149/152 +f 150/153 149/152 190/194 +f 191/195 151/154 190/194 +f 151/154 150/153 190/194 +f 192/196 152/155 191/195 +f 152/155 151/154 191/195 +f 193/197 153/156 192/196 +f 153/156 152/155 192/196 +f 194/198 154/157 193/197 +f 154/157 153/156 193/197 +f 195/199 155/158 194/198 +f 155/158 154/157 194/198 +f 155/158 195/199 156/159 +f 156/159 195/199 196/200 +f 196/200 197/201 156/159 +f 157/160 156/159 197/201 +f 197/201 198/202 157/160 +f 158/161 157/160 198/202 +f 198/202 199/203 158/161 +f 159/162 158/161 199/203 +f 199/203 200/204 159/162 +f 160/163 159/162 200/204 +f 200/204 161/205 160/163 +f 121/164 160/163 161/205 +f 201/206 202/207 161/165 +f 162/166 161/165 202/207 +f 202/207 203/208 162/166 +f 163/167 162/166 203/208 +f 204/209 164/168 203/208 +f 164/168 163/167 203/208 +f 165/169 164/168 205/210 +f 164/168 204/209 205/210 +f 205/210 206/211 165/169 +f 166/170 165/169 206/211 +f 206/211 207/212 166/170 +f 167/171 166/170 207/212 +f 207/212 208/213 167/171 +f 168/172 167/171 208/213 +f 208/213 209/214 168/172 +f 169/173 168/172 209/214 +f 209/214 210/215 169/173 +f 170/174 169/173 210/215 +f 211/216 171/175 210/215 +f 171/175 170/174 210/215 +f 212/217 172/176 211/216 +f 172/176 171/175 211/216 +f 213/218 173/177 212/217 +f 173/177 172/176 212/217 +f 214/219 174/178 213/218 +f 174/178 173/177 213/218 +f 174/178 214/219 175/179 +f 175/179 214/219 215/220 +f 175/179 215/220 176/180 +f 176/180 215/220 216/221 +f 216/221 217/222 176/180 +f 177/181 176/180 217/222 +f 217/222 218/223 177/181 +f 178/182 177/181 218/223 +f 218/223 219/224 178/182 +f 179/183 178/182 219/224 +f 219/224 220/225 179/183 +f 180/184 179/183 220/225 +f 220/225 221/226 180/184 +f 181/185 180/184 221/226 +f 221/226 222/227 181/185 +f 182/186 181/185 222/227 +f 222/227 223/228 182/186 +f 183/187 182/186 223/228 +f 224/229 184/188 223/228 +f 184/188 183/187 223/228 +f 185/189 184/188 225/230 +f 184/188 224/229 225/230 +f 186/190 185/189 226/231 +f 185/189 225/230 226/231 +f 226/231 227/232 186/190 +f 187/191 186/190 227/232 +f 227/232 228/233 187/191 +f 188/192 187/191 228/233 +f 228/233 229/234 188/192 +f 189/193 188/192 229/234 +f 229/234 230/235 189/193 +f 190/194 189/193 230/235 +f 231/236 191/195 230/235 +f 191/195 190/194 230/235 +f 232/237 192/196 231/236 +f 192/196 191/195 231/236 +f 233/238 193/197 232/237 +f 193/197 192/196 232/237 +f 234/239 194/198 233/238 +f 194/198 193/197 233/238 +f 235/240 195/199 234/239 +f 195/199 194/198 234/239 +f 195/199 235/240 196/200 +f 196/200 235/240 236/241 +f 236/241 237/242 196/200 +f 197/201 196/200 237/242 +f 237/242 238/243 197/201 +f 198/202 197/201 238/243 +f 238/243 239/244 198/202 +f 199/203 198/202 239/244 +f 239/244 240/245 199/203 +f 200/204 199/203 240/245 +f 240/245 201/246 200/204 +f 161/205 200/204 201/246 +f 241/247 242/248 201/206 +f 202/207 201/206 242/248 +f 242/248 243/249 202/207 +f 203/208 202/207 243/249 +f 243/249 244/250 203/208 +f 204/209 203/208 244/250 +f 244/250 245/251 204/209 +f 205/210 204/209 245/251 +f 206/211 205/210 246/252 +f 205/210 245/251 246/252 +f 246/252 247/253 206/211 +f 207/212 206/211 247/253 +f 247/253 248/254 207/212 +f 208/213 207/212 248/254 +f 249/255 209/214 248/254 +f 209/214 208/213 248/254 +f 249/255 250/256 209/214 +f 210/215 209/214 250/256 +f 251/257 211/216 250/256 +f 211/216 210/215 250/256 +f 251/257 252/258 211/216 +f 212/217 211/216 252/258 +f 253/259 213/218 252/258 +f 213/218 212/217 252/258 +f 254/260 214/219 253/259 +f 214/219 213/218 253/259 +f 214/219 254/260 215/220 +f 215/220 254/260 255/261 +f 256/262 216/221 255/261 +f 216/221 215/220 255/261 +f 256/262 257/263 216/221 +f 217/222 216/221 257/263 +f 257/263 258/264 217/222 +f 218/223 217/222 258/264 +f 258/264 259/265 218/223 +f 219/224 218/223 259/265 +f 259/265 260/266 219/224 +f 220/225 219/224 260/266 +f 260/266 261/267 220/225 +f 221/226 220/225 261/267 +f 261/267 262/268 221/226 +f 222/227 221/226 262/268 +f 262/268 263/269 222/227 +f 223/228 222/227 263/269 +f 263/269 264/270 223/228 +f 224/229 223/228 264/270 +f 264/270 265/271 224/229 +f 225/230 224/229 265/271 +f 226/231 225/230 266/272 +f 225/230 265/271 266/272 +f 266/272 267/273 226/231 +f 227/232 226/231 267/273 +f 267/273 268/274 227/232 +f 228/233 227/232 268/274 +f 269/275 229/234 268/274 +f 229/234 228/233 268/274 +f 269/275 270/276 229/234 +f 230/235 229/234 270/276 +f 271/277 231/236 270/276 +f 231/236 230/235 270/276 +f 271/277 272/278 231/236 +f 232/237 231/236 272/278 +f 273/279 233/238 272/278 +f 233/238 232/237 272/278 +f 274/280 234/239 273/279 +f 234/239 233/238 273/279 +f 234/239 274/280 235/240 +f 235/240 274/280 275/281 +f 276/282 236/241 275/281 +f 236/241 235/240 275/281 +f 276/282 277/283 236/241 +f 237/242 236/241 277/283 +f 277/283 278/284 237/242 +f 238/243 237/242 278/284 +f 278/284 279/285 238/243 +f 239/244 238/243 279/285 +f 279/285 280/286 239/244 +f 240/245 239/244 280/286 +f 280/286 241/287 240/245 +f 201/246 240/245 241/287 +f 281/288 282/289 241/247 +f 242/248 241/247 282/289 +f 282/289 283/290 242/248 +f 243/249 242/248 283/290 +f 283/290 284/291 243/249 +f 244/250 243/249 284/291 +f 284/291 285/292 244/250 +f 245/251 244/250 285/292 +f 285/292 286/293 245/251 +f 246/252 245/251 286/293 +f 286/293 287/294 246/252 +f 247/253 246/252 287/294 +f 287/294 288/295 247/253 +f 248/254 247/253 288/295 +f 288/295 289/296 248/254 +f 249/255 248/254 289/296 +f 289/296 290/297 249/255 +f 250/256 249/255 290/297 +f 291/298 251/257 290/297 +f 251/257 250/256 290/297 +f 292/299 252/258 291/298 +f 252/258 251/257 291/298 +f 293/300 253/259 292/299 +f 253/259 252/258 292/299 +f 294/301 254/260 293/300 +f 254/260 253/259 293/300 +f 295/302 255/261 294/301 +f 255/261 254/260 294/301 +f 295/302 296/303 255/261 +f 256/262 255/261 296/303 +f 296/303 297/304 256/262 +f 257/263 256/262 297/304 +f 297/304 298/305 257/263 +f 258/264 257/263 298/305 +f 298/305 299/306 258/264 +f 259/265 258/264 299/306 +f 299/306 300/307 259/265 +f 260/266 259/265 300/307 +f 300/307 301/308 260/266 +f 261/267 260/266 301/308 +f 301/308 302/309 261/267 +f 262/268 261/267 302/309 +f 302/309 303/310 262/268 +f 263/269 262/268 303/310 +f 303/310 304/311 263/269 +f 264/270 263/269 304/311 +f 304/311 305/312 264/270 +f 265/271 264/270 305/312 +f 305/312 306/313 265/271 +f 266/272 265/271 306/313 +f 306/313 307/314 266/272 +f 267/273 266/272 307/314 +f 307/314 308/315 267/273 +f 268/274 267/273 308/315 +f 308/315 309/316 268/274 +f 269/275 268/274 309/316 +f 309/316 310/317 269/275 +f 270/276 269/275 310/317 +f 311/318 271/277 310/317 +f 271/277 270/276 310/317 +f 312/319 272/278 311/318 +f 272/278 271/277 311/318 +f 313/320 273/279 312/319 +f 273/279 272/278 312/319 +f 314/321 274/280 313/320 +f 274/280 273/279 313/320 +f 315/322 275/281 314/321 +f 275/281 274/280 314/321 +f 315/322 316/323 275/281 +f 276/282 275/281 316/323 +f 316/323 317/324 276/282 +f 277/283 276/282 317/324 +f 317/324 318/325 277/283 +f 278/284 277/283 318/325 +f 318/325 319/326 278/284 +f 279/285 278/284 319/326 +f 319/326 320/327 279/285 +f 280/286 279/285 320/327 +f 320/327 281/328 280/286 +f 241/287 280/286 281/328 +f 321/329 322/330 281/288 +f 282/289 281/288 322/330 +f 322/330 323/331 282/289 +f 283/290 282/289 323/331 +f 323/331 324/332 283/290 +f 284/291 283/290 324/332 +f 324/332 325/333 284/291 +f 285/292 284/291 325/333 +f 325/333 326/334 285/292 +f 286/293 285/292 326/334 +f 287/294 286/293 327/335 +f 286/293 326/334 327/335 +f 327/335 328/336 287/294 +f 288/295 287/294 328/336 +f 328/336 329/337 288/295 +f 289/296 288/295 329/337 +f 329/337 330/338 289/296 +f 290/297 289/296 330/338 +f 331/339 291/298 330/338 +f 291/298 290/297 330/338 +f 332/340 292/299 331/339 +f 292/299 291/298 331/339 +f 333/341 293/300 332/340 +f 293/300 292/299 332/340 +f 293/300 333/341 294/301 +f 294/301 333/341 334/342 +f 335/343 295/302 334/342 +f 295/302 294/301 334/342 +f 335/343 336/344 295/302 +f 296/303 295/302 336/344 +f 336/344 337/345 296/303 +f 297/304 296/303 337/345 +f 337/345 338/346 297/304 +f 298/305 297/304 338/346 +f 338/346 339/347 298/305 +f 299/306 298/305 339/347 +f 339/347 340/348 299/306 +f 300/307 299/306 340/348 +f 340/348 341/349 300/307 +f 301/308 300/307 341/349 +f 341/349 342/350 301/308 +f 302/309 301/308 342/350 +f 342/350 343/351 302/309 +f 303/310 302/309 343/351 +f 343/351 344/352 303/310 +f 304/311 303/310 344/352 +f 344/352 345/353 304/311 +f 305/312 304/311 345/353 +f 345/353 346/354 305/312 +f 306/313 305/312 346/354 +f 307/314 306/313 347/355 +f 306/313 346/354 347/355 +f 347/355 348/356 307/314 +f 308/315 307/314 348/356 +f 348/356 349/357 308/315 +f 309/316 308/315 349/357 +f 349/357 350/358 309/316 +f 310/317 309/316 350/358 +f 351/359 311/318 350/358 +f 311/318 310/317 350/358 +f 352/360 312/319 351/359 +f 312/319 311/318 351/359 +f 353/361 313/320 352/360 +f 313/320 312/319 352/360 +f 313/320 353/361 314/321 +f 314/321 353/361 354/362 +f 355/363 315/322 354/362 +f 315/322 314/321 354/362 +f 355/363 356/364 315/322 +f 316/323 315/322 356/364 +f 356/364 357/365 316/323 +f 317/324 316/323 357/365 +f 357/365 358/366 317/324 +f 318/325 317/324 358/366 +f 358/366 359/367 318/325 +f 319/326 318/325 359/367 +f 359/367 360/368 319/326 +f 320/327 319/326 360/368 +f 360/368 321/369 320/327 +f 281/328 320/327 321/369 +f 361/370 362/371 321/329 +f 322/330 321/329 362/371 +f 362/371 363/372 322/330 +f 323/331 322/330 363/372 +f 363/372 364/373 323/331 +f 324/332 323/331 364/373 +f 364/373 365/374 324/332 +f 325/333 324/332 365/374 +f 365/374 366/375 325/333 +f 326/334 325/333 366/375 +f 327/335 326/334 367/376 +f 326/334 366/375 367/376 +f 367/376 368/377 327/335 +f 328/336 327/335 368/377 +f 368/377 369/378 328/336 +f 329/337 328/336 369/378 +f 369/378 370/379 329/337 +f 330/338 329/337 370/379 +f 371/380 331/339 370/379 +f 331/339 330/338 370/379 +f 372/381 332/340 371/380 +f 332/340 331/339 371/380 +f 373/382 333/341 372/381 +f 333/341 332/340 372/381 +f 333/341 373/382 334/342 +f 334/342 373/382 374/383 +f 375/384 335/343 374/383 +f 335/343 334/342 374/383 +f 375/384 376/385 335/343 +f 336/344 335/343 376/385 +f 376/385 377/386 336/344 +f 337/345 336/344 377/386 +f 377/386 378/387 337/345 +f 338/346 337/345 378/387 +f 378/387 379/388 338/346 +f 339/347 338/346 379/388 +f 379/388 380/389 339/347 +f 340/348 339/347 380/389 +f 380/389 381/390 340/348 +f 341/349 340/348 381/390 +f 381/390 382/391 341/349 +f 342/350 341/349 382/391 +f 382/391 383/392 342/350 +f 343/351 342/350 383/392 +f 383/392 384/393 343/351 +f 344/352 343/351 384/393 +f 384/393 385/394 344/352 +f 345/353 344/352 385/394 +f 385/394 386/395 345/353 +f 346/354 345/353 386/395 +f 347/355 346/354 387/396 +f 346/354 386/395 387/396 +f 387/396 388/397 347/355 +f 348/356 347/355 388/397 +f 388/397 389/398 348/356 +f 349/357 348/356 389/398 +f 389/398 390/399 349/357 +f 350/358 349/357 390/399 +f 391/400 351/359 390/399 +f 351/359 350/358 390/399 +f 392/401 352/360 391/400 +f 352/360 351/359 391/400 +f 393/402 353/361 392/401 +f 353/361 352/360 392/401 +f 353/361 393/402 354/362 +f 354/362 393/402 394/403 +f 395/404 355/363 394/403 +f 355/363 354/362 394/403 +f 395/404 396/405 355/363 +f 356/364 355/363 396/405 +f 396/405 397/406 356/364 +f 357/365 356/364 397/406 +f 397/406 398/407 357/365 +f 358/366 357/365 398/407 +f 398/407 399/408 358/366 +f 359/367 358/366 399/408 +f 399/408 400/409 359/367 +f 360/368 359/367 400/409 +f 400/409 361/410 360/368 +f 321/369 360/368 361/410 +f 401/411 402/412 361/370 +f 362/371 361/370 402/412 +f 402/412 403/413 362/371 +f 363/372 362/371 403/413 +f 403/413 404/414 363/372 +f 364/373 363/372 404/414 +f 404/414 405/415 364/373 +f 365/374 364/373 405/415 +f 405/415 406/416 365/374 +f 366/375 365/374 406/416 +f 367/376 366/375 407/417 +f 366/375 406/416 407/417 +f 407/417 408/418 367/376 +f 368/377 367/376 408/418 +f 408/418 409/419 368/377 +f 369/378 368/377 409/419 +f 409/419 410/420 369/378 +f 370/379 369/378 410/420 +f 410/420 411/421 370/379 +f 371/380 370/379 411/421 +f 412/422 372/381 411/421 +f 372/381 371/380 411/421 +f 413/423 373/382 412/422 +f 373/382 372/381 412/422 +f 373/382 413/423 374/383 +f 374/383 413/423 414/424 +f 414/424 415/425 374/383 +f 375/384 374/383 415/425 +f 415/425 416/426 375/384 +f 376/385 375/384 416/426 +f 416/426 417/427 376/385 +f 377/386 376/385 417/427 +f 417/427 418/428 377/386 +f 378/387 377/386 418/428 +f 418/428 419/429 378/387 +f 379/388 378/387 419/429 +f 419/429 420/430 379/388 +f 380/389 379/388 420/430 +f 420/430 421/431 380/389 +f 381/390 380/389 421/431 +f 421/431 422/432 381/390 +f 382/391 381/390 422/432 +f 422/432 423/433 382/391 +f 383/392 382/391 423/433 +f 423/433 424/434 383/392 +f 384/393 383/392 424/434 +f 424/434 425/435 384/393 +f 385/394 384/393 425/435 +f 425/435 426/436 385/394 +f 386/395 385/394 426/436 +f 387/396 386/395 427/437 +f 386/395 426/436 427/437 +f 427/437 428/438 387/396 +f 388/397 387/396 428/438 +f 428/438 429/439 388/397 +f 389/398 388/397 429/439 +f 429/439 430/440 389/398 +f 390/399 389/398 430/440 +f 430/440 431/441 390/399 +f 391/400 390/399 431/441 +f 432/442 392/401 431/441 +f 392/401 391/400 431/441 +f 433/443 393/402 432/442 +f 393/402 392/401 432/442 +f 393/402 433/443 394/403 +f 394/403 433/443 434/444 +f 434/444 435/445 394/403 +f 395/404 394/403 435/445 +f 435/445 436/446 395/404 +f 396/405 395/404 436/446 +f 436/446 437/447 396/405 +f 397/406 396/405 437/447 +f 437/447 438/448 397/406 +f 398/407 397/406 438/448 +f 438/448 439/449 398/407 +f 399/408 398/407 439/449 +f 439/449 440/450 399/408 +f 400/409 399/408 440/450 +f 440/450 401/451 400/409 +f 361/410 400/409 401/451 +f 402/412 401/411 442/453 +f 401/411 441/452 442/453 +f 403/413 402/412 443/454 +f 402/412 442/453 443/454 +f 404/414 403/413 444/455 +f 403/413 443/454 444/455 +f 405/415 404/414 445/456 +f 404/414 444/455 445/456 +f 406/416 405/415 446/457 +f 405/415 445/456 446/457 +f 406/416 446/457 407/417 +f 407/417 446/457 447/458 +f 407/417 447/458 408/418 +f 408/418 447/458 448/459 +f 408/418 448/459 409/419 +f 409/419 448/459 449/460 +f 410/420 409/419 450/461 +f 409/419 449/460 450/461 +f 411/421 410/420 451/462 +f 410/420 450/461 451/462 +f 412/422 411/421 452/463 +f 411/421 451/462 452/463 +f 413/423 412/422 453/464 +f 412/422 452/463 453/464 +f 414/424 413/423 454/465 +f 413/423 453/464 454/465 +f 415/425 414/424 455/466 +f 414/424 454/465 455/466 +f 416/426 415/425 456/467 +f 415/425 455/466 456/467 +f 417/427 416/426 457/468 +f 416/426 456/467 457/468 +f 418/428 417/427 458/469 +f 417/427 457/468 458/469 +f 419/429 418/428 459/470 +f 418/428 458/469 459/470 +f 420/430 419/429 460/471 +f 419/429 459/470 460/471 +f 421/431 420/430 461/472 +f 420/430 460/471 461/472 +f 422/432 421/431 462/473 +f 421/431 461/472 462/473 +f 423/433 422/432 463/474 +f 422/432 462/473 463/474 +f 424/434 423/433 464/475 +f 423/433 463/474 464/475 +f 425/435 424/434 465/476 +f 424/434 464/475 465/476 +f 426/436 425/435 466/477 +f 425/435 465/476 466/477 +f 426/436 466/477 427/437 +f 427/437 466/477 467/478 +f 427/437 467/478 428/438 +f 428/438 467/478 468/479 +f 428/438 468/479 429/439 +f 429/439 468/479 469/480 +f 430/440 429/439 470/481 +f 429/439 469/480 470/481 +f 431/441 430/440 471/482 +f 430/440 470/481 471/482 +f 432/442 431/441 472/483 +f 431/441 471/482 472/483 +f 433/443 432/442 473/484 +f 432/442 472/483 473/484 +f 434/444 433/443 474/485 +f 433/443 473/484 474/485 +f 435/445 434/444 475/486 +f 434/444 474/485 475/486 +f 436/446 435/445 476/487 +f 435/445 475/486 476/487 +f 437/447 436/446 477/488 +f 436/446 476/487 477/488 +f 438/448 437/447 478/489 +f 437/447 477/488 478/489 +f 439/449 438/448 479/490 +f 438/448 478/489 479/490 +f 440/450 439/449 480/491 +f 439/449 479/490 480/491 +f 401/451 440/450 441/492 +f 440/450 480/491 441/492 +f 442/453 441/452 482/494 +f 441/452 481/493 482/494 +f 443/454 442/453 483/495 +f 442/453 482/494 483/495 +f 444/455 443/454 484/496 +f 443/454 483/495 484/496 +f 445/456 444/455 485/497 +f 444/455 484/496 485/497 +f 445/456 485/497 446/457 +f 446/457 485/497 486/498 +f 446/457 486/498 447/458 +f 447/458 486/498 487/499 +f 447/458 487/499 448/459 +f 448/459 487/499 488/500 +f 448/459 488/500 449/460 +f 449/460 488/500 489/501 +f 449/460 489/501 450/461 +f 450/461 489/501 490/502 +f 451/462 450/461 491/503 +f 450/461 490/502 491/503 +f 452/463 451/462 492/504 +f 451/462 491/503 492/504 +f 453/464 452/463 493/505 +f 452/463 492/504 493/505 +f 454/465 453/464 494/506 +f 453/464 493/505 494/506 +f 455/466 454/465 495/507 +f 454/465 494/506 495/507 +f 456/467 455/466 496/508 +f 455/466 495/507 496/508 +f 457/468 456/467 497/509 +f 456/467 496/508 497/509 +f 458/469 457/468 498/510 +f 457/468 497/509 498/510 +f 459/470 458/469 499/511 +f 458/469 498/510 499/511 +f 460/471 459/470 500/512 +f 459/470 499/511 500/512 +f 461/472 460/471 501/513 +f 460/471 500/512 501/513 +f 462/473 461/472 502/514 +f 461/472 501/513 502/514 +f 463/474 462/473 503/515 +f 462/473 502/514 503/515 +f 464/475 463/474 504/516 +f 463/474 503/515 504/516 +f 465/476 464/475 505/517 +f 464/475 504/516 505/517 +f 465/476 505/517 466/477 +f 466/477 505/517 506/518 +f 466/477 506/518 467/478 +f 467/478 506/518 507/519 +f 467/478 507/519 468/479 +f 468/479 507/519 508/520 +f 468/479 508/520 469/480 +f 469/480 508/520 509/521 +f 469/480 509/521 470/481 +f 470/481 509/521 510/522 +f 471/482 470/481 511/523 +f 470/481 510/522 511/523 +f 472/483 471/482 512/524 +f 471/482 511/523 512/524 +f 473/484 472/483 513/525 +f 472/483 512/524 513/525 +f 474/485 473/484 514/526 +f 473/484 513/525 514/526 +f 475/486 474/485 515/527 +f 474/485 514/526 515/527 +f 476/487 475/486 516/528 +f 475/486 515/527 516/528 +f 477/488 476/487 517/529 +f 476/487 516/528 517/529 +f 478/489 477/488 518/530 +f 477/488 517/529 518/530 +f 479/490 478/489 519/531 +f 478/489 518/530 519/531 +f 480/491 479/490 520/532 +f 479/490 519/531 520/532 +f 441/492 480/491 481/533 +f 480/491 520/532 481/533 +f 482/494 481/493 522/535 +f 481/493 521/534 522/535 +f 483/495 482/494 523/536 +f 482/494 522/535 523/536 +f 484/496 483/495 524/537 +f 483/495 523/536 524/537 +f 485/497 484/496 525/538 +f 484/496 524/537 525/538 +f 485/497 525/538 486/498 +f 486/498 525/538 526/539 +f 527/540 487/499 526/539 +f 487/499 486/498 526/539 +f 487/499 527/540 488/500 +f 488/500 527/540 528/541 +f 488/500 528/541 489/501 +f 489/501 528/541 529/542 +f 489/501 529/542 490/502 +f 490/502 529/542 530/543 +f 491/503 490/502 531/544 +f 490/502 530/543 531/544 +f 492/504 491/503 532/545 +f 491/503 531/544 532/545 +f 493/505 492/504 533/546 +f 492/504 532/545 533/546 +f 533/546 534/547 493/505 +f 494/506 493/505 534/547 +f 495/507 494/506 535/548 +f 494/506 534/547 535/548 +f 496/508 495/507 536/549 +f 495/507 535/548 536/549 +f 497/509 496/508 537/550 +f 496/508 536/549 537/550 +f 498/510 497/509 538/551 +f 497/509 537/550 538/551 +f 499/511 498/510 539/552 +f 498/510 538/551 539/552 +f 500/512 499/511 540/553 +f 499/511 539/552 540/553 +f 501/513 500/512 541/554 +f 500/512 540/553 541/554 +f 502/514 501/513 542/555 +f 501/513 541/554 542/555 +f 503/515 502/514 543/556 +f 502/514 542/555 543/556 +f 504/516 503/515 544/557 +f 503/515 543/556 544/557 +f 505/517 504/516 545/558 +f 504/516 544/557 545/558 +f 505/517 545/558 506/518 +f 506/518 545/558 546/559 +f 547/560 507/519 546/559 +f 507/519 506/518 546/559 +f 507/519 547/560 508/520 +f 508/520 547/560 548/561 +f 508/520 548/561 509/521 +f 509/521 548/561 549/562 +f 509/521 549/562 510/522 +f 510/522 549/562 550/563 +f 511/523 510/522 551/564 +f 510/522 550/563 551/564 +f 512/524 511/523 552/565 +f 511/523 551/564 552/565 +f 513/525 512/524 553/566 +f 512/524 552/565 553/566 +f 553/566 554/567 513/525 +f 514/526 513/525 554/567 +f 515/527 514/526 555/568 +f 514/526 554/567 555/568 +f 516/528 515/527 556/569 +f 515/527 555/568 556/569 +f 517/529 516/528 557/570 +f 516/528 556/569 557/570 +f 518/530 517/529 558/571 +f 517/529 557/570 558/571 +f 519/531 518/530 559/572 +f 518/530 558/571 559/572 +f 520/532 519/531 560/573 +f 519/531 559/572 560/573 +f 481/533 520/532 521/574 +f 520/532 560/573 521/574 +f 522/535 521/534 562/576 +f 521/534 561/575 562/576 +f 523/536 522/535 563/577 +f 522/535 562/576 563/577 +f 524/537 523/536 564/578 +f 523/536 563/577 564/578 +f 525/538 524/537 565/579 +f 524/537 564/578 565/579 +f 525/538 565/579 526/539 +f 526/539 565/579 566/580 +f 526/539 566/580 527/540 +f 527/540 566/580 567/581 +f 527/540 567/581 528/541 +f 528/541 567/581 568/582 +f 528/541 568/582 529/542 +f 529/542 568/582 569/583 +f 529/542 569/583 530/543 +f 530/543 569/583 570/584 +f 531/544 530/543 571/585 +f 530/543 570/584 571/585 +f 532/545 531/544 572/586 +f 531/544 571/585 572/586 +f 533/546 532/545 573/587 +f 532/545 572/586 573/587 +f 534/547 533/546 574/588 +f 533/546 573/587 574/588 +f 535/548 534/547 575/589 +f 534/547 574/588 575/589 +f 536/549 535/548 576/590 +f 535/548 575/589 576/590 +f 537/550 536/549 577/591 +f 536/549 576/590 577/591 +f 538/551 537/550 578/592 +f 537/550 577/591 578/592 +f 539/552 538/551 579/593 +f 538/551 578/592 579/593 +f 540/553 539/552 580/594 +f 539/552 579/593 580/594 +f 541/554 540/553 581/595 +f 540/553 580/594 581/595 +f 542/555 541/554 582/596 +f 541/554 581/595 582/596 +f 543/556 542/555 583/597 +f 542/555 582/596 583/597 +f 544/557 543/556 584/598 +f 543/556 583/597 584/598 +f 545/558 544/557 585/599 +f 544/557 584/598 585/599 +f 545/558 585/599 546/559 +f 546/559 585/599 586/600 +f 546/559 586/600 547/560 +f 547/560 586/600 587/601 +f 547/560 587/601 548/561 +f 548/561 587/601 588/602 +f 548/561 588/602 549/562 +f 549/562 588/602 589/603 +f 549/562 589/603 550/563 +f 550/563 589/603 590/604 +f 551/564 550/563 591/605 +f 550/563 590/604 591/605 +f 552/565 551/564 592/606 +f 551/564 591/605 592/606 +f 553/566 552/565 593/607 +f 552/565 592/606 593/607 +f 554/567 553/566 594/608 +f 553/566 593/607 594/608 +f 555/568 554/567 595/609 +f 554/567 594/608 595/609 +f 556/569 555/568 596/610 +f 555/568 595/609 596/610 +f 557/570 556/569 597/611 +f 556/569 596/610 597/611 +f 558/571 557/570 598/612 +f 557/570 597/611 598/612 +f 559/572 558/571 599/613 +f 558/571 598/612 599/613 +f 560/573 559/572 600/614 +f 559/572 599/613 600/614 +f 521/574 560/573 561/615 +f 560/573 600/614 561/615 +f 562/576 561/575 602/617 +f 561/575 601/616 602/617 +f 563/577 562/576 603/618 +f 562/576 602/617 603/618 +f 564/578 563/577 604/619 +f 563/577 603/618 604/619 +f 564/578 604/619 565/579 +f 565/579 604/619 605/620 +f 606/621 566/580 605/620 +f 566/580 565/579 605/620 +f 566/580 606/621 567/581 +f 567/581 606/621 607/622 +f 567/581 607/622 568/582 +f 568/582 607/622 608/623 +f 569/583 568/582 609/624 +f 568/582 608/623 609/624 +f 569/583 609/624 570/584 +f 570/584 609/624 610/625 +f 571/585 570/584 611/626 +f 570/584 610/625 611/626 +f 571/585 611/626 572/586 +f 572/586 611/626 612/627 +f 573/587 572/586 613/628 +f 572/586 612/627 613/628 +f 574/588 573/587 614/629 +f 573/587 613/628 614/629 +f 614/629 615/630 574/588 +f 575/589 574/588 615/630 +f 576/590 575/589 616/631 +f 575/589 615/630 616/631 +f 577/591 576/590 617/632 +f 576/590 616/631 617/632 +f 578/592 577/591 618/633 +f 577/591 617/632 618/633 +f 579/593 578/592 619/634 +f 578/592 618/633 619/634 +f 580/594 579/593 620/635 +f 579/593 619/634 620/635 +f 581/595 580/594 621/636 +f 580/594 620/635 621/636 +f 582/596 581/595 622/637 +f 581/595 621/636 622/637 +f 583/597 582/596 623/638 +f 582/596 622/637 623/638 +f 584/598 583/597 624/639 +f 583/597 623/638 624/639 +f 584/598 624/639 585/599 +f 585/599 624/639 625/640 +f 626/641 586/600 625/640 +f 586/600 585/599 625/640 +f 586/600 626/641 587/601 +f 587/601 626/641 627/642 +f 587/601 627/642 588/602 +f 588/602 627/642 628/643 +f 589/603 588/602 629/644 +f 588/602 628/643 629/644 +f 589/603 629/644 590/604 +f 590/604 629/644 630/645 +f 591/605 590/604 631/646 +f 590/604 630/645 631/646 +f 591/605 631/646 592/606 +f 592/606 631/646 632/647 +f 593/607 592/606 633/648 +f 592/606 632/647 633/648 +f 594/608 593/607 634/649 +f 593/607 633/648 634/649 +f 634/649 635/650 594/608 +f 595/609 594/608 635/650 +f 596/610 595/609 636/651 +f 595/609 635/650 636/651 +f 597/611 596/610 637/652 +f 596/610 636/651 637/652 +f 598/612 597/611 638/653 +f 597/611 637/652 638/653 +f 599/613 598/612 639/654 +f 598/612 638/653 639/654 +f 600/614 599/613 640/655 +f 599/613 639/654 640/655 +f 561/615 600/614 601/656 +f 600/614 640/655 601/656 +f 602/617 601/616 642/658 +f 601/616 641/657 642/658 +f 603/618 602/617 643/659 +f 602/617 642/658 643/659 +f 604/619 603/618 644/660 +f 603/618 643/659 644/660 +f 604/619 644/660 605/620 +f 605/620 644/660 645/661 +f 605/620 645/661 606/621 +f 606/621 645/661 646/662 +f 606/621 646/662 607/622 +f 607/622 646/662 647/663 +f 607/622 647/663 608/623 +f 608/623 647/663 648/664 +f 608/623 648/664 609/624 +f 609/624 648/664 649/665 +f 609/624 649/665 610/625 +f 610/625 649/665 650/666 +f 611/626 610/625 651/667 +f 610/625 650/666 651/667 +f 612/627 611/626 652/668 +f 611/626 651/667 652/668 +f 613/628 612/627 653/669 +f 612/627 652/668 653/669 +f 614/629 613/628 654/670 +f 613/628 653/669 654/670 +f 615/630 614/629 655/671 +f 614/629 654/670 655/671 +f 616/631 615/630 656/672 +f 615/630 655/671 656/672 +f 616/631 656/672 617/632 +f 617/632 656/672 657/673 +f 618/633 617/632 658/674 +f 617/632 657/673 658/674 +f 619/634 618/633 659/675 +f 618/633 658/674 659/675 +f 620/635 619/634 660/676 +f 619/634 659/675 660/676 +f 621/636 620/635 661/677 +f 620/635 660/676 661/677 +f 622/637 621/636 662/678 +f 621/636 661/677 662/678 +f 623/638 622/637 663/679 +f 622/637 662/678 663/679 +f 624/639 623/638 664/680 +f 623/638 663/679 664/680 +f 624/639 664/680 625/640 +f 625/640 664/680 665/681 +f 625/640 665/681 626/641 +f 626/641 665/681 666/682 +f 626/641 666/682 627/642 +f 627/642 666/682 667/683 +f 627/642 667/683 628/643 +f 628/643 667/683 668/684 +f 628/643 668/684 629/644 +f 629/644 668/684 669/685 +f 629/644 669/685 630/645 +f 630/645 669/685 670/686 +f 631/646 630/645 671/687 +f 630/645 670/686 671/687 +f 632/647 631/646 672/688 +f 631/646 671/687 672/688 +f 633/648 632/647 673/689 +f 632/647 672/688 673/689 +f 634/649 633/648 674/690 +f 633/648 673/689 674/690 +f 635/650 634/649 675/691 +f 634/649 674/690 675/691 +f 636/651 635/650 676/692 +f 635/650 675/691 676/692 +f 636/651 676/692 637/652 +f 637/652 676/692 677/693 +f 638/653 637/652 678/694 +f 637/652 677/693 678/694 +f 639/654 638/653 679/695 +f 638/653 678/694 679/695 +f 640/655 639/654 680/696 +f 639/654 679/695 680/696 +f 601/656 640/655 641/697 +f 640/655 680/696 641/697 +f 642/658 641/657 682/699 +f 641/657 681/698 682/699 +f 643/659 642/658 683/700 +f 642/658 682/699 683/700 +f 644/660 643/659 684/701 +f 643/659 683/700 684/701 +f 685/702 645/661 684/701 +f 645/661 644/660 684/701 +f 645/661 685/702 646/662 +f 646/662 685/702 686/703 +f 646/662 686/703 647/663 +f 647/663 686/703 687/704 +f 647/663 687/704 648/664 +f 648/664 687/704 688/705 +f 648/664 688/705 649/665 +f 649/665 688/705 689/706 +f 649/665 689/706 650/666 +f 650/666 689/706 690/707 +f 651/667 650/666 691/708 +f 650/666 690/707 691/708 +f 652/668 651/667 692/709 +f 651/667 691/708 692/709 +f 653/669 652/668 693/710 +f 652/668 692/709 693/710 +f 654/670 653/669 694/711 +f 653/669 693/710 694/711 +f 655/671 654/670 695/712 +f 654/670 694/711 695/712 +f 695/712 696/713 655/671 +f 656/672 655/671 696/713 +f 656/672 696/713 657/673 +f 657/673 696/713 697/714 +f 657/673 697/714 658/674 +f 658/674 697/714 698/715 +f 659/675 658/674 699/716 +f 658/674 698/715 699/716 +f 660/676 659/675 700/717 +f 659/675 699/716 700/717 +f 661/677 660/676 701/718 +f 660/676 700/717 701/718 +f 662/678 661/677 702/719 +f 661/677 701/718 702/719 +f 663/679 662/678 703/720 +f 662/678 702/719 703/720 +f 664/680 663/679 704/721 +f 663/679 703/720 704/721 +f 705/722 665/681 704/721 +f 665/681 664/680 704/721 +f 665/681 705/722 666/682 +f 666/682 705/722 706/723 +f 666/682 706/723 667/683 +f 667/683 706/723 707/724 +f 667/683 707/724 668/684 +f 668/684 707/724 708/725 +f 668/684 708/725 669/685 +f 669/685 708/725 709/726 +f 669/685 709/726 670/686 +f 670/686 709/726 710/727 +f 671/687 670/686 711/728 +f 670/686 710/727 711/728 +f 672/688 671/687 712/729 +f 671/687 711/728 712/729 +f 673/689 672/688 713/730 +f 672/688 712/729 713/730 +f 674/690 673/689 714/731 +f 673/689 713/730 714/731 +f 675/691 674/690 715/732 +f 674/690 714/731 715/732 +f 715/732 716/733 675/691 +f 676/692 675/691 716/733 +f 676/692 716/733 677/693 +f 677/693 716/733 717/734 +f 677/693 717/734 678/694 +f 678/694 717/734 718/735 +f 679/695 678/694 719/736 +f 678/694 718/735 719/736 +f 680/696 679/695 720/737 +f 679/695 719/736 720/737 +f 641/697 680/696 681/738 +f 680/696 720/737 681/738 +f 682/699 681/698 722/740 +f 681/698 721/739 722/740 +f 683/700 682/699 723/741 +f 682/699 722/740 723/741 +f 724/742 684/701 723/741 +f 684/701 683/700 723/741 +f 684/701 724/742 685/702 +f 685/702 724/742 725/743 +f 685/702 725/743 686/703 +f 686/703 725/743 726/744 +f 686/703 726/744 687/704 +f 687/704 726/744 727/745 +f 687/704 727/745 688/705 +f 688/705 727/745 728/746 +f 688/705 728/746 689/706 +f 689/706 728/746 729/747 +f 689/706 729/747 690/707 +f 690/707 729/747 730/748 +f 691/708 690/707 731/749 +f 690/707 730/748 731/749 +f 692/709 691/708 732/750 +f 691/708 731/749 732/750 +f 693/710 692/709 733/751 +f 692/709 732/750 733/751 +f 694/711 693/710 734/752 +f 693/710 733/751 734/752 +f 695/712 694/711 735/753 +f 694/711 734/752 735/753 +f 696/713 695/712 736/754 +f 695/712 735/753 736/754 +f 736/754 737/755 696/713 +f 697/714 696/713 737/755 +f 697/714 737/755 698/715 +f 698/715 737/755 738/756 +f 699/716 698/715 739/757 +f 698/715 738/756 739/757 +f 700/717 699/716 740/758 +f 699/716 739/757 740/758 +f 701/718 700/717 741/759 +f 700/717 740/758 741/759 +f 702/719 701/718 742/760 +f 701/718 741/759 742/760 +f 703/720 702/719 743/761 +f 702/719 742/760 743/761 +f 744/762 704/721 743/761 +f 704/721 703/720 743/761 +f 704/721 744/762 705/722 +f 705/722 744/762 745/763 +f 705/722 745/763 706/723 +f 706/723 745/763 746/764 +f 706/723 746/764 707/724 +f 707/724 746/764 747/765 +f 707/724 747/765 708/725 +f 708/725 747/765 748/766 +f 708/725 748/766 709/726 +f 709/726 748/766 749/767 +f 709/726 749/767 710/727 +f 710/727 749/767 750/768 +f 711/728 710/727 751/769 +f 710/727 750/768 751/769 +f 712/729 711/728 752/770 +f 711/728 751/769 752/770 +f 713/730 712/729 753/771 +f 712/729 752/770 753/771 +f 714/731 713/730 754/772 +f 713/730 753/771 754/772 +f 715/732 714/731 755/773 +f 714/731 754/772 755/773 +f 716/733 715/732 756/774 +f 715/732 755/773 756/774 +f 756/774 757/775 716/733 +f 717/734 716/733 757/775 +f 717/734 757/775 718/735 +f 718/735 757/775 758/776 +f 719/736 718/735 759/777 +f 718/735 758/776 759/777 +f 720/737 719/736 760/778 +f 719/736 759/777 760/778 +f 681/738 720/737 721/779 +f 720/737 760/778 721/779 +f 722/740 721/739 762/781 +f 721/739 761/780 762/781 +f 763/782 723/741 762/781 +f 723/741 722/740 762/781 +f 723/741 763/782 724/742 +f 724/742 763/782 764/783 +f 724/742 764/783 725/743 +f 725/743 764/783 765/784 +f 725/743 765/784 726/744 +f 726/744 765/784 766/785 +f 726/744 766/785 727/745 +f 727/745 766/785 767/786 +f 727/745 767/786 728/746 +f 728/746 767/786 768/787 +f 728/746 768/787 729/747 +f 729/747 768/787 769/788 +f 729/747 769/788 730/748 +f 730/748 769/788 770/789 +f 731/749 730/748 771/790 +f 730/748 770/789 771/790 +f 732/750 731/749 772/791 +f 731/749 771/790 772/791 +f 733/751 732/750 773/792 +f 732/750 772/791 773/792 +f 734/752 733/751 774/793 +f 733/751 773/792 774/793 +f 735/753 734/752 775/794 +f 734/752 774/793 775/794 +f 736/754 735/753 776/795 +f 735/753 775/794 776/795 +f 737/755 736/754 777/796 +f 736/754 776/795 777/796 +f 777/796 778/797 737/755 +f 738/756 737/755 778/797 +f 738/756 778/797 739/757 +f 739/757 778/797 779/798 +f 740/758 739/757 780/799 +f 739/757 779/798 780/799 +f 741/759 740/758 781/800 +f 740/758 780/799 781/800 +f 742/760 741/759 782/801 +f 741/759 781/800 782/801 +f 783/802 743/761 782/801 +f 743/761 742/760 782/801 +f 743/761 783/802 744/762 +f 744/762 783/802 784/803 +f 744/762 784/803 745/763 +f 745/763 784/803 785/804 +f 745/763 785/804 746/764 +f 746/764 785/804 786/805 +f 746/764 786/805 747/765 +f 747/765 786/805 787/806 +f 747/765 787/806 748/766 +f 748/766 787/806 788/807 +f 748/766 788/807 749/767 +f 749/767 788/807 789/808 +f 749/767 789/808 750/768 +f 750/768 789/808 790/809 +f 751/769 750/768 791/810 +f 750/768 790/809 791/810 +f 752/770 751/769 792/811 +f 751/769 791/810 792/811 +f 753/771 752/770 793/812 +f 752/770 792/811 793/812 +f 754/772 753/771 794/813 +f 753/771 793/812 794/813 +f 755/773 754/772 795/814 +f 754/772 794/813 795/814 +f 756/774 755/773 796/815 +f 755/773 795/814 796/815 +f 757/775 756/774 797/816 +f 756/774 796/815 797/816 +f 797/816 798/817 757/775 +f 758/776 757/775 798/817 +f 758/776 798/817 759/777 +f 759/777 798/817 799/818 +f 760/778 759/777 800/819 +f 759/777 799/818 800/819 +f 721/779 760/778 761/820 +f 760/778 800/819 761/820 +f 761/780 1/821 762/781 +f 762/781 1/821 2/822 +f 762/781 2/822 763/782 +f 763/782 2/822 3/823 +f 763/782 3/823 764/783 +f 764/783 3/823 4/824 +f 764/783 4/824 765/784 +f 765/784 4/824 5/825 +f 765/784 5/825 766/785 +f 766/785 5/825 6/826 +f 766/785 6/826 767/786 +f 767/786 6/826 7/827 +f 767/786 7/827 768/787 +f 768/787 7/827 8/828 +f 768/787 8/828 769/788 +f 769/788 8/828 9/829 +f 770/789 769/788 10/830 +f 769/788 9/829 10/830 +f 771/790 770/789 11/831 +f 770/789 10/830 11/831 +f 772/791 771/790 12/832 +f 771/790 11/831 12/832 +f 773/792 772/791 13/833 +f 772/791 12/832 13/833 +f 774/793 773/792 14/834 +f 773/792 13/833 14/834 +f 775/794 774/793 15/835 +f 774/793 14/834 15/835 +f 776/795 775/794 16/836 +f 775/794 15/835 16/836 +f 777/796 776/795 17/837 +f 776/795 16/836 17/837 +f 778/797 777/796 18/838 +f 777/796 17/837 18/838 +f 779/798 778/797 19/839 +f 778/797 18/838 19/839 +f 780/799 779/798 20/840 +f 779/798 19/839 20/840 +f 781/800 780/799 21/841 +f 780/799 20/840 21/841 +f 781/800 21/841 782/801 +f 782/801 21/841 22/842 +f 782/801 22/842 783/802 +f 783/802 22/842 23/843 +f 783/802 23/843 784/803 +f 784/803 23/843 24/844 +f 784/803 24/844 785/804 +f 785/804 24/844 25/845 +f 785/804 25/845 786/805 +f 786/805 25/845 26/846 +f 786/805 26/846 787/806 +f 787/806 26/846 27/847 +f 787/806 27/847 788/807 +f 788/807 27/847 28/848 +f 788/807 28/848 789/808 +f 789/808 28/848 29/849 +f 790/809 789/808 30/850 +f 789/808 29/849 30/850 +f 791/810 790/809 31/851 +f 790/809 30/850 31/851 +f 792/811 791/810 32/852 +f 791/810 31/851 32/852 +f 793/812 792/811 33/853 +f 792/811 32/852 33/853 +f 794/813 793/812 34/854 +f 793/812 33/853 34/854 +f 795/814 794/813 35/855 +f 794/813 34/854 35/855 +f 796/815 795/814 36/856 +f 795/814 35/855 36/856 +f 797/816 796/815 37/857 +f 796/815 36/856 37/857 +f 798/817 797/816 38/858 +f 797/816 37/857 38/858 +f 799/818 798/817 39/859 +f 798/817 38/858 39/859 +f 800/819 799/818 40/860 +f 799/818 39/859 40/860 +f 761/820 800/819 1/861 +f 800/819 40/860 1/861 diff --git a/examples/meshing/CGAL/mesh_from_point_cloud_using_convex_hull.py b/examples/meshing/CGAL/mesh_from_point_cloud_using_convex_hull.py new file mode 100644 index 000000000..989cd6c15 --- /dev/null +++ b/examples/meshing/CGAL/mesh_from_point_cloud_using_convex_hull.py @@ -0,0 +1,150 @@ +from CGAL.CGAL_Polyhedron_3 import Polyhedron_3 +from CGAL.CGAL_Mesh_3 import Polyhedral_mesh_domain_3 +from CGAL.CGAL_Mesh_3 import Mesh_3_parameters +from CGAL.CGAL_Mesh_3 import Default_mesh_criteria +from CGAL.CGAL_Kernel import Point_3 +from CGAL.CGAL_Convex_hull_3 import convex_hull_3, is_strongly_convex_3 +from CGAL import CGAL_Mesh_3 +from pypcd4 import PointCloud + +import sys +import argparse +import os +import random +import time + + +from cgal_utils import CGAL_Mesh_from, CGAL_Mesh_3_IO_Util, ReadPolyData, tic, toc +from mesh_from_polyhedron import CGAL_Mesh_from_polyhedron + +class CGAL_Mesh_from_pointcloud(CGAL_Mesh_from): + def __init__(self, pointcloud ): + print(f"Transforming input data into CGAL data structure...") + tic() + self.pointcloud = [] + for point in pointcloud: + self.pointcloud.append(Point_3(point[0],point[1],point[2])) + print(f"Done ! Took {toc()}") + + + def generate(self, criteria , refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.NONE)): + print(f"Generating convex hull...") + tic() + + self.polyhedron = Polyhedron_3() + convex_hull_3(self.pointcloud, self.polyhedron) + + + + print(f"Done ! Took {toc()}") + print(f"Convex hull has {self.polyhedron.size_of_vertices()} vertices and is strongly convex: {is_strongly_convex_3(self.polyhedron)}") + + cmfp = CGAL_Mesh_from_polyhedron(polyhedron=self.polyhedron) + cmfp.generate(criteria, refiner) + self.IOUtil = cmfp.IOUtil + + def write_out(self, filename): + self.IOUtil.write_out(filename) + +# Function called when the scene graph is being created +def createScene(root, file_name): + + root.addObject("RequiredPlugin", pluginName=["Sofa.Component.IO.Mesh", + "Sofa.GL.Component.Rendering3D", "Sofa.Component.Topology.Container.Dynamic","Sofa.Component.StateContainer"]) + + root.addObject("DefaultAnimationLoop") + loader = root.addObject("MeshVTKLoader", name="VTKLoader", filename=file_name) + root.addObject("TetrahedronSetTopologyContainer", name="Topology", src="@VTKLoader") + root.addObject("TetrahedronSetGeometryAlgorithms", name="Geometry", drawTetrahedra=True) + root.addObject("MechanicalObject", name="DoFsContainer") + + return root + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="Creates a mesh from a point cloud using convex hull.") + + parser.add_argument("-i", "--input", default='data/point_cloud.pcd', help="The input file containing the surface. Format must be taken form ['.pcd']") + parser.add_argument( "-o", "--output", default='pointCloudMesh.vtk', help="The output file to save the computed volumetric mesh") + parser.add_argument( "-r", "--refiner", default='None', help="Use refiner to erfine the mesh. Values are amongst ['None', 'Lloyd', 'Odt', 'Perturb']") + parser.add_argument( "-c", "--criteria", default='"facet_angle=25 edge_size=0.4 facet_size=0.15 facet_distance=0.008 cell_radius_edge_ration=3"', help="Set of parameters in the form of \"facet_angle=25 edge_size=0.4 facet_size=0.15 facet_distance=0.008 cell_radius_edge_ration=3\" that can be customized. If one is not specified, its default value is used") + parser.add_argument( "-d", "--display", action='store_true', help="If flag is added, a SOFA scene will be launched showing the generated mesh") + + args = parser.parse_args() + + + tic(1) + + pc = PointCloud.from_path(args.input) + PC = pc.numpy(("x", "y", "z")) + + cmfp = CGAL_Mesh_from_pointcloud(PC.tolist()) + + + #Criterial + class criteriaContainer: + def __init__(self): + self.facet_angle=25 + self.edge_size=0.15 + self.facet_size=0.15 + self.facet_distance=0.008 + self.cell_radius_edge_ratio=3 + + selectedCriterias = criteriaContainer() + + for passedCriteria in args.criteria.split(' '): + if len(passedCriteria.split('=')) >1: + if passedCriteria.split('=')[0] in selectedCriterias.__dict__: + setattr(selectedCriterias, passedCriteria.split('=')[0], float(passedCriteria.split('=')[1]) ) + + criteria = Default_mesh_criteria() + criteria.facet_angle(selectedCriterias.facet_angle).edge_size(selectedCriterias.edge_size).facet_size(selectedCriterias.facet_size).facet_distance(selectedCriterias.facet_distance).cell_radius_edge_ratio(selectedCriterias.cell_radius_edge_ratio) + + #Refiner + match args.refiner: + case 'Lloyd': + refinerName = 'Lloyd' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.LLOYD) + case 'Odt': + refinerName = 'Odt' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.ODT) + case 'Perturb': + refinerName = 'Perturb' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.PERTURB) + case _: + refinerName = 'None' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.NONE) + + + print(f"Launching mesh generation with following parameter : ") + print(f" - Criteria : facet_angle = {selectedCriterias.facet_angle}, edge_size = {selectedCriterias.edge_size}, facet_size = {selectedCriterias.facet_size}, facet_distance = {selectedCriterias.facet_distance}, cell_radius_edge_ratio = {selectedCriterias.cell_radius_edge_ratio}") + print(f" - Refiner : {refinerName}") + cmfp.generate(criteria, refiner) + + cmfp.write_out(args.output) + print(f"The script took a total of {toc(1)}") + + if args.display: + + import Sofa + import SofaImGui + import Sofa.Gui + + #Create the root node + root = Sofa.Core.Node("root") + # Call the below 'createScene' function to create the scene graph + createScene(root, args.output) + Sofa.Simulation.initRoot(root) + + # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") + Sofa.Gui.GUIManager.Init("myscene", "imgui") + 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() + + + + + diff --git a/examples/meshing/CGAL/mesh_from_polyhedron.py b/examples/meshing/CGAL/mesh_from_polyhedron.py new file mode 100644 index 000000000..5dd11f28b --- /dev/null +++ b/examples/meshing/CGAL/mesh_from_polyhedron.py @@ -0,0 +1,173 @@ +from CGAL.CGAL_Polyhedron_3 import Polyhedron_3, Polyhedron_modifier +from CGAL.CGAL_Mesh_3 import Mesh_3_Complex_3_in_triangulation_3 +from CGAL.CGAL_Mesh_3 import Polyhedral_mesh_domain_3 +from CGAL.CGAL_Mesh_3 import Mesh_3_parameters +from CGAL.CGAL_Mesh_3 import Default_mesh_criteria +from CGAL.CGAL_Kernel import Point_3 +from CGAL import CGAL_Mesh_3 + +import sys +import argparse +import os + + +from cgal_utils import CGAL_Mesh_from, CGAL_Mesh_3_IO_Util, ReadPolyData, tic, toc + +class CGAL_Mesh_from_polyhedron(CGAL_Mesh_from): + def __init__(self, filename = None, polyhedron = None): + if(filename is None and polyhedron is None): + raise ValueError("Need either a filename to load or a CGAl polyhedron already build") + if(filename is not None): + print(f"Loading polyhedron from {filename}...") + tic() + + mesh = ReadPolyData(filename) + self.polyhedron = Polyhedron_3() + pm = Polyhedron_modifier() + + pm.begin_surface(3,1) + for i in range(mesh.GetNumberOfPoints()): + pt = mesh.GetPoint(i) + pm.add_vertex(Point_3(pt[0],pt[1],pt[2])) + + + for i in range(mesh.GetNumberOfCells()): + pm.begin_facet() + for j in range(mesh.GetCell(i).GetNumberOfPoints()): + pm.add_vertex_to_facet(int(mesh.GetCell(i).GetPointId(j))) + pm.end_facet() + + + self.polyhedron.delegate(pm) + print(f"Done ! Took {toc()}") + else: + self.polyhedron = polyhedron + + print(f"Polyhedron info from input (vertices, facets, edges) = {(self.polyhedron.size_of_vertices(), self.polyhedron.size_of_facets(), self.polyhedron.size_of_halfedges()/2)}") + + + def generate(self, criteria , refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.NONE)): + print(f"Generating mesh...") + tic() + + # Create domain + domain = Polyhedral_mesh_domain_3(self.polyhedron) + params = Mesh_3_parameters() + params.no_exude() + params.no_perturb() + match refiner.refiner_type: + case CGAL_Mesh_from.Refiner.LLOYD: + params.set_lloyd(refiner.time_limit, refiner.max_iteration_number, refiner.convergence, refiner.free_bound) + case CGAL_Mesh_from.Refiner.ODT: + params.set_odt(refiner.time_limit, refiner.max_iteration_number, refiner.convergence, refiner.free_bound) + case CGAL_Mesh_from.Refiner.PERTURB: + params.set_perturb(refiner.time_limit, refiner.silver_bound) + + + # Mesh generation + c3t3 = CGAL_Mesh_3.make_mesh_3(domain, criteria, params) + print(f"Done ! Took {toc()}") + + self.IOUtil = CGAL_Mesh_3_IO_Util(c3t3) + self.IOUtil.extract([CGAL_Mesh_3_IO_Util.Elem.POINTS, CGAL_Mesh_3_IO_Util.Elem.TETRA]) + + def write_out(self, filename): + self.IOUtil.write_out(filename) + + +# Function called when the scene graph is being created +def createScene(root, file_name): + + root.addObject("RequiredPlugin", pluginName=["Sofa.Component.IO.Mesh", + "Sofa.GL.Component.Rendering3D", "Sofa.Component.Topology.Container.Dynamic","Sofa.Component.StateContainer"]) + + root.addObject("DefaultAnimationLoop") + loader = root.addObject("MeshVTKLoader", name="VTKLoader", filename=file_name) + root.addObject("TetrahedronSetTopologyContainer", name="Topology", src="@VTKLoader") + root.addObject("TetrahedronSetGeometryAlgorithms", name="Geometry", drawTetrahedra=True) + root.addObject("MechanicalObject", name="DoFsContainer") + + return root + + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="Creates a mesh from an input file containing a polyhedron representing the surface.") + + parser.add_argument("-i", "--input", default='data/torus.obj', help="The input file containing the surface. Format must be taken form ['.g', '.obj', '.stl', '.ply', '.vtk', '.vtp']") + parser.add_argument( "-o", "--output", default='torusVol.vtk', help="The output file to save the computed volumetric mesh") + parser.add_argument( "-r", "--refiner", default='None', help="Use refiner to erfine the mesh. Values are amongst ['None', 'Lloyd', 'Odt', 'Perturb']") + parser.add_argument( "-c", "--criteria", default='"facet_angle=25 edge_size=0.4 facet_size=0.15 facet_distance=0.008 cell_radius_edge_ration=3"', help="Set of parameters in the form of \"facet_angle=25 edge_size=0.4 facet_size=0.15 facet_distance=0.008 cell_radius_edge_ration=3\" that can be customized. If one is not specified, its default value is used") + parser.add_argument( "-d", "--display", action='store_true', help="If flag is added, a SOFA scene will be launched showing the generated mesh") + + args = parser.parse_args() + + tic(1) + cmfp = CGAL_Mesh_from_polyhedron(filename=args.input) + + #Criterial + class criteriaContainer: + def __init__(self): + self.facet_angle=25 + self.edge_size=0.15 + self.facet_size=0.15 + self.facet_distance=0.008 + self.cell_radius_edge_ratio=3 + + selectedCriterias = criteriaContainer() + + for passedCriteria in args.criteria.split(' '): + if len(passedCriteria.split('=')) >1: + if passedCriteria.split('=')[0] in selectedCriterias.__dict__: + setattr(selectedCriterias, passedCriteria.split('=')[0], float(passedCriteria.split('=')[1]) ) + + criteria = Default_mesh_criteria() + criteria.facet_angle(selectedCriterias.facet_angle).edge_size(selectedCriterias.edge_size).facet_size(selectedCriterias.facet_size).facet_distance(selectedCriterias.facet_distance).cell_radius_edge_ratio(selectedCriterias.cell_radius_edge_ratio) + + #Refiner + match args.refiner: + case 'Lloyd': + refinerName = 'Lloyd' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.LLOYD) + case 'Odt': + refinerName = 'Odt' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.ODT) + case 'Perturb': + refinerName = 'Perturb' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.PERTURB) + case _: + refinerName = 'None' + refiner = CGAL_Mesh_from.Refiner_input(refiner_type=CGAL_Mesh_from.Refiner.NONE) + + print(f"Launching mesh generation with following parameter : ") + print(f" - Criteria : facet_angle = {selectedCriterias.facet_angle}, edge_size = {selectedCriterias.edge_size}, facet_size = {selectedCriterias.facet_size}, facet_distance = {selectedCriterias.facet_distance}, cell_radius_edge_ratio = {selectedCriterias.cell_radius_edge_ratio}") + print(f" - Refiner : {refinerName}") + cmfp.generate(criteria, refiner) + + cmfp.write_out(args.output) + print(f"The script took a total of {toc(1)}") + + + if args.display: + + import Sofa + import SofaImGui + import Sofa.Gui + + #Create the root node + root = Sofa.Core.Node("root") + # Call the below 'createScene' function to create the scene graph + createScene(root, args.output) + Sofa.Simulation.initRoot(root) + + # Launch the GUI (imgui is now by default, to use Qt please refer to the example "basic-useQtGui.py") + Sofa.Gui.GUIManager.Init("myscene", "imgui") + 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() + + + diff --git a/examples/meshing/CGAL/requirements.txt b/examples/meshing/CGAL/requirements.txt new file mode 100644 index 000000000..15643fac5 --- /dev/null +++ b/examples/meshing/CGAL/requirements.txt @@ -0,0 +1,4 @@ +numpy == 1.26 +cgal == 6.0 +vtk +pypcd4 diff --git a/examples/meshing/Gmsh/generateMesh.py b/examples/meshing/Gmsh/generateMesh.py new file mode 100644 index 000000000..840bc7ba6 --- /dev/null +++ b/examples/meshing/Gmsh/generateMesh.py @@ -0,0 +1,97 @@ +import meshio +import gmsh +import pygmsh +import Sofa + +resolution = 0.01 +# Channel parameters +L = 2.2 +H = 0.41 +c = [0.2, 0.2, 0] +r = 0.05 + + +def generateMesh(): + # Initialize empty geometry using the build in kernel in GMSH + geometry = pygmsh.geo.Geometry() + # Fetch model we would like to add data to + model = geometry.__enter__() + # Add circle + circle = model.add_circle(c, r, mesh_size=resolution) + # Add points with finer resolution on left side + points = [ + model.add_point((0, 0, 0), mesh_size=resolution), + model.add_point((L, 0, 0), mesh_size=5 * resolution), + model.add_point((L, H, 0), mesh_size=5 * resolution), + model.add_point((0, H, 0), mesh_size=resolution), + ] + + # Add lines between all points creating the rectangle + channel_lines = [ + model.add_line(points[i], points[i + 1]) for i in range(-1, len(points) - 1) + ] + + # Create a line loop and plane surface for meshing + channel_loop = model.add_curve_loop(channel_lines) + plane_surface = model.add_plane_surface(channel_loop, holes=[circle.curve_loop]) + + # Call gmsh kernel before add physical entities + model.synchronize() + volume_marker = 6 + model.add_physical([plane_surface], "Volume") + model.add_physical([channel_lines[0]], "Inflow") + model.add_physical([channel_lines[2]], "Outflow") + model.add_physical([channel_lines[1], channel_lines[3]], "Walls") + model.add_physical(circle.curve_loop.curves, "Obstacle") + geometry.generate_mesh(dim=3) + gmsh.write("mesh.msh") + gmsh.clear() + geometry.__exit__() + + +def createScene(root): + + root.addObject("RequiredPlugin", pluginName=["Sofa.Component.IO.Mesh", + "Sofa.GL.Component.Rendering3D", "Sofa.Component.Topology.Container.Dynamic","Sofa.Component.StateContainer"]) + + root.addObject("DefaultAnimationLoop") + loader = root.addObject("MeshGmshLoader", name="GmshLoader", filename="mesh.msh") + root.addObject("TriangleSetTopologyContainer", name="Topology", src="@GmshLoader") + root.addObject("TriangleSetGeometryAlgorithms", name="Geometry", drawTriangles=True) + root.addObject("MechanicalObject", name="DoFsContainer") + + return root + + + + +def main(): + import SofaImGui + import Sofa.Gui + + generateMesh() + + # Create the root node + root = Sofa.Core.Node("root") + # Call the below 'createScene' function to create the scene graph + createScene(root) + # Initialize the graph from the root node + Sofa.Simulation.initRoot(root) + + # Find out the supported GUIs + print ("Supported GUIs are: " + Sofa.Gui.GUIManager.ListSupportedGUI(",")) + # Launch the GUI (qt or qglviewer) + 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 + Sofa.Gui.GUIManager.MainLoop(root) + Sofa.Gui.GUIManager.closeGUI() + print("GUI was closed") + print("Simulation is done.") + + +# Function used only if this script is called from a python environment +if __name__ == '__main__': + main() + diff --git a/examples/stlib/EulalieScene.py b/examples/stlib/EulalieScene.py new file mode 100644 index 000000000..47d0ca2f8 --- /dev/null +++ b/examples/stlib/EulalieScene.py @@ -0,0 +1,14 @@ +from stlib.entities import Entity, EntityParameters +from stlib.modifiers import SingleEntityModifier +from stlib.modifiers.fixing import FixingModifierParameters +from stlib.settings.simulation import addSimulationSettings + + +def createScene(rootnode): + + simulation = addSimulationSettings(rootnode) + + cube1 = simulation.add(Entity, parameters=EntityParameters(name = "FixedCube")) + cube2 = simulation.add(Entity, parameters=EntityParameters(name = "FallingCube")) + + simulation.create(SingleEntityModifier, parameters=FixingModifierParameters()).apply(entity=cube1) \ No newline at end of file diff --git a/examples/stlib/PrefabScene_beginner.py b/examples/stlib/PrefabScene_beginner.py new file mode 100644 index 000000000..2c4552c59 --- /dev/null +++ b/examples/stlib/PrefabScene_beginner.py @@ -0,0 +1,30 @@ +from stlib.materials.rigid import Rigid +from stlib.materials.deformable import Deformable +from stlib.geometries.cube import CubeParameters +from stlib.geometries.file import FileParameters +from splib.simulation.headers import setupLagrangianCollision +from splib.simulation.linear_solvers import addLinearSolver +from splib.simulation.ode_solvers import addImplicitODE + +#To be added in splib +def addSolvers(root): + addLinearSolver(root) + addImplicitODE(root) + root.addObject("LinearSolverConstraintCorrection", linearsolver="@LinearSolver") + +def createScene(root): + root.gravity = [0, 0, -9.81] + + setupLagrangianCollision(root) + addSolvers(root) + + rigidParams = Rigid.getParameters() + rigidParams.geometry = CubeParameters([0, 0, 0], 1, 3) + root.add(Rigid,rigidParams) + + deformableParams = Deformable.getParameters() + #Add transformation somewhere here + deformableParams.geometry = FileParameters("SofaScene/Logo.vtk") + root.add(Deformable,deformableParams) + + return root diff --git a/examples/stlib/PrefabScene_expert.py b/examples/stlib/PrefabScene_expert.py new file mode 100644 index 000000000..0bc427d37 --- /dev/null +++ b/examples/stlib/PrefabScene_expert.py @@ -0,0 +1,14 @@ +from splib.helper import exportScene +from stlib.misc.entity import Entity + + +def createScene(root): + Entity.Deformable(Mesh,MechaProperties) + + return root + + +if __name__=="__main__": + root = exportScene() + createScene(root) + pass \ No newline at end of file diff --git a/examples/stlib/PrefabScene_intermediate.py b/examples/stlib/PrefabScene_intermediate.py new file mode 100644 index 000000000..7ce22c745 --- /dev/null +++ b/examples/stlib/PrefabScene_intermediate.py @@ -0,0 +1,8 @@ +from splib.helper import exportScene +from stlib.misc.entity import Entity + + +def createScene(root): + + + return root \ No newline at end of file diff --git a/examples/stlib/SofaScene.py b/examples/stlib/SofaScene.py new file mode 100644 index 000000000..67f667a5a --- /dev/null +++ b/examples/stlib/SofaScene.py @@ -0,0 +1,121 @@ +from fontTools.afmLib import preferredAttributeOrder + +from stlib.geometries.plane import PlaneParameters +from stlib.geometries.file import FileParameters +from stlib.geometries.extract import ExtractParameters +from stlib.materials.deformable import DeformableBehaviorParameters +from stlib.collision import Collision, CollisionParameters +from stlib.entities import Entity, EntityParameters +from stlib.visual import Visual, VisualParameters +from splib.core.enum_types import CollisionPrimitive, ElementType, ConstitutiveLaw +from splib.simulation.headers import setupLagrangianCollision, setupDefaultHeader +from splib.simulation.ode_solvers import addImplicitODE +from splib.simulation.linear_solvers import addLinearSolver +import dataclasses +import numpy as np + + +def createScene(root): + root.gravity=[0,0,9.81] + ##Solvers + # setupDefaultHeader(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1], + # parallelComputing = True) + setupLagrangianCollision(root, displayFlags = "showVisualModels",backgroundColor=[0.8, 0.8, 0.8, 1], + parallelComputing = True,alarmDistance=0.3, contactDistance=0.02, + frictionCoef=0.5, tolerance=1.0e-4, maxIterations=20) + ##Environement + planes_lengthNormal = np.array([0, 1, 0]) + planes_lengthNbEdge = 1 + planes_widthNbEdge = 2 + planes_lengthSize = 30 + planes_widthSize = 70 + + plane1_collisionParams = CollisionParameters() + plane1_collisionParams.name = "UP" + plane1_collisionParams.primitives = [CollisionPrimitive.TRIANGLES] + plane1_collisionParams.kwargs = {"TriangleCollision" : {"moving" : False, "simulated" : False}} + plane1_collisionParams.geometry = PlaneParameters(np.array([15,0,1]), np.array([0,0,-1]), + planes_lengthNormal, planes_lengthNbEdge, planes_widthNbEdge, planes_lengthSize, planes_widthSize) + plane1 = root.add(Collision, parameters = plane1_collisionParams) + # TODO being able to reuse already loaded geometry of current prefab to add any new sub prefab + # We need to enable to directly pass a link to an already existing prefab in place of a prefab parameter object + plane1_visu = plane1.addChild("Visu") + plane1_visu.addObject("OglModel", name="VisualModel", src="@../Geometry/container") + + + plane2_collisionParams = CollisionParameters() + plane2_collisionParams.name = "DOWN" + plane2_collisionParams.primitives = [CollisionPrimitive.TRIANGLES] + plane2_collisionParams.kwargs = {"TriangleCollision" : {"moving" : False, "simulated" : False}} + plane2_collisionParams.geometry = PlaneParameters(np.array([15,0,-20]), np.array([0,0,1]), + planes_lengthNormal, planes_lengthNbEdge, planes_widthNbEdge, planes_lengthSize, planes_widthSize) + plane2 = root.add(Collision, parameters = plane2_collisionParams) + plane2_visu = plane2.addChild("Visu") + plane2_visu.addObject("OglModel", name="VisualModel", src="@../Geometry/container") + + + ## Real models + # VolumetricObjects = root.addChild("VolumetricObjects") + # addImplicitODE(VolumetricObjects) + # addLinearSolver(VolumetricObjects, constantSparsity=False, ) + + ### Logo + LogoNode = root.addChild("LogoNode") + addImplicitODE(LogoNode) + addLinearSolver(LogoNode, constantSparsity=False, ) + + LogoParams = EntityParameters(name = "Logo", + geometry = FileParameters(filename="mesh/SofaScene/Logo.vtk"), + material = DeformableBehaviorParameters(), + collision = CollisionParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoColli.sph")), + visual = VisualParameters(geometry = FileParameters(filename="mesh/SofaScene/LogoVisu.obj"))) + + LogoParams.geometry.elementType = ElementType.TETRAHEDRA + LogoParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + LogoParams.material.parameters = [200, 0.4] + LogoParams.material.massDensity = 0.003261 + LogoParams.collision.primitives = [CollisionPrimitive.SPHERES] + #TODO make this flawless with spheres. Here collisions elements are not in the topology and a link is to be made between the loader and the collision object + LogoParams.collision.kwargs = {"SphereCollision" : {"radius" : "@Geometry/loader.listRadius"}} + LogoParams.visual.color = [0.7, .35, 0, 0.8] + + Logo = LogoNode.add(Entity, parameters = LogoParams) + + Logo.material.addObject("ConstantForceField", name="ConstantForceUpwards", totalForce=[0, 0, -5.0]) + Logo.material.addObject("LinearSolverConstraintCorrection", name="ConstraintCorrection", linearSolver=LogoNode.LinearSolver.linkpath, ODESolver=LogoNode.ODESolver.linkpath) + + + ### S + SNode = root.addChild("SNode") + addImplicitODE(SNode) + addLinearSolver(SNode, constantSparsity=False, ) + + SParams = EntityParameters("bob.yaml") + SParams.name = "S" + SParams.geometry = FileParameters(filename="mesh/SofaScene/S.vtk") + SParams.geometry.elementType = ElementType.TETRAHEDRA + SParams.material = DeformableBehaviorParameters() + SParams.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + SParams.material.parameters = [200, 0.45] + + def SAddMaterial(node): + DeformableBehaviorParameters.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) + + SParams.material.addMaterial = SAddMaterial + SParams.material.massDensity = 0.011021 + SParams.collision = CollisionParameters() + SParams.collision.primitives = [CollisionPrimitive.TRIANGLES] + # # #TODO: to fix link issues for extracted geometry, it might be better to give source geometry relative link + parameters + SParams.collision.geometry = ExtractParameters(destinationType=ElementType.TRIANGLES, sourceParameters=SParams.geometry ) + SParams.visual = VisualParameters() + SParams.visual.geometry = FileParameters(filename="mesh/SofaScene/SVisu.obj") + SParams.visual.color = [0.7, .7, 0.7, 0.8] + + S = SNode.add(Entity, parameters = SParams) + + + SDensity = 0.011021 + ODensity = SDensity + ADensity = 0.00693695 diff --git a/examples/thermoelasticity.py b/examples/thermoelasticity.py new file mode 100644 index 000000000..ba29187ea --- /dev/null +++ b/examples/thermoelasticity.py @@ -0,0 +1,115 @@ +# Required import for python +import Sofa +import numpy as np + + +def main(): + import SofaRuntime + import Sofa.Gui + import SofaImGui + + root = Sofa.Core.Node("root") + createScene(root) + Sofa.Simulation.init(root) + + Sofa.Gui.GUIManager.Init("myscene", "imgui") + Sofa.Gui.GUIManager.createGUI(root, __file__) + Sofa.Gui.GUIManager.SetDimension(1080, 600) + Sofa.Gui.GUIManager.MainLoop(root) + Sofa.Gui.GUIManager.closeGUI() + + +def createScene(root): + root.gravity=[0, -9.81, 0] + root.dt=0.01 + root.bbox=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]] + + root.addObject('DefaultAnimationLoop') + root.addObject('DefaultVisualManagerLoop') + root.addObject('RequiredPlugin', pluginName=['Sofa.Component.Constraint.Projective', 'Sofa.Component.Diffusion', 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', 'Sofa.Component.LinearSolver.Iterative', 'Sofa.Component.Mass', 'Sofa.Component.ODESolver.Backward', 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.Topology.Container.Dynamic', 'Sofa.Component.Topology.Container.Grid', 'Sofa.Component.Topology.Mapping', 'Sofa.Component.Visual', 'Sofa.GL.Component.Engine', 'Sofa.GL.Component.Rendering2D', + 'Sofa.GL.Component.Rendering3D', 'Sofa.Component.StateContainer', 'Sofa.Component.Mapping.Linear']) + root.addObject('VisualStyle', displayFlags="hideCollisionModels showVisualModels hideForceFields showBehaviorModels") + + gridGenerator = root.addObject('RegularGridTopology', name="gridGenerator", nx=16, ny=6, nz=6, xmin=0, xmax=1, ymin=0, ymax=0.2, zmin=0, zmax=0.2) + root.addObject("OglColorMap", legendTitle="A-dimensional temperature", legendOffset=[60,0], legendSize=20, min=0, max=1, showLegend=True, colorScheme="Blue to Red") + + + tetraTopo = root.addChild('TetraTopology') + tetrahedraContainer = tetraTopo.addObject("TetrahedronSetTopologyContainer", name="tetContainer", tags="3dgeometry") + tetraTopo.addObject("TetrahedronSetTopologyModifier", name="Modifier") + tetraTopo.addObject("Hexa2TetraTopologicalMapping", input=gridGenerator.linkpath, output=tetrahedraContainer.linkpath) + tetraTopo.addObject("MechanicalObject", name="tetraO", position="@../gridGenerator.position", tags="3dgeometry") + tetraTopo.addObject("BoxROI", name="BoundaryCondition", box=[-0.01, -0.01, -0.01, 0.01, 0.21, 0.21]) + + + meca = tetraTopo.addChild("Mechanics") + meca.addObject("EulerImplicitSolver", name="Euler Impl IntegrationScheme") + meca.addObject("SparseLDLSolver", name="LDL LinearSolver", template="CompressedRowSparseMatrixMat3x3d") + meca.addObject("TetrahedronSetTopologyContainer", name="tetContainer", src=tetrahedraContainer.linkpath) + meca.addObject("TetrahedronSetGeometryAlgorithms", name="tetGeometry", template="Vec3d") + elasticMO = meca.addObject("MechanicalObject", name="MO", position="@../tetraO.position") + meca.addObject("MeshMatrixMass", template="Vec3d,Vec3d", name="Mass", massDensity=100, topology=tetrahedraContainer.linkpath, geometryState="@MO") + meca.addObject("FixedConstraint", name="FixedBoundaryCondition", indices="@../BoundaryCondition.indices") + + #initialization of the vector multiplying the local Young's modulus (need to activate updateStiffness=True) + initVector = np.full((1, 576), 1.0) + meca.addObject("TetrahedronFEMForceField", name="LinearElasticity", youngModulus=3e5, poissonRatio=0.4, computeVonMisesStress=1, showVonMisesStressPerElement=True, localStiffnessFactor=initVector, updateStiffness=True) + + + thermo = tetraTopo.addChild("Thermodynamics") + thermo.addObject("EulerImplicitSolver", name="Euler Impl IntegrationScheme", firstOrder=True) + thermo.addObject("CGLinearSolver", name="Conjugate Gradient", iterations="1000", tolerance=1.0e-10, threshold=1.0e-30) + thermo.addObject("TetrahedronSetTopologyContainer", name="tetContainer", src="@../tetContainer") + thermo.addObject("TetrahedronSetGeometryAlgorithms", name="tetGeometry", template="Vec3d") + thermo.addObject("MechanicalObject", name="Temperatures", template="Vec1d", size=576, showObject=True) + thermo.addObject("MeshMatrixMass", template="Vec1d,Vec3d", name="Conductivity", massDensity=1.0, topology=tetrahedraContainer.linkpath, geometryState=elasticMO.linkpath) + thermo.addObject("FixedConstraint", name="Heating", indices=495) + thermo.addObject("TetrahedronDiffusionFEMForceField", name="DiffusionForceField", template="Vec1d", constantDiffusionCoefficient=0.05, tagMechanics="3dgeometry", mstate="@Temperatures") + + thermoVisu = thermo.addChild("Visu") + thermoVisu.addObject("TextureInterpolation", template="Vec1d", name="EngineInterpolation", input_states="@../Temperatures.position", input_coordinates="@../../Mechanics/MO.position", min_value=0., max_value=1., manual_scale=True , drawPotentiels=False, showIndicesScale=5e-05) + thermoVisu.addObject("OglModel", template="Vec3d", name="oglPotentiel", handleDynamicTopology="0", texcoords="@EngineInterpolation.output_coordinates" ,texturename="textures/heatColor.bmp", scale3d=[1, 1, 1], material="Default Diffuse 1 1 1 1 0.5 Ambient 1 1 1 1 0.3 Specular 0 0.5 0.5 0.5 1 Emissive 0 0.5 0.5 0.5 1 Shininess 0 45 No texture linked to the material No bump texture linked to the material ") + thermoVisu.addObject("IdentityMapping", input=elasticMO.linkpath, output="@oglPotentiel") + + # Add the controller + root.addObject(ThermoMecaController(name = "MultiPhysicsController", ThermalObject=thermo.Temperatures, MechanicalForceField=meca.LinearElasticity)) + + return root + + +class ThermoMecaController(Sofa.Core.Controller): + def __init__(self, *args, **kwargs): + # These are needed (and the normal way to override from a python class) + Sofa.Core.Controller.__init__(self, *args, **kwargs) + # Recover the pointers to the thermal object and the ForceField responsible of the linear elastic constitutive law + self.ThermalObject = kwargs.get("ThermalObject") + self.MechanicalForceField = kwargs.get("MechanicalForceField") + + def onAnimateEndEvent(self, event): + time = self.getContext().getTime() + + self.temperatures = self.ThermalObject.position.array() + + if time > 0.5 : + + # Make the Young's modulus related to the temperature: E = E_init / (1 + 50*Temperature) + with self.MechanicalForceField.localStiffnessFactor.writeableArray() as wa: + for i in range(576) : + wa[i] = 1.0 / ( 1.+ 50.*self.temperatures[i][0] ) + + #Enforce temperature of the node 495 (extremity of the bar) + # Heating phase until t = 4 + if time < 4.0 : + with self.ThermalObject.position.writeableArray() as wt: + wt[495][0] = 1.0 + # Cooling down phase when t > 4 + else : + with self.ThermalObject.position.writeableArray() as wt: + wt[495][0] = 0.0 + + +# Function used only if this script is called from a python environment +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/splib/Testing.py b/splib/Testing.py new file mode 100644 index 000000000..146b4096b --- /dev/null +++ b/splib/Testing.py @@ -0,0 +1,86 @@ +from splib.topology.dynamic import * +from splib.simulation.headers import * +from splib.simulation.ode_solvers import * +from splib.simulation.linear_solvers import * +from splib.mechanics.linear_elasticity import * +from splib.mechanics.mass import * +from splib.mechanics.fix_points import * +from splib.topology.loader import * +from splib.core.node_wrapper import * + +from splib.core.node_wrapper import ChildWrapper, ObjectWrapper +from splib.helper import exportScene + + +def createScene(rootNode): + rootNode.dt = 0.03 + rootNode.gravity = [0,-9.81,0] + + setupLagrangianCollision(rootNode,requiredPlugins={"pluginName":['Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual']}) + # + # + # ## TODO : Being able to call "childNode.addAnything" by using the __getattr__ method + Liver0=rootNode.addChild("Liver0") + Liver0.addObject("EulerImplicitSolver",name="ODESolver",rayleighStiffness="0",rayleighMass="0") + Liver0.addObject("SparseLDLSolver",name="LinearSolver",template="CompressedRowSparseMatrixMat3x3",parallelInverseProduct="False") + Liver0.addObject("MeshGmshLoader",name="meshLoader",filename="mesh/liver.msh") + Liver0.addObject("TetrahedronSetTopologyModifier",name="modifier") + Liver0.addObject("TetrahedronSetTopologyContainer",name="container",src="@meshLoader") + Liver0.addObject("TetrahedronSetGeometryAlgorithms",name="algorithms") + Liver0.addObject("MechanicalObject",name="mstate",template="Vec3d") + Liver0.addObject("LinearSolverConstraintCorrection",name="constraintCorrection") + Liver0.addObject("TetrahedronFEMForceField",name="constitutiveLaw",youngModulus="3000",poissonRatio="0.3",method="large") + Liver0.addObject("MeshMatrixMass",name="mass",massDensity="1") + Liver0.addObject("BoxROI",name="fixedBoxROI",box="0 3 0 2 5 2") + Liver0.addObject("FixedProjectiveConstraint",name="fixedConstraints",indices="@fixedBoxROI.indices") + Visu=Liver0.addChild("Visu") + Visu.addObject("TriangleSetTopologyModifier",name="modifier") + Visu.addObject("TriangleSetTopologyContainer",name="container") + Visu.addObject("TriangleSetGeometryAlgorithms",name="algorithms") + Visu.addObject("Tetra2TriangleTopologicalMapping",name="TopologicalMapping",input="@../container",output="@container") + Visu.addObject("OglModel",name="OglModel",topology="@container",color="[1.0, 0.2, 0.8]") + Visu.addObject("IdentityMapping",name="Mapping",isMechanical="False") + + + SimulatedLiver1 = rootNode.addChild("Liver1") + addImplicitODE(SimulatedLiver1) + addLinearSolver(SimulatedLiver1,iterative=False, template="CompressedRowSparseMatrixMat3x3") + loadMesh(SimulatedLiver1,filename="mesh/liver.msh") + addDynamicTopology(SimulatedLiver1,type=ElementType.TETRAHEDRA,source="@meshLoader") + SimulatedLiver1.addObject("MechanicalObject",name="mstate", template='Vec3d') + SimulatedLiver1.addObject("LinearSolverConstraintCorrection",name="constraintCorrection") + addLinearElasticity(SimulatedLiver1,ElementType.TETRAHEDRA, poissonRatio="0.3", youngModulus="3000", method='large') + addMass(SimulatedLiver1,template='Vec3d',massDensity="2") + addFixation(SimulatedLiver1,ConstraintType.PROJECTIVE,boxROIs=[0, 3, 0, 2, 5, 2]) + + SimulatedLiverVisu = SimulatedLiver1.addChild("Visu") + addDynamicTopology(SimulatedLiverVisu,ElementType.TRIANGLES) + SimulatedLiverVisu.addObject("Tetra2TriangleTopologicalMapping", name="TopologicalMapping", input="@../container", output="@container") + SimulatedLiverVisu.addObject("OglModel", name="OglModel", topology="@container",color=[1.0,0.2,0.8]) + SimulatedLiverVisu.addObject("IdentityMapping",name="Mapping",isMechanical=False) + + + return rootNode + + + +if __name__ == "__main__": + Node = exportScene() + createScene(Node) + + + + + + diff --git a/splib/__init__.py b/splib/__init__.py index 34e9c316c..6b380c11a 100644 --- a/splib/__init__.py +++ b/splib/__init__.py @@ -1,4 +1,2 @@ -"""SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it""" -raise Exception("SPLIB is now relocated at: https://github.com/SofaDefrost/STLIB, please clone and install the plugin to use it") - +__all__ = ["core","topology", "simulation","modeler","mechanics","Testing","helper"] diff --git a/splib/core/__init__.py b/splib/core/__init__.py new file mode 100644 index 000000000..78deb3fad --- /dev/null +++ b/splib/core/__init__.py @@ -0,0 +1 @@ +__all__ = ["node_wrapper", "utils","enum_types"] diff --git a/splib/core/enum_types.py b/splib/core/enum_types.py new file mode 100644 index 000000000..5d4504311 --- /dev/null +++ b/splib/core/enum_types.py @@ -0,0 +1,51 @@ +from enum import Enum + +class ConstitutiveLaw(Enum): + ELASTIC = 1 + HYPERELASTIC = 2 + +class ODEType(Enum): + EXPLICIT = 1 + IMPLICIT = 2 + +class SolverType(Enum): + DIRECT = 1 + ITERATIVE = 2 + +class MappingType(Enum): + BARYCENTRIC = 1 + IDENTITY = 2 + RIGID = 3 + +class ConstraintType(Enum): + PROJECTIVE = 1 + WEAK = 2 + LAGRANGIAN = 3 + +class CollisionPrimitive(Enum): + POINTS = 1 + LINES = 2 + TRIANGLES = 3 + SPHERES = 4 + +class ElementType(Enum): + POINTS = 1 + EDGES = 2 + TRIANGLES = 3 + QUADS = 4 + TETRAHEDRA = 5 + HEXAHEDRA = 6 + +class StateType(Enum): + VEC3 = 3 + VEC1 = 1 + RIGID = 7 + + def __str__(self): + if self == StateType.VEC3: + return "Vec3" + if self == StateType.VEC1: + return "Vec1" + if self == StateType.RIGID: + return "Rigid3" + return 'Unknown' diff --git a/splib/core/node_wrapper.py b/splib/core/node_wrapper.py new file mode 100644 index 000000000..a7931f088 --- /dev/null +++ b/splib/core/node_wrapper.py @@ -0,0 +1,75 @@ +from functools import wraps +from splib.core.utils import defaultValueType +# The two classes are not merged because one could want to use a ReusableMethod +# (enabling to pass dictionary fixing params) without wanting to use a full PrefabSimulation + + +class BaseWrapper(object): + + def __init__(self,node): + self.node = node + + def __getattr__(self, item): + return getattr(self.node,item) + + +class ObjectWrapper(BaseWrapper): + def __init__(self,*args,**kwargs): + super().__init__(*args,**kwargs) + + def addObject(self,*args, **kwargs): + parameters = {} + # Expand any parameter that has been set by the user in a custom dictionary + # and having the same key as the component name + if "name" in kwargs: + parameters["name"] = kwargs["name"] + if kwargs["name"] in kwargs: + if isinstance(kwargs[kwargs["name"]], dict): + for param in kwargs[kwargs["name"]]: + if not(isinstance(kwargs[kwargs["name"]][param],defaultValueType)): + parameters = {**parameters, param : kwargs[kwargs["name"]][param]} + else: + print("[Warning] You are passing a keyword arg with the same name as one obj without it being a Dict, it will not be used. ") + + # Add all the parameters from kwargs that are not dictionary + for param in kwargs: + if param in parameters: + if not(param == "name"): + print("[Warning] You are redefining the parameter '"+ param + "' of object " + str(args[0])) + elif not(isinstance(kwargs[param], dict)) and not(isinstance(kwargs[param],defaultValueType)): + parameters = {**parameters,param:kwargs[param]} + + return self.node.addObject(*args,**parameters) + + + + +class ChildWrapper(ObjectWrapper): + def __init__(self,*args,**kwargs): + super().__init__(*args,**kwargs) + + # This method enforces that the object that is created by the addChild method keeps the prefab type + def addChild(self,*args, **kwargs): + child = self.node.addChild(*args,**kwargs) + returnObject = self.__new__(type(self)) + returnObject.__init__(child) + return returnObject + + + + +def ReusableMethod(method): + @wraps(method) + def wrapper(*args, **kwargs): + + node = args[0] + # We don't want to wrap an object that is already wrapped + # It wouldn't break anything but nodes might get wrapped and wrapped again multiplying redirections... + if( not isinstance(node,ObjectWrapper) ): + node = ObjectWrapper(node) + + if len(args)>1: + return method(node,*args[1:],**kwargs) + else: + return method(node,**kwargs) + return wrapper diff --git a/splib/core/utils.py b/splib/core/utils.py new file mode 100644 index 000000000..9896a259d --- /dev/null +++ b/splib/core/utils.py @@ -0,0 +1,54 @@ +from typing import List, Callable, Tuple, Dict +from functools import wraps +import Sofa +import Sofa.Core + +class defaultValueType(): + def __init__(self): + pass + +DEFAULT_VALUE = defaultValueType() + +def isDefault(obj): + return isinstance(obj,defaultValueType) + +def getParameterSet(name : str,parameterSet : Dict) -> Dict: + if name in parameterSet: + if isinstance(parameterSet[name], dict): + return parameterSet[name] + return {} + +def MapKeywordArg(objectName,*argumentMaps): + def MapArg(method): + @wraps(method) + def wrapper(*args, **kwargs): + containerParams = getParameterSet(objectName,kwargs) + for argMap in argumentMaps: + if (argMap[0] in kwargs) and not(kwargs[argMap[0]] is None): + containerParams[argMap[1]] = kwargs[argMap[0]] + kwargs[objectName] = containerParams + return method(*args,**kwargs) + return wrapper + return MapArg + +REQUIRES_COLLISIONPIPELINE = "requiresCollisionPipeline" + +def setRequiresCollisionPipeline(rootnode): + if rootnode is not None: + if rootnode.findData(REQUIRES_COLLISIONPIPELINE) is None: + rootnode.addData(name=REQUIRES_COLLISIONPIPELINE, type="bool", default=False, help="Some prefabs in the scene requires a collision pipeline.") + else: + rootnode.requiresCollisionPipeline.value = True + +REQUIRES_LAGRANGIANCONSTRAINTSOLVER = "requiresLagrangianConstraintSolver" + +def setRequiresLagrangianConstraintSolver(rootnode): + if rootnode is not None: + if rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER) is None: + rootnode.addData(name=REQUIRES_LAGRANGIANCONSTRAINTSOLVER, type="bool", default=False, help="Some prefabs in the scene requires a Lagrangian constraint solver.") + else: + rootnode.requiresLagrangianConstraintSolver.value = True + + + + diff --git a/splib/helper.py b/splib/helper.py new file mode 100644 index 000000000..b4881af5f --- /dev/null +++ b/splib/helper.py @@ -0,0 +1,35 @@ +class displayNode(): + def __init__(self,_level=0): + self.prefix = "" + for i in range(_level): + self.prefix += "| " + + def addObject(self,type:str,**kwargs): + print(self.prefix + type + " with " + str(kwargs)) + + def addChild(self,name:str): + print(self.prefix + "-> Node : " + name) + return displayNode(len(self.prefix) + 1) + + +class exportScene(): + def __init__(self,name="rootNode"): + self.name = name + + def addObject(self,type:str,**kwargs): + suffix = "" + for i in kwargs: + suffix += "," + str(i) + "=\"" + str(kwargs[i]) + "\"" + print(self.name+".addObject(\"" + type + "\"" + suffix + ")") + + def addChild(self,name:str): + print(name + '=' + self.name+".addChild(\"" + name + "\")") + setattr(self,name,exportScene(name)) + return getattr(self,name) + + def __setattr__(self, key, value): + if(not(key == "name")): + print(self.__dict__["name"] + "." + key + " = " + str(value)) + self.__dict__[key] = value + else: + self.__dict__[key] = value diff --git a/splib/mechanics/__init__.py b/splib/mechanics/__init__.py new file mode 100644 index 000000000..3761021a2 --- /dev/null +++ b/splib/mechanics/__init__.py @@ -0,0 +1 @@ +__all__ = ["linear_elasticity","hyperelasticity","fix_points","collision_model","mass"] \ No newline at end of file diff --git a/splib/mechanics/collision_model.py b/splib/mechanics/collision_model.py new file mode 100644 index 000000000..6758ee6d4 --- /dev/null +++ b/splib/mechanics/collision_model.py @@ -0,0 +1,34 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import CollisionPrimitive + + +@ReusableMethod +def addCollisionModels(node, primitive : CollisionPrimitive, + topology=DEFAULT_VALUE, + selfCollision=DEFAULT_VALUE, + proximity=DEFAULT_VALUE, + group=DEFAULT_VALUE, + contactStiffness=DEFAULT_VALUE, + contactFriction=DEFAULT_VALUE, + spheresRadius=DEFAULT_VALUE, + **kwargs): + match primitive: + case CollisionPrimitive.POINTS: + node.addObject("PointCollisionModel", name="PointCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) + return + case CollisionPrimitive.LINES: + node.addObject("LineCollisionModel", name="EdgeCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, **kwargs) + return + case CollisionPrimitive.TRIANGLES: + node.addObject("TriangleCollisionModel", name="TriangleCollision", topology=topology, selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group,**kwargs) + return + case CollisionPrimitive.SPHERES: + node.addObject("SphereCollisionModel", name="SphereCollision", selfCollision=selfCollision, proximity=proximity, contactStiffness=contactStiffness, contactFriction=contactFriction, group=group, radius=spheresRadius, **kwargs) + return + case _: + return + + #Cube and tetra are missing. + + \ No newline at end of file diff --git a/splib/mechanics/fix_points.py b/splib/mechanics/fix_points.py new file mode 100644 index 000000000..ddbbd6308 --- /dev/null +++ b/splib/mechanics/fix_points.py @@ -0,0 +1,30 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import isDefault, DEFAULT_VALUE +from splib.core.enum_types import ConstraintType +from enum import Enum + + +##box +@ReusableMethod +def addFixation(node,type:ConstraintType,boxROIs=DEFAULT_VALUE, sphereROIs=DEFAULT_VALUE, indices=DEFAULT_VALUE, fixAll=DEFAULT_VALUE,**kwargs): + if (isDefault(indices)): + if(not isDefault(boxROIs)): + node.addObject("BoxROI",name='fixedBoxROI',box=boxROIs,**kwargs) + indices="@fixedBoxROI.indices" + if(not isDefault(sphereROIs)): + node.addObject("SphereROI",name='fixedSphereROI',centers=sphereROIs[0],radii=sphereROIs[1],**kwargs) + indices="@fixedSphereROI.indices" + + match type: + case ConstraintType.WEAK: + node.addObject("FixedWeakConstraint",name="fixedConstraints", indices=indices, fixAll=fixAll, **kwargs) + return + case ConstraintType.PROJECTIVE: + node.addObject("FixedProjectiveConstraint",name="fixedConstraints", indices=indices, fixAll=fixAll, **kwargs) + return + case ConstraintType.LAGRANGIAN: + node.addObject("LagrangianFixedConstraint",name="fixedConstraints", indices=indices, fixAll=fixAll, **kwargs) + return + case _: + print('Contraint type is either ConstraintType.PROJECTIVE, ConstraintType.WEAK or ConstraintType.LAGRANGIAN') + return diff --git a/splib/mechanics/hyperelasticity.py b/splib/mechanics/hyperelasticity.py new file mode 100644 index 000000000..fb255df41 --- /dev/null +++ b/splib/mechanics/hyperelasticity.py @@ -0,0 +1,14 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import ElementType + + +@ReusableMethod +def addHyperelasticity(node,elem:ElementType,materialName=DEFAULT_VALUE, parameterSet=DEFAULT_VALUE, matrixRegularization=DEFAULT_VALUE,**kwargs): + match elem: + case ElementType.TETRAHEDRA: + node.addObject("TetrahedronHyperelasticityFEMForceField",name="constitutiveLaw", materialName=materialName, parameterSet=parameterSet, matrixRegularization=matrixRegularization, **kwargs) + return + case _: + print('Hyperelasticity model only exist for Tetrahedron elements.') + return \ No newline at end of file diff --git a/splib/mechanics/linear_elasticity.py b/splib/mechanics/linear_elasticity.py new file mode 100644 index 000000000..1e9b30290 --- /dev/null +++ b/splib/mechanics/linear_elasticity.py @@ -0,0 +1,26 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import ElementType + + +@ReusableMethod +def addLinearElasticity(node, elementType:ElementType, youngModulus=DEFAULT_VALUE, poissonRatio=DEFAULT_VALUE, method=DEFAULT_VALUE, **kwargs): + match elementType: + case ElementType.EDGES: + node.addObject("BeamFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) + return + case ElementType.TRIANGLES: + node.addObject("TriangleFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) + return + case ElementType.QUADS: + node.addObject("QuadBendingFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) + return + case ElementType.TETRAHEDRA: + node.addObject("TetrahedronFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) + return + case ElementType.HEXAHEDRA: + node.addObject("HexahedronFEMForceField", name="constitutiveLaw", youngModulus=youngModulus, poissonRatio=poissonRatio, method=method, **kwargs) + return + case _: + print('Linear elasticity is only available for topology of type EDGES, TRIANGLES, QUADS, TETRAHEDRON, HEXAHEDRON') + return \ No newline at end of file diff --git a/splib/mechanics/mass.py b/splib/mechanics/mass.py new file mode 100644 index 000000000..883b6d698 --- /dev/null +++ b/splib/mechanics/mass.py @@ -0,0 +1,21 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE, isDefault +from splib.core.enum_types import ElementType + +# TODO : use the massDensity ONLY and deduce totalMass if necessary from it + volume +@ReusableMethod +def addMass(node, elementType:ElementType, totalMass=DEFAULT_VALUE, massDensity=DEFAULT_VALUE, lumping=DEFAULT_VALUE, topology=DEFAULT_VALUE, **kwargs): + if (not isDefault(totalMass)) and (not isDefault(massDensity)) : + print("[warning] You defined the totalMass and the massDensity in the same time, only taking massDensity into account") + del kwargs["massDensity"] + + if(elementType is not None and elementType !=ElementType.POINTS and elementType !=ElementType.EDGES): + node.addObject("MeshMatrixMass",name="mass", totalMass=totalMass, massDensity=massDensity, lumping=lumping, topology=topology, **kwargs) + else: + if (not isDefault(massDensity)) : + print("[warning] mass density can only be used on a surface or volumetric topology. Please use totalMass instead") + if (not isDefault(lumping)) : + print("[warning] lumping can only be set for surface or volumetric topology") + + node.addObject("UniformMass",name="mass", totalMass=totalMass, topology=topology,**kwargs) + diff --git a/splib/simulation/__init__.py b/splib/simulation/__init__.py new file mode 100644 index 000000000..d59a665ef --- /dev/null +++ b/splib/simulation/__init__.py @@ -0,0 +1 @@ +__all__ = ["headers","linear_solvers","ode_solvers"] diff --git a/splib/simulation/headers.py b/splib/simulation/headers.py new file mode 100644 index 000000000..d3c61a21b --- /dev/null +++ b/splib/simulation/headers.py @@ -0,0 +1,128 @@ +from splib.core.node_wrapper import ReusableMethod +from enum import Enum + +from splib.core.utils import DEFAULT_VALUE + + +class CollisionType(Enum): + NONE = 1 + PENALITY = 2 + LAGRANGIAN = 3 + + +@ReusableMethod +def setupDefaultHeader(node, displayFlags = "showVisualModels", backgroundColor=[1,1,1,1], parallelComputing=False,**kwargs): + + node.addObject('VisualStyle', displayFlags=displayFlags) + node.addObject('BackgroundSetting', color=backgroundColor) + + node.addObject("RequiredPlugin", name="requiredPlugins", pluginName=['Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual', + ], + **kwargs) + node.addObject('DefaultAnimationLoop',name="animation", parallelODESolving=parallelComputing, **kwargs) + + return node + + +@ReusableMethod +def setupPenalityCollisionHeader(node, displayFlags = "showVisualModels",backgroundColor=[1,1,1,1], stick=False, parallelComputing=False, alarmDistance=DEFAULT_VALUE, contactDistance=DEFAULT_VALUE, **kwargs): + node.addObject('VisualStyle', displayFlags=displayFlags) + node.addObject('BackgroundSetting', color=backgroundColor) + + node.addObject("RequiredPlugin", name="requiredPlugins", pluginName=['Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual', + ], + **kwargs) + + parallelPrefix = "" + if(parallelComputing): + parallelPrefix="Parallel" + + node.addObject('DefaultAnimationLoop',name="animation", **kwargs) + node.addObject('CollisionPipeline', name="collisionPipeline", **kwargs) + node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase", **kwargs) + node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase", **kwargs) + + if(stick): + node.addObject('CollisionResponse',name="ContactManager", response="BarycentricStickContact",**kwargs) + else: + node.addObject('CollisionResponse',name="ContactManager", response="BarycentricPenalityContact",**kwargs) + node.addObject('LocalMinDistance' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs) + + return node + + +@ReusableMethod +def setupLagrangianCollision(node, displayFlags = "showVisualModels",backgroundColor=[1,1,1,1], parallelComputing=False, stick=False, alarmDistance=DEFAULT_VALUE, contactDistance=DEFAULT_VALUE, frictionCoef=0.0, tolerance=0.0, maxIterations=100, **kwargs): + node.addObject('VisualStyle', displayFlags=displayFlags) + node.addObject('BackgroundSetting', color=backgroundColor) + + node.addObject("RequiredPlugin", name="requiredPlugins", pluginName=['Sofa.Component.Constraint.Lagrangian', + 'Sofa.Component.Constraint.Projective', + 'Sofa.Component.Engine.Select', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.Mass', + 'Sofa.Component.ODESolver.Backward', + 'Sofa.Component.SolidMechanics.FEM.Elastic', + 'Sofa.Component.StateContainer', + 'Sofa.Component.Topology.Container.Grid', + 'Sofa.Component.IO.Mesh', + 'Sofa.Component.LinearSolver.Direct', + 'Sofa.Component.ODESolver.Forward', + 'Sofa.Component.Topology.Container.Dynamic', + 'Sofa.Component.Visual', + ], + **kwargs) + + + node.addObject('FreeMotionAnimationLoop',name="animation", + parallelCollisionDetectionAndFreeMotion=parallelComputing, + parallelODESolving=parallelComputing, + **kwargs) + + parallelPrefix = "" + if(parallelComputing): + parallelPrefix="Parallel" + + node.addObject('CollisionPipeline', name="collisionPipeline", + **kwargs) + + node.addObject(parallelPrefix+'BruteForceBroadPhase', name="broadPhase", + **kwargs) + + node.addObject(parallelPrefix+'BVHNarrowPhase', name="narrowPhase", + **kwargs) + + if(stick): + node.addObject('CollisionResponse',name="ContactManager", response="StickContactConstraint", responseParams="tol="+str(tolerance),**kwargs) + else: + node.addObject('CollisionResponse',name="ContactManager", response="FrictionContactConstraint", responseParams="mu="+str(frictionCoef),**kwargs) + + node.addObject('NewProximityIntersection' ,name="Distance", alarmDistance=alarmDistance, contactDistance=contactDistance, **kwargs) + node.addObject('BlockGaussSeidelConstraintSolver',name="ConstraintSolver", tolerance=tolerance, maxIterations=maxIterations, multithreading=parallelComputing,**kwargs) + node.addObject("ConstraintAttachButtonSetting") + + return node diff --git a/splib/simulation/linear_solvers.py b/splib/simulation/linear_solvers.py new file mode 100644 index 000000000..b6b90f027 --- /dev/null +++ b/splib/simulation/linear_solvers.py @@ -0,0 +1,31 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import * + +@ReusableMethod +def addLinearSolver(node,iterative=False,iterations=DEFAULT_VALUE,tolerance=DEFAULT_VALUE,threshold=DEFAULT_VALUE,template=DEFAULT_VALUE,constantSparsity=False,parallelInverseProduct=DEFAULT_VALUE,**kwargs): + containerParams = getParameterSet("LinearSolver",kwargs) + if iterative: + if not isDefault(iterations): + containerParams["iterations"] = iterations + if not isDefault(tolerance): + containerParams["tolerance"] = tolerance + if not isDefault(threshold): + containerParams["threshold"] = threshold + else: + if isDefault(template) and not("template" in containerParams): + containerParams["template"] = 'CompressedRowSparseMatrix' + elif not isDefault(template) : + containerParams["template"] = template + kwargs["LinearSolver"] = containerParams + + if(constantSparsity): + node.addObject("ConstantSparsityPatternSystem",name='LinearSystem',**kwargs) + kwargs["LinearSolver"]["template"] = 'CompressedRowSparseMatrix' + kwargs["LinearSolver"]["linearSystem"]="@LinearSystem" + + + + if iterative: + node.addObject('CGLinearSolver', name='LinearSolver', parallelInverseProduct=parallelInverseProduct, **kwargs) + else: + node.addObject('SparseLDLSolver', name='LinearSolver', parallelInverseProduct=parallelInverseProduct, **kwargs) \ No newline at end of file diff --git a/splib/simulation/ode_solvers.py b/splib/simulation/ode_solvers.py new file mode 100644 index 000000000..eed57bab8 --- /dev/null +++ b/splib/simulation/ode_solvers.py @@ -0,0 +1,14 @@ +from splib.core.node_wrapper import ReusableMethod + +@ReusableMethod +def addImplicitODE(node,static=False,**kwargs): + if( not(static) ): + node.addObject("EulerImplicitSolver",name="ODESolver",**kwargs) + else: + node.addObject("StaticSolver",name="ODESolver",**kwargs) + +@ReusableMethod +def addExplicitODE(node,**kwargs): + node.addObject("EulerExplicitSolver",name="ODESolver",**kwargs) + + diff --git a/splib/topology/__init__.py b/splib/topology/__init__.py new file mode 100644 index 000000000..23469f77d --- /dev/null +++ b/splib/topology/__init__.py @@ -0,0 +1 @@ +__all__ = ["static","dynamic","loader"] \ No newline at end of file diff --git a/splib/topology/dynamic.py b/splib/topology/dynamic.py new file mode 100644 index 000000000..1f18be29d --- /dev/null +++ b/splib/topology/dynamic.py @@ -0,0 +1,67 @@ +from splib.core.node_wrapper import ReusableMethod +from enum import Enum +from splib.core.utils import MapKeywordArg, DEFAULT_VALUE +from splib.core.enum_types import ElementType + + + +@ReusableMethod +def addPointTopology(node,position=DEFAULT_VALUE,source=DEFAULT_VALUE, **kwargs): + node.addObject("PointSetTopologyModifier", name="modifier", **kwargs) + node.addObject("PointSetTopologyContainer", name="container", src=source, position=position, **kwargs) + # node.addObject("PointSetGeometryAlgorithms", name="algorithms", **kwargs) + +@ReusableMethod +def addEdgeTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("EdgeSetTopologyModifier", name="modifier",**kwargs) + node.addObject("EdgeSetTopologyContainer", name="container", src=source, position=position, edges=edges, **kwargs) + # node.addObject("EdgeSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addTriangleTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,triangles=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("TriangleSetTopologyModifier", name="modifier",**kwargs) + node.addObject("TriangleSetTopologyContainer", name="container", src=source, position=position, edges=edges, triangles=triangles, **kwargs) + # node.addObject("TriangleSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addQuadTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,quads=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("QuadSetTopologyModifier", name="modifier",**kwargs) + node.addObject("QuadSetTopologyContainer", name="container", src=source, position=position, edges=edges, quads=quads, **kwargs) + # node.addObject("QuadSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addTetrahedronTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,triangles=DEFAULT_VALUE,tetrahedra=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("TetrahedronSetTopologyModifier", name="modifier",**kwargs) + node.addObject("TetrahedronSetTopologyContainer", name="container", src=source, position=position, edges=edges, triangles=triangles, tetrahedra=tetrahedra, **kwargs) + # node.addObject("TetrahedronSetGeometryAlgorithms", name="algorithms",**kwargs) + +@ReusableMethod +def addHexahedronTopology(node,position=DEFAULT_VALUE,edges=DEFAULT_VALUE,quads=DEFAULT_VALUE,hexahedra=DEFAULT_VALUE,source=DEFAULT_VALUE,**kwargs): + node.addObject("HexahedronSetTopologyModifier", name="modifier",**kwargs) + node.addObject("HexahedronSetTopologyContainer", name="container", src=source, position=position, edges=edges, quads=quads, hexahedra=hexahedra, **kwargs) + # node.addObject("HexahedronSetGeometryAlgorithms", name="algorithms",**kwargs) + +def addDynamicTopology(node, elementType:ElementType, **kwargs): + match elementType: + case ElementType.POINTS: + addPointTopology(node,**kwargs) + return + case ElementType.EDGES: + addEdgeTopology(node,**kwargs) + return + case ElementType.TRIANGLES: + addTriangleTopology(node,**kwargs) + return + case ElementType.QUADS: + addQuadTopology(node,**kwargs) + return + case ElementType.TETRAHEDRA: + addTetrahedronTopology(node,**kwargs) + return + case ElementType.HEXAHEDRA: + addHexahedronTopology(node,**kwargs) + return + case _: + print('Topology type should be one of the following : "ElementType.POINTS, ElementType.EDGES, ElementType.TRIANGLES, ElementType.QUADS, ElementType.TETRAHEDRA, ElementType.HEXAHEDRA" ') + return + \ No newline at end of file diff --git a/splib/topology/loader.py b/splib/topology/loader.py new file mode 100644 index 000000000..c4f54b01f --- /dev/null +++ b/splib/topology/loader.py @@ -0,0 +1,22 @@ +from splib.core.node_wrapper import ReusableMethod + +@ReusableMethod +def loadMesh(node,filename,**kwargs): + splitedName = filename.split('.') + if len(splitedName) == 1: + print('[Error] : A file name without extension was provided.') + return + + if splitedName[-1] in ['vtk', 'obj', 'stl', 'msh', 'sph']: + if splitedName[-1] == "msh": + return node.addObject("MeshGmshLoader", name="loader",filename=filename, **kwargs) + elif splitedName[-1] == "sph": + return node.addObject("SphereLoader", name="loader",filename=filename, **kwargs) + else: + return node.addObject("Mesh"+splitedName[-1].upper()+"Loader", name="loader", filename=filename, **kwargs) + else: + print('[Error] : File extension ' + splitedName[-1] + ' not recognised.') + + + + diff --git a/splib/topology/static.py b/splib/topology/static.py new file mode 100644 index 000000000..591460425 --- /dev/null +++ b/splib/topology/static.py @@ -0,0 +1,7 @@ +from splib.core.node_wrapper import ReusableMethod +from splib.core.utils import DEFAULT_VALUE + +@ReusableMethod +def addStaticTopology(node, source=DEFAULT_VALUE, **kwargs): + node.addObject("MeshTopology", name="container", src=source, **kwargs) + diff --git a/stlib/README.md b/stlib/README.md new file mode 100644 index 000000000..c4e7dd901 --- /dev/null +++ b/stlib/README.md @@ -0,0 +1,52 @@ +# STLIB + +## Terminology + +| Term | Description | +| -------------- | -------------------------------------------------------------- | +| Component* | Element of the scene hierarchy implementing a given behavior | +| ~~Object~~ | A deprecated synonym of a Component | +| Node* | Element of the scene hierarchy holding other Nodes (often refered as childs) or Components | +| Data* | Attribute of a Component or a Node | +| Prefab | A Node assembling of Components and Nodes (a "fragment" of a scene) | +| Geometry | A Prefab that describes shapes with their topologies (i.e a shapes with their space descritization and their associated connectivity) | +| Entity | A physical Prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry but includes neither a linear solver nor an integration scheme.| +| Parameters | Every Prefab has a set of parameters. These parameters can contain data, links, callable or being composed of other parameters. Some of them can be optional. ~~Must inherit from `stlib.core.baseParameter.BaseParameter` and have a `@dataclasses.dataclass` decorator~~. Must have a `@stlib.parameters` decorator. | + +\*Defined in SOFA documentation [here](https://www.sofa-framework.org/doc/using-sofa/terminology). + + +## Concepts & Structure + +This library is structured to provide a set of _Prefabs_ that can be used to build complex simulations in SOFA. +Prefabs are reusable fragments of a scene that can be specified through Parameters. +We introduce two different concepts, Prefab and Parameters: +- Prefabs defining the logic of instantiation +- Parameters providing the information (e.g data, links, callable) needed by a Prefab for its own instantiation + +We introduce two types of Prefabs: +- __Geometry__: A prefab that describes shapes with their topologies (i.e a shape with its space discretization and its associated connectivity). +- __Entity__: A physical prefab that represents real-world properties and behaviors used in a simulation. An entity should always have a geometry but includes neither a linear solver nor an integration scheme. + + +## Usage + +STLIB has been designed to suit the following levels of use: + +- __Beginners__: + - Create simple simulations using predefined Prefabs. + - Use the provided Prefabs and Parameters without needing to understand the underlying implementation. +- __Intermediate users__: + - Create more complex simulations by combining existing Prefabs. + - Redefine Parameters for their own usage. +- __Advanced users__: + - Create their own Prefabs from scratch or by extending the provided ones. + - Enrich the library with new Prefabs and Parameters. + +## Available Parameters + +Each Prefab comes with a set of Parameters, these have been selected when the following criteria are met: +- Data which corresponds to an intraseque property of the Prefab, required for its instantiation +- Data which does not have a default value +- Data which cannot be inferred +- Data which are commun to all possible usage of the Prefab diff --git a/stlib/__init__.py b/stlib/__init__.py new file mode 100644 index 000000000..3f5594daa --- /dev/null +++ b/stlib/__init__.py @@ -0,0 +1,105 @@ +__all__ = ["core","entities","geometries","materials","modifiers","collision","visual"] + +import Sofa.Core +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseEntity import BaseEntity +from stlib.core.baseEntityModifier import BaseEntityModifier + + +def __findName(cname, names): + """Compute a working unique name in the node + """ + rname = cname + for i in range(0, len(names)): + if rname not in names: + return rname + rname = cname + str(i+1) + return rname + + +def __checkName(context : Sofa.Core.Node, name): + """Check if the name already exists, if this happens, create a new one. + """ + if name in context.children or name in context.objects: + names = {node.name.value for node in context.children} + names = names.union({object.name.value for object in context.objects}) + name = __findName(name, names) + return name + + +def __processParameters(self : Sofa.Core.Node, typeName, **kwargs): + """Check if a name is provided, if not, use the one of the class + """ + params = kwargs.copy() + if isinstance(typeName, type) and issubclass(typeName, BasePrefab): #Only for prefabs + if len(params.keys()) > 1 or (len(params.keys()) == 1 and "parameters" not in params): + raise RuntimeError("Invalid argument, a prefab takes only the \"parameters\" kwargs as input") + + elif "name" not in params : #This doesn't apply to prefab + if isinstance(typeName, str): + params["name"] = typeName + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Node): + params["name"] = typeName.__name__ + elif isinstance(typeName, Sofa.Core.Node): + params["name"] = "Node" + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): + params["name"] = typeName.name.value + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.ObjectDeclaration): + params["name"] = typeName.__name__ + else: + raise RuntimeError("Invalid argument ", typeName) + + if isinstance(typeName, type) and issubclass(typeName, BasePrefab) and len(params.keys()) == 1: + params["parameters"].name = __checkName(self, params["parameters"].name) + else: + params["name"] = __checkName(self, params["name"]) + + return params + + +def __create(self: Sofa.Core.Node, typeName, **kwargs): + + params = __processParameters(self, typeName, **kwargs) + + if isinstance(typeName, type) and issubclass(typeName, BaseEntityModifier): + node = typeName(**params) + node.creator = self + return node + if isinstance(typeName, type) and issubclass(typeName, BasePrefab): + return typeName(**params) + elif isinstance(typeName, Sofa.Core.Node): + return typeName(**params) + + return + + +def __genericAdd(self: Sofa.Core.Node, typeName, **kwargs): + + # Dispatch the creation to either addObject or addChild + if isinstance(typeName, type) and issubclass(typeName, BasePrefab): + params = __processParameters(self, typeName, **kwargs) + pref = self.addChild(typeName(**params)) + pref.init() + elif isinstance(typeName, Sofa.Core.Node) and issubclass(typeName.__class__, BaseEntity): + pref = self.addChild(typeName) + pref.init() + elif isinstance(typeName, Sofa.Core.Node): + pref = self.addChild(typeName) + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.Object): + params = __processParameters(self, typeName, **kwargs) + pref = self.addObject(typeName(**params)) + elif isinstance(typeName, type) and issubclass(typeName, Sofa.Core.ObjectDeclaration): + params = __processParameters(self, typeName, **kwargs) + pref = self.addObject(typeName.__name__, **params) + elif isinstance(typeName, str): + params = __processParameters(self, typeName, **kwargs) + pref = self.addObject(typeName, **params) + else: + raise RuntimeError("Invalid argument", typeName) + + return pref + + +# Inject the method so it become available as if it was part of Sofa.Core.Node +Sofa.Core.Node.add = __genericAdd +Sofa.Core.Node.create = __create diff --git a/stlib/collision.py b/stlib/collision.py new file mode 100644 index 000000000..2c674137c --- /dev/null +++ b/stlib/collision.py @@ -0,0 +1,60 @@ +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses +from stlib.geometries import Geometry, GeometryParameters +from stlib.geometries.file import FileParameters +from splib.core.enum_types import CollisionPrimitive +from splib.core.utils import DEFAULT_VALUE +from splib.mechanics.collision_model import addCollisionModels +from splib.core.utils import setRequiresCollisionPipeline + +@dataclasses.dataclass +class CollisionParameters(BaseParameters): + name : str = "Collision" + + primitives : list[CollisionPrimitive] = dataclasses.field(default_factory = lambda :[CollisionPrimitive.TRIANGLES]) + + selfCollision : Optional[bool] = DEFAULT_VALUE + bothSide : Optional[bool] = DEFAULT_VALUE + group : Optional[int] = DEFAULT_VALUE + contactDistance : Optional[float] = DEFAULT_VALUE + + geometry : GeometryParameters = GeometryParameters() + + +class Collision(BasePrefab): + def __init__(self, parameters: CollisionParameters): + BasePrefab.__init__(self, parameters) + + def init(self): + + geom = self.add(Geometry, parameters = self.parameters.geometry) + + setRequiresCollisionPipeline(rootnode=self.getRoot()) + self.addObject("MechanicalObject", template="Vec3", position=f"@{self.parameters.geometry.name}/container.position") + for primitive in self.parameters.primitives: + addCollisionModels(self, primitive, + topology=f"@{self.parameters.geometry.name}/container", + selfCollision=self.parameters.selfCollision, + group=self.parameters.group, + **self.parameters.kwargs) + + +def createScene(root): + + root.add("VisualStyle", displayFlags="showCollisionModels") + + # Create a visual from a mesh file + parameters = CollisionParameters() + parameters.group = 1 + parameters.geometry = FileParameters(filename="mesh/cube.obj") + # Expert parameters + # parameters.kwargs = { + # "TriangleCollisionModel":{"contactStiffness": 100.0, "contactFriction": 0.5} + # } + collision = root.add(Collision, parameters=parameters) + + # OR set the parameters post creation + # collision.TriangleCollisionModel.contactStiffness = 100.0 + # collision.TriangleCollisionModel.contactFriction = 0.5 + # collision.TriangleCollisionModel.set(contactStiffness=100.0, contactFriction=0.5) # we have information of what is possible + # collision.TriangleCollisionModel.set({"contactStiffness": 100.0, "contactFriction": 0.5}) # we can do n'importe quoi diff --git a/stlib/core/baseEntity.py b/stlib/core/baseEntity.py new file mode 100644 index 000000000..f5cf7f00b --- /dev/null +++ b/stlib/core/baseEntity.py @@ -0,0 +1,9 @@ +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters + +class BaseEntity(BasePrefab): + + def __init__(self, parameters: BaseParameters): + BasePrefab.__init__(self, parameters=parameters) + + diff --git a/stlib/core/baseEntityModifier.py b/stlib/core/baseEntityModifier.py new file mode 100644 index 000000000..a72a787bd --- /dev/null +++ b/stlib/core/baseEntityModifier.py @@ -0,0 +1,24 @@ +from stlib.core.basePrefab import BasePrefab +import Sofa.Core + + +class BaseEntityModifier(BasePrefab): + """ + An EntityModifier is a Prefab that modifies a set of Entities + """ + + nodeName = "Modifiers" + creator : Sofa.Core.Node = None + + def __init__(self, parameters): + BasePrefab.__init__(self, parameters) + + def apply(self, **kwargs): + if not self.creator.getChild(self.nodeName): + self.creator.modifiers = self.creator.add(Sofa.Core.Node(self.nodeName)) + + self.creator.modifiers.add(self) + self._apply(**kwargs) + + def _apply(self, **kwargs): + raise NotImplemented("To be overridden by child class") \ No newline at end of file diff --git a/stlib/core/baseGeometry.py b/stlib/core/baseGeometry.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/core/baseParameters.py b/stlib/core/baseParameters.py new file mode 100644 index 000000000..be7a633b8 --- /dev/null +++ b/stlib/core/baseParameters.py @@ -0,0 +1,13 @@ +import dataclasses +from splib.core.utils import DEFAULT_VALUE + +import dataclasses +from typing import Callable, Optional, Any + +@dataclasses.dataclass +class BaseParameters(object): + name : str = "Object" + kwargs : dict = dataclasses.field(default_factory=dict) + + def toDict(self): + return dataclasses.asdict(self) diff --git a/stlib/core/basePrefab.py b/stlib/core/basePrefab.py new file mode 100644 index 000000000..0430175a8 --- /dev/null +++ b/stlib/core/basePrefab.py @@ -0,0 +1,20 @@ +import Sofa +import Sofa.Core +from stlib.core.basePrefabParameters import BasePrefabParameters + +class BasePrefab(Sofa.Core.Node): + """ + A Prefab is a Sofa.Core.Node that assembles a set of components and nodes + """ + + parameters : BasePrefabParameters + + def __init__(self, parameters: BasePrefabParameters): + Sofa.Core.Node.__init__(self, name=parameters.name) + self.parameters = parameters + + def init(self): + raise NotImplemented("To be overridden by child class") + + + diff --git a/stlib/core/basePrefabParameters.py b/stlib/core/basePrefabParameters.py new file mode 100644 index 000000000..d6a69dee9 --- /dev/null +++ b/stlib/core/basePrefabParameters.py @@ -0,0 +1,15 @@ +import dataclasses + +@dataclasses.dataclass +class BasePrefabParameters(object): + name : str = "object" + kwargs : dict = dataclasses.field(default_factory=dict) + + # Transformation information + # TODO: these data are going to be added in Node in SOFA (C++ implementation) + translation : list[float] = dataclasses.field(default_factory = lambda : [0., 0., 0.]) + rotation : list[float] = dataclasses.field(default_factory = lambda : [0., 0., 0.]) + scale : list[float] = dataclasses.field(default_factory = lambda : [1., 1., 1.]) + + def toDict(self): + return dataclasses.asdict(self) diff --git a/stlib/entities/__entity__.py b/stlib/entities/__entity__.py new file mode 100644 index 000000000..39771bf2a --- /dev/null +++ b/stlib/entities/__entity__.py @@ -0,0 +1,83 @@ +from stlib.core.baseParameters import BaseParameters +from splib.core.enum_types import StateType +from stlib.core.basePrefab import BasePrefab + +from stlib.geometries import Geometry +from stlib.geometries import GeometryParameters, InternalDataProvider +from stlib.geometries.file import FileParameters +from stlib.materials.rigid import RigidParameters +from stlib.materials import Material, MaterialParameters +from stlib.visual import VisualParameters, Visual +from stlib.collision import CollisionParameters, Collision +from splib.core.enum_types import ElementType + +import dataclasses +from typing import Optional + + +@dataclasses.dataclass +class EntityParameters(BaseParameters): + name : str = "Entity" + + stateType : StateType = StateType.RIGID + + geometry : GeometryParameters = GeometryParameters(elementType = ElementType.POINTS, data = InternalDataProvider(position = [[0., 0., 0.]])) + material : MaterialParameters = RigidParameters() + visual : Optional[VisualParameters] = VisualParameters(geometry = FileParameters(filename="mesh/cube.obj")) + collision : Optional[CollisionParameters] = None + + +class Entity(BasePrefab): + + material : Material + visual : Visual + collision : Collision + geometry : Geometry + + parameters : EntityParameters + + + def __init__(self, parameters : EntityParameters): + BasePrefab.__init__(self, parameters) + + + def init(self): + self.geometry = self.add(Geometry, parameters=self.parameters.geometry) + self.checkMaterialCompatibility + + self.material = self.add(Material, parameters=self.parameters.material) + self.material.getMechanicalState().position.parent = self.geometry.container.position.linkpath + + if self.parameters.collision is not None: + self.collision = self.add(Collision, parameters=self.parameters.collision) + self.addMapping(self.collision) + + if self.parameters.visual is not None: + self.visual = self.add(Visual, parameters=self.parameters.visual) + self.addMapping(self.visual) + + + def checkMaterialCompatibility(self): + if self.parameters.material.stateType != self.parameters.stateType: + print("WARNING: imcompatibility between templates of both the entity and the material") + self.parameters.material.stateType = self.parameters.stateType + + + def addMapping(self, destinationPrefab: BasePrefab): + + template = f'{self.parameters.stateType},Vec3' # TODO: check that it is always true + + #TODO: all paths are expecting Geometry to be called Geomtry and so on. + # We need to robustify this by using the name parameter somehow + if( self.parameters.stateType == StateType.VEC3): + destinationPrefab.addObject("BarycentricMapping", + output=destinationPrefab.linkpath, + output_topology=destinationPrefab.Geometry.container.linkpath, + input=self.material.linkpath, + input_topology=self.geometry.container.linkpath, + template=template) + else: + destinationPrefab.addObject("RigidMapping", + output=destinationPrefab.linkpath, + input=self.material.linkpath, + template=template) diff --git a/stlib/entities/__init__.py b/stlib/entities/__init__.py new file mode 100644 index 000000000..bfad7c6c5 --- /dev/null +++ b/stlib/entities/__init__.py @@ -0,0 +1 @@ +from .__entity__ import * \ No newline at end of file diff --git a/stlib/geometries/__geometry__.py b/stlib/geometries/__geometry__.py new file mode 100644 index 000000000..f9abb1649 --- /dev/null +++ b/stlib/geometries/__geometry__.py @@ -0,0 +1,75 @@ +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses, Any +from splib.topology.dynamic import addDynamicTopology +from splib.topology.static import addStaticTopology +from splib.core.enum_types import ElementType +from splib.core.utils import DEFAULT_VALUE +from Sofa.Core import Object + +import numpy as np + + +class Geometry(BasePrefab):... + +@dataclasses.dataclass +class InternalDataProvider: + position : Any = None + + edges : Any = DEFAULT_VALUE + triangles : Any = DEFAULT_VALUE + quads : Any = DEFAULT_VALUE + tetrahedra : Any = DEFAULT_VALUE + hexahedra : Any = DEFAULT_VALUE + + def generateAttribute(self, parent : Geometry): + pass + + +@dataclasses.dataclass +class GeometryParameters(BaseParameters): + name : str = "Geometry" + + elementType : Optional[ElementType] = None # Type of the highest degree element + data : Optional[InternalDataProvider] = None + + dynamicTopology : bool = False + + +class Geometry(BasePrefab): + + parameters : GeometryParameters + + def __init__(self, parameters: GeometryParameters): + BasePrefab.__init__(self, parameters) + + def init(self): + + # Generate attribute (positions, edges, triangles, quads, tetrahedra, hexahedra) from the internal data provider + if isinstance(self.parameters.data, InternalDataProvider) : + self.parameters.data.generateAttribute(self) + + if self.parameters.dynamicTopology : + if self.parameters.elementType is not None : + addDynamicTopology(self, + elementType=self.parameters.elementType, + container = { + "position": self.parameters.data.position, + "edges": self.parameters.data.edges, + "triangles": self.parameters.data.triangles, + "quads": self.parameters.data.quads, + "tetrahedra": self.parameters.data.tetrahedra, + "hexahedra": self.parameters.data.hexahedra + }) + else: + raise ValueError + else: + addStaticTopology(self, + container = + { + "position": self.parameters.data.position, + "edges": self.parameters.data.edges, + "triangles": self.parameters.data.triangles, + "quads": self.parameters.data.quads, + "tetrahedra": self.parameters.data.tetrahedra, + "hexahedra": self.parameters.data.hexahedra + }) \ No newline at end of file diff --git a/stlib/geometries/__init__.py b/stlib/geometries/__init__.py new file mode 100644 index 000000000..34a466c77 --- /dev/null +++ b/stlib/geometries/__init__.py @@ -0,0 +1 @@ +from .__geometry__ import * \ No newline at end of file diff --git a/stlib/geometries/cube.py b/stlib/geometries/cube.py new file mode 100644 index 000000000..3444692ce --- /dev/null +++ b/stlib/geometries/cube.py @@ -0,0 +1,9 @@ +from stlib.geometries import GeometryParameters +from stlib.geometries.file import FileParameters +import dataclasses + + +@dataclasses.dataclass +class CubeParameters(FileParameters): + + filename : str = "mesh/cube.obj" diff --git a/stlib/geometries/extract.py b/stlib/geometries/extract.py new file mode 100644 index 000000000..5ee1b3762 --- /dev/null +++ b/stlib/geometries/extract.py @@ -0,0 +1,71 @@ +from stlib.geometries import GeometryParameters, InternalDataProvider, Geometry +from stlib.core.baseParameters import dataclasses +from splib.topology.dynamic import addDynamicTopology +from splib.topology.loader import loadMesh +from splib.core.enum_types import ElementType + +import Sofa +from Sofa.Core import Node + + +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 generateAttribute(self, parent : Geometry): + node = parent.addChild("ExtractedGeometry") + + #TODO: Specify somewhere in the doc that this should only be used for mapped topologies that extract parent topology surface + # 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): + GeometryParameters.__init__(self, + data = ExtractInternalDataProvider(destinationType = destinationType, + sourceType = sourceParameters.elementType, + sourceName = sourceParameters.name), + dynamicTopology = dynamicTopology, + elementType = destinationType) + diff --git a/stlib/geometries/file.py b/stlib/geometries/file.py new file mode 100644 index 000000000..87e67e5d7 --- /dev/null +++ b/stlib/geometries/file.py @@ -0,0 +1,41 @@ +from stlib.geometries import GeometryParameters, InternalDataProvider, Geometry +from stlib.core.baseParameters import dataclasses +from splib.topology.loader import loadMesh +from splib.core.enum_types import ElementType + +from Sofa.Core import Node + +@dataclasses.dataclass +class FileInternalDataProvider(InternalDataProvider): + filename : str = "mesh/cube.obj" + + def __post_init__(self, **kwargs): + InternalDataProvider.__init__(self,**kwargs) + + def generateAttribute(self, parent : Geometry): + loadMesh(parent, self.filename) + + if hasattr(parent.loader, 'position'): + self.position = str(parent.loader.position.linkpath) + if hasattr(parent.loader, 'edges'): + self.edges = str(parent.loader.edges.linkpath) + if hasattr(parent.loader, 'triangles'): + self.triangles = str(parent.loader.triangles.linkpath) + if hasattr(parent.loader, 'quads'): + self.quads = str(parent.loader.quads.linkpath) + if hasattr(parent.loader, 'hexahedra'): + self.hexahedra = str(parent.loader.hexahedra.linkpath) + if hasattr(parent.loader, 'tetrahedra'): + self.tetrahedra = str(parent.loader.tetrahedra.linkpath) + + +@dataclasses.dataclass +class FileParameters(GeometryParameters): + + filename : str = "mesh/cube.obj" + dynamicTopology : bool = False + elementType : ElementType = None + + def __post_init__(self): + self.data = FileInternalDataProvider(filename=self.filename) + diff --git a/stlib/geometries/plane.py b/stlib/geometries/plane.py new file mode 100644 index 000000000..2c3c637a4 --- /dev/null +++ b/stlib/geometries/plane.py @@ -0,0 +1,45 @@ +from stlib.geometries import GeometryParameters, InternalDataProvider, Geometry +import dataclasses +import numpy as np + +@dataclasses.dataclass +class PlaneDataProvider(InternalDataProvider): + center : np.ndarray[float] = dataclasses.field(default_factory = lambda : np.array([0,0,0])) + normal : np.ndarray[float] = dataclasses.field(default_factory = lambda : np.array([0,0,1])) + lengthNormal : np.ndarray[float] = dataclasses.field(default_factory = lambda : np.array([1,0,0])) + lengthNbEdge : int = 1 + widthNbEdge : int = 1 + lengthSize : float = 1.0 + widthSize : float = 1.0 + + def __post_init__(self, **kwargs): + InternalDataProvider.__init__(self,**kwargs) + + def generateAttribute(self, parent : Geometry): + + lengthEdgeSize = self.lengthSize / self.lengthNbEdge + widthEdgeSize = self.widthSize / self.widthNbEdge + + self.widthNormal = np.cross(self.normal,self.lengthNormal) + bottomLeftCorner = self.center - self.lengthNormal * self.lengthNbEdge * lengthEdgeSize / 2 - self.widthNormal * self.widthNbEdge * widthEdgeSize / 2 + + self.position = np.array([[ bottomLeftCorner + j * self.widthNormal * widthEdgeSize + i * self.lengthNormal * lengthEdgeSize for j in range(self.widthNbEdge + 1) ] for i in range(self.lengthNbEdge + 1)]) + + self.triangles = np.empty((2 * self.widthNbEdge * self.lengthNbEdge, 3), dtype = int) + for i in range(self.lengthNbEdge): + for j in range(self.widthNbEdge): + self.triangles[i*self.widthNbEdge*2 + j * 2 , 0] = j + i * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 , 1] = j + (i+1) * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 , 2] = j + 1 + i * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 + 1, 0] = j + 1 + i * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 + 1, 1] = j + (i+1) * (self.widthNbEdge + 1) + self.triangles[i*self.widthNbEdge*2 + j * 2 + 1, 2] = j + 1 + (i+1) * (self.widthNbEdge + 1) + + + + +class PlaneParameters(GeometryParameters): + + def __init__(self, center, normal, lengthNormal, lengthNbEdge, widthNbEdge, lengthSize, widthSize, dynamicTopology = False): + GeometryParameters.__init__(self, data = PlaneDataProvider(center=center, normal=normal, lengthNormal=lengthNormal, lengthNbEdge=lengthNbEdge, widthNbEdge=widthNbEdge, lengthSize=lengthSize, widthSize=widthSize), + dynamicTopology = dynamicTopology) diff --git a/stlib/geometries/sphere.py b/stlib/geometries/sphere.py new file mode 100644 index 000000000..2cb64f6ee --- /dev/null +++ b/stlib/geometries/sphere.py @@ -0,0 +1,14 @@ +from stlib.geometries import GeometryParameters + +class SphereParameters(GeometryParameters): + def __init__(self, center, radius, pointPerRad, dynamicTopology = False): + + customGeom = SphereParameters.createData(center, radius, pointPerRad) + GeometryParameters.__init__(data = customGeom, dynamicTopology = dynamicTopology) + + @staticmethod + def createData(center, radius, pointPerRad) -> GeometryParameters.Data : + data = GeometryParameters.Data() + #Fill data + return data + \ No newline at end of file diff --git a/stlib/materials/__init__.py b/stlib/materials/__init__.py new file mode 100644 index 000000000..6a748749a --- /dev/null +++ b/stlib/materials/__init__.py @@ -0,0 +1 @@ +from .__material__ import * \ No newline at end of file diff --git a/stlib/materials/__material__.py b/stlib/materials/__material__.py new file mode 100644 index 000000000..b1cd9af24 --- /dev/null +++ b/stlib/materials/__material__.py @@ -0,0 +1,30 @@ +from stlib.core.baseParameters import BaseParameters, Callable, Optional, dataclasses +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import StateType + +from stlib.core.basePrefab import BasePrefab +from splib.mechanics.mass import addMass + +@dataclasses.dataclass +class MaterialParameters(BaseParameters): + name : str = "Material" + + massDensity : float = 1e-6 + massLumping : bool = DEFAULT_VALUE + + stateType : StateType = StateType.VEC3 + + addMaterial : Optional[Callable] = lambda node : addMass(node, elementType=None, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping) + + +class Material(BasePrefab): + + parameters : MaterialParameters + + def __init__(self, parameters: MaterialParameters): + BasePrefab.__init__(self, parameters) + + + def init(self): + self.addObject("MechanicalObject", template=str(self.parameters.stateType)) + self.parameters.addMaterial(self) diff --git a/stlib/materials/deformable.py b/stlib/materials/deformable.py new file mode 100644 index 000000000..a24763df5 --- /dev/null +++ b/stlib/materials/deformable.py @@ -0,0 +1,52 @@ +from stlib.materials import MaterialParameters +from splib.core.enum_types import ConstitutiveLaw +from stlib.core.baseParameters import Callable, Optional, dataclasses +from splib.mechanics.linear_elasticity import * +from splib.mechanics.hyperelasticity import * +from splib.mechanics.mass import addMass + + +@dataclasses.dataclass +class DeformableBehaviorParameters(MaterialParameters): + + constitutiveLawType : ConstitutiveLaw = ConstitutiveLaw.ELASTIC + elementType : ElementType = ElementType.TETRAHEDRA + parameters : list[float] = dataclasses.field(default_factory=lambda: [1000, 0.45]) # young modulus, poisson ratio + + def __addDeformableMaterial(node): + + massKwargs = {} + if node.parameters.elementType != ElementType.EDGES: #If we use the MeshMatrixMass, then the mass will need us to specify the mstate to use + massKwargs["geometryState"] = "@States" + + addMass(node, node.parameters.elementType, massDensity=node.parameters.massDensity, lumping=node.parameters.massLumping, topology="@../Geometry/container", mass=massKwargs) + # TODO : change this with inheritance + if(node.parameters.constitutiveLawType == ConstitutiveLaw.HYPERELASTIC): + addHyperelasticity(node, node.parameters.elementType, node.parameters.parameters, topology="@../Geometry/container") + else: + addLinearElasticity(node, node.parameters.elementType, node.parameters.parameters[0], node.parameters.parameters[1], topology="@../Geometry/container") + + addMaterial : Optional[Callable] = __addDeformableMaterial + + +def createScene(root) : + from stlib.entities import Entity, EntityParameters + from stlib.visual import VisualParameters + from stlib.geometries.file import FileParameters + + root.addObject('RequiredPlugin', name='Sofa.Component.Visual') # Needed to use components [VisualStyle] + root.addObject("VisualStyle", displayFlags=["showBehavior"]) + + bunnyParameters = EntityParameters() + bunnyParameters.geometry = FileParameters(filename="mesh/Bunny.vtk") + bunnyParameters.geometry.elementType = ElementType.TETRAHEDRA # TODO: this is required by extract.py. Should it be done automatically in geometry.py ? + bunnyParameters.material = DeformableBehaviorParameters() + bunnyParameters.material.constitutiveLawType = ConstitutiveLaw.ELASTIC + bunnyParameters.material.parameters = [1000, 0.45] + bunnyParameters.visual = VisualParameters() + # bunnyParameters.visual.geometry = ExtractParameters(sourceParameters=bunnyParameters.geometry, + # destinationType=ElementType.TRIANGLES) + bunnyParameters.visual.geometry = FileParameters(filename="mesh/Bunny.stl") + bunnyParameters.visual.color = [1, 1, 1, 0.5] + bunny = root.add(Entity, parameters=bunnyParameters) + # bunny.init() \ No newline at end of file diff --git a/stlib/materials/rigid.py b/stlib/materials/rigid.py new file mode 100644 index 000000000..a4096036f --- /dev/null +++ b/stlib/materials/rigid.py @@ -0,0 +1,11 @@ +from stlib.core.baseParameters import Optional, dataclasses +from stlib.materials import MaterialParameters +from splib.core.utils import DEFAULT_VALUE +from splib.core.enum_types import StateType + + +@dataclasses.dataclass +class RigidParameters(MaterialParameters): + + stateType : StateType = StateType.RIGID + diff --git a/stlib/modifiers/__init__.py b/stlib/modifiers/__init__.py new file mode 100644 index 000000000..186ce5b68 --- /dev/null +++ b/stlib/modifiers/__init__.py @@ -0,0 +1 @@ +from .__modifier__ import * \ No newline at end of file diff --git a/stlib/modifiers/__modifier__.py b/stlib/modifiers/__modifier__.py new file mode 100644 index 000000000..cfec85060 --- /dev/null +++ b/stlib/modifiers/__modifier__.py @@ -0,0 +1,24 @@ +from stlib.core.baseParameters import BaseParameters +from stlib.core.baseEntityModifier import BaseEntityModifier +from stlib.entities import Entity +import dataclasses + + +@dataclasses.dataclass +class ModifierParameters(BaseParameters): + + name : str = "Modifier" + creator : str = None + + def modifier(entity: Entity): + pass + + +class SingleEntityModifier(BaseEntityModifier): + + def __init__(self, parameters: ModifierParameters): + BaseEntityModifier.__init__(self, parameters) + + def _apply(self, entity: Entity): + entity.add(self) + self.parameters.modifier(entity) \ No newline at end of file diff --git a/stlib/modifiers/fixing.py b/stlib/modifiers/fixing.py new file mode 100644 index 000000000..b354c66f0 --- /dev/null +++ b/stlib/modifiers/fixing.py @@ -0,0 +1,13 @@ +from stlib.modifiers import ModifierParameters +from stlib.entities import Entity +from stlib.core.baseParameters import Optional, dataclasses +from splib.core.utils import DEFAULT_VALUE + +@dataclasses.dataclass +class FixingModifierParameters(ModifierParameters): + + name : str = "FixingModifier" + indices : Optional[list[int]] = DEFAULT_VALUE + + def modifier(self, entity: Entity): + entity.material.addObject("FixedProjectiveConstraint", indices=self.indices) \ No newline at end of file diff --git a/stlib/settings/__init__.py b/stlib/settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/stlib/settings/simulation.py b/stlib/settings/simulation.py new file mode 100644 index 000000000..d6c739808 --- /dev/null +++ b/stlib/settings/simulation.py @@ -0,0 +1,31 @@ +import Sofa.Core +from splib.core.utils import REQUIRES_COLLISIONPIPELINE, REQUIRES_LAGRANGIANCONSTRAINTSOLVER + +def addSimulationSettings(rootnode: Sofa.Core.Node): + + rootnode.add('DefaultVisualManagerLoop') + rootnode.add('InteractiveCamera') + + if rootnode.findData(REQUIRES_COLLISIONPIPELINE) and rootnode.findData(REQUIRES_COLLISIONPIPELINE).value: + rootnode.add('CollisionPipeline') + rootnode.add('RuleBasedContactManager', responseParams='mu=0', response='FrictionContactConstraint') + rootnode.add('ParallelBruteForceBroadPhase') + rootnode.add('ParallelBVHNarrowPhase') + rootnode.add('LocalMinDistance', alarmDistance=5, contactDistance=1) + + if rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER) and rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER).value: + rootnode.add('FreeMotionAnimationLoop') + rootnode.add('BlockGaussSeidelConstraintSolver') + else: + rootnode.add('DefaultAnimationLoop') + + simulation = rootnode.add(Sofa.Core.Node('Simulation')) + + simulation.add('EulerImplicitSolver') + simulation.add('SparseLDLSolver') + + if rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER) and rootnode.findData(REQUIRES_LAGRANGIANCONSTRAINTSOLVER).value: + simulation.add('GenericConstraintCorrection') + + return simulation + diff --git a/stlib/visual.py b/stlib/visual.py new file mode 100644 index 000000000..0a6d4ee3b --- /dev/null +++ b/stlib/visual.py @@ -0,0 +1,34 @@ +from stlib.core.basePrefab import BasePrefab +from stlib.core.baseParameters import BaseParameters, Optional, dataclasses +from stlib.geometries import Geometry, GeometryParameters +from stlib.geometries.file import FileParameters +from splib.core.utils import DEFAULT_VALUE +from Sofa.Core import Object + +@dataclasses.dataclass +class VisualParameters(BaseParameters): + name : str = "Visual" + + color : Optional[list[float]] = DEFAULT_VALUE + texture : Optional[str] = DEFAULT_VALUE + + geometry : GeometryParameters = None + + +class Visual(BasePrefab): + + def __init__(self, parameters: VisualParameters): + BasePrefab.__init__(self, parameters) + + def init(self): + self.geometry = self.add(Geometry, parameters=self.parameters.geometry) + self.addObject("OglModel", color=self.parameters.color, src=self.geometry.container.linkpath) + + +def createScene(root): + + # Create a visual from a mesh file + parameters = VisualParameters() + parameters.name = "LiverVisual" + parameters.geometry = FileParameters(filename="mesh/liver.obj") + root.add(Visual, parameters=parameters) \ No newline at end of file diff --git a/tests/test_new_add.py b/tests/test_new_add.py new file mode 100644 index 000000000..0c1036b65 --- /dev/null +++ b/tests/test_new_add.py @@ -0,0 +1,84 @@ +import unittest +import Sofa +import SofaRuntime +import Sofa.Core +import stlib + +class ObjectDeclaration(object): + ... + +Sofa.Core.ObjectDeclaration = ObjectDeclaration + +class MechanicalObject(ObjectDeclaration): + pass + +class TestNewAdd(unittest.TestCase): + def test_add_node_with_node_type(self): + root = Sofa.Core.Node("root") + root.add(Sofa.Core.Node, name="aNodeA") + self.assertEqual(len(root.children), 1) + self.assertEqual(root.children[0].name.value, "aNodeA") + + def test_add_node_with_node_instance(self): + root = Sofa.Core.Node("root") + root.add(Sofa.Core.Node("aNodeB")) + self.assertEqual(len(root.children), 1) + self.assertEqual(root.children[0].name.value, "aNodeB") + + def test_add_object_with_string_type(self): + root = Sofa.Core.Node("root") + root.add("MechanicalObject", name="anObject1", position=[[1,2,3]]) + self.assertEqual(len(root.objects), 1) + self.assertEqual(root.objects[0].name.value, "anObject1") + self.assertEqual(root.objects[0].position.value.shape, (1,3)) + + def test_add_object_with_object_type(self): + root = Sofa.Core.Node("root") + root.add(MechanicalObject, name="anObject2", position=[[1,2,3]]) + self.assertEqual(len(root.objects), 1) + self.assertEqual(root.objects[0].name.value, "anObject2") + self.assertEqual(root.objects[0].position.value.shape, (1,3)) + + def test_automatic_name_generation(self): + root = Sofa.Core.Node("root") + root.add(MechanicalObject, position=[[1,2,3]]) + root.add(MechanicalObject, position=[[1,2,3]]) + root.add(MechanicalObject, position=[[1,2,3]]) + self.assertEqual(root.objects[0].name.value, "MechanicalObject") + self.assertEqual(root.objects[1].name.value, "MechanicalObject1") + self.assertEqual(root.objects[2].name.value, "MechanicalObject2") + + root.add(Sofa.Core.Node, name="TestNode") + root.add(Sofa.Core.Node, name="TestNode") + self.assertEqual(root.children[0].name.value, "TestNode") + self.assertEqual(root.children[1].name.value, "TestNode1") + + root.add(Sofa.Core.Node) + root.add(Sofa.Core.Node) + self.assertEqual(root.children[2].name.value, "Node") + self.assertEqual(root.children[3].name.value, "Node1") + + def test_add_node_with_kwargs(self): + root = Sofa.Core.Node("root") + root.add(Sofa.Core.Node, name="aNodeC", gravity=[1,2,3]) + self.assertEqual(root.children[0].gravity.value, [1,2,3]) + + def test_add_instanciated_prefab(self): + root = Sofa.Core.Node("root") + from stlib.entities import Entity, EntityParameters + + bunnyParameters = EntityParameters() + bunny = root.add(Entity(bunnyParameters)) + + def test_add_prefab_with_parameter_object(self): + root = Sofa.Core.Node("root") + from stlib.entities import Entity, EntityParameters + + bunnyParameters = EntityParameters() + bunny = root.add(Entity, bunnyParameters) + + +if __name__ == '__main__': + + SofaRuntime.importPlugin("Sofa.Component.StateContainer") + unittest.main()