diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39e243a..a2065de 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -158,6 +158,17 @@ if(COSSERAT_BUILD_TESTS)
add_subdirectory(Tests)
endif()
+# Benchmarks
+# Only build benchmarks if Google Benchmark is available
+find_package(benchmark QUIET)
+if(benchmark_FOUND)
+ message(STATUS "Google Benchmark found, enabling benchmark support")
+ cmake_dependent_option(COSSERAT_BUILD_BENCHMARKS "Compile the benchmarks" ON "COSSERAT_BUILD_TESTS" OFF)
+else()
+ message(STATUS "Google Benchmark not found, benchmarks will be disabled")
+ set(COSSERAT_BUILD_BENCHMARKS OFF CACHE BOOL "Compile the benchmarks" FORCE)
+endif()
+
# Config files and install rules for pythons scripts
sofa_install_pythonscripts(PLUGIN_NAME ${PROJECT_NAME} PYTHONSCRIPTS_SOURCE_DIR "examples/python3/")
diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt
index d15db72..8c89bd5 100644
--- a/Tests/CMakeLists.txt
+++ b/Tests/CMakeLists.txt
@@ -1,42 +1,38 @@
cmake_minimum_required(VERSION 3.12)
-set(This Cosserat_test)
+project(Cosserat_test)
-project(${This} C CXX)
+if (UNIT_TEST)
-#find_package(Cosserat REQUIRED)
-find_package(Sofa.Testing REQUIRED)
+ enable_testing()
-enable_testing()
+ set(HEADER_FILES
+ )
-set(HEADER_FILES
- Example.h
- constraint/Constraint.h
- )
-set(SOURCE_FILES
- Example.cpp
- constraint/ExampleTest.cpp
-# constraint/CosseratUnilateralInteractionConstraintTest.cpp
+ set(SOURCE_FILES
+ engine/GeometricStiffnessEngineTest.cpp
forcefield/BeamHookeLawForceFieldTest.cpp
+ mapping/AdaptiveBeamMappingTest.cpp
+ mapping/CosseratNonLinearMapping2DTest.cpp
+ liegroups/RealSpaceTest.cpp
+ liegroups/SO2Test.cpp
+ liegroups/SE2Test.cpp
)
+ add_executable(${PROJECT_NAME} ${HEADER_FILES} ${SOURCE_FILES})
+ target_link_libraries(${PROJECT_NAME} Cosserat_SRC)
+ target_link_libraries(${PROJECT_NAME} gtest gtest_main)
+ target_link_libraries(${PROJECT_NAME} SofaGTestMain)
-add_executable(${This} ${SOURCE_FILES} ${HEADER_FILES})
-
-target_link_libraries(${PROJECT_NAME}
- Sofa.Testing
- Cosserat
-)
-
-target_include_directories(${This}
- PUBLIC
- "$"
-)
+ if(APPLE)
+ target_link_libraries(${PROJECT_NAME} "-framework Cocoa")
+ endif()
+ add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})
-add_test(
- NAME ${This}
- COMMAND ${This}
-)
+endif()
-#[[add_subdirectory(constraint)]]
+# Add benchmarks subdirectory if benchmarks are enabled
+if(COSSERAT_BUILD_BENCHMARKS)
+ add_subdirectory(benchmarks)
+endif()
diff --git a/Tests/forcefield/BeamHookeLawForceFieldTest.cpp b/Tests/forcefield/BeamHookeLawForceFieldTest.cpp
index c5921f3..a0f1fe1 100644
--- a/Tests/forcefield/BeamHookeLawForceFieldTest.cpp
+++ b/Tests/forcefield/BeamHookeLawForceFieldTest.cpp
@@ -17,6 +17,10 @@
#include
#include
#include
+#include
+#include
+#include
+#include
#include
#include
@@ -42,6 +46,8 @@ struct BeamHookeLawForceFieldTest : public testing::NumericTest<> {
createObject(root, "DefaultAnimationLoop");
createObject(root, "DefaultVisualManagerLoop");
+ sofa::simpleapi::importPlugin("Sofa.Component");
+ sofa::simpleapi::importPlugin("Cosserat");
}
@@ -52,8 +58,6 @@ struct BeamHookeLawForceFieldTest : public testing::NumericTest<> {
void doSetUp() override {
// initialization or some code to run before each test
fprintf(stderr, "Starting up ! \n");
- sofa::simpleapi::importPlugin("Sofa.Component");
- sofa::simpleapi::importPlugin("Cosserat");
}
// Tears down the test fixture.
@@ -84,11 +88,13 @@ struct BeamHookeLawForceFieldTest : public testing::NumericTest<> {
}
/**
+ *
*
*/
void basicAttributesTest();
- void triangle();
-
+ void testUniformSection();
+ void testVariantSection();
+ void testParallelPerformance();
// void addForceTest(const MechanicalParams* mparams,
// DataVecDeriv& f ,
// const DataVecCoord& x ,
@@ -110,38 +116,406 @@ struct BeamHookeLawForceFieldTest : public testing::NumericTest<> {
///< Root of the scene graph, created by the constructor an re-used in the tests
simulation::Node::SPtr root;
+ // Helper function to create a basic beam model
+ TheBeamHookeLawForceField* createBeamModel(bool variantSections, bool useMultiThreading);
+
+ // Compare performance between single-threaded and multi-threaded force computation
+ void comparePerformance(int numElements, bool variantSections);
+
void testFonctionnel();
};
-template<>
-void BeamHookeLawForceFieldTest::testFonctionnel() {
- EXPECT_MSG_NOEMIT(Error, Warning) ;
+template
+typename BeamHookeLawForceFieldTest::TheBeamHookeLawForceField*
+BeamHookeLawForceFieldTest::createBeamModel(bool variantSections, bool useMultiThreading) {
+ EXPECT_MSG_NOEMIT(Error, Warning);
ASSERT_NE(root, nullptr);
- createObject(root, "MechanicalObject", {{"position", "-1 0 1 1 0 1 -1 0 -1 1 0 -1 0 0 1 0 0 -1 -1 0 0 1 0 0 0 0 0"}});
- createObject(root, "TriangleSetTopologyContainer", {{"triangles", "7 5 8 8 2 6 4 6 0 1 8 4 7 3 5 8 5 2 4 8 6 1 7 8"}});
-
- auto traction = dynamic_cast(
- createObject(root, "BeamHookeLawForceField", {{"name", "beamHookeLaw"},
- {"crossSectionShape", "circular"},
- {"lengthY", "35e-5"},
- {"lengthZ", "1374e-5"},
- {"radius", "0.25"},
- {"variantSections", "true"}}).get()
+
+ // Create mechanical object with positions for testing
+ sofa::component::statecontainer::MechanicalObject* mstate = dynamic_cast*>(
+ createObject(root, "MechanicalObject", {
+ {"name", "mstate"},
+ {"position", "0 0 0 0.1 0 0 0.2 0 0 0.3 0 0 0.4 0 0 0.5 0 0 0.6 0 0 0.7 0 0 0.8 0 0 0.9 0 0"}
+ }).get()
+ );
+
+ // Set rest position to match initial position
+ mstate->writeRestPositions(mstate->x);
+
+ // Create beam force field
+ std::vector> attributes = {
+ {"name", "beamForceField"},
+ {"crossSectionShape", "circular"},
+ {"youngModulus", "1e5"},
+ {"poissonRatio", "0.3"},
+ {"radius", "0.01"},
+ {"variantSections", variantSections ? "true" : "false"},
+ {"useMultiThreading", useMultiThreading ? "true" : "false"},
+ {"length", "0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1"}
+ };
+
+ // Add variant section data if needed
+ if (variantSections) {
+ attributes.push_back({"youngModulusList", "1e5 1.1e5 1.2e5 1.3e5 1.4e5 1.5e5 1.6e5 1.7e5 1.8e5"});
+ attributes.push_back({"poissonRatioList", "0.3 0.31 0.32 0.33 0.34 0.35 0.36 0.37 0.38"});
+ }
+
+ auto forcefield = dynamic_cast(
+ createObject(root, "BeamHookeLawForceField", attributes).get()
);
+
+ EXPECT_NE(forcefield, nullptr);
+ root->init(sofa::core::execparams::defaultInstance());
+
+ return forcefield;
+}
- EXPECT_NE(traction, nullptr);
- EXPECT_NE(root.get(), nullptr) ;
- root->init(sofa::core::execparams::defaultInstance()) ;
+template
+void BeamHookeLawForceFieldTest::comparePerformance(int numElements, bool variantSections) {
+ // Reset the root node for this test
+ if (root) {
+ sofa::simulation::node::unload(root);
+ }
+ root = sofa::simpleapi::createRootNode(sofa::simulation::getSimulation(), "root");
+ createObject(root, "DefaultAnimationLoop");
+ createObject(root, "DefaultVisualManagerLoop");
+
+ // Create a large mechanical object for performance testing
+ std::stringstream positionStr;
+ std::stringstream lengthStr;
+ std::stringstream youngModuliStr;
+ std::stringstream poissonRatioStr;
+
+ for (int i = 0; i < numElements; i++) {
+ positionStr << i/10.0 << " 0 0 ";
+ if (i < numElements - 1) {
+ lengthStr << "0.1 ";
+ youngModuliStr << (1.0 + 0.1*i) << "e5 ";
+ poissonRatioStr << (0.3 + 0.01*i) << " ";
+ }
+ }
+
+ // Create mechanical object
+ sofa::component::statecontainer::MechanicalObject* mstate = dynamic_cast*>(
+ createObject(root, "MechanicalObject", {
+ {"name", "mstate"},
+ {"position", positionStr.str()}
+ }).get()
+ );
+
+ // Set rest positions
+ mstate->writeRestPositions(mstate->x);
+
+ // Apply a small deformation to test forces
+ auto x = mstate->x.beginEdit();
+ for (size_t i = 0; i < x->size(); i++) {
+ (*x)[i][0] += 0.001 * i; // Apply small deformation
+ }
+ mstate->x.endEdit();
+
+ // Run with single-threading
+ auto singleThreadForceField = dynamic_cast(
+ createObject(root, "BeamHookeLawForceField", {
+ {"name", "singleThreadFF"},
+ {"crossSectionShape", "circular"},
+ {"youngModulus", "1e5"},
+ {"poissonRatio", "0.3"},
+ {"radius", "0.01"},
+ {"variantSections", variantSections ? "true" : "false"},
+ {"useMultiThreading", "false"},
+ {"length", lengthStr.str()}
+ }).get()
+ );
+
+ if (variantSections) {
+ singleThreadForceField->findData("youngModulusList")->read(youngModuliStr.str());
+ singleThreadForceField->findData("poissonRatioList")->read(poissonRatioStr.str());
+ }
+
+ root->init(sofa::core::execparams::defaultInstance());
+
+ // Measure single-threaded performance
+ auto start_single = std::chrono::high_resolution_clock::now();
+ for (int i = 0; i < 10; i++) { // Run multiple iterations for more accurate timing
+ sofa::simulation::node::animate(root.get(), 0.01);
+ }
+ auto end_single = std::chrono::high_resolution_clock::now();
+ std::chrono::duration single_thread_time = end_single - start_single;
+
+ // Remove single threaded force field
+ root->removeObject(singleThreadForceField);
+
+ // Run with multi-threading
+ auto multiThreadForceField = dynamic_cast(
+ createObject(root, "BeamHookeLawForceField", {
+ {"name", "multiThreadFF"},
+ {"crossSectionShape", "circular"},
+ {"youngModulus", "1e5"},
+ {"poissonRatio", "0.3"},
+ {"radius", "0.01"},
+ {"variantSections", variantSections ? "true" : "false"},
+ {"useMultiThreading", "true"},
+ {"length", lengthStr.str()}
+ }).get()
+ );
+
+ if (variantSections) {
+ multiThreadForceField->findData("youngModulusList")->read(youngModuliStr.str());
+ multiThreadForceField->findData("poissonRatioList")->read(poissonRatioStr.str());
+ }
+
+ root->init(sofa::core::execparams::defaultInstance());
+
+ // Measure multi-threaded performance
+ auto start_multi = std::chrono::high_resolution_clock::now();
+ for (int i = 0; i < 10; i++) { // Run multiple iterations for more accurate timing
+ sofa::simulation::node::animate(root.get(), 0.01);
+ }
+ auto end_multi = std::chrono::high_resolution_clock::now();
+ std::chrono::duration multi_thread_time = end_multi - start_multi;
+
+ // Calculate speedup
+ double speedup = single_thread_time.count() / multi_thread_time.count();
+
+ // Output performance results
+ std::cout << "Performance comparison for " << (variantSections ? "variant" : "uniform")
+ << " sections with " << numElements << " elements:" << std::endl;
+ std::cout << " Single-threaded time: " << single_thread_time.count() << " ms" << std::endl;
+ std::cout << " Multi-threaded time: " << multi_thread_time.count() << " ms" << std::endl;
+ std::cout << " Speedup factor: " << speedup << "x" << std::endl;
+
+ // We expect some speedup with multithreading, though the exact amount depends on hardware
+ // For a reasonable number of elements, we should see at least some improvement
+ EXPECT_GT(speedup, 1.0) << "Multithreading should provide some speedup";
+}
- auto total_load = dynamic_cast *>(traction->findData("lengthY"));
- for (unsigned int step = 1; step <= 5; ++step) {
- sofa::simulation::node::animate(root.get(), 1);
- EXPECT_DOUBLE_EQ(total_load->getValue(), 4*step) << "Total load at time step " << step << " is incorrect.";
+template
+void BeamHookeLawForceFieldTest::testUniformSection() {
+ EXPECT_MSG_NOEMIT(Error, Warning);
+
+ // Create models for testing - one single-threaded and one multi-threaded
+ if (root) {
+ sofa::simulation::node::unload(root);
+ }
+ root = sofa::simpleapi::createRootNode(sofa::simulation::getSimulation(), "root");
+ createObject(root, "DefaultAnimationLoop");
+ createObject(root, "DefaultVisualManagerLoop");
+
+ // Create a force field with uniform sections using single threading
+ auto singleThreadFF = createBeamModel(false, false);
+ ASSERT_NE(singleThreadFF, nullptr);
+
+ // Create a mechanical object and apply a known deformation
+ auto mstate = dynamic_cast*>(
+ root->getTreeObject("mstate"));
+ ASSERT_NE(mstate, nullptr);
+
+ // Apply deformation
+ auto x = mstate->x.beginEdit();
+ (*x)[4][0] += 0.01; // Apply deformation to middle node
+ mstate->x.endEdit();
+
+ // Compute forces with single-threading
+ sofa::simulation::node::animate(root.get(), 0.01);
+
+ // Store the forces from single-threaded computation
+ const auto& singleThreadForces = mstate->f.getValue();
+
+ // Remove single-threaded force field
+ root->removeObject(singleThreadFF);
+
+ // Create a force field with uniform sections using multi-threading
+ auto multiThreadFF = createBeamModel(false, true);
+ ASSERT_NE(multiThreadFF, nullptr);
+
+ // Compute forces with multi-threading
+ sofa::simulation::node::animate(root.get(), 0.01);
+
+ // Compare forces from single-threaded and multi-threaded computations
+ const auto& multiThreadForces = mstate->f.getValue();
+
+ // The forces should be identical regardless of threading
+ ASSERT_EQ(singleThreadForces.size(), multiThreadForces.size());
+ for (size_t i = 0; i < singleThreadForces.size(); i++) {
+ for (size_t j = 0; j < 3; j++) { // Compare each component (assuming Vec3)
+ EXPECT_NEAR(singleThreadForces[i][j], multiThreadForces[i][j], 1e-10)
+ << "Force difference at index " << i << ", component " << j;
+ }
+ }
+}
+
+template
+void BeamHookeLawForceFieldTest::testVariantSection() {
+ EXPECT_MSG_NOEMIT(Error, Warning);
+
+ // Create models for testing - one single-threaded and one multi-threaded
+ if (root) {
+ sofa::simulation::node::unload(root);
+ }
+ root = sofa::simpleapi::createRootNode(sofa::simulation::getSimulation(), "root");
+ createObject(root, "DefaultAnimationLoop");
+ createObject(root, "DefaultVisualManagerLoop");
+
+ // Create a force field with variant sections using single threading
+ auto singleThreadFF = createBeamModel(true, false);
+ ASSERT_NE(singleThreadFF, nullptr);
+
+ // Create a mechanical object and apply a known deformation
+ auto mstate = dynamic_cast*>(
+ root->getTreeObject("mstate"));
+ ASSERT_NE(mstate, nullptr);
+
+ // Apply deformation
+ auto x = mstate->x.beginEdit();
+ for (size_t i = 0; i < x->size(); i++) {
+ (*x)[i][0] += 0.001 * i; // Apply varying deformation
+ }
+ mstate->x.endEdit();
+
+ // Compute forces with single-threading
+ sofa::simulation::node::animate(root.get(), 0.01);
+
+ // Store the forces from single-threaded computation
+ const auto& singleThreadForces = mstate->f.getValue();
+
+ // Remove single-threaded force field
+ root->removeObject(singleThreadFF);
+
+ // Create a force field with variant sections using multi-threading
+ auto multiThreadFF = createBeamModel(true, true);
+ ASSERT_NE(multiThreadFF, nullptr);
+
+ // Compute forces with multi-threading
+ sofa::simulation::node::animate(root.get(), 0.01);
+
+ // Compare forces from single-threaded and multi-threaded computations
+ const auto& multiThreadForces = mstate->f.getValue();
+
+ // The forces should be identical regardless of threading
+ ASSERT_EQ(singleThreadForces.size(), multiThreadForces.size());
+ for (size_t i = 0; i < singleThreadForces.size(); i++) {
+ for (size_t j = 0; j < 3; j++) { // Compare each component (assuming Vec3)
+ EXPECT_NEAR(singleThreadForces[i][j], multiThreadForces[i][j], 1e-10)
+ << "Force difference at index " << i << ", component " << j;
+ }
}
}
+template
+void BeamHookeLawForceFieldTest::testParallelPerformance() {
+ EXPECT_MSG_NOEMIT(Error, Warning);
+
+ // Test performance with different numbers of elements
+ comparePerformance(100, false); // Uniform sections with 100 elements
+ comparePerformance(500, false); // Uniform sections with 500 elements
+ comparePerformance(100, true); // Variant sections with 100 elements
+ comparePerformance(500, true); // Variant sections with 500 elements
+}
+template
+void BeamHookeLawForceFieldTest::testFonctionnel() {
+ EXPECT_MSG_NOEMIT(Error, Warning);
+ ASSERT_NE(root, nullptr);
+
+ // Reset root for this test
+ if (root) {
+ sofa::simulation::node::unload(root);
+ }
+ root = sofa::simpleapi::createRootNode(sofa::simulation::getSimulation(), "root");
+ createObject(root, "DefaultAnimationLoop");
+ createObject(root, "DefaultVisualManagerLoop");
+
+ // Create a mechanical object with positions for beam simulation
+ sofa::component::statecontainer::MechanicalObject* mstate = dynamic_cast*>(
+ createObject(root, "MechanicalObject", {
+ {"name", "mstate"},
+ {"position", "0 0 0 0.1 0 0 0.2 0 0 0.3 0 0 0.4 0 0"}
+ }).get()
+ );
+
+ // Set rest position to match initial position
+ mstate->writeRestPositions(mstate->x);
+
+ // Create beam force field with both uniform and variant options for testing
+ auto forcefield = dynamic_cast(
+ createObject(root, "BeamHookeLawForceField", {
+ {"name", "beamForceField"},
+ {"crossSectionShape", "circular"},
+ {"youngModulus", "1e5"},
+ {"poissonRatio", "0.3"},
+ {"radius", "0.01"},
+ {"variantSections", "false"},
+ {"useMultiThreading", "true"},
+ {"length", "0.1 0.1 0.1 0.1"}
+ }).get()
+ );
+
+ EXPECT_NE(forcefield, nullptr);
+ root->init(sofa::core::execparams::defaultInstance());
+
+ // Apply deformation and verify forces
+ auto x = mstate->x.beginEdit();
+ (*x)[2][0] += 0.01; // Apply deformation to middle node
+ mstate->x.endEdit();
+
+ // Run one step of simulation
+ sofa::simulation::node::animate(root.get(), 0.01);
+
+ // Check that forces are computed and are non-zero
+ const auto& forces = mstate->f.getValue();
+ ASSERT_EQ(forces.size(), 5);
+
+ // Verify that forces are computed correctly (non-zero at deformed points)
+ bool foundNonZeroForce = false;
+ for (size_t i = 0; i < forces.size(); i++) {
+ if (forces[i].norm() > 1e-10) {
+ foundNonZeroForce = true;
+ break;
+ }
+ }
+
+ EXPECT_TRUE(foundNonZeroForce) << "Expected non-zero forces due to deformation";
+
+ // Now switch to variant sections and test again
+ root->removeObject(forcefield);
+
+ auto variantForcefield = dynamic_cast(
+ createObject(root, "BeamHookeLawForceField", {
+ {"name", "beamForceField"},
+ {"crossSectionShape", "circular"},
+ {"youngModulus", "1e5"},
+ {"poissonRatio", "0.3"},
+ {"radius", "0.01"},
+ {"variantSections", "true"},
+ {"youngModulusList", "1e5 1.1e5 1.2e5 1.3e5"},
+ {"poissonRatioList", "0.3 0.31 0.32 0.33"},
+ {"useMultiThreading", "true"},
+ {"length", "0.1 0.1 0.1 0.1"}
+ }).get()
+ );
+
+ EXPECT_NE(variantForcefield, nullptr);
+ root->init(sofa::core::execparams::defaultInstance());
+
+ // Run simulation with variant sections
+ sofa::simulation::node::animate(root.get(), 0.01);
+
+ // Check that forces are computed and are non-zero with variant sections
+ const auto& variantForces = mstate->f.getValue();
+ ASSERT_EQ(variantForces.size(), 5);
+
+ // Verify that forces are computed correctly with variant sections
+ foundNonZeroForce = false;
+ for (size_t i = 0; i < variantForces.size(); i++) {
+ if (variantForces[i].norm() > 1e-10) {
+ foundNonZeroForce = true;
+ break;
+ }
+ }
+
+ EXPECT_TRUE(foundNonZeroForce) << "Expected non-zero forces with variant sections";
+}
template<>
void BeamHookeLawForceFieldTest::basicAttributesTest(){
@@ -199,7 +573,6 @@ TYPED_TEST( BeamHookeLawForceFieldTest , basicAttributesTest )
ASSERT_NO_THROW (this->basicAttributesTest());
}
-TYPED_TEST(BeamHookeLawForceFieldTest, DISABLED_testFonctionnel) {
ASSERT_NO_THROW (this->testFonctionnel());
}
diff --git a/Tests/liegroups/BundleTest.cpp b/Tests/liegroups/BundleTest.cpp
new file mode 100644
index 0000000..ddc801c
--- /dev/null
+++ b/Tests/liegroups/BundleTest.cpp
@@ -0,0 +1,267 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::testing {
+
+using namespace sofa::testing;
+
+/**
+ * Test suite for Bundle Lie group implementation
+ */
+class BundleTest : public BaseTest
+{
+protected:
+ // Define common types
+ using RealSpace3d = RealSpace;
+ using SO3d = SO3;
+ using SE3d = SE3;
+ using Vector3 = Eigen::Vector3d;
+ using Matrix3 = Eigen::Matrix3d;
+ using Quaternion = Eigen::Quaterniond;
+
+ // Define test bundle types
+ using PoseVel = Bundle; // Pose + velocity
+ using MultiBody = Bundle; // Three rigid bodies
+ using ComplexSystem = Bundle; // Mixed system
+
+ const double pi = M_PI;
+ const double eps = 1e-10;
+
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/**
+ * Test identity element properties
+ */
+TEST_F(BundleTest, Identity)
+{
+ // Test PoseVel identity
+ PoseVel id_pv = PoseVel::identity();
+ EXPECT_TRUE(id_pv.get<0>().isApprox(SE3d::identity()));
+ EXPECT_TRUE(id_pv.get<1>().isApprox(RealSpace3d::identity()));
+
+ // Test MultiBody identity
+ MultiBody id_mb = MultiBody::identity();
+ EXPECT_TRUE(id_mb.get<0>().isApprox(SE3d::identity()));
+ EXPECT_TRUE(id_mb.get<1>().isApprox(SE3d::identity()));
+ EXPECT_TRUE(id_mb.get<2>().isApprox(SE3d::identity()));
+
+ // Test right and left identity
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ PoseVel g(SE3d(SO3d(pi/4, axis), Vector3(1, 2, 3)),
+ RealSpace3d(Vector3(0.1, 0.2, 0.3)));
+
+ EXPECT_TRUE((g * id_pv).isApprox(g));
+ EXPECT_TRUE((id_pv * g).isApprox(g));
+}
+
+/**
+ * Test group operation (component-wise composition)
+ */
+TEST_F(BundleTest, GroupOperation)
+{
+ // Test PoseVel composition
+ Vector3 axis1(1, 0, 0), axis2(0, 1, 0);
+ Vector3 trans1(1, 0, 0), trans2(0, 1, 0);
+ Vector3 vel1(0.1, 0, 0), vel2(0, 0.2, 0);
+
+ PoseVel g1(SE3d(SO3d(pi/2, axis1), trans1), RealSpace3d(vel1));
+ PoseVel g2(SE3d(SO3d(pi/2, axis2), trans2), RealSpace3d(vel2));
+
+ PoseVel g12 = g1 * g2;
+
+ // Verify component-wise composition
+ EXPECT_TRUE(g12.get<0>().isApprox(g1.get<0>() * g2.get<0>()));
+ EXPECT_TRUE(g12.get<1>().isApprox(g1.get<1>() * g2.get<1>()));
+
+ // Test MultiBody composition
+ MultiBody mb1(SE3d(SO3d(pi/2, axis1), trans1),
+ SE3d(SO3d(pi/3, axis1), trans1),
+ SE3d(SO3d(pi/4, axis1), trans1));
+
+ MultiBody mb2(SE3d(SO3d(pi/2, axis2), trans2),
+ SE3d(SO3d(pi/3, axis2), trans2),
+ SE3d(SO3d(pi/4, axis2), trans2));
+
+ MultiBody mb12 = mb1 * mb2;
+
+ // Verify all bodies composed correctly
+ EXPECT_TRUE(mb12.get<0>().isApprox(mb1.get<0>() * mb2.get<0>()));
+ EXPECT_TRUE(mb12.get<1>().isApprox(mb1.get<1>() * mb2.get<1>()));
+ EXPECT_TRUE(mb12.get<2>().isApprox(mb1.get<2>() * mb2.get<2>()));
+}
+
+/**
+ * Test inverse operation
+ */
+TEST_F(BundleTest, Inverse)
+{
+ // Test PoseVel inverse
+ Vector3 axis = Vector3(1, 1, 0).normalized();
+ Vector3 trans(1, 2, 3);
+ Vector3 vel(0.1, 0.2, 0.3);
+
+ PoseVel g(SE3d(SO3d(pi/3, axis), trans), RealSpace3d(vel));
+ PoseVel inv = g.inverse();
+
+ // Test inverse properties
+ EXPECT_TRUE((g * inv).isApprox(PoseVel::identity()));
+ EXPECT_TRUE((inv * g).isApprox(PoseVel::identity()));
+
+ // Verify component-wise inverse
+ EXPECT_TRUE(inv.get<0>().isApprox(g.get<0>().inverse()));
+ EXPECT_TRUE(inv.get<1>().isApprox(g.get<1>().inverse()));
+}
+
+/**
+ * Test exponential and logarithm maps
+ */
+TEST_F(BundleTest, ExpLog)
+{
+ // Test PoseVel exp/log
+ Vector3 axis = Vector3(1, 2, 3).normalized();
+ Vector3 trans(4, 5, 6);
+ Vector3 vel(0.1, 0.2, 0.3);
+
+ PoseVel g(SE3d(SO3d(pi/4, axis), trans), RealSpace3d(vel));
+ auto xi = g.log();
+ PoseVel g2 = PoseVel().exp(xi);
+
+ EXPECT_TRUE(g.isApprox(g2));
+
+ // Test MultiBody exp/log
+ MultiBody mb(SE3d(SO3d(pi/3, axis), trans),
+ SE3d(SO3d(pi/4, axis), trans),
+ SE3d(SO3d(pi/6, axis), trans));
+
+ auto xi_mb = mb.log();
+ MultiBody mb2 = MultiBody().exp(xi_mb);
+
+ EXPECT_TRUE(mb.isApprox(mb2));
+}
+
+/**
+ * Test adjoint representation
+ */
+TEST_F(BundleTest, Adjoint)
+{
+ // Test PoseVel adjoint (should be block diagonal)
+ Vector3 axis = Vector3(0, 0, 1);
+ Vector3 trans(1, 2, 0);
+ Vector3 vel(0.1, 0.2, 0);
+
+ PoseVel g(SE3d(SO3d(pi/2, axis), trans), RealSpace3d(vel));
+ auto Ad = g.adjoint();
+
+ // Verify block diagonal structure
+ auto Ad1 = g.get<0>().adjoint();
+ auto Ad2 = g.get<1>().adjoint();
+
+ EXPECT_TRUE(Ad.block(0, 0, Ad1.rows(), Ad1.cols()).isApprox(Ad1));
+ EXPECT_TRUE(Ad.block(Ad1.rows(), Ad1.cols(), Ad2.rows(), Ad2.cols()).isApprox(Ad2));
+}
+
+/**
+ * Test group action
+ */
+TEST_F(BundleTest, Action)
+{
+ // Test PoseVel action
+ Vector3 axis = Vector3(0, 0, 1);
+ Vector3 trans(1, 0, 0);
+ Vector3 vel(0.1, 0, 0);
+
+ PoseVel g(SE3d(SO3d(pi/2, axis), trans), RealSpace3d(vel));
+
+ // Create point and velocity
+ Vector3 p(1, 0, 0);
+ Vector3 v(0.1, 0, 0);
+ Eigen::VectorXd state(6);
+ state << p, v;
+
+ // Test action
+ auto result = g.act(state);
+
+ // Verify components transformed correctly
+ Vector3 p_new = result.head<3>();
+ Vector3 v_new = result.tail<3>();
+
+ EXPECT_TRUE(p_new.isApprox(g.get<0>().act(p)));
+ EXPECT_TRUE(v_new.isApprox(v + vel));
+}
+
+/**
+ * Test interpolation
+ */
+TEST_F(BundleTest, Interpolation)
+{
+ // Test PoseVel interpolation
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ PoseVel start = PoseVel::identity();
+ PoseVel end(SE3d(SO3d(pi/2, axis), Vector3(2, 0, 0)),
+ RealSpace3d(Vector3(0.2, 0, 0)));
+
+ // Test midpoint
+ PoseVel mid = interpolate(start, end, 0.5);
+
+ // Verify component-wise interpolation
+ EXPECT_TRUE(mid.get<0>().isApprox(interpolate(start.get<0>(), end.get<0>(), 0.5)));
+ EXPECT_TRUE(mid.get<1>().isApprox(interpolate(start.get<1>(), end.get<1>(), 0.5)));
+
+ // Test boundary conditions
+ EXPECT_TRUE(interpolate(start, end, 0.0).isApprox(start));
+ EXPECT_TRUE(interpolate(start, end, 1.0).isApprox(end));
+}
+
+/**
+ * Test mixed bundle operations
+ */
+TEST_F(BundleTest, ComplexSystem)
+{
+ // Create a complex system with different types
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ ComplexSystem sys(
+ SE3d(SO3d(pi/4, axis), Vector3(1, 2, 3)), // Rigid body
+ SO3d(pi/3, Vector3(0, 0, 1)), // Pure rotation
+ RealSpace3d(Vector3(0.1, 0.2, 0.3)) // Vector space
+ );
+
+ // Test identity composition
+ EXPECT_TRUE((sys * ComplexSystem::identity()).isApprox(sys));
+
+ // Test inverse
+ auto inv = sys.inverse();
+ EXPECT_TRUE((sys * inv).isApprox(ComplexSystem::identity()));
+
+ // Test exp/log
+ auto xi = sys.log();
+ auto sys2 = ComplexSystem().exp(xi);
+ EXPECT_TRUE(sys.isApprox(sys2));
+}
+
+} // namespace sofa::component::cosserat::liegroups::testing
diff --git a/Tests/liegroups/LieGroupBenchmark.cpp b/Tests/liegroups/LieGroupBenchmark.cpp
new file mode 100644
index 0000000..a34d25b
--- /dev/null
+++ b/Tests/liegroups/LieGroupBenchmark.cpp
@@ -0,0 +1,254 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::benchmark {
+
+using Vector3 = Eigen::Vector3d;
+using Matrix3 = Eigen::Matrix3d;
+using Quaternion = Eigen::Quaterniond;
+
+// Helper for random generation
+class RandomGenerator {
+private:
+ std::mt19937 gen{std::random_device{}()};
+ std::uniform_real_distribution angle_dist{0, 2*M_PI};
+ std::normal_distribution vec_dist{0, 1.0};
+
+public:
+ Vector3 randomVector() {
+ return Vector3(vec_dist(gen), vec_dist(gen), vec_dist(gen));
+ }
+
+ Vector3 randomUnitVector() {
+ Vector3 v = randomVector();
+ return v.normalized();
+ }
+
+ double randomAngle() {
+ return angle_dist(gen);
+ }
+};
+
+/**
+ * Benchmark SO3 operations
+ */
+static void BM_SO3_Operations(benchmark::State& state) {
+ RandomGenerator rng;
+ SO3 rot(rng.randomAngle(), rng.randomUnitVector());
+ Vector3 point = rng.randomVector();
+
+ for (auto _ : state) {
+ // Test common operations
+ auto result1 = rot.act(point);
+ auto result2 = rot.inverse();
+ auto result3 = rot.log();
+ auto result4 = rot.adjoint();
+ benchmark::DoNotOptimize(result1);
+ benchmark::DoNotOptimize(result2);
+ benchmark::DoNotOptimize(result3);
+ benchmark::DoNotOptimize(result4);
+ }
+}
+BENCHMARK(BM_SO3_Operations);
+
+/**
+ * Benchmark SE3 operations
+ */
+static void BM_SE3_Operations(benchmark::State& state) {
+ RandomGenerator rng;
+ SE3 transform(
+ SO3(rng.randomAngle(), rng.randomUnitVector()),
+ rng.randomVector()
+ );
+ Vector3 point = rng.randomVector();
+
+ for (auto _ : state) {
+ // Test common operations
+ auto result1 = transform.act(point);
+ auto result2 = transform.inverse();
+ auto result3 = transform.log();
+ auto result4 = transform.adjoint();
+ benchmark::DoNotOptimize(result1);
+ benchmark::DoNotOptimize(result2);
+ benchmark::DoNotOptimize(result3);
+ benchmark::DoNotOptimize(result4);
+ }
+}
+BENCHMARK(BM_SE3_Operations);
+
+/**
+ * Benchmark SE_2(3) operations
+ */
+static void BM_SE23_Operations(benchmark::State& state) {
+ RandomGenerator rng;
+ SE23 extended_pose(
+ SE3(
+ SO3(rng.randomAngle(), rng.randomUnitVector()),
+ rng.randomVector()
+ ),
+ rng.randomVector()
+ );
+ Vector3 point = rng.randomVector();
+
+ for (auto _ : state) {
+ // Test common operations
+ auto result1 = extended_pose.act(point);
+ auto result2 = extended_pose.inverse();
+ auto result3 = extended_pose.log();
+ auto result4 = extended_pose.adjoint();
+ benchmark::DoNotOptimize(result1);
+ benchmark::DoNotOptimize(result2);
+ benchmark::DoNotOptimize(result3);
+ benchmark::DoNotOptimize(result4);
+ }
+}
+BENCHMARK(BM_SE23_Operations);
+
+/**
+ * Benchmark Bundle operations
+ */
+static void BM_Bundle_Operations(benchmark::State& state) {
+ RandomGenerator rng;
+ using PoseVel = Bundle, RealSpace>;
+
+ PoseVel bundle(
+ SE3(
+ SO3(rng.randomAngle(), rng.randomUnitVector()),
+ rng.randomVector()
+ ),
+ RealSpace(rng.randomVector())
+ );
+
+ for (auto _ : state) {
+ // Test common operations
+ auto result1 = bundle.inverse();
+ auto result2 = bundle.log();
+ auto result3 = bundle.adjoint();
+ benchmark::DoNotOptimize(result1);
+ benchmark::DoNotOptimize(result2);
+ benchmark::DoNotOptimize(result3);
+ }
+}
+BENCHMARK(BM_Bundle_Operations);
+
+/**
+ * Benchmark Cosserat rod operations
+ */
+static void BM_CosseratRod_Operations(benchmark::State& state) {
+ RandomGenerator rng;
+ const int num_segments = state.range(0);
+ using RodSegment = Bundle, RealSpace>;
+ std::vector segments;
+
+ // Initialize rod segments
+ for (int i = 0; i < num_segments; ++i) {
+ segments.push_back(RodSegment(
+ SE3(
+ SO3(rng.randomAngle(), rng.randomUnitVector()),
+ rng.randomVector()
+ ),
+ RealSpace(rng.randomVector())
+ ));
+ }
+
+ for (auto _ : state) {
+ // Simulate rod deformation
+ for (int i = 1; i < num_segments; ++i) {
+ auto rel_transform = segments[i-1].inverse() * segments[i];
+ auto strain = rel_transform.log();
+ benchmark::DoNotOptimize(strain);
+ }
+ }
+}
+BENCHMARK(BM_CosseratRod_Operations)
+ ->RangeMultiplier(2)
+ ->Range(8, 128);
+
+/**
+ * Benchmark exponential map implementations
+ */
+static void BM_ExpMap_Operations(benchmark::State& state) {
+ RandomGenerator rng;
+ Vector3 omega = rng.randomVector();
+
+ for (auto _ : state) {
+ // SO3 exponential
+ auto rot = SO3().exp(omega);
+
+ // SE3 exponential
+ Vector3 v = rng.randomVector();
+ auto transform = SE3().exp(
+ (Eigen::Matrix() << v, omega).finished()
+ );
+
+ benchmark::DoNotOptimize(rot);
+ benchmark::DoNotOptimize(transform);
+ }
+}
+BENCHMARK(BM_ExpMap_Operations);
+
+/**
+ * Benchmark interpolation operations
+ */
+static void BM_Interpolation_Operations(benchmark::State& state) {
+ RandomGenerator rng;
+
+ // Create random transformations
+ SE3 T1(
+ SO3(rng.randomAngle(), rng.randomUnitVector()),
+ rng.randomVector()
+ );
+ SE3 T2(
+ SO3(rng.randomAngle(), rng.randomUnitVector()),
+ rng.randomVector()
+ );
+
+ const int num_steps = state.range(0);
+ std::vector times(num_steps);
+ for (int i = 0; i < num_steps; ++i) {
+ times[i] = static_cast(i) / (num_steps - 1);
+ }
+
+ for (auto _ : state) {
+ // Interpolate between transformations
+ for (double t : times) {
+ auto result = interpolate(T1, T2, t);
+ benchmark::DoNotOptimize(result);
+ }
+ }
+}
+BENCHMARK(BM_Interpolation_Operations)
+ ->RangeMultiplier(2)
+ ->Range(8, 128);
+
+} // namespace sofa::component::cosserat::liegroups::benchmark
+
+BENCHMARK_MAIN();
diff --git a/Tests/liegroups/LieGroupIntegrationTest.cpp b/Tests/liegroups/LieGroupIntegrationTest.cpp
new file mode 100644
index 0000000..17387b5
--- /dev/null
+++ b/Tests/liegroups/LieGroupIntegrationTest.cpp
@@ -0,0 +1,307 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::testing {
+
+using namespace sofa::testing;
+
+/**
+ * Integration tests demonstrating real-world applications of Lie groups
+ */
+class LieGroupIntegrationTest : public BaseTest
+{
+protected:
+ // Define common types
+ using Vector3 = Eigen::Vector3d;
+ using Vector6 = Eigen::Matrix;
+ using Matrix3 = Eigen::Matrix3d;
+ using Matrix4 = Eigen::Matrix4d;
+ using Quaternion = Eigen::Quaterniond;
+
+ // Basic Lie groups
+ using SO3d = SO3;
+ using SE3d = SE3;
+ using SE23d = SE23;
+ using SGal3d = SGal3;
+ using RealSpace3d = RealSpace;
+
+ // Specialized bundle types for Cosserat mechanics
+ using CosseratSection = Bundle; // Position, strain
+ using CosseratNode = Bundle; // Configuration with velocity
+ using CosseratRod = Bundle; // Position, strain, stress
+
+ const double pi = M_PI;
+ const double eps = 1e-10;
+
+ /**
+ * Helper to create a rotation from axis-angle
+ */
+ SO3d makeRotation(double angle, const Vector3& axis) {
+ return SO3d(angle, axis.normalized());
+ }
+
+ /**
+ * Helper to create a material frame
+ */
+ SE3d makeMaterialFrame(const Vector3& position, const Vector3& tangent,
+ const Vector3& normal) {
+ // Create rotation that aligns z-axis with tangent
+ Vector3 z_axis(0, 0, 1);
+ Vector3 rot_axis = z_axis.cross(tangent);
+ double rot_angle = std::acos(z_axis.dot(tangent));
+ SO3d R(rot_angle, rot_axis.normalized());
+
+ // Additional rotation around tangent to align normal
+ Vector3 current_normal = R.act(Vector3(1, 0, 0));
+ Vector3 target_normal = normal - normal.dot(tangent) * tangent;
+ target_normal.normalize();
+
+ double twist_angle = std::acos(current_normal.dot(target_normal));
+ if (std::abs(twist_angle) > eps) {
+ SO3d twist(twist_angle, tangent);
+ R = twist * R;
+ }
+
+ return SE3d(R, position);
+ }
+
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/**
+ * Test Cosserat rod centerline representation
+ */
+TEST_F(LieGroupIntegrationTest, CosseratCenterline)
+{
+ // Create a circular arc centerline
+ const int num_points = 50;
+ const double radius = 1.0;
+ const double arc_angle = pi; // Half circle
+
+ std::vector frames;
+ frames.reserve(num_points);
+
+ for (int i = 0; i < num_points; ++i) {
+ double t = static_cast(i) / (num_points - 1);
+ double theta = t * arc_angle;
+
+ // Position on circle
+ Vector3 position(radius * std::cos(theta), radius * std::sin(theta), 0);
+
+ // Tangent vector (normalized derivative)
+ Vector3 tangent(-std::sin(theta), std::cos(theta), 0);
+
+ // Normal vector (points inward)
+ Vector3 normal(-std::cos(theta), -std::sin(theta), 0);
+
+ frames.push_back(makeMaterialFrame(position, tangent, normal));
+ }
+
+ // Verify geometric properties
+ for (size_t i = 1; i < frames.size(); ++i) {
+ // Get relative transform between consecutive frames
+ SE3d rel = frames[i-1].inverse() * frames[i];
+
+ // Extract curvature from relative rotation
+ Vector3 omega = rel.rotation().log();
+ double curvature = omega.norm() * (num_points - 1) / arc_angle;
+
+ // Should be constant and equal to 1/radius
+ EXPECT_NEAR(curvature, 1.0/radius, 0.1);
+ }
+}
+
+/**
+ * Test Cosserat rod dynamics
+ */
+TEST_F(LieGroupIntegrationTest, CosseratDynamics)
+{
+ // Create a rod configuration with velocity
+ Vector3 position(0, 0, 0);
+ Vector3 tangent(0, 0, 1);
+ Vector3 normal(1, 0, 0);
+
+ // Initial material frame
+ SE3d frame = makeMaterialFrame(position, tangent, normal);
+
+ // Initial velocity state (linear and angular velocity)
+ Vector3 linear_vel(0.1, 0, 0); // Moving in x direction
+ Vector3 angular_vel(0, 0.5, 0); // Rotating around y axis
+
+ // Create extended pose with velocity
+ SE23d state(frame, linear_vel);
+
+ // Initial strain and stress
+ Vector3 strain(0, 0, 1); // Unit stretch
+ Vector3 stress(0, 0, 0); // No initial stress
+
+ // Create full Cosserat state
+ CosseratRod rod(frame, strain, stress);
+
+ // Simulate simple time evolution
+ const double dt = 0.01;
+ const int steps = 100;
+
+ for (int i = 0; i < steps; ++i) {
+ // Update position and orientation using current velocity
+ Vector6 twist;
+ twist << linear_vel, angular_vel;
+ frame = frame * SE3d().exp(dt * twist);
+
+ // Update strain based on deformation
+ strain = frame.rotation().inverse().act(tangent);
+
+ // Simple linear stress response
+ stress = 100.0 * (strain - Vector3(0, 0, 1)); // Hook's law
+
+ // Store new state
+ rod = CosseratRod(frame, strain, stress);
+
+ // Verify physical constraints
+ EXPECT_NEAR(strain.norm(), 1.0, 0.1); // Length preservation
+ EXPECT_TRUE(frame.rotation().matrix().determinant() > 0); // Proper rotation
+ }
+}
+
+/**
+ * Test multi-body articulation
+ */
+TEST_F(LieGroupIntegrationTest, ArticulatedSystem)
+{
+ // Create a simple 3-link articulated system
+ using ThreeLink = Bundle;
+
+ // Initial configuration (vertical stack)
+ std::vector links;
+ const double link_length = 1.0;
+
+ for (int i = 0; i < 3; ++i) {
+ Vector3 position(0, 0, i * link_length);
+ SO3d orientation = SO3d::identity();
+ links.push_back(SE3d(orientation, position));
+ }
+
+ ThreeLink system(links[0], links[1], links[2]);
+
+ // Apply joint motions
+ const double joint_angle = pi/4; // 45 degrees
+
+ // Rotate first joint around y-axis
+ SO3d R1(joint_angle, Vector3(0, 1, 0));
+ links[0] = SE3d(R1, links[0].translation());
+
+ // Rotate second joint around x-axis
+ SO3d R2(joint_angle, Vector3(1, 0, 0));
+ SE3d T1 = links[0]; // Transform from first link
+ links[1] = T1 * SE3d(R2, Vector3(0, 0, link_length));
+
+ // Rotate third joint around y-axis
+ SO3d R3(-joint_angle, Vector3(0, 1, 0));
+ SE3d T2 = links[1]; // Transform from second link
+ links[2] = T2 * SE3d(R3, Vector3(0, 0, link_length));
+
+ // Update system state
+ system = ThreeLink(links[0], links[1], links[2]);
+
+ // Verify kinematic chain properties
+ for (int i = 1; i < 3; ++i) {
+ // Check link connections
+ Vector3 parent_end = links[i-1].translation() +
+ links[i-1].rotation().act(Vector3(0, 0, link_length));
+ Vector3 child_start = links[i].translation();
+
+ EXPECT_TRUE((parent_end - child_start).norm() < eps);
+ }
+}
+
+/**
+ * Test time-varying trajectories
+ */
+TEST_F(LieGroupIntegrationTest, TimeVaryingTrajectory)
+{
+ // Create a helix trajectory using SGal(3)
+ const double radius = 1.0;
+ const double pitch = 0.5;
+ const double angular_vel = 1.0;
+ const double vertical_vel = pitch * angular_vel;
+
+ std::vector trajectory;
+ const int num_points = 50;
+
+ for (int i = 0; i < num_points; ++i) {
+ double t = static_cast(i) / (num_points - 1);
+ double theta = t * 2 * pi;
+
+ // Position on helix
+ Vector3 position(radius * std::cos(theta),
+ radius * std::sin(theta),
+ pitch * theta);
+
+ // Tangent vector
+ Vector3 tangent(-radius * std::sin(theta),
+ radius * std::cos(theta),
+ pitch);
+ tangent.normalize();
+
+ // Create frame from position and tangent
+ SE3d frame = makeMaterialFrame(position, tangent, Vector3(0, 0, 1));
+
+ // Velocity components
+ Vector3 linear_vel(-radius * angular_vel * std::sin(theta),
+ radius * angular_vel * std::cos(theta),
+ vertical_vel);
+
+ // Create Galilean transformation
+ trajectory.push_back(SGal3d(frame, linear_vel, t));
+ }
+
+ // Verify trajectory properties
+ for (size_t i = 1; i < trajectory.size(); ++i) {
+ const auto& g1 = trajectory[i-1];
+ const auto& g2 = trajectory[i];
+
+ // Time should increase monotonically
+ EXPECT_GT(g2.time(), g1.time());
+
+ // Velocity should be constant in magnitude
+ double vel_mag1 = g1.velocity().norm();
+ double vel_mag2 = g2.velocity().norm();
+ EXPECT_NEAR(vel_mag1, vel_mag2, eps);
+
+ // Position should follow helix equation
+ Vector3 pos = g1.pose().translation();
+ double theta = std::atan2(pos.y(), pos.x());
+ double expected_z = pitch * theta;
+ EXPECT_NEAR(pos.z(), expected_z, 0.1);
+ }
+}
+
+} // namespace sofa::component::cosserat::liegroups::testing
diff --git a/Tests/liegroups/SE23Test.cpp b/Tests/liegroups/SE23Test.cpp
new file mode 100644
index 0000000..0642487
--- /dev/null
+++ b/Tests/liegroups/SE23Test.cpp
@@ -0,0 +1,263 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::testing {
+
+using namespace sofa::testing;
+
+/**
+ * Test suite for SE_2(3) Lie group implementation
+ */
+class SE23Test : public BaseTest
+{
+protected:
+ using SE23d = SE23;
+ using SE3d = SE3;
+ using SO3d = SO3;
+ using Vector3 = Eigen::Vector3d;
+ using Vector9 = Eigen::Matrix;
+ using Matrix3 = Eigen::Matrix3d;
+ using Matrix4 = Eigen::Matrix4d;
+ using Matrix9 = Eigen::Matrix;
+ using Quaternion = Eigen::Quaterniond;
+
+ const double pi = M_PI;
+ const double eps = 1e-10;
+
+ // Helper function to create twist vector with acceleration
+ Vector9 twist(const Vector3& v, const Vector3& omega, const Vector3& a) {
+ Vector9 xi;
+ xi << v, omega, a;
+ return xi;
+ }
+
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/**
+ * Test identity element properties
+ */
+TEST_F(SE23Test, Identity)
+{
+ SE23d id = SE23d::identity();
+
+ // Test identity components
+ EXPECT_TRUE(id.pose().rotation().quaternion().isApprox(Quaternion::Identity()));
+ EXPECT_TRUE(id.pose().translation().isZero());
+ EXPECT_TRUE(id.velocity().isZero());
+
+ // Test identity matrix
+ EXPECT_TRUE(id.matrix().isIdentity());
+
+ // Test composition with identity
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ Vector3 trans(1, 2, 3);
+ Vector3 vel(0.1, 0.2, 0.3);
+ SE23d g(SE3d(SO3d(pi/4, axis), trans), vel);
+ EXPECT_TRUE((g * id).isApprox(g));
+ EXPECT_TRUE((id * g).isApprox(g));
+}
+
+/**
+ * Test group operation (extended pose composition)
+ */
+TEST_F(SE23Test, GroupOperation)
+{
+ // Create transformations with different rotations, translations, and velocities
+ Vector3 axis1 = Vector3(1, 0, 0); // X-axis
+ Vector3 axis2 = Vector3(0, 1, 0); // Y-axis
+ Vector3 t1(1, 0, 0); // X-translation
+ Vector3 t2(0, 1, 0); // Y-translation
+ Vector3 v1(0.1, 0, 0); // X-velocity
+ Vector3 v2(0, 0.2, 0); // Y-velocity
+
+ SE23d g1(SE3d(SO3d(pi/2, axis1), t1), v1);
+ SE23d g2(SE3d(SO3d(pi/2, axis2), t2), v2);
+
+ // Test composition
+ SE23d g12 = g1 * g2;
+
+ // Verify pose composition
+ EXPECT_TRUE(g12.pose().isApprox(g1.pose() * g2.pose()));
+
+ // Verify velocity transformation
+ Vector3 expected_vel = v1 + g1.pose().rotation().act(v2);
+ EXPECT_TRUE(g12.velocity().isApprox(expected_vel));
+
+ // Test non-commutativity
+ SE23d g21 = g2 * g1;
+ EXPECT_FALSE(g12.isApprox(g21));
+}
+
+/**
+ * Test inverse operation
+ */
+TEST_F(SE23Test, Inverse)
+{
+ Vector3 axis = Vector3(1, 1, 0).normalized();
+ Vector3 trans(1, 2, 3);
+ Vector3 vel(0.1, 0.2, 0.3);
+ SE23d g(SE3d(SO3d(pi/3, axis), trans), vel);
+ SE23d inv = g.inverse();
+
+ // Test inverse properties
+ EXPECT_TRUE((g * inv).isApprox(SE23d::identity()));
+ EXPECT_TRUE((inv * g).isApprox(SE23d::identity()));
+
+ // Test inverse pose
+ EXPECT_TRUE(inv.pose().isApprox(g.pose().inverse()));
+
+ // Test inverse velocity
+ Vector3 expected_vel = -g.pose().rotation().inverse().act(vel);
+ EXPECT_TRUE(inv.velocity().isApprox(expected_vel));
+}
+
+/**
+ * Test exponential and logarithm maps
+ */
+TEST_F(SE23Test, ExpLog)
+{
+ // Test exp(log(g)) = g
+ Vector3 axis = Vector3(1, 2, 3).normalized();
+ Vector3 trans(4, 5, 6);
+ Vector3 vel(0.1, 0.2, 0.3);
+ SE23d g(SE3d(SO3d(pi/4, axis), trans), vel);
+ Vector9 xi = g.log();
+ SE23d g2 = SE23d().exp(xi);
+ EXPECT_TRUE(g.isApprox(g2));
+
+ // Test pure translation
+ Vector9 xi_trans = twist(Vector3(1, 2, 3), Vector3::Zero(), Vector3::Zero());
+ SE23d g_trans = SE23d().exp(xi_trans);
+ EXPECT_TRUE(g_trans.pose().rotation().isApprox(SO3d::identity()));
+ EXPECT_TRUE(g_trans.pose().translation().isApprox(Vector3(1, 2, 3)));
+ EXPECT_TRUE(g_trans.velocity().isZero());
+
+ // Test pure velocity
+ Vector9 xi_vel = twist(Vector3::Zero(), Vector3::Zero(), Vector3(0.1, 0.2, 0.3));
+ SE23d g_vel = SE23d().exp(xi_vel);
+ EXPECT_TRUE(g_vel.pose().isApprox(SE3d::identity()));
+ EXPECT_TRUE(g_vel.velocity().isApprox(Vector3(0.1, 0.2, 0.3)));
+
+ // Test small motion approximation
+ Vector9 xi_small = twist(Vector3(0.001, 0.001, 0.001),
+ Vector3(0.001, 0.001, 0.001),
+ Vector3(0.001, 0.001, 0.001));
+ SE23d g_small = SE23d().exp(xi_small);
+ Matrix4 expected_pose = Matrix4::Identity();
+ expected_pose.block<3,3>(0,0) = Matrix3::Identity() + SO3d::hat(xi_small.segment<3>(3));
+ expected_pose.block<3,1>(0,3) = xi_small.head<3>();
+ EXPECT_TRUE(g_small.pose().matrix().isApprox(expected_pose, 1e-6));
+ EXPECT_TRUE(g_small.velocity().isApprox(xi_small.tail<3>(), 1e-6));
+}
+
+/**
+ * Test adjoint representation
+ */
+TEST_F(SE23Test, Adjoint)
+{
+ Vector3 axis = Vector3(0, 0, 1); // Z-axis
+ Vector3 trans(1, 2, 0);
+ Vector3 vel(0.1, 0.2, 0);
+ SE23d g(SE3d(SO3d(pi/2, axis), trans), vel);
+ Matrix9 Ad = g.adjoint();
+
+ // Test adjoint structure
+ Matrix3 R = g.pose().rotation().matrix();
+ Matrix3 t_hat = SO3d::hat(trans);
+ Matrix3 v_hat = SO3d::hat(vel);
+
+ EXPECT_TRUE(Ad.block<3,3>(0,0).isApprox(R));
+ EXPECT_TRUE(Ad.block<3,3>(0,3).isApprox(t_hat * R));
+ EXPECT_TRUE(Ad.block<3,3>(0,6).isApprox(v_hat * R));
+ EXPECT_TRUE(Ad.block<3,3>(3,0).isZero());
+ EXPECT_TRUE(Ad.block<3,3>(3,3).isApprox(R));
+ EXPECT_TRUE(Ad.block<3,3>(6,6).isApprox(R));
+
+ // Test adjoint action
+ Vector9 xi = twist(Vector3(1, 0, 0), Vector3(0, 0, 1), Vector3(0.1, 0, 0));
+ Vector9 xi_transformed = Ad * xi;
+
+ // Verify transformation matches conjugation
+ SE23d h = SE23d().exp(xi);
+ SE23d h_transformed = g * h * g.inverse();
+ EXPECT_TRUE(h_transformed.isApprox(SE23d().exp(xi_transformed)));
+}
+
+/**
+ * Test group action on points with velocity
+ */
+TEST_F(SE23Test, Action)
+{
+ // 90° rotation around Z-axis + translation in X + velocity
+ SE23d g(SE3d(SO3d(pi/2, Vector3(0, 0, 1)), Vector3(1, 0, 0)),
+ Vector3(0.1, 0.2, 0));
+
+ // Point with velocity
+ Vector3 p(1, 0, 0);
+ Vector3 v(0.1, 0, 0);
+ Vector9 state;
+ state << p, v, Vector3::Zero();
+
+ // Test transformation
+ Vector9 transformed = g.act(state);
+ Vector3 p_new = transformed.head<3>();
+ Vector3 v_new = transformed.segment<3>(3);
+
+ // Check position transformation
+ EXPECT_NEAR(p_new[0], 1.0, eps); // Original x-translation + point x
+ EXPECT_NEAR(p_new[1], 1.0, eps); // Rotated point x
+ EXPECT_NEAR(p_new[2], 0.0, eps);
+
+ // Check velocity transformation
+ Vector3 expected_vel = g.pose().rotation().act(v) + g.velocity();
+ EXPECT_TRUE(v_new.isApprox(expected_vel));
+}
+
+/**
+ * Test interpolation
+ */
+TEST_F(SE23Test, Interpolation)
+{
+ // Create start and end states
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ SE23d start = SE23d::identity();
+ SE23d end(SE3d(SO3d(pi/2, axis), Vector3(2, 0, 0)),
+ Vector3(0.2, 0, 0));
+
+ // Test midpoint interpolation
+ SE23d mid = interpolate(start, end, 0.5);
+
+ // Verify midpoint properties
+ EXPECT_TRUE(mid.pose().rotation().isApprox(SO3d(pi/4, axis)));
+ EXPECT_TRUE(mid.pose().translation().isApprox(Vector3(1, 0, 0)));
+ EXPECT_TRUE(mid.velocity().isApprox(Vector3(0.1, 0, 0)));
+
+ // Test boundary conditions
+ EXPECT_TRUE(interpolate(start, end, 0.0).isApprox(start));
+ EXPECT_TRUE(interpolate(start, end, 1.0).isApprox(end));
+}
+
+} // namespace sofa::component::cosserat::liegroups::testing
diff --git a/Tests/liegroups/SE3Test.cpp b/Tests/liegroups/SE3Test.cpp
new file mode 100644
index 0000000..4c64ea3
--- /dev/null
+++ b/Tests/liegroups/SE3Test.cpp
@@ -0,0 +1,259 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::testing {
+
+using namespace sofa::testing;
+
+/**
+ * Test suite for SE3 Lie group implementation
+ */
+class SE3Test : public BaseTest
+{
+protected:
+ using SE3d = SE3;
+ using SO3d = SO3;
+ using Vector3 = Eigen::Vector3d;
+ using Vector6 = Eigen::Matrix;
+ using Matrix3 = Eigen::Matrix3d;
+ using Matrix4 = Eigen::Matrix4d;
+ using Matrix6 = Eigen::Matrix;
+ using Quaternion = Eigen::Quaterniond;
+
+ const double pi = M_PI;
+ const double eps = 1e-10;
+
+ // Helper function to create twist vector
+ Vector6 twist(const Vector3& v, const Vector3& omega) {
+ Vector6 xi;
+ xi << v, omega;
+ return xi;
+ }
+
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/**
+ * Test identity element properties
+ */
+TEST_F(SE3Test, Identity)
+{
+ SE3d id = SE3d::identity();
+
+ // Test identity components
+ EXPECT_TRUE(id.rotation().quaternion().isApprox(Quaternion::Identity()));
+ EXPECT_TRUE(id.translation().isZero());
+
+ // Test identity matrix
+ EXPECT_TRUE(id.matrix().isApprox(Matrix4::Identity()));
+
+ // Test composition with identity
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ Vector3 trans(1, 2, 3);
+ SE3d g(SO3d(pi/4, axis), trans); // 45° rotation + translation
+ EXPECT_TRUE((g * id).isApprox(g));
+ EXPECT_TRUE((id * g).isApprox(g));
+}
+
+/**
+ * Test group operation (rigid transformation composition)
+ */
+TEST_F(SE3Test, GroupOperation)
+{
+ // Create transformations with different rotations and translations
+ Vector3 axis1 = Vector3(1, 0, 0); // X-axis
+ Vector3 axis2 = Vector3(0, 1, 0); // Y-axis
+ Vector3 t1(1, 0, 0); // X-translation
+ Vector3 t2(0, 1, 0); // Y-translation
+
+ SE3d g1(SO3d(pi/2, axis1), t1); // 90° around X + X-translation
+ SE3d g2(SO3d(pi/2, axis2), t2); // 90° around Y + Y-translation
+
+ // Test composition
+ SE3d g12 = g1 * g2;
+
+ // Verify using homogeneous matrices
+ Matrix4 T1 = g1.matrix();
+ Matrix4 T2 = g2.matrix();
+ Matrix4 T12 = g12.matrix();
+ EXPECT_TRUE((T1 * T2).isApprox(T12));
+
+ // Test non-commutativity
+ SE3d g21 = g2 * g1;
+ EXPECT_FALSE(g12.isApprox(g21));
+}
+
+/**
+ * Test inverse operation
+ */
+TEST_F(SE3Test, Inverse)
+{
+ Vector3 axis = Vector3(1, 1, 0).normalized();
+ Vector3 trans(1, 2, 3);
+ SE3d g(SO3d(pi/3, axis), trans); // 60° rotation + translation
+ SE3d inv = g.inverse();
+
+ // Test inverse properties
+ EXPECT_TRUE((g * inv).isApprox(SE3d::identity()));
+ EXPECT_TRUE((inv * g).isApprox(SE3d::identity()));
+
+ // Test matrix inverse
+ EXPECT_TRUE(inv.matrix().isApprox(g.matrix().inverse()));
+
+ // Test inverse translation
+ Vector3 expected_trans = -(g.rotation().inverse().act(trans));
+ EXPECT_TRUE(inv.translation().isApprox(expected_trans));
+}
+
+/**
+ * Test exponential and logarithm maps
+ */
+TEST_F(SE3Test, ExpLog)
+{
+ // Test exp(log(g)) = g
+ Vector3 axis = Vector3(1, 2, 3).normalized();
+ Vector3 trans(4, 5, 6);
+ SE3d g(SO3d(pi/4, axis), trans);
+ Vector6 xi = g.log();
+ SE3d g2 = SE3d().exp(xi);
+ EXPECT_TRUE(g.isApprox(g2));
+
+ // Test pure translation
+ Vector6 xi_trans = twist(Vector3(1, 2, 3), Vector3::Zero());
+ SE3d g_trans = SE3d().exp(xi_trans);
+ EXPECT_TRUE(g_trans.rotation().isApprox(SO3d::identity()));
+ EXPECT_TRUE(g_trans.translation().isApprox(Vector3(1, 2, 3)));
+
+ // Test pure rotation
+ Vector6 xi_rot = twist(Vector3::Zero(), Vector3(0, 0, pi/2));
+ SE3d g_rot = SE3d().exp(xi_rot);
+ EXPECT_TRUE(g_rot.translation().isZero());
+ EXPECT_TRUE(g_rot.rotation().isApprox(SO3d(pi/2, Vector3(0, 0, 1))));
+
+ // Test small motion approximation
+ Vector6 xi_small = twist(Vector3(0.001, 0.001, 0.001),
+ Vector3(0.001, 0.001, 0.001));
+ SE3d g_small = SE3d().exp(xi_small);
+ Matrix4 expected = Matrix4::Identity();
+ expected.block<3,3>(0,0) = Matrix3::Identity() + SO3d::hat(xi_small.tail<3>());
+ expected.block<3,1>(0,3) = xi_small.head<3>();
+ EXPECT_TRUE(g_small.matrix().isApprox(expected, 1e-6));
+}
+
+/**
+ * Test adjoint representation
+ */
+TEST_F(SE3Test, Adjoint)
+{
+ Vector3 axis = Vector3(0, 0, 1); // Z-axis
+ Vector3 trans(1, 2, 0);
+ SE3d g(SO3d(pi/2, axis), trans); // 90° around Z + translation
+ Matrix6 Ad = g.adjoint();
+
+ // Test adjoint structure
+ Matrix3 R = g.rotation().matrix();
+ Matrix3 t_hat = SO3d::hat(trans);
+
+ EXPECT_TRUE(Ad.block<3,3>(0,0).isApprox(R));
+ EXPECT_TRUE(Ad.block<3,3>(0,3).isApprox(t_hat * R));
+ EXPECT_TRUE(Ad.block<3,3>(3,0).isZero());
+ EXPECT_TRUE(Ad.block<3,3>(3,3).isApprox(R));
+
+ // Test adjoint action on twists
+ Vector6 xi = twist(Vector3(1, 0, 0), Vector3(0, 0, 1));
+ Vector6 xi_transformed = Ad * xi;
+
+ // Verify transformation matches conjugation
+ SE3d h = SE3d().exp(xi);
+ SE3d h_transformed = g * h * g.inverse();
+ EXPECT_TRUE(h_transformed.isApprox(SE3d().exp(xi_transformed)));
+}
+
+/**
+ * Test group action on points
+ */
+TEST_F(SE3Test, Action)
+{
+ // 90° rotation around Z-axis + translation in X
+ SE3d g(SO3d(pi/2, Vector3(0, 0, 1)), Vector3(1, 0, 0));
+ Vector3 p(1, 0, 0); // Point on x-axis
+
+ // Should map (1,0,0) to (1,1,0)
+ Vector3 q = g.act(p);
+ EXPECT_NEAR(q[0], 1.0, eps);
+ EXPECT_NEAR(q[1], 1.0, eps);
+ EXPECT_NEAR(q[2], 0.0, eps);
+
+ // Test that action matches homogeneous transformation
+ Vector4 p_hom;
+ p_hom << p, 1.0;
+ Vector4 q_hom = g.matrix() * p_hom;
+ EXPECT_TRUE(q.isApprox(q_hom.head<3>()));
+}
+
+/**
+ * Test interpolation
+ */
+TEST_F(SE3Test, Interpolation)
+{
+ // Create start and end transformations
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ SE3d start = SE3d::identity();
+ SE3d end(SO3d(pi/2, axis), Vector3(2, 0, 0));
+
+ // Test midpoint interpolation
+ SE3d mid = interpolate(start, end, 0.5);
+
+ // Verify midpoint properties
+ EXPECT_TRUE(mid.rotation().isApprox(SO3d(pi/4, axis)));
+ EXPECT_TRUE(mid.translation().isApprox(Vector3(1, 0, 0)));
+
+ // Test boundary conditions
+ EXPECT_TRUE(interpolate(start, end, 0.0).isApprox(start));
+ EXPECT_TRUE(interpolate(start, end, 1.0).isApprox(end));
+}
+
+/**
+ * Test conversion between different representations
+ */
+TEST_F(SE3Test, Conversions)
+{
+ // Create transformation from rotation and translation
+ Vector3 axis = Vector3(1, 0, 0); // X-axis
+ double angle = pi/3; // 60°
+ Vector3 trans(1, 2, 3);
+ SE3d g(SO3d(angle, axis), trans);
+
+ // Test conversion to/from homogeneous matrix
+ Matrix4 T = Matrix4::Identity();
+ T.block<3,3>(0,0) = Eigen::AngleAxisd(angle, axis).toRotationMatrix();
+ T.block<3,1>(0,3) = trans;
+
+ SE3d g2(T);
+ EXPECT_TRUE(g.isApprox(g2));
+ EXPECT_TRUE(g.matrix().isApprox(T));
+}
+
+} // namespace sofa::component::cosserat::liegroups::testing
diff --git a/Tests/liegroups/SGal3Test.cpp b/Tests/liegroups/SGal3Test.cpp
new file mode 100644
index 0000000..a4f68e2
--- /dev/null
+++ b/Tests/liegroups/SGal3Test.cpp
@@ -0,0 +1,268 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::testing {
+
+using namespace sofa::testing;
+
+/**
+ * Test suite for SGal(3) Lie group implementation
+ */
+class SGal3Test : public BaseTest
+{
+protected:
+ using SGal3d = SGal3;
+ using SE3d = SE3;
+ using SO3d = SO3;
+ using Vector3 = Eigen::Vector3d;
+ using Vector10 = Eigen::Matrix;
+ using Matrix3 = Eigen::Matrix3d;
+ using Matrix4 = Eigen::Matrix4d;
+ using Matrix10 = Eigen::Matrix;
+ using Quaternion = Eigen::Quaterniond;
+
+ const double pi = M_PI;
+ const double eps = 1e-10;
+
+ // Helper function to create Galilean twist vector
+ Vector10 twist(const Vector3& v, const Vector3& omega, const Vector3& beta, double tau) {
+ Vector10 xi;
+ xi << v, omega, beta, tau;
+ return xi;
+ }
+
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/**
+ * Test identity element properties
+ */
+TEST_F(SGal3Test, Identity)
+{
+ SGal3d id = SGal3d::identity();
+
+ // Test identity components
+ EXPECT_TRUE(id.pose().rotation().quaternion().isApprox(Quaternion::Identity()));
+ EXPECT_TRUE(id.pose().translation().isZero());
+ EXPECT_TRUE(id.velocity().isZero());
+ EXPECT_NEAR(id.time(), 0.0, eps);
+
+ // Test composition with identity
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ Vector3 trans(1, 2, 3);
+ Vector3 vel(0.1, 0.2, 0.3);
+ double t = 1.0;
+ SGal3d g(SE3d(SO3d(pi/4, axis), trans), vel, t);
+ EXPECT_TRUE((g * id).isApprox(g));
+ EXPECT_TRUE((id * g).isApprox(g));
+}
+
+/**
+ * Test group operation (Galilean transformation composition)
+ */
+TEST_F(SGal3Test, GroupOperation)
+{
+ // Create transformations with different components
+ Vector3 axis1 = Vector3(1, 0, 0); // X-axis
+ Vector3 axis2 = Vector3(0, 1, 0); // Y-axis
+ Vector3 t1(1, 0, 0); // X-translation
+ Vector3 t2(0, 1, 0); // Y-translation
+ Vector3 v1(0.1, 0, 0); // X-velocity
+ Vector3 v2(0, 0.2, 0); // Y-velocity
+ double time1 = 1.0;
+ double time2 = 2.0;
+
+ SGal3d g1(SE3d(SO3d(pi/2, axis1), t1), v1, time1);
+ SGal3d g2(SE3d(SO3d(pi/2, axis2), t2), v2, time2);
+
+ // Test composition
+ SGal3d g12 = g1 * g2;
+
+ // Verify pose composition
+ EXPECT_TRUE(g12.pose().isApprox(g1.pose() * g2.pose()));
+
+ // Verify velocity transformation
+ Vector3 expected_vel = v1 + g1.pose().rotation().act(v2);
+ EXPECT_TRUE(g12.velocity().isApprox(expected_vel));
+
+ // Verify time addition
+ EXPECT_NEAR(g12.time(), time1 + time2, eps);
+
+ // Test non-commutativity
+ SGal3d g21 = g2 * g1;
+ EXPECT_FALSE(g12.isApprox(g21));
+}
+
+/**
+ * Test inverse operation
+ */
+TEST_F(SGal3Test, Inverse)
+{
+ Vector3 axis = Vector3(1, 1, 0).normalized();
+ Vector3 trans(1, 2, 3);
+ Vector3 vel(0.1, 0.2, 0.3);
+ double time = 1.5;
+ SGal3d g(SE3d(SO3d(pi/3, axis), trans), vel, time);
+ SGal3d inv = g.inverse();
+
+ // Test inverse properties
+ EXPECT_TRUE((g * inv).isApprox(SGal3d::identity()));
+ EXPECT_TRUE((inv * g).isApprox(SGal3d::identity()));
+
+ // Test inverse pose
+ EXPECT_TRUE(inv.pose().isApprox(g.pose().inverse()));
+
+ // Test inverse velocity
+ Vector3 expected_vel = -g.pose().rotation().inverse().act(vel);
+ EXPECT_TRUE(inv.velocity().isApprox(expected_vel));
+
+ // Test inverse time
+ EXPECT_NEAR(inv.time(), -time, eps);
+}
+
+/**
+ * Test exponential and logarithm maps
+ */
+TEST_F(SGal3Test, ExpLog)
+{
+ // Test exp(log(g)) = g
+ Vector3 axis = Vector3(1, 2, 3).normalized();
+ Vector3 trans(4, 5, 6);
+ Vector3 vel(0.1, 0.2, 0.3);
+ double time = 1.0;
+ SGal3d g(SE3d(SO3d(pi/4, axis), trans), vel, time);
+ Vector10 xi = g.log();
+ SGal3d g2 = SGal3d().exp(xi);
+ EXPECT_TRUE(g.isApprox(g2));
+
+ // Test pure translation
+ Vector10 xi_trans = twist(Vector3(1, 2, 3), Vector3::Zero(), Vector3::Zero(), 0);
+ SGal3d g_trans = SGal3d().exp(xi_trans);
+ EXPECT_TRUE(g_trans.pose().rotation().isApprox(SO3d::identity()));
+ EXPECT_TRUE(g_trans.pose().translation().isApprox(Vector3(1, 2, 3)));
+ EXPECT_TRUE(g_trans.velocity().isZero());
+ EXPECT_NEAR(g_trans.time(), 0.0, eps);
+
+ // Test pure velocity and time
+ Vector10 xi_vel = twist(Vector3::Zero(), Vector3::Zero(), Vector3(0.1, 0.2, 0.3), 1.0);
+ SGal3d g_vel = SGal3d().exp(xi_vel);
+ EXPECT_TRUE(g_vel.pose().isApprox(SE3d::identity()));
+ EXPECT_TRUE(g_vel.velocity().isApprox(Vector3(0.1, 0.2, 0.3)));
+ EXPECT_NEAR(g_vel.time(), 1.0, eps);
+}
+
+/**
+ * Test adjoint representation
+ */
+TEST_F(SGal3Test, Adjoint)
+{
+ Vector3 axis = Vector3(0, 0, 1); // Z-axis
+ Vector3 trans(1, 2, 0);
+ Vector3 vel(0.1, 0.2, 0);
+ double time = 1.0;
+ SGal3d g(SE3d(SO3d(pi/2, axis), trans), vel, time);
+ Matrix10 Ad = g.adjoint();
+
+ // Test adjoint structure
+ Matrix3 R = g.pose().rotation().matrix();
+ Matrix3 t_hat = SO3d::hat(trans);
+ Matrix3 v_hat = SO3d::hat(vel);
+
+ // Verify block structure
+ EXPECT_TRUE(Ad.block<3,3>(0,0).isApprox(R));
+ EXPECT_TRUE(Ad.block<3,3>(0,3).isApprox(t_hat * R));
+ EXPECT_TRUE(Ad.block<3,3>(0,6).isApprox(v_hat * R));
+ EXPECT_TRUE(Ad.block<3,3>(3,0).isZero());
+ EXPECT_TRUE(Ad.block<3,3>(3,3).isApprox(R));
+ EXPECT_TRUE(Ad.block<3,3>(6,6).isApprox(R));
+ EXPECT_NEAR(Ad(9,9), 1.0, eps);
+}
+
+/**
+ * Test group action on points with velocity and time
+ */
+TEST_F(SGal3Test, Action)
+{
+ // Create a Galilean transformation
+ SGal3d g(SE3d(SO3d(pi/2, Vector3(0, 0, 1)), Vector3(1, 0, 0)),
+ Vector3(0.1, 0.2, 0), 1.0);
+
+ // Point with velocity and time
+ Vector3 p(1, 0, 0);
+ Vector3 v(0.1, 0, 0);
+ Vector3 boost(0.01, 0, 0);
+ double t = 0.5;
+ Vector10 state;
+ state << p, v, boost, t;
+
+ // Test transformation
+ Vector10 transformed = g.act(state);
+ Vector3 p_new = transformed.head<3>();
+ Vector3 v_new = transformed.segment<3>(3);
+ Vector3 boost_new = transformed.segment<3>(6);
+ double t_new = transformed[9];
+
+ // Check position transformation with time evolution
+ Vector3 expected_pos = g.pose().act(p) + g.velocity() * t;
+ EXPECT_TRUE(p_new.isApprox(expected_pos));
+
+ // Check velocity transformation
+ Vector3 expected_vel = g.pose().rotation().act(v) + g.velocity();
+ EXPECT_TRUE(v_new.isApprox(expected_vel));
+
+ // Check boost transformation
+ Vector3 expected_boost = g.pose().rotation().act(boost);
+ EXPECT_TRUE(boost_new.isApprox(expected_boost));
+
+ // Check time transformation
+ EXPECT_NEAR(t_new, t + g.time(), eps);
+}
+
+/**
+ * Test interpolation
+ */
+TEST_F(SGal3Test, Interpolation)
+{
+ // Create start and end states
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ SGal3d start = SGal3d::identity();
+ SGal3d end(SE3d(SO3d(pi/2, axis), Vector3(2, 0, 0)),
+ Vector3(0.2, 0, 0), 2.0);
+
+ // Test midpoint interpolation
+ SGal3d mid = interpolate(start, end, 0.5);
+
+ // Verify midpoint properties
+ EXPECT_TRUE(mid.pose().rotation().isApprox(SO3d(pi/4, axis)));
+ EXPECT_TRUE(mid.pose().translation().isApprox(Vector3(1, 0, 0)));
+ EXPECT_TRUE(mid.velocity().isApprox(Vector3(0.1, 0, 0)));
+ EXPECT_NEAR(mid.time(), 1.0, eps);
+
+ // Test boundary conditions
+ EXPECT_TRUE(interpolate(start, end, 0.0).isApprox(start));
+ EXPECT_TRUE(interpolate(start, end, 1.0).isApprox(end));
+}
+
+} // namespace sofa::component::cosserat::liegroups::testing
diff --git a/Tests/liegroups/SO2Test.cpp b/Tests/liegroups/SO2Test.cpp
new file mode 100644
index 0000000..9287665
--- /dev/null
+++ b/Tests/liegroups/SO2Test.cpp
@@ -0,0 +1,170 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::testing {
+
+using namespace sofa::testing;
+
+/**
+ * Test suite for SO2 Lie group implementation
+ */
+class SO2Test : public BaseTest
+{
+protected:
+ using SO2d = SO2;
+ using Vector2 = Eigen::Vector2d;
+ using Matrix2 = Eigen::Matrix2d;
+
+ const double pi = M_PI;
+ const double eps = 1e-10;
+
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/**
+ * Test identity element properties
+ */
+TEST_F(SO2Test, Identity)
+{
+ SO2d id = SO2d::identity();
+ EXPECT_NEAR(id.angle(), 0.0, eps);
+
+ // Test right and left identity
+ SO2d rot(pi/4); // 45-degree rotation
+ EXPECT_TRUE((rot * id).isApprox(rot));
+ EXPECT_TRUE((id * rot).isApprox(rot));
+
+ // Test identity matrix
+ EXPECT_TRUE(id.matrix().isApprox(Matrix2::Identity()));
+}
+
+/**
+ * Test group operation (rotation composition)
+ */
+TEST_F(SO2Test, GroupOperation)
+{
+ SO2d a(pi/4); // 45 degrees
+ SO2d b(pi/3); // 60 degrees
+ SO2d c = a * b; // 105 degrees
+
+ EXPECT_NEAR(c.angle(), pi/4 + pi/3, eps);
+
+ // Test that composition matches matrix multiplication
+ Matrix2 Ra = a.matrix();
+ Matrix2 Rb = b.matrix();
+ Matrix2 Rc = c.matrix();
+ EXPECT_TRUE((Ra * Rb).isApprox(Rc));
+}
+
+/**
+ * Test inverse operation
+ */
+TEST_F(SO2Test, Inverse)
+{
+ SO2d rot(pi/3); // 60-degree rotation
+ SO2d inv = rot.inverse();
+
+ // Test that inverse rotation has opposite angle
+ EXPECT_NEAR(inv.angle(), -pi/3, eps);
+
+ // Test that rot * inv = inv * rot = identity
+ EXPECT_TRUE((rot * inv).isApprox(SO2d::identity()));
+ EXPECT_TRUE((inv * rot).isApprox(SO2d::identity()));
+
+ // Test that inverse matches matrix inverse
+ EXPECT_TRUE(inv.matrix().isApprox(rot.matrix().inverse()));
+}
+
+/**
+ * Test exponential and logarithm maps
+ */
+TEST_F(SO2Test, ExpLog)
+{
+ // Test exp(log(g)) = g
+ SO2d rot(pi/6); // 30-degree rotation
+ auto angle = rot.log();
+ auto rot2 = SO2d().exp(angle);
+ EXPECT_TRUE(rot.isApprox(rot2));
+
+ // Test log(exp(w)) = w
+ double w = pi/4; // Angular velocity
+ auto rot3 = SO2d().exp(Vector2::Constant(w));
+ EXPECT_NEAR(rot3.log()[0], w, eps);
+}
+
+/**
+ * Test adjoint representation
+ */
+TEST_F(SO2Test, Adjoint)
+{
+ SO2d rot(pi/4); // 45-degree rotation
+
+ // For SO(2), adjoint should always be identity
+ EXPECT_TRUE(rot.adjoint().isApprox(Matrix2::Identity()));
+}
+
+/**
+ * Test group action on points
+ */
+TEST_F(SO2Test, Action)
+{
+ SO2d rot(pi/2); // 90-degree rotation
+ Vector2 p(1.0, 0.0); // Point on x-axis
+
+ // 90-degree rotation should map (1,0) to (0,1)
+ Vector2 q = rot.act(p);
+ EXPECT_NEAR(q[0], 0.0, eps);
+ EXPECT_NEAR(q[1], 1.0, eps);
+
+ // Test that action matches matrix multiplication
+ EXPECT_TRUE(q.isApprox(rot.matrix() * p));
+}
+
+/**
+ * Test angle normalization
+ */
+TEST_F(SO2Test, AngleNormalization)
+{
+ // Test that angles are normalized to [-π, π]
+ SO2d rot1(3*pi/2); // 270 degrees
+ SO2d rot2(-3*pi/2); // -270 degrees
+
+ EXPECT_NEAR(rot1.angle(), -pi/2, eps);
+ EXPECT_NEAR(rot2.angle(), pi/2, eps);
+}
+
+/**
+ * Test interpolation
+ */
+TEST_F(SO2Test, Interpolation)
+{
+ SO2d start(0); // 0 degrees
+ SO2d end(pi/2); // 90 degrees
+ SO2d mid = slerp(start, end, 0.5);
+
+ // Midpoint should be 45-degree rotation
+ EXPECT_NEAR(mid.angle(), pi/4, eps);
+}
+
+} // namespace sofa::component::cosserat::liegroups::testing
diff --git a/Tests/liegroups/SO3Test.cpp b/Tests/liegroups/SO3Test.cpp
new file mode 100644
index 0000000..3ede165
--- /dev/null
+++ b/Tests/liegroups/SO3Test.cpp
@@ -0,0 +1,248 @@
+/******************************************************************************
+* 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 . *
+******************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::cosserat::liegroups::testing {
+
+using namespace sofa::testing;
+
+/**
+ * Test suite for SO3 Lie group implementation
+ */
+class SO3Test : public BaseTest
+{
+protected:
+ using SO3d = SO3;
+ using Vector3 = Eigen::Vector3d;
+ using Matrix3 = Eigen::Matrix3d;
+ using Quaternion = Eigen::Quaterniond;
+
+ const double pi = M_PI;
+ const double eps = 1e-10;
+
+ // Helper function to create rotation vector
+ Vector3 rotationVector(double angle, const Vector3& axis) {
+ return axis.normalized() * angle;
+ }
+
+ void SetUp() override {}
+ void TearDown() override {}
+};
+
+/**
+ * Test identity element properties
+ */
+TEST_F(SO3Test, Identity)
+{
+ SO3d id = SO3d::identity();
+
+ // Test identity quaternion properties
+ EXPECT_NEAR(id.quaternion().w(), 1.0, eps);
+ EXPECT_NEAR(id.quaternion().x(), 0.0, eps);
+ EXPECT_NEAR(id.quaternion().y(), 0.0, eps);
+ EXPECT_NEAR(id.quaternion().z(), 0.0, eps);
+
+ // Test identity matrix
+ EXPECT_TRUE(id.matrix().isApprox(Matrix3::Identity()));
+
+ // Test composition with identity
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ SO3d rot(pi/4, axis); // 45-degree rotation around (1,1,1)
+ EXPECT_TRUE((rot * id).isApprox(rot));
+ EXPECT_TRUE((id * rot).isApprox(rot));
+}
+
+/**
+ * Test group operation (rotation composition)
+ */
+TEST_F(SO3Test, GroupOperation)
+{
+ // Create two rotations with different axes
+ Vector3 axis1 = Vector3(1, 0, 0); // X-axis
+ Vector3 axis2 = Vector3(0, 1, 0); // Y-axis
+ SO3d rx(pi/2, axis1); // 90° around X
+ SO3d ry(pi/2, axis2); // 90° around Y
+
+ // Test composition
+ SO3d rxy = rx * ry;
+
+ // Verify using matrix multiplication
+ Matrix3 Rx = rx.matrix();
+ Matrix3 Ry = ry.matrix();
+ Matrix3 Rxy = rxy.matrix();
+ EXPECT_TRUE((Rx * Ry).isApprox(Rxy));
+
+ // Test non-commutativity
+ SO3d ryx = ry * rx;
+ EXPECT_FALSE(rxy.isApprox(ryx));
+}
+
+/**
+ * Test inverse operation
+ */
+TEST_F(SO3Test, Inverse)
+{
+ Vector3 axis = Vector3(1, 1, 0).normalized();
+ SO3d rot(pi/3, axis); // 60° rotation
+ SO3d inv = rot.inverse();
+
+ // Test inverse properties
+ EXPECT_TRUE((rot * inv).isApprox(SO3d::identity()));
+ EXPECT_TRUE((inv * rot).isApprox(SO3d::identity()));
+
+ // Test matrix inverse
+ EXPECT_TRUE(inv.matrix().isApprox(rot.matrix().inverse()));
+
+ // Test quaternion conjugate
+ EXPECT_TRUE(inv.quaternion().coeffs().isApprox(rot.quaternion().conjugate().coeffs()));
+}
+
+/**
+ * Test exponential and logarithm maps
+ */
+TEST_F(SO3Test, ExpLog)
+{
+ // Test exp(log(g)) = g
+ Vector3 axis = Vector3(1, 2, 3).normalized();
+ SO3d rot(pi/4, axis); // 45° rotation
+ Vector3 omega = rot.log();
+ SO3d rot2 = SO3d().exp(omega);
+ EXPECT_TRUE(rot.isApprox(rot2));
+
+ // Test small rotation approximation
+ Vector3 small_omega = Vector3(0.001, 0.001, 0.001);
+ SO3d small_rot = SO3d().exp(small_omega);
+ Matrix3 expected = Matrix3::Identity() + SO3d::hat(small_omega);
+ EXPECT_TRUE(small_rot.matrix().isApprox(expected, 1e-6));
+
+ // Test rotation vector recovery
+ Vector3 rot_vec = rotationVector(pi/3, Vector3(1,0,0));
+ SO3d g = SO3d().exp(rot_vec);
+ EXPECT_TRUE(g.log().isApprox(rot_vec));
+}
+
+/**
+ * Test adjoint representation
+ */
+TEST_F(SO3Test, Adjoint)
+{
+ Vector3 axis = Vector3(0, 0, 1); // Z-axis
+ SO3d rot(pi/2, axis); // 90° around Z
+ Matrix3 Ad = rot.adjoint();
+
+ // Adjoint should be the rotation matrix itself for SO(3)
+ EXPECT_TRUE(Ad.isApprox(rot.matrix()));
+
+ // Test adjoint action on vectors
+ Vector3 v(1, 0, 0);
+ EXPECT_TRUE((Ad * v).isApprox(rot.act(v)));
+}
+
+/**
+ * Test group action on points
+ */
+TEST_F(SO3Test, Action)
+{
+ // 90° rotation around Z-axis
+ SO3d rot(pi/2, Vector3(0, 0, 1));
+ Vector3 p(1, 0, 0); // Point on x-axis
+
+ // Should map (1,0,0) to (0,1,0)
+ Vector3 q = rot.act(p);
+ EXPECT_NEAR(q[0], 0.0, eps);
+ EXPECT_NEAR(q[1], 1.0, eps);
+ EXPECT_NEAR(q[2], 0.0, eps);
+
+ // Test that action preserves length
+ EXPECT_NEAR(p.norm(), q.norm(), eps);
+
+ // Test that action matches matrix multiplication
+ EXPECT_TRUE(q.isApprox(rot.matrix() * p));
+}
+
+/**
+ * Test hat and vee operators
+ */
+TEST_F(SO3Test, HatVee)
+{
+ Vector3 omega(1, 2, 3);
+ Matrix3 Omega = SO3d::hat(omega);
+
+ // Test skew-symmetry
+ EXPECT_TRUE(Omega.transpose().isApprox(-Omega));
+
+ // Test vee operator (inverse of hat)
+ EXPECT_TRUE(SO3d::vee(Omega).isApprox(omega));
+
+ // Test that hat(omega) * v = omega × v
+ Vector3 v(4, 5, 6);
+ EXPECT_TRUE((Omega * v).isApprox(omega.cross(v)));
+}
+
+/**
+ * Test interpolation
+ */
+TEST_F(SO3Test, Interpolation)
+{
+ // Create start and end rotations
+ Vector3 axis = Vector3(1, 1, 1).normalized();
+ SO3d start = SO3d::identity();
+ SO3d end(pi/2, axis); // 90° rotation
+
+ // Test midpoint interpolation
+ SO3d mid = interpolate(start, end, 0.5);
+
+ // Midpoint should be 45° rotation around same axis
+ SO3d expected(pi/4, axis);
+ EXPECT_TRUE(mid.isApprox(expected));
+
+ // Test boundary conditions
+ EXPECT_TRUE(interpolate(start, end, 0.0).isApprox(start));
+ EXPECT_TRUE(interpolate(start, end, 1.0).isApprox(end));
+}
+
+/**
+ * Test conversion between different rotation representations
+ */
+TEST_F(SO3Test, Conversions)
+{
+ // Create rotation from angle-axis
+ Vector3 axis = Vector3(1, 0, 0); // X-axis
+ double angle = pi/3; // 60°
+ SO3d rot(angle, axis);
+
+ // Test conversion to/from quaternion
+ Quaternion quat(Eigen::AngleAxisd(angle, axis));
+ EXPECT_TRUE(rot.quaternion().coeffs().isApprox(quat.coeffs()));
+
+ // Test conversion to/from rotation matrix
+ Matrix3 R = quat.toRotationMatrix();
+ EXPECT_TRUE(rot.matrix().isApprox(R));
+
+ // Test conversion to/from angle-axis
+ Eigen::AngleAxisd aa = rot.angleAxis();
+ EXPECT_NEAR(aa.angle(), angle, eps);
+ EXPECT_TRUE(aa.axis().isApprox(axis));
+}
+
+} // namespace sofa::component::cosserat::liegroups::testing
diff --git a/src/Cosserat/CMakeLists.txt b/src/Cosserat/CMakeLists.txt
index fb8ef70..ec4c6f8 100644
--- a/src/Cosserat/CMakeLists.txt
+++ b/src/Cosserat/CMakeLists.txt
@@ -1,71 +1,73 @@
-project(Cosserat.src VERSION 21.06.99)
-
-set(HEADER_FILES
- config.h
-
- mapping/BaseCosserat.h
- mapping/BaseCosserat.inl
- mapping/DiscreteCosseratMapping.h
- mapping/DiscreteCosseratMapping.inl
- mapping/DiscretDynamicCosseratMapping.h
- mapping/DiscreteDynamicCosseratMapping.inl
- mapping/DifferenceMultiMapping.h
- mapping/DifferenceMultiMapping.inl
- mapping/RigidDistanceMapping.h
- mapping/RigidDistanceMapping.inl
- mapping/LegendrePolynomialsMapping.h
- mapping/LegendrePolynomialsMapping.inl
-
- engine/ProjectionEngine.h
- engine/ProjectionEngine.inl
-
+cmake_minimum_required(VERSION 3.12)
+project(Cosserat_SRC)
+set(HEADER_FILES
+ config.h.in
+ fwd.h
+ types.h
+ initCosserat.cpp
+ # Lie group implementations
+ liegroups/Types.h
+ liegroups/LieGroupBase.h
+ liegroups/LieGroupBase.inl
+ liegroups/RealSpace.h
+ liegroups/RealSpace.inl
+ liegroups/SO2.h
+ liegroups/SO2.inl
+ liegroups/SE2.h
+ liegroups/SE2.inl
+ liegroups/SO3.h
+ liegroups/SO3.inl
+ liegroups/SE3.h
+ liegroups/SE3.inl
+ liegroups/SE23.h
+ liegroups/SE23.inl
+ liegroups/SGal3.h
+ liegroups/SGal3.inl
+ liegroups/Bundle.h
+ liegroups/Bundle.inl
+ # Existing components
+ engine/GeometricStiffnessEngine.h
+ engine/GeometricStiffnessEngine.inl
forcefield/BeamHookeLawForceField.h
forcefield/BeamHookeLawForceField.inl
- forcefield/BeamHookeLawForceFieldRigid.h
- forcefield/BeamHookeLawForceFieldRigid.inl
- forcefield/CosseratInternalActuation.h
- forcefield/CosseratInternalActuation.inl
- forcefield/MyUniformVelocityDampingForceField.h
- forcefield/MyUniformVelocityDampingForceField.inl
-
- constraint/CosseratSlidingConstraint.h
- constraint/CosseratSlidingConstraint.inl
- constraint/QPSlidingConstraint.h
- constraint/QPSlidingConstraint.inl
- constraint/CosseratNeedleSlidingConstraint.h
- constraint/CosseratNeedleSlidingConstraint.inl
- )
+ forcefield/LinearForceField.h
+ forcefield/LinearForceField.inl
+ mapping/AdaptiveBeamMapping.h
+ mapping/AdaptiveBeamMapping.inl
+ mapping/CosseratNonLinearMapping2D.h
+ mapping/CosseratNonLinearMapping2D.inl
+ mapping/DifferenceMultiMapping.h
+ mapping/DifferenceMultiMapping.inl
+ constraint/UnilateralPlaneConstraint.h
+ constraint/UnilateralPlaneConstraint.inl
+)
set(SOURCE_FILES
initCosserat.cpp
-
- mapping/BaseCosserat.cpp
- mapping/DiscreteCosseratMapping.cpp
- mapping/DiscreteDynamicCosseratMapping.cpp
- mapping/DifferenceMultiMapping.cpp
- mapping/RigidDistanceMapping.cpp
- mapping/LegendrePolynomialsMapping.cpp
-
- engine/ProjectionEngine.cpp
-
+ engine/GeometricStiffnessEngine.cpp
forcefield/BeamHookeLawForceField.cpp
- forcefield/BeamHookeLawForceFieldRigid.cpp
- forcefield/CosseratInternalActuation.cpp
- forcefield/MyUniformVelocityDampingForceField.cpp
-
- constraint/CosseratSlidingConstraint.cpp
- constraint/QPSlidingConstraint.cpp
- constraint/CosseratNeedleSlidingConstraint.cpp
- )
-
-# add_subdirectory(Binding)
+ forcefield/LinearForceField.cpp
+ mapping/AdaptiveBeamMapping.cpp
+ mapping/CosseratNonLinearMapping2D.cpp
+ mapping/DifferenceMultiMapping.cpp
+ constraint/UnilateralPlaneConstraint.cpp
+)
-add_executable(${PROJECT_NAME} ${SOURCE_FILES})
+find_package(SofaFramework REQUIRED)
+find_package(SofaBase REQUIRED)
+find_package(SofaOpenglVisual REQUIRED)
+find_package(Geometric_stiffness REQUIRED)
+find_package(Eigen3 REQUIRED)
-target_include_directories(${PROJECT_NAME} PUBLIC
- "$")
+add_library(${PROJECT_NAME} SHARED ${HEADER_FILES} ${SOURCE_FILES})
+target_link_libraries(${PROJECT_NAME} PUBLIC
+ SofaBase
+ SofaOpenglVisual
+ Geometric_stiffness
+)
+target_include_directories(${PROJECT_NAME} PUBLIC "$")
+target_include_directories(${PROJECT_NAME} PUBLIC ${EIGEN3_INCLUDE_DIR})
-target_link_libraries(${PROJECT_NAME} SofaTest SofaGTestMain SofaCore
- SofaConstraint SofaBaseMechanics SofaUserInteraction)
+set_target_properties(${PROJECT_NAME} PROPERTIES COMPILE_FLAGS "-DSOFA_BUILD_COSSERAT")
diff --git a/src/Cosserat/constraint/CosseratActuatorConstraint.h b/src/Cosserat/constraint/CosseratActuatorConstraint.h
index 7513e07..b279452 100644
--- a/src/Cosserat/constraint/CosseratActuatorConstraint.h
+++ b/src/Cosserat/constraint/CosseratActuatorConstraint.h
@@ -176,6 +176,7 @@ class CosseratActuatorConstraint : public CableModel
using CableModel::d_componentState ;
using CableModel::m_nbLines ;
using CableModel::d_constraintIndex ;
+
using CableModel::m_state ;
using CableModel::d_indices ;
using CableModel::d_pullPoint;
diff --git a/src/Cosserat/forcefield/BaseBeamHookeLawForceField.h b/src/Cosserat/forcefield/BaseBeamHookeLawForceField.h
new file mode 100644
index 0000000..b6ffbfa
--- /dev/null
+++ b/src/Cosserat/forcefield/BaseBeamHookeLawForceField.h
@@ -0,0 +1,346 @@
+/******************************************************************************
+* SOFA, Simulation Open-Framework Architecture *
+* (c) 2006-2018 INRIA, USTL, UJF, CNRS, MGH *
+* *
+* This library 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 library 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 library; if not, write to the Free Software Foundation, *
+* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
+*******************************************************************************
+* Plugin Cosserat v1.0 *
+* *
+* This plugin is also distributed under the GNU LGPL (Lesser General *
+* Public License) license with the same conditions than SOFA. *
+* *
+* Contributors: Defrost team (INRIA, University of Lille, CNRS, *
+* Ecole Centrale de Lille) *
+* *
+* Contact information: https://project.inria.fr/softrobot/contact/ *
+* *
+******************************************************************************/
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace sofa::component::forcefield
+{
+
+/**
+ * @brief Base class for beam force fields using Hooke's law
+ *
+ * This force field simulates the mechanical behavior of beam elements using Hooke's law.
+ * It provides core functionality for both uniform sections (same properties throughout the beam)
+ * and variant sections (different properties for different segments).
+ *
+ * The class handles both circular and rectangular cross-sections, and supports
+ * computation of beam properties either from material properties (Young's modulus,
+ * Poisson ratio) or from directly specified inertia parameters.
+ */
+template
+class BaseBeamHookeLawForceField : public sofa::core::behavior::ForceField
+{
+public:
+ SOFA_CLASS(SOFA_TEMPLATE(BaseBeamHookeLawForceField, DataTypes), SOFA_TEMPLATE(sofa::core::behavior::ForceField, DataTypes));
+
+ typedef sofa::core::behavior::ForceField Inherit1;
+ typedef typename DataTypes::Real Real;
+ typedef typename DataTypes::Coord Coord;
+ typedef typename DataTypes::Deriv Deriv;
+ typedef typename DataTypes::VecCoord VecCoord;
+ typedef typename DataTypes::VecDeriv VecDeriv;
+ typedef typename DataTypes::MatrixDeriv MatrixDeriv;
+ typedef Data DataVecCoord;
+ typedef Data DataVecDeriv;
+ typedef Data DataMatrixDeriv;
+ typedef sofa::core::behavior::MultiMatrixAccessor MultiMatrixAccessor;
+ typedef sofa::linearalgebra::BaseMatrix BaseMatrix;
+ typedef sofa::type::vector Vector;
+
+ // For task scheduling in multi-threaded computation
+ typedef sofa::simulation::TaskScheduler::Task Task;
+
+ // Matrix types for stiffness representation
+ typedef sofa::type::Mat<3, 3, Real> Mat33;
+ typedef sofa::type::Mat<6, 6, Real> Mat66;
+ typedef sofa::type::vector VecMat33;
+ typedef sofa::type::vector VecMat66;
+
+protected:
+ // Default constructor
+ BaseBeamHookeLawForceField();
+
+ // Destructor
+ virtual ~BaseBeamHookeLawForceField();
+
+ // Cross-section inertia values
+ Real Iy, Iz, J;
+
+ // Stiffness matrices
+ Mat33 m_K_section; // 3x3 stiffness matrix for rotational DOFs
+ Mat66 m_K_section66; // 6x6 stiffness matrix for rotational and translational DOFs
+ VecMat33 m_K_sectionList; // List of stiffness matrices for variant sections
+ VecMat66 m_K_section66List; // List of 6x6 stiffness matrices for variant sections
+
+ // Flag to track whether to compute derivative forces
+ bool compute_df;
+
+ // Flag to track initialization state
+ bool m_initialized;
+
+ // Cross-section area
+ Real m_crossSectionArea;
+
+public:
+ //////////////////////////////////////////////////////////////////////////
+ // DATA MEMBERS
+ //////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @brief Shape of the cross-section
+ * Can be "circular" (tube with external radius being radius and internal radius
+ * being innerRadius) or "rectangular" (lengthY and lengthZ)
+ */
+ Data d_crossSectionShape;
+
+ /// Young's modulus - describes the stiffness of the material
+ Data d_youngModulus;
+
+ /// Poisson's ratio - describes the compressibility of the material
+ Data d_poissonRatio;
+
+ /// List of lengths of the different beam's sections
+ Data d_length;
+
+ /// External radius of the cross section (if circular)
+ Data d_radius;
+
+ /// Internal radius of the cross section (if circular)
+ Data d_innerRadius;
+
+ /// Side length of the cross section along local y axis (if rectangular)
+ Data d_lengthY;
+
+ /// Side length of the cross section along local z axis (if rectangular)
+ Data d_lengthZ;
+
+ /// Set to true if we have variant beam sections
+ Data d_variantSections;
+
+ /// List of Young's modulus values for variant sections
+ Data d_youngModulusList;
+
+ /// List of Poisson's ratio values for variant sections
+ Data d_poissonRatioList;
+
+ /// Set to true to use provided inertia parameters instead of Young's modulus and Poisson ratio
+ Data d_useInertiaParams;
+
+ /// The inertia parameter GI (torsional rigidity)
+ Data d_GI;
+
+ /// The inertia parameter GA (shear rigidity)
+ Data d_GA;
+
+ /// The inertia parameter EA (axial rigidity)
+ Data d_EA;
+
+ /// The inertia parameter EI (bending rigidity)
+ Data d_EI;
+
+ /// The inertia parameter EIy (bending rigidity around y-axis)
+ Data d_EIy;
+
+ /// The inertia parameter EIz (bending rigidity around z-axis)
+ Data d_EIz;
+
+ /// Enable parallel processing for force computation
+ Data d_useMultiThreading;
+
+ //////////////////////////////////////////////////////////////////////////
+ // METHODS
+ //////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @brief Initializes the force field
+ *
+ * This method is called during initialization to set up the force field parameters.
+ * It validates input data and computes cross-section properties.
+ */
+ virtual void init() override;
+
+ /**
+ * @brief Reinitializes the force field
+ *
+ * Recalculates cross-section properties and stiffness matrices based on current parameters.
+ */
+ virtual void reinit() override;
+
+ /**
+ * @brief Validates input data for force computation
+ *
+ * Checks that the input data is valid for force computation, including:
+ * - The mechanical state exists
+ * - Position vectors are not empty and have matching sizes
+ * - The length vector has the correct size
+ * - For variant sections, the section lists have the correct sizes
+ *
+ * @param f Force vector
+ * @param x Position vector
+ * @param x0 Rest position vector
+ * @return true if data is valid, false otherwise
+ */
+ virtual bool validateInputData(const DataVecDeriv& f, const DataVecCoord& x, const DataVecCoord& x0) const;
+
+ /**
+ * @brief Validates material parameters
+ *
+ * Ensures that Young's modulus and Poisson ratio have physically valid values.
+ *
+ * @return true if parameters are valid, false otherwise
+ */
+ virtual bool validateMaterialParameters() const;
+
+ /**
+ * @brief Validates cross-section parameters
+ *
+ * Ensures that cross-section dimensions are physically valid.
+ *
+ * @return true if parameters are valid, false otherwise
+ */
+ virtual bool validateSectionParameters() const;
+
+ /**
+ * @brief Computes cross-section properties
+ *
+ * Calculates moment of inertia, cross-section area, and stiffness matrices
+ * based on the cross-section shape and material properties.
+ */
+ virtual void computeSectionProperties();
+
+ /**
+ * @brief Adds forces for uniform section beams
+ *
+ * Computes and adds forces to the force vector for beams with uniform cross-section
+ * properties. Supports both single-threaded and multi-threaded computation.
+ *
+ * @param f Force vector
+ * @param x Position vector
+ * @param x0 Rest position vector
+ * @param lengths Vector of beam section lengths
+ */
+ virtual void addForceUniformSection(DataVecDeriv& f, const DataVecCoord& x, const DataVecCoord& x0,
+ const type::vector& lengths);
+
+ /**
+ * @brief Adds forces for variant section beams
+ *
+ * Computes and adds forces to the force vector for beams with varying cross-section
+ * properties. Supports both single-threaded and multi-threaded computation.
+ *
+ * @param f Force vector
+ * @param x Position vector
+ * @param x0 Rest position vector
+ * @param lengths Vector of beam section lengths
+ */
+ virtual void addForceVariantSection(DataVecDeriv& f, const DataVecCoord& x, const DataVecCoord& x0,
+ const type::vector& lengths);
+
+ /**
+ * @brief Main method for force computation
+ *
+ * This method dispatches the force computation to either addForceUniformSection or
+ * addForceVariantSection based on the beam configuration.
+ *
+ * @param mparams Mechanical parameters
+ * @param f Force vector
+ * @param x Position vector
+ * @param v Velocity vector
+ */
+ virtual void addForce(const sofa::core::MechanicalParams* mparams, DataVecDeriv& f,
+ const DataVecCoord& x, const DataVecDeriv& v) override;
+
+ /**
+ * @brief Computes the product of the stiffness matrix and a displacement vector
+ *
+ * This method is used for implicit integration schemes to compute the change in force
+ * due to a displacement.
+ *
+ * @param mparams Mechanical parameters
+ * @param df Differential force vector
+ * @param dx Differential displacement vector
+ */
+ virtual void addDForce(const sofa::core::MechanicalParams* mparams, DataVecDeriv& df,
+ const DataVecDeriv& dx) override;
+
+ /**
+ * @brief Adds the stiffness matrix contribution to the global matrix
+ *
+ * This method is called during the assembly of the global stiffness matrix to add
+ * the contribution of this force field.
+ *
+ * @param mparams Mechanical parameters
+ * @param matrix Multi-matrix accessor
+ */
+ virtual void addKToMatrix(const sofa::core::MechanicalParams* mparams,
+ const MultiMatrixAccessor* matrix) override;
+
+ /**
+ * @brief Template implementation to add stiffness to matrix
+ *
+ * Implementation details for adding stiffness to the global matrix.
+ * This is specialized in derived classes for different data types.
+ *
+ * @param mat Base matrix
+ * @param offset Matrix offset
+ * @param pos Position vector
+ * @param lengths Vector of beam section lengths
+ * @param kFact Stiffness factor
+ */
+ template
+ void addKToMatrixImpl(MatrixType* mat, unsigned int offset, const VecCoord& pos,
+ const Vector& lengths, Real kFact);
+
+ /**
+ * @brief Computes the potential energy of the beam
+ *
+ * This method returns the strain energy stored in the beam due to deformation.
+ *
+ * @param mparams Mechanical parameters
+ * @param x Position vector
+ * @return Potential energy
+ */
+ virtual double getPotentialEnergy(const sofa::core::MechanicalParams* mparams,
+ const DataVecCoord& x) const override;
+
+ /**
+ * @brief Returns the external radius of the beam
+ *
+ * Utility method to access the beam's external radius.
+ *
+ * @return The external radius of the beam
+ */
+ virtual Real getRadius();
+};
+
+#if !defined(SOFA_COMPONENT_FORCEFIELD_BASEBEAMHOOKELAWFORCEFIELD_CPP)
+extern template class SOFA_COSSERAT_API BaseBeamHookeLawForceField;
+extern template class SOFA_COSSERAT_API BaseBeamHookeLawForceField;
+#endif
+
+} // namespace sofa::component::forcefield
+
diff --git a/src/Cosserat/forcefield/BaseBeamHookeLawForceField.inl b/src/Cosserat/forcefield/BaseBeamHookeLawForceField.inl
new file mode 100644
index 0000000..7fa284e
--- /dev/null
+++ b/src/Cosserat/forcefield/BaseBeamHookeLawForceField.inl
@@ -0,0 +1,1047 @@
+/******************************************************************************
+* SOFA, Simulation Open-Framework Architecture *
+* (c) 2006-2018 INRIA, USTL, UJF, CNRS, MGH *
+* *
+* This library 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 library 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 library; if not, write to the Free Software Foundation, *
+* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
+*******************************************************************************
+* Plugin Cosserat v1.0 *
+* *
+* This plugin is also distributed under the GNU LGPL (Lesser General *
+* Public License) license with the same conditions than SOFA. *
+* *
+* Contributors: Defrost team (INRIA, University of Lille, CNRS, *
+* Ecole Centrale de Lille) *
+* *
+* Contact information: https://project.inria.fr/softrobot/contact/ *
+* *
+******************************************************************************/
+#pragma once
+#include
+
+#include
+#include
+#include
+#include
+
+// Standard includes
+#include
+#include
+#include
+
+// Using declarations for common types
+using sofa::core::behavior::MechanicalState;
+using sofa::core::objectmodel::BaseContext;
+using sofa::helper::ReadAccessor;
+using sofa::helper::WriteAccessor;
+using sofa::core::VecCoordId;
+using std::cout;
+using std::endl;
+
+namespace sofa::component::forcefield
+{
+
+using sofa::core::behavior::MultiMatrixAccessor;
+using sofa::core::behavior::BaseMechanicalState;
+using sofa::helper::WriteAccessor;
+/**
+ * @brief Get the current frame transformation for a specific cross-section
+ *
+ * @param index Cross-section index
+ * @return Rigid transformation (SE3) representing the cross-section frame
+ */
+template
+typename BaseBeamHookeLawForceField::SE3Type
+BaseBeamHookeLawForceField::getFrame(size_t index) const
+{
+ assert(index < m_currentFrames.size());
+ return m_currentFrames[index];
+}
+
+/**
+ * @brief Get the reference frame transformation for a specific cross-section
+ *
+ * @param index Cross-section index
+ * @return Rigid transformation (SE3) representing the reference cross-section frame
+ */
+template
+typename BaseBeamHookeLawForceField::SE3Type
+BaseBeamHookeLawForceField::getReferenceFrame(size_t index) const
+{
+ assert(index < m_referenceFrames.size());
+ return m_referenceFrames[index];
+}
+
+/**
+ * @brief Get the relative rotation between consecutive cross-sections
+ *
+ * @param index Segment index (between cross-sections index and index+1)
+ * @return Rotation (SO3) representing the relative orientation
+ */
+template
+typename BaseBeamHookeLawForceField::SO3Type
+BaseBeamHookeLawForceField::getRelativeRotation(size_t index) const
+{
+ assert(index < m_relativeRotations.size());
+ return m_relativeRotations[index];
+}
+
+} // namespace sofa::component::forcefield
+ * (different properties for different segments).
+ */
+
+/******************************************************************************
+* CONSTRUCTORS / DESTRUCTORS *
+******************************************************************************/
+ttemplate
+void BaseBeamHookeLawForceField::init()
+{
+ Inherit1::init();
+ if (!validateMaterialParameters()) {
+ msg_error("BaseBeamHookeLawForceField") << "Invalid material parameters detected during initialization.";
+ return;
+ }
+
+ if (!validateSectionParameters()) {
+ msg_error("BaseBeamHookeLawForceField") << "Invalid cross-section parameters detected during initialization.";
+ return;
+ }
+
+ computeSectionProperties();
+
+ // Initialize reference frames
+ const VecCoord& x0 = this->mstate->read(sofa::core::vec_id::read_access::restPosition)->getValue();
+ const size_t numNodes = x0.size();
+
+ m_referenceFrames.resize(numNodes);
+ m_currentFrames.resize(numNodes);
+ m_relativeRotations.resize(numNodes > 0 ? numNodes - 1 : 0);
+
+ // Set up initial reference frames
+ for (size_t i = 0; i < numNodes; ++i) {
+ // Extract position and rotation from x0[i]
+ Vector3 position = getPosition(x0[i]);
+ SO3Type rotation = getRotation(x0[i]);
+
+ // Create an SE3 transformation
+ m_referenceFrames[i] = SE3Type(rotation, position);
+ m_currentFrames[i] = m_referenceFrames[i]; // Initially, current frames match reference frames
+ }
+
+ // Compute initial relative rotations
+ computeRelativeRotations();
+
+ m_initialized = true;
+}
+// For variant section
+ localForce = -(m_K_sectionList[i] * strains[i]) * lengths[i];
+ }
+
+ // Transform to global frame and apply to nodes
+ Deriv globalForce = transformWrenchToGlobal(localForce, i);
+
+ // Apply forces to both nodes of the segment
+ forces[i] += globalForce;
+ forces[i+1] -= globalForce;
+ }
+
+ d_f.endEdit();
+ }
+ else {
+ // Call the original specialized force calculation method
+ if (!d_variantSections.getValue()) {
+ addForceUniformSection(d_f, d_x, d_x0, lengths);
+ } else {
+ addForceVariantSection(d_f, d_x, d_x0, lengths);
+ }
+ }
+}
+* INITIALIZATION METHODS *
+******************************************************************************/
+
+/**
+ * @brief Initialize the force field, setting up parameters and cross-section properties
+ *
+ * This method is called during initialization to set up the force field.
+ * It validates parameters and calculates cross-section properties.
+ */
+template
+void BaseBeamHookeLawForceField::init()
+{
+ Inherit1::init();
+ if (!validateMaterialParameters()) {
+ msg_error("BaseBeamHookeLawForceField") << "Invalid material parameters detected during initialization.";
+ return;
+ }
+
+ if (!validateSectionParameters()) {
+ msg_error("BaseBeamHookeLawForceField") << "Invalid cross-section parameters detected during initialization.";
+ return;
+ }
+
+ computeSectionProperties();
+ m_initialized = true;
+}
+/**
+ * @brief Main method for force computation
+ *
+ * This method dispatches the force computation to either addForceUniformSection or
+ * addForceVariantSection based on the beam configuration.
+ *
+ * @param mparams Mechanical parameters
+ * @param d_f Force vector
+ * @param d_x Position vector
+ * @param d_v Velocity vector (unused)
+ */
+
+/**
+ * @brief Validates input data for force computation
+ *
+ * Checks that the input data is valid for force computation, including:
+ * - The mechanical state exists
+ * - Position vectors are not empty and have matching sizes
+ * - The length vector has the correct size
+ * - For variant sections, the section lists have the correct sizes
+ *
+ * @param d_f Force vector
+ * @param d_x Position vector
+ * @param d_x0 Rest position vector
+ * @return true if data is valid, false otherwise
+ */
+template
+bool BaseBeamHookeLawForceField::validateInputData(const DataVecDeriv& d_f,
+ const DataVecCoord& d_x,
+ const DataVecCoord& d_x0) const
+{
+ if (!this->getMState()) {
+ msg_info("BaseBeamHookeLawForceField") << "No Mechanical State found, no force will be computed..." << "\n";
+ return false;
+ }
+
+ const VecCoord& x = d_x.getValue();
+ const VecCoord& x0 = d_x0.getValue();
+
+ if (x.empty() || x0.empty()) {
+ msg_warning("BaseBeamHookeLawForceField") << "Empty input vectors, no force will be computed" << "\n";
+ return false;
+ }
+
+ if (x.size() != x0.size()) {
+ msg_warning("BaseBeamHookeLawForceField") << "Position vector size (" << x.size()
+ << ") doesn't match rest position size (" << x0.size() << ")" << "\n";
+ return false;
+ }
+
+ unsigned int sz = d_length.getValue().size();
+ if (x.size() != sz) {
+ msg_warning("BaseBeamHookeLawForceField") << "Length vector size (" << sz
+ << ") should have the same size as position vector (" << x.size() << ")" << "\n";
+ return false;
+ }
+
+ if (d_variantSections.getValue()) {
+ if (d_youngModulusList.getValue().size() != x.size()) {
+ msg_warning("BaseBeamHookeLawForceField") << "Using variant sections but youngModulusList size ("
+ << d_youngModulusList.getValue().size()
+ << ") doesn't match position vector size (" << x.size() << ")" << "\n";
+ return false;
+ }
+
+ if (d_poissonRatioList.getValue().size() != x.size()) {
+ msg_warning("BaseBeamHookeLawForceField") << "Using variant sections but poissonRatioList size ("
+ << d_poissonRatioList.getValue().size()
+ << ") doesn't match position vector size (" << x.size() << ")" << "\n";
+ return false;
+ }
+
+ if (m_K_sectionList.size() != x.size()) {
+ msg_warning("BaseBeamHookeLawForceField") << "Using variant sections but section list size ("
+ << m_K_sectionList.size()
+ << ") doesn't match position vector size (" << x.size() << ")" << "\n";
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * @brief Validates material parameters
+ *
+ * Ensures that Young's modulus and Poisson ratio have physically valid values.
+ *
+ * @return true if parameters are valid, false otherwise
+ */
+template
+bool BaseBeamHookeLawForceField::validateMaterialParameters() const
+{
+ // Check Young's modulus (must be positive)
+ if (d_youngModulus.getValue() <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "Young's modulus must be positive: " << d_youngModulus.getValue();
+ return false;
+ }
+
+ // Check Poisson ratio (theoretical limits: -1.0 < nu < 0.5)
+ if (d_poissonRatio.getValue() <= -1.0 || d_poissonRatio.getValue() >= 0.5) {
+ msg_error("BaseBeamHookeLawForceField") << "Poisson ratio must be in range (-1.0, 0.5): " << d_poissonRatio.getValue();
+ return false;
+ }
+
+ // For variant sections, check all values
+ if (d_variantSections.getValue()) {
+ const Vector& youngModulusList = d_youngModulusList.getValue();
+ const Vector& poissonRatioList = d_poissonRatioList.getValue();
+
+ for (size_t i = 0; i < youngModulusList.size(); ++i) {
+ if (youngModulusList[i] <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "Young's modulus in list at index " << i << " must be positive: " << youngModulusList[i];
+ return false;
+ }
+ }
+
+ for (size_t i = 0; i < poissonRatioList.size(); ++i) {
+ if (poissonRatioList[i] <= -1.0 || poissonRatioList[i] >= 0.5) {
+ msg_error("BaseBeamHookeLawForceField") << "Poisson ratio in list at index " << i << " must be in range (-1.0, 0.5): " << poissonRatioList[i];
+ return false;
+ }
+ }
+ }
+
+ // If using inertia parameters directly, validate them
+ if (d_useInertiaParams.getValue()) {
+ // Only check these if they're actually provided (not default initialized)
+ if (d_EIy.isSet() && d_EIy.getValue() <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "EIy parameter must be positive: " << d_EIy.getValue();
+ return false;
+ }
+
+ if (d_EIz.isSet() && d_EIz.getValue() <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "EIz parameter must be positive: " << d_EIz.getValue();
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * @brief Validates cross-section parameters
+ *
+ * Ensures that cross-section dimensions are physically valid.
+ *
+ * @return true if parameters are valid, false otherwise
+ */
+template
+bool BaseBeamHookeLawForceField::validateSectionParameters() const
+{
+ // Get cross-section shape
+ const std::string& shape = d_crossSectionShape.getValue().getSelectedItem();
+
+ if (shape == "circular") {
+ // Check radius (must be positive)
+ if (d_radius.getValue() <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "External radius must be positive: " << d_radius.getValue();
+ return false;
+ }
+
+ // Check inner radius (must be non-negative and less than external radius)
+ if (d_innerRadius.getValue() < 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "Inner radius cannot be negative: " << d_innerRadius.getValue();
+ return false;
+ }
+
+ if (d_innerRadius.getValue() >= d_radius.getValue()) {
+ msg_error("BaseBeamHookeLawForceField") << "Inner radius (" << d_innerRadius.getValue()
+ << ") must be less than external radius (" << d_radius.getValue() << ")";
+ return false;
+ }
+ }
+ else if (shape == "rectangular") {
+ // Check rectangular dimensions (must be positive)
+ if (d_lengthY.getValue() <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "Side length Y must be positive: " << d_lengthY.getValue();
+ return false;
+ }
+
+ if (d_lengthZ.getValue() <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "Side length Z must be positive: " << d_lengthZ.getValue();
+ return false;
+ }
+ }
+ else {
+ msg_error("BaseBeamHookeLawForceField") << "Unknown cross-section shape: " << shape;
+ return false;
+ }
+
+ // Check section lengths
+ const Vector& lengths = d_length.getValue();
+ for (size_t i = 0; i < lengths.size(); ++i) {
+ if (lengths[i] <= 0.0) {
+ msg_error("BaseBeamHookeLawForceField") << "Section length at index " << i << " must be positive: " << lengths[i];
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * @brief Computes cross-section properties
+ *
+ * Calculates moment of inertia, cross-section area, and stiffness matrices
+ * based on the cross-section shape and material properties.
+ */
+template
+void BaseBeamHookeLawForceField::computeSectionProperties()
+{
+ // Initialize matrices to zero
+ m_K_section66.fill(0);
+ m_K_sectionList.clear();
+ m_K_section66List.clear();
+
+ // Calculate moments of inertia based on cross-section shape
+ if (d_crossSectionShape.getValue().getSelectedItem() == "rectangular") {
+ // Rectangular cross-section
+ const Real Ly = d_lengthY.getValue();
+ const Real Lz = d_lengthZ.getValue();
+
+ const Real LyLzLzLz = Ly * Lz * Lz * Lz;
+ const Real LzLyLyLy = Lz * Ly * Ly * Ly;
+
+ Iy = LyLzLzLz / 12.0;
+ Iz = LzLyLyLy / 12.0;
+ J = Iy + Iz;
+ m_crossSectionArea = Ly * Lz;
+ }
+ else {
+ // Circular cross-section
+ const Real r = d_radius.getValue();
+ const Real rInner = d_innerRadius.getValue();
+ const Real r4 = r * r * r * r;
+ const Real rInner4 = rInner * rInner * rInner * rInner;
+
+ Iy = M_PI * (r4 - rInner4) / 4.0;
+ Iz = Iy;
+ J = Iy + Iz;
+ m_crossSectionArea = M_PI * (r * r - rInner * rInner);
+ }
+
+ // Calculate stiffness matrices
+ if (!d_variantSections.getValue()) {
+ // Uniform section
+ if (!d_useInertiaParams.getValue()) {
+ // Calculate from material properties
+ const Real E = d_youngModulus.getValue();
+ const Real G = E / (2.0 * (1.0 + d_poissonRatio.getValue()));
+
+ // 3x3 stiffness matrix for rotational DOFs
+ m_K_section[0][0] = G * J;
+ m_K_section[1][1] = E * Iy;
+ m_K_section[2][2] = E * Iz;
+
+ // 6x6 stiffness matrix if needed for more complex beam formulations
+ if (DataTypes::spatial_dimensions > 3) {
+ m_K_section66[0][0] = E * m_crossSectionArea; // Axial stiffness
+ m_K_section66[1][1] = G * m_crossSectionArea; // Shear stiffness y
+ m_K_section66[2][2] = G * m_crossSectionArea; // Shear stiffness z
+ m_K_section66[3][3] = G * J; // Torsional stiffness
+ m_K_section66[4][4] = E * Iy; // Bending stiffness y
+ m_K_section66[5][5] = E * Iz; // Bending stiffness z
+ }
+ }
+ else {
+ // Use user-provided inertia parameters
+ msg_info("BaseBeamHookeLawForceField") << "Using pre-calculated inertia parameters for stiffness matrix.";
+
+ // 3x3 stiffness matrix
+ m_K_section[0][0] = d_GI.getValue();
+
+ // Use specific EIy/EIz if provided, otherwise use general EI
+ if (d_EIy.isSet()) {
+ m_K_section[1][1] = d_EIy.getValue();
+ } else {
+ m_K_section[1][1] = d_EI.getValue();
+ }
+
+ if (d_EIz.isSet()) {
+ m_K_section[2][2] = d_EIz.getValue();
+ } else {
+ m_K_section[2][2] = d_EI.getValue();
+ }
+
+ // 6x6 stiffness matrix if needed
+ if (DataTypes::spatial_dimensions > 3) {
+ m_K_section66[0][0] = d_EA.getValue(); // Axial stiffness
+ m_K_section66[1][1] = d_GA.getValue(); // Shear stiffness y
+ m_K_section66[2][2] = d_GA.getValue(); // Shear stiffness z
+ m_K_section66[3][3] = d_GI.getValue(); // Torsional stiffness
+
+ // Use specific EIy/EIz if provided, otherwise use general EI
+ if (d_EIy.isSet()) {
+ m_K_section66[4][4] = d_EIy.getValue();
+ } else {
+ m_K_section66[4][4] = d_EI.getValue();
+ }
+
+ if (d_EIz.isSet()) {
+ m_K_section66[5][5] = d_EIz.getValue();
+ } else {
+ m_K_section66[5][5] = d_EI.getValue();
+ }
+ }
+ }
+ }
+ else {
+ // Variant sections
+ msg_info("BaseBeamHookeLawForceField") << "Computing properties for variant beam sections.";
+
+ const auto szL = d_length.getValue().size();
+ if ((szL != d_poissonRatioList.getValue().size()) || (szL != d_youngModulusList.getValue().size())) {
+ msg_error("BaseBeamHookeLawForceField") << "The size of data 'length', 'youngModulusList', and 'poissonRatioList' should be the same!";
+ return;
+ }
+
+ for (auto k = 0; k < szL; k++) {
+ // 3x3 stiffness matrix for each section
+ Mat33 _m_K_section;
+ const Real E = d_youngModulusList.getValue()[k];
+ const Real G = E / (2.0 * (1.0 + d_poissonRatioList.getValue()[k]));
+
+ _m_K_section[0][0] = G * J;
+ _m_K_section[1][1] = E * Iy;
+ _m_K_section[2][2] = E * Iz;
+ m_K_sectionList.push_back(_m_K_section);
+
+ // 6x6 stiffness matrix if needed
+ if (DataTypes::spatial_dimensions > 3) {
+ Mat66 _m_K_section66;
+ _m_K_section66[0][0] = E * m_crossSectionArea; // Axial stiffness
+ _m_K_section66[1][1] = G * m_crossSectionArea; // Shear stiffness y
+ _m_K_section66[2][2] = G * m_crossSectionArea; // Shear stiffness z
+ _m_K_section66[3][3] = G * J; // Torsional stiffness
+ _m_K_section66[4][4] = E * Iy; // Bending stiffness y
+ _m_K_section66[5][5] = E * Iz; // Bending stiffness z
+ m_K_section66List.push_back(_m_K_section66);
+ }
+ }
+
+ if (d_useInertiaParams.getValue()) {
+ msg_warning("BaseBeamHookeLawForceField") << "Using variant sections with pre-calculated inertia parameters is not yet supported.";
+ }
+ }
+}
+
+/******************************************************************************
+* FORCE COMPUTATION METHODS *
+******************************************************************************/
+
+/**
+ * @brief Adds forces for uniform section beams
+ *
+ * Computes and adds forces to the force vector for beams with uniform cross-section
+ * properties. Supports both single-threaded and multi-threaded computation.
+ *
+ * @param d_f Force vector
+ * @param d_x Position vector
+ * @param d_x0 Rest position vector
+ * @param lengths Vector of beam section lengths
+ */
+template
+void BaseBeamHookeLawForceField::addForceUniformSection(DataVecDeriv& d_f,
+ const DataVecCoord& d_x,
+ const DataVecCoord& d_x0,
+ const type::vector& lengths)
+{
+ VecDeriv& f = *d_f.beginEdit();
+ const VecCoord& x = d_x.getValue();
+ const VecCoord& x0 = d_x0.getValue();
+ const size_t size = x.size();
+ sofa::helper::ScopedAdvancedTimer timer("UniformSection");
+
+ if (d_useMultiThreading.getValue() && size > 1) {
+ sofa::simulation::TaskScheduler::Task::Status status;
+ sofa::simulation::TaskScheduler& scheduler = *(sofa::simulation::TaskScheduler::getInstance());
+ // Define a lambda for parallel execution
+ auto calcForce = [this, &f, &x, &x0, &lengths](size_t start, size_t end) {
+ for (size_t i = start; i < end; ++i) {
+ f[i] -= (m_K_section * (x[i] - x0[i])) * lengths[i];
+ }
+ };
+
+ // Determine chunk size for parallel processing
+ const size_t chunk_size = std::max(1, size / (scheduler.getThreadCount() * 2));
+ size_t start = 0;
+
+ // Create and queue tasks for parallel execution
+ while (start < size) {
+ size_t end = std::min(start + chunk_size, size);
+ Task* task = new Task(std::bind(calcForce, start, end), &status);
+ scheduler.addTask(task);
+ start = end;
+ }
+
+ // Wait for all tasks to complete
+ scheduler.workUntilDone(&status);
+ }
+ else {
+ // Single-threaded fallback
+ for (size_t i = 0; i < size; ++i) {
+ f[i] -= (m_K_section * (x[i] - x0[i])) * lengths[i];
+ }
+ }
+
+ d_f.endEdit();
+}
+/**
+ * @brief Adds forces for variant section beams
+ *
+ * Computes and adds forces to the force vector for beams with varying cross-section
+ * properties. Supports both single-threaded and multi-threaded computation.
+ *
+ * @param d_f Force vector
+ * @param d_x Position vector
+ * @param d_x0 Rest position vector
+ * @param lengths Vector of beam section lengths
+ */
+template
+void BaseBeamHookeLawForceField::addForceVariantSection(DataVecDeriv& d_f,
+ const DataVecCoord& d_x,
+ const DataVecCoord& d_x0,
+ const type::vector& lengths)
+{
+ VecDeriv& f = *d_f.beginEdit();
+ const VecCoord& x = d_x.getValue();
+ const VecCoord& x0 = d_x0.getValue();
+ const size_t size = x.size();
+ sofa::helper::ScopedAdvancedTimer timer("VariantSection");
+
+ if (d_useMultiThreading.getValue() && size > 1) {
+ sofa::simulation::TaskScheduler::Task::Status status;
+ sofa::simulation::TaskScheduler& scheduler = *(sofa::simulation::TaskScheduler::getInstance());
+
+ // Define a lambda for parallel execution
+ auto calcForce = [this, &f, &x, &x0, &lengths](size_t start, size_t end) {
+ for (size_t i = start; i < end; ++i) {
+ f[i] -= (m_K_sectionList[i] * (x[i] - x0[i])) * lengths[i];
+ }
+ };
+
+ // Determine chunk size for parallel processing
+ const size_t chunk_size = std::max