From 136406a1a768ab3019f7f5d3180e7debc66c8768 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Wed, 4 Feb 2026 18:02:43 +0100 Subject: [PATCH 01/31] Add Rotation class with docs and bindings Introduce a new Rotation subsystem: adds a C++ Rotation class (include/.../rotation_class.hpp and src/Simulation/Maths/rotation_class.cpp) implementing quaternion-based 3D rotations, Voigt operations, Euler conventions, SLERP, and apply methods for vectors/tensors. Add Python bindings (pybind11) and headers in simcoon-python-builder, update its CMake to build the rotation_class wrapper, and extend the python module registration. Add comprehensive documentation pages (continuum mechanics and C++ API) and index updates. Include a unit test (test/Libraries/Maths/TRotationClass.cpp) and adjust top-level CMakeLists where needed. This patch adds rotation functionality, docs, bindings, and tests to enable quaternion-based rotation operations from C++ and Python. --- .../functions/doc_rotation.rst | 355 ++++++++++ docs/continuum_mechanics/functions/index.rst | 1 + docs/cpp_api/simulation/index.rst | 21 +- docs/cpp_api/simulation/maths.rst | 36 +- docs/cpp_api/simulation/rotation.rst | 553 +++++++++++++++ .../Simulation/Maths/rotation_class.hpp | 410 +++++++++++ simcoon-python-builder/CMakeLists.txt | 1 + .../Libraries/Maths/rotation_class.hpp | 10 + .../Libraries/Maths/rotation_class.cpp | 510 ++++++++++++++ .../src/python_wrappers/python_module.cpp | 4 + src/CMakeLists.txt | 1 + src/Simulation/Maths/rotation_class.cpp | 634 ++++++++++++++++++ test/CMakeLists.txt | 1 + test/Libraries/Maths/TRotationClass.cpp | 451 +++++++++++++ 14 files changed, 2985 insertions(+), 3 deletions(-) create mode 100644 docs/continuum_mechanics/functions/doc_rotation.rst create mode 100644 docs/cpp_api/simulation/rotation.rst create mode 100644 include/simcoon/Simulation/Maths/rotation_class.hpp create mode 100644 simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation_class.hpp create mode 100644 simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp create mode 100644 src/Simulation/Maths/rotation_class.cpp create mode 100644 test/Libraries/Maths/TRotationClass.cpp diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst new file mode 100644 index 000000000..809094630 --- /dev/null +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -0,0 +1,355 @@ +The Rotation Library +==================== + +The rotation library provides comprehensive tools for 3D rotations in continuum mechanics. +Simcoon 2.0 introduces a powerful ``Rotation`` class alongside the existing rotation functions. + +.. contents:: Contents + :local: + :depth: 2 + +The Rotation Class +------------------ + +The ``Rotation`` class provides an object-oriented interface for working with 3D rotations. +It uses unit quaternions internally for numerical stability and efficient composition. + +Creating Rotations +~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Identity rotation (no rotation) + r = smc.Rotation.identity() + + # From Euler angles (default: zxz convention, intrinsic, radians) + r = smc.Rotation.from_euler(psi, theta, phi) + + # From Euler angles with options + r = smc.Rotation.from_euler(psi, theta, phi, conv="xyz", intrinsic=False, degrees=True) + + # From rotation matrix + R = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]]) # 90° around x + r = smc.Rotation.from_matrix(R) + + # From quaternion [qx, qy, qz, qw] (scalar-last) + q = np.array([0, 0, np.sin(np.pi/4), np.cos(np.pi/4)]) # 90° around z + r = smc.Rotation.from_quat(q) + + # From rotation vector (axis × angle) + rotvec = np.array([0, 0, np.pi/2]) # 90° around z + r = smc.Rotation.from_rotvec(rotvec) + + # From axis and angle + r = smc.Rotation.from_axis_angle(np.pi/2, 3) # 90° around z (axis 3) + + # Random rotation (uniform distribution) + r = smc.Rotation.random() + +Converting Between Representations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + + # To rotation matrix + R = r.as_matrix() # 3×3 numpy array + + # To quaternion + q = r.as_quat() # [qx, qy, qz, qw] + + # To Euler angles + angles = r.as_euler("zxz") # [psi, theta, phi] + angles_deg = r.as_euler("zxz", degrees=True) + + # To rotation vector + rotvec = r.as_rotvec() + rotvec_deg = r.as_rotvec(degrees=True) + + # To Voigt rotation matrices + QS = r.as_QS() # 6×6 for stress + QE = r.as_QE() # 6×6 for strain + +Applying Rotations +~~~~~~~~~~~~~~~~~~ + +**To 3D vectors:** + +.. code-block:: python + + r = smc.Rotation.from_axis_angle(np.pi/2, 3) + + v = np.array([1.0, 0.0, 0.0]) + v_rot = r.apply(v) # [0, 1, 0] + + # Inverse rotation + v_back = r.apply(v_rot, inverse=True) # [1, 0, 0] + +**To 3×3 tensors:** + +.. code-block:: python + + # Rotate a tensor: R · T · R^T + T = np.eye(3) + T_rot = r.apply_tensor(T) + +**To Voigt notation tensors:** + +.. code-block:: python + + # Stress vector [σ11, σ22, σ33, σ12, σ13, σ23] + sigma = np.array([100.0, 50.0, 25.0, 10.0, 5.0, 2.0]) + sigma_rot = r.apply_stress(sigma) + + # Strain vector [ε11, ε22, ε33, 2ε12, 2ε13, 2ε23] + epsilon = np.array([0.01, -0.005, -0.005, 0.002, 0.001, 0.0]) + epsilon_rot = r.apply_strain(epsilon) + + # Stiffness matrix (6×6) + L = smc.L_iso([210e9, 0.3], "Enu") + L_rot = r.apply_stiffness(L) + + # Compliance matrix (6×6) + M = smc.M_iso([210e9, 0.3], "Enu") + M_rot = r.apply_compliance(M) + +Composing Rotations +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + r1 = smc.Rotation.from_axis_angle(np.pi/4, 3) # 45° around z + r2 = smc.Rotation.from_axis_angle(np.pi/6, 1) # 30° around x + + # Compose: apply r1 first, then r2 + r_combined = r2 * r1 + + # This is equivalent to: + v = np.array([1.0, 0.0, 0.0]) + v1 = r2.apply(r1.apply(v)) + v2 = r_combined.apply(v) + # v1 == v2 + + # Inverse + r_inv = r1.inv() + r_identity = r1 * r_inv # Should be identity + +Interpolating Rotations +~~~~~~~~~~~~~~~~~~~~~~~ + +SLERP (Spherical Linear Interpolation) provides smooth interpolation: + +.. code-block:: python + + r_start = smc.Rotation.identity() + r_end = smc.Rotation.from_euler(np.pi/2, np.pi/4, 0, "zxz") + + # Interpolate at t=0.5 (halfway) + r_mid = r_start.slerp(r_end, 0.5) + + # Interpolate along path + for t in np.linspace(0, 1, 11): + r_t = r_start.slerp(r_end, t) + print(f"t={t:.1f}: angle={r_t.magnitude(degrees=True):.1f}°") + +Utility Methods +~~~~~~~~~~~~~~~ + +.. code-block:: python + + r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + + # Get rotation magnitude (angle) + angle = r.magnitude() # radians + angle_deg = r.magnitude(degrees=True) + + # Check if identity + is_id = r.is_identity() + + # Check equality + r2 = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + are_equal = r.equals(r2, tol=1e-12) + +Euler Angle Conventions +----------------------- + +Simcoon supports multiple Euler angle conventions: + +**Proper Euler angles** (axis sequence where first = last): + +- ``zxz`` (default in simcoon) +- ``zyz`` +- ``xyx``, ``xzx`` +- ``yxy``, ``yzy`` + +**Tait-Bryan angles** (all axes different): + +- ``xyz`` (roll-pitch-yaw) +- ``xzy``, ``yxz``, ``yzx``, ``zxy``, ``zyx`` + +**Intrinsic vs Extrinsic:** + +- **Intrinsic** (default): rotations about axes of the rotating frame +- **Extrinsic**: rotations about fixed world axes + +.. code-block:: python + + # Intrinsic ZXZ (simcoon default) + r1 = smc.Rotation.from_euler(psi, theta, phi, "zxz", intrinsic=True) + + # Extrinsic XYZ (aerospace convention) + r2 = smc.Rotation.from_euler(roll, pitch, yaw, "xyz", intrinsic=False) + +Rotation Functions +------------------ + +The following functions provide direct rotation operations without creating a ``Rotation`` object. + +Rotation Matrix Generation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: simcoon.fillR_angle + +.. autofunction:: simcoon.fillR_euler + +Voigt Rotation Matrices +~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: simcoon.fillQS_angle + +.. autofunction:: simcoon.fillQS_R + +.. autofunction:: simcoon.fillQE_angle + +.. autofunction:: simcoon.fillQE_R + +Vector and Matrix Rotation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: simcoon.rotate_vec_R + +.. autofunction:: simcoon.rotate_vec_angle + +.. autofunction:: simcoon.rotate_mat_R + +.. autofunction:: simcoon.rotate_mat_angle + +Stress and Strain Rotation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: simcoon.rotate_stress_angle + +.. autofunction:: simcoon.rotate_stress_R + +.. autofunction:: simcoon.rotate_strain_angle + +.. autofunction:: simcoon.rotate_strain_R + +Stiffness and Compliance Rotation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: simcoon.rotateL_angle + +.. autofunction:: simcoon.rotateL_R + +.. autofunction:: simcoon.rotateM_angle + +.. autofunction:: simcoon.rotateM_R + +Local-Global Transformations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: simcoon.rotate_l2g_L + +.. autofunction:: simcoon.rotate_g2l_L + +.. autofunction:: simcoon.rotate_l2g_M + +.. autofunction:: simcoon.rotate_g2l_M + +.. autofunction:: simcoon.rotate_l2g_strain + +.. autofunction:: simcoon.rotate_g2l_strain + +.. autofunction:: simcoon.rotate_l2g_stress + +.. autofunction:: simcoon.rotate_g2l_stress + +Practical Examples +------------------ + +Example 1: Rotating Orthotropic Material Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Define orthotropic material in local coordinates + # [E1, E2, E3, nu12, nu13, nu23, G12, G13, G23] + props = [150e9, 10e9, 10e9, 0.3, 0.3, 0.45, 5e9, 5e9, 3.5e9] + L_local = smc.L_ortho(props, "EnuG") + + # Fiber orientation: 45° rotation around z-axis + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + + # Rotate stiffness to global coordinates + L_global = r.apply_stiffness(L_local) + + print("Local stiffness L11:", L_local[0, 0] / 1e9, "GPa") + print("Global stiffness L11:", L_global[0, 0] / 1e9, "GPa") + +Example 2: Stress Transformation in a Rotated Element +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Stress in global coordinates (Voigt notation) + sigma_global = np.array([100.0, 50.0, 0.0, 25.0, 0.0, 0.0]) # MPa + + # Element orientation (Euler angles) + psi, theta, phi = np.radians([30, 45, 60]) + + # Create rotation + r = smc.Rotation.from_euler(psi, theta, phi, "zxz") + + # Transform stress to local element coordinates + sigma_local = r.inv().apply_stress(sigma_global) + + print("Global stress:", sigma_global) + print("Local stress:", sigma_local) + +Example 3: Averaging Orientations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Generate random orientations + rotations = [smc.Rotation.random() for _ in range(10)] + + # Approximate mean using iterative SLERP + r_mean = rotations[0] + for i, r in enumerate(rotations[1:], 2): + t = 1.0 / i # Weight for new rotation + r_mean = r_mean.slerp(r, t) + + print("Mean rotation angle:", r_mean.magnitude(degrees=True), "degrees") + +See Also +-------- + +- :doc:`doc_constitutive` - Constitutive matrices (L_iso, L_ortho, etc.) +- :doc:`doc_transfer` - Voigt notation conversions +- :doc:`doc_kinematics` - Finite strain kinematics +- :doc:`/cpp_api/simulation/rotation` - Full C++ API reference diff --git a/docs/continuum_mechanics/functions/index.rst b/docs/continuum_mechanics/functions/index.rst index 137edc73c..96ec73a72 100755 --- a/docs/continuum_mechanics/functions/index.rst +++ b/docs/continuum_mechanics/functions/index.rst @@ -6,6 +6,7 @@ Continuum Mechanics generic functions :caption: Contents: doc_constitutive + doc_rotation doc_contimech doc_criteria doc_transfer diff --git a/docs/cpp_api/simulation/index.rst b/docs/cpp_api/simulation/index.rst index a9322a848..a0a8b4617 100644 --- a/docs/cpp_api/simulation/index.rst +++ b/docs/cpp_api/simulation/index.rst @@ -2,7 +2,7 @@ Simulation ========== -The Simulation module provides tools for running simulations, including solvers, +The Simulation module provides tools for running simulations, including solvers, identification algorithms, and mathematical utilities. .. toctree:: @@ -10,6 +10,7 @@ identification algorithms, and mathematical utilities. :caption: Submodules: maths + rotation solver identification phase @@ -19,7 +20,23 @@ Overview This module contains simulation and numerical tools: -- **Maths**: Mathematical utilities (rotation, random numbers, solvers) +- **Maths**: Mathematical utilities (random numbers, statistics, solvers) +- **Rotation**: Comprehensive 3D rotation tools with ``Rotation`` class and Voigt notation support - **Solver**: Material point simulation solvers - **Identification**: Parameter identification algorithms - **Phase**: Phase management and properties + +What's New in Simcoon 2.0 +========================= + +The **Rotation module** has been significantly enhanced with: + +- New ``Rotation`` class inspired by ``scipy.spatial.transform.Rotation`` +- Unit quaternion internal representation for numerical stability +- Support for multiple Euler angle conventions (zxz, zyz, xyz, etc.) +- Intrinsic and extrinsic rotation modes +- SLERP interpolation for smooth orientation transitions +- Direct Voigt notation operations (stress, strain, stiffness, compliance) +- Full Python bindings with NumPy integration + +See :doc:`rotation` for complete documentation. diff --git a/docs/cpp_api/simulation/maths.rst b/docs/cpp_api/simulation/maths.rst index 3607ecde6..5ca721e9a 100644 --- a/docs/cpp_api/simulation/maths.rst +++ b/docs/cpp_api/simulation/maths.rst @@ -5,6 +5,40 @@ Mathematical Functions Mathematical utility functions for rotations, random number generation, statistics, and numerical solving. -.. doxygengroup:: maths +.. toctree:: + :maxdepth: 2 + + rotation + +Overview +======== + +The Maths module provides essential mathematical utilities for continuum mechanics +simulations: + +- **Rotation**: Comprehensive 3D rotation tools including the new ``Rotation`` class + and Voigt notation transformations (see :doc:`rotation`) +- **Random**: Random number generation utilities +- **Statistics**: Statistical functions +- **Numerical Solvers**: Root finding and optimization + +Random Number Generation +======================== + +.. doxygengroup:: random + :project: simcoon + :content-only: + +Statistics +========== + +.. doxygengroup:: stats + :project: simcoon + :content-only: + +Numerical Solvers +================= + +.. doxygengroup:: solvers :project: simcoon :content-only: diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst new file mode 100644 index 000000000..d9e3b5cf2 --- /dev/null +++ b/docs/cpp_api/simulation/rotation.rst @@ -0,0 +1,553 @@ +================== +The Rotation Module +================== + +The Rotation module provides comprehensive tools for 3D rotations in continuum mechanics +applications. It includes both a modern **Rotation class** (inspired by ``scipy.spatial.transform.Rotation``) +and **free functions** for tensor rotation in Voigt notation. + +.. contents:: Table of Contents + :local: + :depth: 2 + +Overview +======== + +Simcoon 2.0 introduces a new ``Rotation`` class that encapsulates rotation operations using +unit quaternions as the internal representation. This design provides: + +- **Numerical stability**: Avoids gimbal lock issues present in Euler angle representations +- **Efficient composition**: Quaternion multiplication is faster than matrix multiplication +- **Multiple representations**: Easy conversion between quaternions, matrices, Euler angles, and rotation vectors +- **Voigt notation support**: Direct application to stress/strain tensors and stiffness/compliance matrices + +The existing free functions are preserved for backward compatibility and provide direct +operations on matrices and vectors. + +The Rotation Class +================== + +The ``Rotation`` class uses unit quaternions in **scalar-last convention** ``[qx, qy, qz, qw]`` +as its internal representation. + +C++ API +------- + +.. code-block:: cpp + + #include + + using namespace simcoon; + + // Create rotations + Rotation r1 = Rotation::identity(); + Rotation r2 = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + Rotation r3 = Rotation::from_axis_angle(M_PI/4, 3); // 45° around z-axis + Rotation r4 = Rotation::from_matrix(R); + Rotation r5 = Rotation::random(); + + // Convert to different representations + arma::mat::fixed<3,3> R = r2.as_matrix(); + arma::vec::fixed<4> q = r2.as_quat(); + arma::vec::fixed<3> euler = r2.as_euler("zxz"); + arma::vec::fixed<3> rotvec = r2.as_rotvec(); + + // Apply to vectors and tensors + arma::vec::fixed<3> v_rot = r2.apply(v); + arma::vec::fixed<6> sigma_rot = r2.apply_stress(sigma); + arma::mat::fixed<6,6> L_rot = r2.apply_stiffness(L); + + // Compose rotations + Rotation r_combined = r2 * r3; + Rotation r_inv = r2.inv(); + + // Interpolation + Rotation r_mid = r2.slerp(r3, 0.5); + +Python API +---------- + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Create rotations + r1 = smc.Rotation.identity() + r2 = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + r3 = smc.Rotation.from_axis_angle(np.pi/4, 3) + r4 = smc.Rotation.from_matrix(R) + r5 = smc.Rotation.random() + + # Convert to different representations + R = r2.as_matrix() + q = r2.as_quat() + euler = r2.as_euler("zxz") + rotvec = r2.as_rotvec() + + # Apply to vectors and tensors + v_rot = r2.apply(v) + sigma_rot = r2.apply_stress(sigma) + L_rot = r2.apply_stiffness(L) + + # Compose rotations + r_combined = r2 * r3 + r_inv = r2.inv() + + # Interpolation + r_mid = r2.slerp(r3, 0.5) + +Factory Methods +--------------- + +.. list-table:: Factory Methods + :widths: 30 70 + :header-rows: 1 + + * - Method + - Description + * - ``identity()`` + - Create an identity rotation (no rotation) + * - ``from_quat(quat)`` + - Create from quaternion [qx, qy, qz, qw] (scalar-last) + * - ``from_matrix(R)`` + - Create from 3x3 rotation matrix + * - ``from_euler(psi, theta, phi, conv, intrinsic, degrees)`` + - Create from Euler angles with specified convention + * - ``from_rotvec(rotvec, degrees)`` + - Create from rotation vector (axis × angle) + * - ``from_axis_angle(angle, axis, degrees)`` + - Create from angle and axis (1=x, 2=y, 3=z) + * - ``random()`` + - Create uniformly distributed random rotation + +Euler Angle Conventions +----------------------- + +The ``from_euler`` and ``as_euler`` methods support multiple conventions: + +**Proper Euler angles** (first and last axis are the same): + +- ``zxz``, ``zyz``, ``xyx``, ``xzx``, ``yxy``, ``yzy`` + +**Tait-Bryan angles** (all three axes are different): + +- ``xyz``, ``xzy``, ``yxz``, ``yzx``, ``zxy``, ``zyx`` + +**Parameters:** + +- ``intrinsic`` (bool): If ``true`` (default), rotations are about the axes of the rotating + coordinate system. If ``false``, rotations are about the fixed coordinate system (extrinsic). +- ``degrees`` (bool): If ``true``, angles are in degrees. Default is ``false`` (radians). + +.. code-block:: cpp + + // Intrinsic ZXZ Euler angles (default in simcoon) + Rotation r1 = Rotation::from_euler(psi, theta, phi, "zxz", true, false); + + // Extrinsic XYZ (aerospace convention) + Rotation r2 = Rotation::from_euler(roll, pitch, yaw, "xyz", false, true); + +Conversion Methods +------------------ + +.. list-table:: Conversion Methods + :widths: 30 70 + :header-rows: 1 + + * - Method + - Description + * - ``as_quat()`` + - Return quaternion [qx, qy, qz, qw] + * - ``as_matrix()`` + - Return 3×3 rotation matrix + * - ``as_euler(conv, intrinsic, degrees)`` + - Return Euler angles [psi, theta, phi] + * - ``as_rotvec(degrees)`` + - Return rotation vector (axis × angle) + * - ``as_QS(active)`` + - Return 6×6 stress rotation matrix + * - ``as_QE(active)`` + - Return 6×6 strain rotation matrix + +Apply Methods +------------- + +**3D Vectors and Tensors:** + +.. list-table:: Apply Methods for 3D Objects + :widths: 30 70 + :header-rows: 1 + + * - Method + - Description + * - ``apply(v, inverse)`` + - Rotate a 3D vector + * - ``apply_tensor(m, inverse)`` + - Rotate a 3×3 tensor: R·m·R\ :sup:`T` + +**Voigt Notation (Continuum Mechanics):** + +.. list-table:: Apply Methods for Voigt Notation + :widths: 30 70 + :header-rows: 1 + + * - Method + - Description + * - ``apply_stress(sigma, active)`` + - Rotate 6-component stress vector + * - ``apply_strain(epsilon, active)`` + - Rotate 6-component strain vector + * - ``apply_stiffness(L, active)`` + - Rotate 6×6 stiffness matrix: QS·L·QS\ :sup:`T` + * - ``apply_compliance(M, active)`` + - Rotate 6×6 compliance matrix: QE·M·QE\ :sup:`T` + +The ``active`` parameter controls whether the rotation is **active** (alibi, rotating the object) +or **passive** (alias, rotating the coordinate system). + +Operations +---------- + +.. list-table:: Rotation Operations + :widths: 30 70 + :header-rows: 1 + + * - Method + - Description + * - ``inv()`` + - Return inverse rotation + * - ``magnitude(degrees)`` + - Return rotation angle + * - ``r1 * r2`` + - Compose rotations (apply r2 first, then r1) + * - ``r1 *= r2`` + - Compose in-place + * - ``slerp(other, t)`` + - Spherical linear interpolation (t ∈ [0, 1]) + * - ``equals(other, tol)`` + - Check equality within tolerance + * - ``is_identity(tol)`` + - Check if identity rotation + +Rotation Free Functions +======================= + +The following free functions provide direct rotation operations without creating a ``Rotation`` +object. They are preserved for backward compatibility and direct matrix operations. + +Rotation Matrix Generation +-------------------------- + +.. doxygenfunction:: simcoon::fillR(const double&, const int&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::fillR(const double&, const double&, const double&, const bool&, const std::string&) + :project: simcoon + +Voigt Rotation Matrices +----------------------- + +The 6×6 rotation matrices for Voigt notation stress and strain tensors: + +.. doxygenfunction:: simcoon::fillQS(const double&, const int&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::fillQS(const arma::mat&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::fillQE(const double&, const int&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::fillQE(const arma::mat&, const bool&) + :project: simcoon + +Vector and Matrix Rotation +-------------------------- + +.. doxygenfunction:: simcoon::rotate_vec(const arma::vec&, const arma::mat&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_vec(const arma::vec&, const double&, const int&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_mat(const arma::mat&, const arma::mat&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_mat(const arma::mat&, const double&, const int&) + :project: simcoon + +Stress and Strain Rotation +-------------------------- + +.. doxygenfunction:: simcoon::rotate_stress(const arma::vec&, const double&, const int&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_stress(const arma::vec&, const arma::mat&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_strain(const arma::vec&, const double&, const int&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_strain(const arma::vec&, const arma::mat&, const bool&) + :project: simcoon + +Stiffness and Compliance Rotation +--------------------------------- + +.. doxygenfunction:: simcoon::rotateL(const arma::mat&, const double&, const int&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotateL(const arma::mat&, const arma::mat&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotateM(const arma::mat&, const double&, const int&, const bool&) + :project: simcoon + +.. doxygenfunction:: simcoon::rotateM(const arma::mat&, const arma::mat&, const bool&) + :project: simcoon + +Local-to-Global and Global-to-Local Transformations +--------------------------------------------------- + +These functions use Euler angles to transform between local and global coordinate systems: + +.. doxygenfunction:: simcoon::rotate_l2g_strain + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_g2l_strain + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_l2g_stress + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_g2l_stress + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_l2g_L + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_g2l_L + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_l2g_M + :project: simcoon + +.. doxygenfunction:: simcoon::rotate_g2l_M + :project: simcoon + +Python Functions +================ + +The following functions are available in Python for direct rotation operations: + +.. autofunction:: simcoon.fillR_angle + +.. autofunction:: simcoon.fillR_euler + +.. autofunction:: simcoon.fillQS_angle + +.. autofunction:: simcoon.fillQS_R + +.. autofunction:: simcoon.fillQE_angle + +.. autofunction:: simcoon.fillQE_R + +.. autofunction:: simcoon.rotate_vec_R + +.. autofunction:: simcoon.rotate_vec_angle + +.. autofunction:: simcoon.rotate_mat_R + +.. autofunction:: simcoon.rotate_mat_angle + +.. autofunction:: simcoon.rotate_stress_angle + +.. autofunction:: simcoon.rotate_stress_R + +.. autofunction:: simcoon.rotate_strain_angle + +.. autofunction:: simcoon.rotate_strain_R + +.. autofunction:: simcoon.rotateL_angle + +.. autofunction:: simcoon.rotateL_R + +.. autofunction:: simcoon.rotateM_angle + +.. autofunction:: simcoon.rotateM_R + +Examples +======== + +Example 1: Material Orientation +------------------------------- + +Rotating material properties from local to global coordinates: + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Define orthotropic stiffness in material coordinates + L_local = smc.L_ortho([E1, E2, E3, nu12, nu13, nu23, G12, G13, G23], "EnuG") + + # Material orientation: Euler angles (radians) + psi, theta, phi = 0.5, 0.3, 0.7 + + # Method 1: Using Rotation class + r = smc.Rotation.from_euler(psi, theta, phi, "zxz") + L_global = r.apply_stiffness(L_local) + + # Method 2: Using free function + L_global = smc.rotate_l2g_L(L_local, psi, theta, phi) + +Example 2: Stress Transformation +-------------------------------- + +Transforming stress from global to local coordinates: + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Stress in global coordinates (Voigt notation) + sigma_global = np.array([100.0, 50.0, 25.0, 10.0, 5.0, 2.0]) + + # Create rotation from orientation + r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + + # Transform to local coordinates (inverse rotation) + sigma_local = r.inv().apply_stress(sigma_global) + +Example 3: Rotation Composition +------------------------------- + +Combining multiple rotations: + +.. code-block:: cpp + + #include + + using namespace simcoon; + + // First rotation: 45° around z-axis + Rotation r1 = Rotation::from_axis_angle(M_PI/4, 3); + + // Second rotation: 30° around x-axis + Rotation r2 = Rotation::from_axis_angle(M_PI/6, 1); + + // Combined rotation: apply r1 first, then r2 + Rotation r_combined = r2 * r1; + + // Apply to vector + arma::vec::fixed<3> v = {1, 0, 0}; + arma::vec::fixed<3> v_rot = r_combined.apply(v); + + // This is equivalent to: + arma::vec::fixed<3> v_rot2 = r2.apply(r1.apply(v)); + +Example 4: Interpolating Orientations +------------------------------------- + +Smooth interpolation between two orientations using SLERP: + +.. code-block:: python + + import simcoon as smc + import numpy as np + + # Start and end orientations + r_start = smc.Rotation.identity() + r_end = smc.Rotation.from_euler(np.pi/2, np.pi/4, 0, "zxz") + + # Interpolate at 10 steps + for t in np.linspace(0, 1, 10): + r_t = r_start.slerp(r_end, t) + R = r_t.as_matrix() + print(f"t={t:.1f}: angle={r_t.magnitude(degrees=True):.1f}°") + +Theory +====== + +Quaternion Representation +------------------------- + +The ``Rotation`` class uses unit quaternions in scalar-last convention: + +.. math:: + + q = [q_x, q_y, q_z, q_w] = [\mathbf{v}, w] + +where :math:`\|\mathbf{q}\| = 1`. + +A rotation by angle :math:`\theta` around unit axis :math:`\mathbf{u}` is represented as: + +.. math:: + + q = \left[\mathbf{u} \sin\frac{\theta}{2}, \cos\frac{\theta}{2}\right] + +Quaternion to Rotation Matrix +----------------------------- + +The rotation matrix is computed from the quaternion as: + +.. math:: + + R = \begin{bmatrix} + 1 - 2(q_y^2 + q_z^2) & 2(q_x q_y - q_z q_w) & 2(q_x q_z + q_y q_w) \\ + 2(q_x q_y + q_z q_w) & 1 - 2(q_x^2 + q_z^2) & 2(q_y q_z - q_x q_w) \\ + 2(q_x q_z - q_y q_w) & 2(q_y q_z + q_x q_w) & 1 - 2(q_x^2 + q_y^2) + \end{bmatrix} + +Voigt Notation +-------------- + +For stress and strain tensors, simcoon uses Voigt notation: + +.. math:: + + \boldsymbol{\sigma} = [\sigma_{11}, \sigma_{22}, \sigma_{33}, \sigma_{12}, \sigma_{13}, \sigma_{23}]^T + +.. math:: + + \boldsymbol{\varepsilon} = [\varepsilon_{11}, \varepsilon_{22}, \varepsilon_{33}, 2\varepsilon_{12}, 2\varepsilon_{13}, 2\varepsilon_{23}]^T + +The 6×6 rotation matrices :math:`Q_\sigma` (for stress) and :math:`Q_\varepsilon` (for strain) +are related to the 3×3 rotation matrix :math:`R` and satisfy: + +.. math:: + + \boldsymbol{\sigma}' = Q_\sigma \boldsymbol{\sigma} + +.. math:: + + \boldsymbol{\varepsilon}' = Q_\varepsilon \boldsymbol{\varepsilon} + +.. math:: + + L' = Q_\sigma L Q_\sigma^T + +.. math:: + + M' = Q_\varepsilon M Q_\varepsilon^T + +SLERP Interpolation +------------------- + +Spherical Linear Interpolation (SLERP) provides smooth interpolation between rotations: + +.. math:: + + \text{slerp}(q_0, q_1, t) = \frac{\sin((1-t)\Omega)}{\sin\Omega} q_0 + \frac{\sin(t\Omega)}{\sin\Omega} q_1 + +where :math:`\Omega = \arccos(q_0 \cdot q_1)` is the angle between the quaternions. + +See Also +======== + +- :doc:`../continuum_mechanics/functions/kinematics` - Finite strain kinematics +- :doc:`../continuum_mechanics/functions/constitutive` - Constitutive matrices +- :doc:`../continuum_mechanics/functions/transfer` - Voigt/tensor conversions diff --git a/include/simcoon/Simulation/Maths/rotation_class.hpp b/include/simcoon/Simulation/Maths/rotation_class.hpp new file mode 100644 index 000000000..adc024f8c --- /dev/null +++ b/include/simcoon/Simulation/Maths/rotation_class.hpp @@ -0,0 +1,410 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file rotation_class.hpp +///@brief Rotation class inspired by scipy.spatial.transform.Rotation +///@version 1.0 + +#pragma once + +#include +#include + +namespace simcoon { + +/** + * @class Rotation + * @brief A class representing 3D rotations using unit quaternions. + * + * Inspired by scipy.spatial.transform.Rotation, this class provides a unified + * interface for working with rotations in various representations (quaternion, + * rotation matrix, Euler angles, rotation vector) while maintaining high + * numerical precision and performance. + * + * Internal representation uses unit quaternions in scalar-last convention [qx, qy, qz, qw]. + * + * @details Example usage: + * @code + * // Create rotation from Euler angles + * Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + * + * // Apply to a vector + * arma::vec::fixed<3> v = {1, 0, 0}; + * arma::vec::fixed<3> v_rot = r.apply(v); + * + * // Compose rotations + * Rotation r2 = Rotation::from_axis_angle(M_PI/4, 3); + * Rotation r_combined = r * r2; + * + * // Apply to Voigt stress tensor + * arma::vec::fixed<6> sigma = {100, 50, 0, 25, 0, 0}; + * arma::vec::fixed<6> sigma_rot = r.apply_stress(sigma); + * @endcode + */ +class Rotation { +private: + arma::vec::fixed<4> _quat; // [qx, qy, qz, qw] - scalar-last convention, stack allocated + + // Private constructor from quaternion (assumes already normalized) + explicit Rotation(const arma::vec::fixed<4>& quat); + +public: + /** + * @brief Default constructor creating identity rotation. + */ + Rotation(); + + /** + * @brief Copy constructor. + */ + Rotation(const Rotation& other) = default; + + /** + * @brief Move constructor. + */ + Rotation(Rotation&& other) noexcept = default; + + /** + * @brief Copy assignment operator. + */ + Rotation& operator=(const Rotation& other) = default; + + /** + * @brief Move assignment operator. + */ + Rotation& operator=(Rotation&& other) noexcept = default; + + /** + * @brief Destructor. + */ + ~Rotation() = default; + + // ========================================================================= + // Factory Methods + // ========================================================================= + + /** + * @brief Create an identity rotation. + * @return Identity rotation (no rotation) + */ + static Rotation identity(); + + /** + * @brief Create rotation from a unit quaternion. + * @param quat Quaternion in [qx, qy, qz, qw] format (scalar-last) + * @return Rotation object + * @note The quaternion will be normalized if not already unit length + */ + static Rotation from_quat(const arma::vec::fixed<4>& quat); + + /** + * @brief Create rotation from a unit quaternion. + * @param quat Quaternion in [qx, qy, qz, qw] format (scalar-last), dynamic size + * @return Rotation object + */ + static Rotation from_quat(const arma::vec& quat); + + /** + * @brief Create rotation from a 3x3 rotation matrix. + * @param R 3x3 rotation matrix (must be orthogonal with det=1) + * @return Rotation object + */ + static Rotation from_matrix(const arma::mat::fixed<3,3>& R); + + /** + * @brief Create rotation from a 3x3 rotation matrix. + * @param R 3x3 rotation matrix (dynamic size) + * @return Rotation object + */ + static Rotation from_matrix(const arma::mat& R); + + /** + * @brief Create rotation from Euler angles. + * @param psi First Euler angle in radians (or degrees if degrees=true) + * @param theta Second Euler angle in radians (or degrees if degrees=true) + * @param phi Third Euler angle in radians (or degrees if degrees=true) + * @param conv Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", + * "xyx", "xzx", "yxy", "yzy", or "user" + * @param intrinsic If true (default), intrinsic rotations (rotating frame); + * if false, extrinsic rotations (fixed frame) + * @param degrees If true, angles are in degrees; if false (default), radians + * @return Rotation object + */ + static Rotation from_euler(double psi, double theta, double phi, + const std::string& conv = "zxz", + bool intrinsic = true, bool degrees = false); + + /** + * @brief Create rotation from Euler angles (vector input). + * @param angles 3-element vector of Euler angles + * @param conv Euler angle convention + * @param intrinsic If true, intrinsic rotations; if false, extrinsic rotations + * @param degrees If true, angles are in degrees + * @return Rotation object + */ + static Rotation from_euler(const arma::vec::fixed<3>& angles, + const std::string& conv = "zxz", + bool intrinsic = true, bool degrees = false); + + /** + * @brief Create rotation from a rotation vector (axis-angle representation). + * @param rotvec Rotation vector where direction is axis and magnitude is angle + * @param degrees If true, magnitude is in degrees; if false (default), radians + * @return Rotation object + */ + static Rotation from_rotvec(const arma::vec::fixed<3>& rotvec, bool degrees = false); + + /** + * @brief Create rotation from a rotation vector (dynamic size). + * @param rotvec Rotation vector (3 elements) + * @param degrees If true, magnitude is in degrees + * @return Rotation object + */ + static Rotation from_rotvec(const arma::vec& rotvec, bool degrees = false); + + /** + * @brief Create rotation around a single axis. + * @param angle Rotation angle in radians (or degrees if degrees=true) + * @param axis Axis of rotation: 1=x, 2=y, 3=z + * @param degrees If true, angle is in degrees + * @return Rotation object + */ + static Rotation from_axis_angle(double angle, int axis, bool degrees = false); + + /** + * @brief Create a random rotation (uniformly distributed). + * @return Random rotation object + */ + static Rotation random(); + + // ========================================================================= + // Conversion Methods + // ========================================================================= + + /** + * @brief Get rotation as a unit quaternion. + * @return Quaternion in [qx, qy, qz, qw] format (scalar-last) + */ + inline arma::vec::fixed<4> as_quat() const { return _quat; } + + /** + * @brief Get rotation as a 3x3 rotation matrix. + * @return 3x3 orthogonal rotation matrix + */ + arma::mat::fixed<3,3> as_matrix() const; + + /** + * @brief Get rotation as Euler angles. + * @param conv Euler angle convention + * @param intrinsic If true, return intrinsic angles; if false, extrinsic + * @param degrees If true, return angles in degrees + * @return 3-element vector of Euler angles [psi, theta, phi] + */ + arma::vec::fixed<3> as_euler(const std::string& conv = "zxz", + bool intrinsic = true, bool degrees = false) const; + + /** + * @brief Get rotation as a rotation vector. + * @param degrees If true, magnitude is in degrees + * @return Rotation vector (axis * angle) + */ + arma::vec::fixed<3> as_rotvec(bool degrees = false) const; + + /** + * @brief Get 6x6 rotation matrix for stress tensors in Voigt notation. + * @param active If true (default), active rotation; if false, passive + * @return 6x6 stress rotation matrix (QS) + */ + arma::mat::fixed<6,6> as_QS(bool active = true) const; + + /** + * @brief Get 6x6 rotation matrix for strain tensors in Voigt notation. + * @param active If true (default), active rotation; if false, passive + * @return 6x6 strain rotation matrix (QE) + */ + arma::mat::fixed<6,6> as_QE(bool active = true) const; + + // ========================================================================= + // Apply Methods (3D objects) + // ========================================================================= + + /** + * @brief Apply rotation to a 3D vector. + * @param v 3D vector to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated vector + * @details Uses direct quaternion-vector rotation (faster than matrix multiplication) + */ + arma::vec::fixed<3> apply(const arma::vec::fixed<3>& v, bool inverse = false) const; + + /** + * @brief Apply rotation to a 3D vector (dynamic size). + * @param v 3D vector to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated vector + */ + arma::vec apply(const arma::vec& v, bool inverse = false) const; + + /** + * @brief Apply rotation to a 3x3 tensor (matrix). + * @param m 3x3 tensor to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated tensor: R * m * R^T (or R^T * m * R for inverse) + */ + arma::mat::fixed<3,3> apply_tensor(const arma::mat::fixed<3,3>& m, bool inverse = false) const; + + /** + * @brief Apply rotation to a 3x3 tensor (dynamic size). + * @param m 3x3 tensor to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated tensor + */ + arma::mat apply_tensor(const arma::mat& m, bool inverse = false) const; + + // ========================================================================= + // Apply Methods (Voigt notation) + // ========================================================================= + + /** + * @brief Apply rotation to a stress vector in Voigt notation. + * @param sigma 6-component stress vector [s11, s22, s33, s12, s13, s23] + * @param active If true (default), active rotation + * @return Rotated stress vector + */ + arma::vec::fixed<6> apply_stress(const arma::vec::fixed<6>& sigma, bool active = true) const; + + /** + * @brief Apply rotation to a stress vector (dynamic size). + * @param sigma 6-component stress vector + * @param active If true, active rotation + * @return Rotated stress vector + */ + arma::vec apply_stress(const arma::vec& sigma, bool active = true) const; + + /** + * @brief Apply rotation to a strain vector in Voigt notation. + * @param epsilon 6-component strain vector [e11, e22, e33, 2*e12, 2*e13, 2*e23] + * @param active If true (default), active rotation + * @return Rotated strain vector + */ + arma::vec::fixed<6> apply_strain(const arma::vec::fixed<6>& epsilon, bool active = true) const; + + /** + * @brief Apply rotation to a strain vector (dynamic size). + * @param epsilon 6-component strain vector + * @param active If true, active rotation + * @return Rotated strain vector + */ + arma::vec apply_strain(const arma::vec& epsilon, bool active = true) const; + + /** + * @brief Apply rotation to a 6x6 stiffness matrix. + * @param L 6x6 stiffness matrix in Voigt notation + * @param active If true (default), active rotation + * @return Rotated stiffness matrix: QS * L * QS^T + */ + arma::mat::fixed<6,6> apply_stiffness(const arma::mat::fixed<6,6>& L, bool active = true) const; + + /** + * @brief Apply rotation to a stiffness matrix (dynamic size). + * @param L 6x6 stiffness matrix + * @param active If true, active rotation + * @return Rotated stiffness matrix + */ + arma::mat apply_stiffness(const arma::mat& L, bool active = true) const; + + /** + * @brief Apply rotation to a 6x6 compliance matrix. + * @param M 6x6 compliance matrix in Voigt notation + * @param active If true (default), active rotation + * @return Rotated compliance matrix: QE * M * QE^T + */ + arma::mat::fixed<6,6> apply_compliance(const arma::mat::fixed<6,6>& M, bool active = true) const; + + /** + * @brief Apply rotation to a compliance matrix (dynamic size). + * @param M 6x6 compliance matrix + * @param active If true, active rotation + * @return Rotated compliance matrix + */ + arma::mat apply_compliance(const arma::mat& M, bool active = true) const; + + // ========================================================================= + // Operations + // ========================================================================= + + /** + * @brief Get the inverse (conjugate) rotation. + * @return Inverse rotation + */ + Rotation inv() const; + + /** + * @brief Get the magnitude (rotation angle) of this rotation. + * @param degrees If true, return in degrees + * @return Rotation angle in radians (or degrees) + */ + double magnitude(bool degrees = false) const; + + /** + * @brief Compose this rotation with another (this * other). + * @param other Rotation to compose with + * @return Composed rotation + * @details The composition represents applying 'other' first, then 'this' + */ + Rotation operator*(const Rotation& other) const; + + /** + * @brief Compose this rotation with another in-place. + * @param other Rotation to compose with + * @return Reference to this rotation + */ + Rotation& operator*=(const Rotation& other); + + /** + * @brief Spherical linear interpolation between this rotation and another. + * @param other Target rotation + * @param t Interpolation parameter in [0, 1] (0 = this, 1 = other) + * @return Interpolated rotation + */ + Rotation slerp(const Rotation& other, double t) const; + + /** + * @brief Check if this rotation equals another within tolerance. + * @param other Rotation to compare with + * @param tol Tolerance for comparison + * @return True if rotations are equal (or antipodal quaternions) + */ + inline bool equals(const Rotation& other, double tol = 1e-12) const { + // Quaternions q and -q represent the same rotation + double diff1 = arma::norm(_quat - other._quat); + double diff2 = arma::norm(_quat + other._quat); + return (diff1 < tol) || (diff2 < tol); + } + + /** + * @brief Check if this rotation is identity (no rotation). + * @param tol Tolerance for comparison + * @return True if this is the identity rotation + */ + inline bool is_identity(double tol = 1e-12) const { + // Identity quaternion is [0, 0, 0, 1] or [0, 0, 0, -1] + return std::abs(std::abs(_quat(3)) - 1.0) < tol; + } +}; + +} // namespace simcoon diff --git a/simcoon-python-builder/CMakeLists.txt b/simcoon-python-builder/CMakeLists.txt index 168ffa22b..339abd060 100755 --- a/simcoon-python-builder/CMakeLists.txt +++ b/simcoon-python-builder/CMakeLists.txt @@ -51,6 +51,7 @@ pybind11_add_module(_core # Maths src/python_wrappers/Libraries/Maths/lagrange.cpp src/python_wrappers/Libraries/Maths/rotation.cpp + src/python_wrappers/Libraries/Maths/rotation_class.cpp # Solver src/python_wrappers/Libraries/Solver/read.cpp diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation_class.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation_class.hpp new file mode 100644 index 000000000..3a0a8e9a5 --- /dev/null +++ b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation_class.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +#include + +namespace simpy { + +// Function to register the Rotation class with pybind11 +void register_rotation_class(pybind11::module_& m); + +} // namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp new file mode 100644 index 000000000..0d9bfac76 --- /dev/null +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp @@ -0,0 +1,510 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include + +using namespace std; +using namespace arma; +namespace py = pybind11; + +namespace simpy { + +void register_rotation_class(py::module_& m) { + py::class_(m, "Rotation", + R"doc( + A class representing 3D rotations using unit quaternions. + + Inspired by scipy.spatial.transform.Rotation, this class provides a unified + interface for working with rotations in various representations (quaternion, + rotation matrix, Euler angles, rotation vector) while maintaining high + numerical precision and performance. + + The internal representation uses unit quaternions in scalar-last convention [qx, qy, qz, qw]. + + Example + ------- + >>> import simcoon as smc + >>> import numpy as np + >>> + >>> # Create rotation from Euler angles + >>> r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + >>> + >>> # Apply to a vector + >>> v = np.array([1.0, 0.0, 0.0]) + >>> v_rot = r.apply(v) + >>> + >>> # Compose rotations + >>> r2 = smc.Rotation.from_axis_angle(np.pi/4, 3) + >>> r_combined = r * r2 + >>> + >>> # Apply to Voigt stress tensor + >>> sigma = np.array([100.0, 50.0, 0.0, 25.0, 0.0, 0.0]) + >>> sigma_rot = r.apply_stress(sigma) + )doc") + + // Default constructor + .def(py::init<>(), "Create an identity rotation") + + // Static factory methods + .def_static("identity", &simcoon::Rotation::identity, + "Create an identity rotation (no rotation)") + + .def_static("from_quat", + [](py::array_t quat) { + vec q = carma::arr_to_col(quat); + return simcoon::Rotation::from_quat(q); + }, + py::arg("quat"), + R"doc( + Create rotation from a unit quaternion. + + Parameters + ---------- + quat : array_like + Quaternion in [qx, qy, qz, qw] format (scalar-last). + Will be normalized if not already unit length. + + Returns + ------- + Rotation + Rotation object + )doc") + + .def_static("from_matrix", + [](py::array_t R) { + mat R_mat = carma::arr_to_mat(R); + return simcoon::Rotation::from_matrix(R_mat); + }, + py::arg("R"), + R"doc( + Create rotation from a 3x3 rotation matrix. + + Parameters + ---------- + R : array_like + 3x3 rotation matrix (must be orthogonal with det=1) + + Returns + ------- + Rotation + Rotation object + )doc") + + .def_static("from_euler", + py::overload_cast( + &simcoon::Rotation::from_euler), + py::arg("psi"), py::arg("theta"), py::arg("phi"), + py::arg("conv") = "zxz", py::arg("intrinsic") = true, py::arg("degrees") = false, + R"doc( + Create rotation from Euler angles. + + Parameters + ---------- + psi : float + First Euler angle + theta : float + Second Euler angle + phi : float + Third Euler angle + conv : str, optional + Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", + "xyx", "xzx", "yxy", "yzy", or "user". Default is "zxz". + intrinsic : bool, optional + If True (default), intrinsic rotations (rotating frame); + if False, extrinsic rotations (fixed frame) + degrees : bool, optional + If True, angles are in degrees; if False (default), radians + + Returns + ------- + Rotation + Rotation object + )doc") + + .def_static("from_rotvec", + [](py::array_t rotvec, bool degrees) { + vec rv = carma::arr_to_col(rotvec); + return simcoon::Rotation::from_rotvec(rv, degrees); + }, + py::arg("rotvec"), py::arg("degrees") = false, + R"doc( + Create rotation from a rotation vector. + + Parameters + ---------- + rotvec : array_like + Rotation vector where direction is axis and magnitude is angle + degrees : bool, optional + If True, magnitude is in degrees; if False (default), radians + + Returns + ------- + Rotation + Rotation object + )doc") + + .def_static("from_axis_angle", &simcoon::Rotation::from_axis_angle, + py::arg("angle"), py::arg("axis"), py::arg("degrees") = false, + R"doc( + Create rotation around a single axis. + + Parameters + ---------- + angle : float + Rotation angle + axis : int + Axis of rotation: 1=x, 2=y, 3=z + degrees : bool, optional + If True, angle is in degrees; if False (default), radians + + Returns + ------- + Rotation + Rotation object + )doc") + + .def_static("random", &simcoon::Rotation::random, + "Create a uniformly distributed random rotation") + + // Conversion methods + .def("as_quat", + [](const simcoon::Rotation& self) { + return carma::col_to_arr(vec(self.as_quat())); + }, + R"doc( + Get rotation as a unit quaternion. + + Returns + ------- + ndarray + Quaternion in [qx, qy, qz, qw] format (scalar-last) + )doc") + + .def("as_matrix", + [](const simcoon::Rotation& self) { + return carma::mat_to_arr(mat(self.as_matrix())); + }, + R"doc( + Get rotation as a 3x3 rotation matrix. + + Returns + ------- + ndarray + 3x3 orthogonal rotation matrix + )doc") + + .def("as_euler", + [](const simcoon::Rotation& self, const string& conv, bool intrinsic, bool degrees) { + return carma::col_to_arr(vec(self.as_euler(conv, intrinsic, degrees))); + }, + py::arg("conv") = "zxz", py::arg("intrinsic") = true, py::arg("degrees") = false, + R"doc( + Get rotation as Euler angles. + + Parameters + ---------- + conv : str, optional + Euler angle convention. Default is "zxz". + intrinsic : bool, optional + If True (default), return intrinsic angles; if False, extrinsic + degrees : bool, optional + If True, return angles in degrees; if False (default), radians + + Returns + ------- + ndarray + 3-element array of Euler angles [psi, theta, phi] + )doc") + + .def("as_rotvec", + [](const simcoon::Rotation& self, bool degrees) { + return carma::col_to_arr(vec(self.as_rotvec(degrees))); + }, + py::arg("degrees") = false, + R"doc( + Get rotation as a rotation vector. + + Parameters + ---------- + degrees : bool, optional + If True, magnitude is in degrees; if False (default), radians + + Returns + ------- + ndarray + Rotation vector (axis * angle) + )doc") + + .def("as_QS", + [](const simcoon::Rotation& self, bool active) { + return carma::mat_to_arr(mat(self.as_QS(active))); + }, + py::arg("active") = true, + R"doc( + Get 6x6 rotation matrix for stress tensors in Voigt notation. + + Parameters + ---------- + active : bool, optional + If True (default), active rotation; if False, passive + + Returns + ------- + ndarray + 6x6 stress rotation matrix (QS) + )doc") + + .def("as_QE", + [](const simcoon::Rotation& self, bool active) { + return carma::mat_to_arr(mat(self.as_QE(active))); + }, + py::arg("active") = true, + R"doc( + Get 6x6 rotation matrix for strain tensors in Voigt notation. + + Parameters + ---------- + active : bool, optional + If True (default), active rotation; if False, passive + + Returns + ------- + ndarray + 6x6 strain rotation matrix (QE) + )doc") + + // Apply methods + .def("apply", + [](const simcoon::Rotation& self, py::array_t v, bool inverse) { + vec v_cpp = carma::arr_to_col(v); + vec result = self.apply(v_cpp, inverse); + return carma::col_to_arr(result); + }, + py::arg("v"), py::arg("inverse") = false, + R"doc( + Apply rotation to a 3D vector. + + Parameters + ---------- + v : array_like + 3D vector to rotate + inverse : bool, optional + If True, apply inverse rotation. Default is False. + + Returns + ------- + ndarray + Rotated vector + )doc") + + .def("apply_tensor", + [](const simcoon::Rotation& self, py::array_t m, bool inverse) { + mat m_cpp = carma::arr_to_mat(m); + mat result = self.apply_tensor(m_cpp, inverse); + return carma::mat_to_arr(result); + }, + py::arg("m"), py::arg("inverse") = false, + R"doc( + Apply rotation to a 3x3 tensor (matrix). + + Parameters + ---------- + m : array_like + 3x3 tensor to rotate + inverse : bool, optional + If True, apply inverse rotation. Default is False. + + Returns + ------- + ndarray + Rotated tensor: R * m * R^T (or R^T * m * R for inverse) + )doc") + + .def("apply_stress", + [](const simcoon::Rotation& self, py::array_t sigma, bool active) { + vec sigma_cpp = carma::arr_to_col(sigma); + vec result = self.apply_stress(sigma_cpp, active); + return carma::col_to_arr(result); + }, + py::arg("sigma"), py::arg("active") = true, + R"doc( + Apply rotation to a stress vector in Voigt notation. + + Parameters + ---------- + sigma : array_like + 6-component stress vector [s11, s22, s33, s12, s13, s23] + active : bool, optional + If True (default), active rotation + + Returns + ------- + ndarray + Rotated stress vector + )doc") + + .def("apply_strain", + [](const simcoon::Rotation& self, py::array_t epsilon, bool active) { + vec epsilon_cpp = carma::arr_to_col(epsilon); + vec result = self.apply_strain(epsilon_cpp, active); + return carma::col_to_arr(result); + }, + py::arg("epsilon"), py::arg("active") = true, + R"doc( + Apply rotation to a strain vector in Voigt notation. + + Parameters + ---------- + epsilon : array_like + 6-component strain vector [e11, e22, e33, 2*e12, 2*e13, 2*e23] + active : bool, optional + If True (default), active rotation + + Returns + ------- + ndarray + Rotated strain vector + )doc") + + .def("apply_stiffness", + [](const simcoon::Rotation& self, py::array_t L, bool active) { + mat L_cpp = carma::arr_to_mat(L); + mat result = self.apply_stiffness(L_cpp, active); + return carma::mat_to_arr(result); + }, + py::arg("L"), py::arg("active") = true, + R"doc( + Apply rotation to a 6x6 stiffness matrix. + + Parameters + ---------- + L : array_like + 6x6 stiffness matrix in Voigt notation + active : bool, optional + If True (default), active rotation + + Returns + ------- + ndarray + Rotated stiffness matrix: QS * L * QS^T + )doc") + + .def("apply_compliance", + [](const simcoon::Rotation& self, py::array_t M, bool active) { + mat M_cpp = carma::arr_to_mat(M); + mat result = self.apply_compliance(M_cpp, active); + return carma::mat_to_arr(result); + }, + py::arg("M"), py::arg("active") = true, + R"doc( + Apply rotation to a 6x6 compliance matrix. + + Parameters + ---------- + M : array_like + 6x6 compliance matrix in Voigt notation + active : bool, optional + If True (default), active rotation + + Returns + ------- + ndarray + Rotated compliance matrix: QE * M * QE^T + )doc") + + // Operations + .def("inv", &simcoon::Rotation::inv, + "Get the inverse (conjugate) rotation") + + .def("magnitude", &simcoon::Rotation::magnitude, + py::arg("degrees") = false, + R"doc( + Get the magnitude (rotation angle) of this rotation. + + Parameters + ---------- + degrees : bool, optional + If True, return in degrees; if False (default), radians + + Returns + ------- + float + Rotation angle + )doc") + + .def("__mul__", &simcoon::Rotation::operator*, + py::arg("other"), + "Compose this rotation with another (self * other)") + + .def("__imul__", &simcoon::Rotation::operator*=, + py::arg("other"), + "Compose this rotation with another in-place") + + .def("slerp", &simcoon::Rotation::slerp, + py::arg("other"), py::arg("t"), + R"doc( + Spherical linear interpolation between this rotation and another. + + Parameters + ---------- + other : Rotation + Target rotation + t : float + Interpolation parameter in [0, 1] (0 = self, 1 = other) + + Returns + ------- + Rotation + Interpolated rotation + )doc") + + .def("equals", &simcoon::Rotation::equals, + py::arg("other"), py::arg("tol") = 1e-12, + R"doc( + Check if this rotation equals another within tolerance. + + Parameters + ---------- + other : Rotation + Rotation to compare with + tol : float, optional + Tolerance for comparison. Default is 1e-12. + + Returns + ------- + bool + True if rotations are equal (or antipodal quaternions) + )doc") + + .def("is_identity", &simcoon::Rotation::is_identity, + py::arg("tol") = 1e-12, + R"doc( + Check if this rotation is identity (no rotation). + + Parameters + ---------- + tol : float, optional + Tolerance for comparison. Default is 1e-12. + + Returns + ------- + bool + True if this is the identity rotation + )doc") + + .def("__repr__", + [](const simcoon::Rotation& self) { + vec::fixed<4> q = self.as_quat(); + return ""; + }); +} + +} // namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/python_module.cpp b/simcoon-python-builder/src/python_wrappers/python_module.cpp index 04489d88b..ab850fd72 100755 --- a/simcoon-python-builder/src/python_wrappers/python_module.cpp +++ b/simcoon-python-builder/src/python_wrappers/python_module.cpp @@ -20,6 +20,7 @@ // #include #include +#include #include #include #include @@ -244,6 +245,9 @@ PYBIND11_MODULE(_core, m) m.def("rotate_l2g_stress", &rotate_l2g_stress, "input"_a, "psi"_a, "theta"_a, "phi"_a, "copy"_a = true, "Return the rotated stress matrix using voigt notations from local to global frame"); m.def("rotate_g2l_stress", &rotate_g2l_stress, "input"_a, "psi"_a, "theta"_a, "phi"_a, "copy"_a = true, "Return the rotated stress matrix using voigt notations from global to local frame"); + // Register the Rotation class + register_rotation_class(m); + // Register the from-python converters for lagrange m.def("lagrange_exp", &lagrange_exp, "This function is used to determine an exponential Lagrange Multiplier (like contact in Abaqus)"); m.def("dlagrange_exp", &dlagrange_exp, "This function is used to determine the first derivative of an exponential Lagrange Multiplier"); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 439b5125f..6cd448d21 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -127,6 +127,7 @@ target_sources(simcoon PRIVATE Simulation/Maths/num_solve.cpp Simulation/Maths/random.cpp Simulation/Maths/rotation.cpp + Simulation/Maths/rotation_class.cpp Simulation/Maths/solve.cpp Simulation/Maths/stats.cpp diff --git a/src/Simulation/Maths/rotation_class.cpp b/src/Simulation/Maths/rotation_class.cpp new file mode 100644 index 000000000..9a7e96212 --- /dev/null +++ b/src/Simulation/Maths/rotation_class.cpp @@ -0,0 +1,634 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file rotation_class.cpp +///@brief Implementation of the Rotation class +///@version 1.0 + +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace arma; + +namespace simcoon { + +// ============================================================================= +// Helper functions (internal) +// ============================================================================= + +namespace { + +// Convert axis number (1,2,3) to axis index (0,1,2) +inline int axis_to_index(int axis) { + if (axis < 1 || axis > 3) { + throw invalid_argument("Axis must be 1 (x), 2 (y), or 3 (z)"); + } + return axis - 1; +} + +// Parse Euler convention string to get axis sequence +// Returns axes as 0=x, 1=y, 2=z +void parse_euler_convention(const string& conv, int& axis1, int& axis2, int& axis3) { + if (conv.length() != 3) { + throw invalid_argument("Euler convention must be 3 characters (e.g., 'zxz', 'xyz')"); + } + + auto char_to_axis = [](char c) -> int { + switch (c) { + case 'x': case 'X': return 0; + case 'y': case 'Y': return 1; + case 'z': case 'Z': return 2; + default: + throw invalid_argument("Invalid axis character in Euler convention"); + } + }; + + axis1 = char_to_axis(conv[0]); + axis2 = char_to_axis(conv[1]); + axis3 = char_to_axis(conv[2]); +} + +// Normalize a quaternion to unit length +vec::fixed<4> normalize_quat(const vec::fixed<4>& q) { + double n = norm(q); + if (n < sim_iota) { + // Return identity quaternion if input is near zero + return {0.0, 0.0, 0.0, 1.0}; + } + return q / n; +} + +// Hamilton product of two quaternions (scalar-last convention) +// q = [qx, qy, qz, qw] +vec::fixed<4> quat_multiply(const vec::fixed<4>& q1, const vec::fixed<4>& q2) { + double x1 = q1(0), y1 = q1(1), z1 = q1(2), w1 = q1(3); + double x2 = q2(0), y2 = q2(1), z2 = q2(2), w2 = q2(3); + + return { + w1*x2 + x1*w2 + y1*z2 - z1*y2, // x + w1*y2 - x1*z2 + y1*w2 + z1*x2, // y + w1*z2 + x1*y2 - y1*x2 + z1*w2, // z + w1*w2 - x1*x2 - y1*y2 - z1*z2 // w + }; +} + +// Quaternion conjugate (inverse for unit quaternion) +vec::fixed<4> quat_conjugate(const vec::fixed<4>& q) { + return {-q(0), -q(1), -q(2), q(3)}; +} + +} // anonymous namespace + +// ============================================================================= +// Constructors +// ============================================================================= + +Rotation::Rotation() : _quat({0.0, 0.0, 0.0, 1.0}) { + // Identity rotation +} + +Rotation::Rotation(const vec::fixed<4>& quat) : _quat(quat) { + // Private constructor - assumes quaternion is already normalized +} + +// ============================================================================= +// Factory Methods +// ============================================================================= + +Rotation Rotation::identity() { + return Rotation(); +} + +Rotation Rotation::from_quat(const vec::fixed<4>& quat) { + return Rotation(normalize_quat(quat)); +} + +Rotation Rotation::from_quat(const vec& quat) { + if (quat.n_elem != 4) { + throw invalid_argument("Quaternion must have 4 elements"); + } + vec::fixed<4> q_fixed = {quat(0), quat(1), quat(2), quat(3)}; + return from_quat(q_fixed); +} + +Rotation Rotation::from_matrix(const mat::fixed<3,3>& R) { + // Shepperd's method for numerical stability + // Reference: S.W. Shepperd, "Quaternion from Rotation Matrix" + // Journal of Guidance and Control, Vol. 1, No. 3, 1978 + + double trace = R(0,0) + R(1,1) + R(2,2); + vec::fixed<4> q; + + if (trace > 0) { + double s = 0.5 / sqrt(trace + 1.0); + q(3) = 0.25 / s; + q(0) = (R(2,1) - R(1,2)) * s; + q(1) = (R(0,2) - R(2,0)) * s; + q(2) = (R(1,0) - R(0,1)) * s; + } else if (R(0,0) > R(1,1) && R(0,0) > R(2,2)) { + double s = 2.0 * sqrt(1.0 + R(0,0) - R(1,1) - R(2,2)); + q(3) = (R(2,1) - R(1,2)) / s; + q(0) = 0.25 * s; + q(1) = (R(0,1) + R(1,0)) / s; + q(2) = (R(0,2) + R(2,0)) / s; + } else if (R(1,1) > R(2,2)) { + double s = 2.0 * sqrt(1.0 + R(1,1) - R(0,0) - R(2,2)); + q(3) = (R(0,2) - R(2,0)) / s; + q(0) = (R(0,1) + R(1,0)) / s; + q(1) = 0.25 * s; + q(2) = (R(1,2) + R(2,1)) / s; + } else { + double s = 2.0 * sqrt(1.0 + R(2,2) - R(0,0) - R(1,1)); + q(3) = (R(1,0) - R(0,1)) / s; + q(0) = (R(0,2) + R(2,0)) / s; + q(1) = (R(1,2) + R(2,1)) / s; + q(2) = 0.25 * s; + } + + return Rotation(normalize_quat(q)); +} + +Rotation Rotation::from_matrix(const mat& R) { + if (R.n_rows != 3 || R.n_cols != 3) { + throw invalid_argument("Rotation matrix must be 3x3"); + } + mat::fixed<3,3> R_fixed; + R_fixed = R; + return from_matrix(R_fixed); +} + +Rotation Rotation::from_euler(double psi, double theta, double phi, + const string& conv, bool intrinsic, bool degrees) { + // Convert to radians if needed + if (degrees) { + psi *= sim_pi / 180.0; + theta *= sim_pi / 180.0; + phi *= sim_pi / 180.0; + } + + // Handle "user" convention using the existing fillR function + if (conv == "user") { + mat R = fillR(psi, theta, phi, true, "user"); + mat::fixed<3,3> R_fixed; + R_fixed = R; + return from_matrix(R_fixed); + } + + // Parse the convention + int axis1, axis2, axis3; + parse_euler_convention(conv, axis1, axis2, axis3); + + // Build quaternions for each elementary rotation + auto axis_quat = [](double angle, int axis) -> vec::fixed<4> { + double half_angle = angle / 2.0; + double s = sin(half_angle); + double c = cos(half_angle); + vec::fixed<4> q = {0, 0, 0, c}; + q(axis) = s; + return q; + }; + + vec::fixed<4> q1 = axis_quat(psi, axis1); + vec::fixed<4> q2 = axis_quat(theta, axis2); + vec::fixed<4> q3 = axis_quat(phi, axis3); + + // Compose quaternions + vec::fixed<4> result; + if (intrinsic) { + // Intrinsic: rotations about axes of the rotating frame + // Order: q3 * q2 * q1 (applied right-to-left) + result = quat_multiply(quat_multiply(q3, q2), q1); + } else { + // Extrinsic: rotations about fixed axes + // Order: q1 * q2 * q3 (applied left-to-right) + result = quat_multiply(quat_multiply(q1, q2), q3); + } + + return Rotation(normalize_quat(result)); +} + +Rotation Rotation::from_euler(const vec::fixed<3>& angles, const string& conv, + bool intrinsic, bool degrees) { + return from_euler(angles(0), angles(1), angles(2), conv, intrinsic, degrees); +} + +Rotation Rotation::from_rotvec(const vec::fixed<3>& rotvec, bool degrees) { + double angle = norm(rotvec); + + if (degrees) { + angle *= sim_pi / 180.0; + } + + if (angle < sim_iota) { + return identity(); + } + + vec::fixed<3> axis = rotvec / (degrees ? norm(rotvec) * 180.0 / sim_pi : norm(rotvec)); + double half_angle = angle / 2.0; + double s = sin(half_angle); + double c = cos(half_angle); + + return Rotation({axis(0)*s, axis(1)*s, axis(2)*s, c}); +} + +Rotation Rotation::from_rotvec(const vec& rotvec, bool degrees) { + if (rotvec.n_elem != 3) { + throw invalid_argument("Rotation vector must have 3 elements"); + } + vec::fixed<3> rv_fixed = {rotvec(0), rotvec(1), rotvec(2)}; + return from_rotvec(rv_fixed, degrees); +} + +Rotation Rotation::from_axis_angle(double angle, int axis, bool degrees) { + if (degrees) { + angle *= sim_pi / 180.0; + } + + int ax_idx = axis_to_index(axis); + double half_angle = angle / 2.0; + double s = sin(half_angle); + double c = cos(half_angle); + + vec::fixed<4> q = {0, 0, 0, c}; + q(ax_idx) = s; + + return Rotation(q); +} + +Rotation Rotation::random() { + // Generate uniformly distributed random rotation using Shoemake's method + // Reference: K. Shoemake, "Uniform random rotations", Graphics Gems III, 1992 + + vec u = randu(3); + + double sqrt1_u0 = sqrt(1.0 - u(0)); + double sqrt_u0 = sqrt(u(0)); + double two_pi_u1 = 2.0 * sim_pi * u(1); + double two_pi_u2 = 2.0 * sim_pi * u(2); + + vec::fixed<4> q = { + sqrt1_u0 * sin(two_pi_u1), + sqrt1_u0 * cos(two_pi_u1), + sqrt_u0 * sin(two_pi_u2), + sqrt_u0 * cos(two_pi_u2) + }; + + return Rotation(q); +} + +// ============================================================================= +// Conversion Methods +// ============================================================================= + +mat::fixed<3,3> Rotation::as_matrix() const { + double qx = _quat(0), qy = _quat(1), qz = _quat(2), qw = _quat(3); + + // Precompute products + double qx2 = qx * qx, qy2 = qy * qy, qz2 = qz * qz; + double qxqy = qx * qy, qxqz = qx * qz, qxqw = qx * qw; + double qyqz = qy * qz, qyqw = qy * qw, qzqw = qz * qw; + + mat::fixed<3,3> R; + R(0,0) = 1.0 - 2.0*(qy2 + qz2); + R(0,1) = 2.0*(qxqy - qzqw); + R(0,2) = 2.0*(qxqz + qyqw); + R(1,0) = 2.0*(qxqy + qzqw); + R(1,1) = 1.0 - 2.0*(qx2 + qz2); + R(1,2) = 2.0*(qyqz - qxqw); + R(2,0) = 2.0*(qxqz - qyqw); + R(2,1) = 2.0*(qyqz + qxqw); + R(2,2) = 1.0 - 2.0*(qx2 + qy2); + + return R; +} + +vec::fixed<3> Rotation::as_euler(const string& conv, bool intrinsic, bool degrees) const { + mat::fixed<3,3> R = as_matrix(); + + // Parse the convention + int axis1, axis2, axis3; + + if (conv == "user") { + // Use default axes from parameter.hpp + axis1 = axis_psi - 1; // Convert 1-indexed to 0-indexed + axis2 = axis_theta - 1; + axis3 = axis_phi - 1; + } else { + parse_euler_convention(conv, axis1, axis2, axis3); + } + + // For extrinsic, we transpose R and swap axis order + mat::fixed<3,3> R_work = intrinsic ? R : R.t(); + if (!intrinsic) { + swap(axis1, axis3); + } + + vec::fixed<3> angles; + + // Determine if proper Euler (axis1 == axis3) or Tait-Bryan (axis1 != axis3) + bool proper_euler = (axis1 == axis3); + + // Find the "other" axis for proper Euler, or compute for Tait-Bryan + int i = axis1; + int j = axis2; + int k = proper_euler ? (3 - i - j) : axis3; // For proper Euler, k is the third axis + + // Sign based on cyclic permutation + // Cyclic: (0,1,2), (1,2,0), (2,0,1) give +1 + // Anti-cyclic: (0,2,1), (2,1,0), (1,0,2) give -1 + double sign = ((j - i + 3) % 3 == 1) ? 1.0 : -1.0; + + if (proper_euler) { + // Proper Euler angles (e.g., zxz, zyz) + double sy = sqrt(R_work(i,j)*R_work(i,j) + R_work(i,k)*R_work(i,k)); + + if (sy > sim_iota) { + angles(0) = atan2(R_work(i,j), sign*R_work(i,k)); + angles(1) = atan2(sy, R_work(i,i)); + angles(2) = atan2(R_work(j,i), -sign*R_work(k,i)); + } else { + // Gimbal lock + angles(0) = atan2(-sign*R_work(j,k), R_work(j,j)); + angles(1) = atan2(sy, R_work(i,i)); + angles(2) = 0.0; + } + } else { + // Tait-Bryan angles (e.g., xyz, zyx) + double cy = sqrt(R_work(i,i)*R_work(i,i) + R_work(j,i)*R_work(j,i)); + + if (cy > sim_iota) { + angles(0) = atan2(sign*R_work(k,j), R_work(k,k)); + angles(1) = atan2(-sign*R_work(k,i), cy); + angles(2) = atan2(sign*R_work(j,i), R_work(i,i)); + } else { + // Gimbal lock + angles(0) = atan2(-sign*R_work(j,k), R_work(j,j)); + angles(1) = atan2(-sign*R_work(k,i), cy); + angles(2) = 0.0; + } + } + + // For extrinsic, swap back + if (!intrinsic) { + swap(angles(0), angles(2)); + } + + if (degrees) { + angles *= 180.0 / sim_pi; + } + + return angles; +} + +vec::fixed<3> Rotation::as_rotvec(bool degrees) const { + // Get rotation angle from quaternion + double qw = _quat(3); + vec::fixed<3> qv = {_quat(0), _quat(1), _quat(2)}; + + double sin_half = norm(qv); + + if (sin_half < sim_iota) { + // Near identity rotation + vec::fixed<3> result = {0.0, 0.0, 0.0}; + return result; + } + + // Angle = 2 * atan2(||qv||, qw) + double angle = 2.0 * atan2(sin_half, qw); + + // Normalize to [-pi, pi] + if (angle > sim_pi) { + angle -= 2.0 * sim_pi; + } else if (angle < -sim_pi) { + angle += 2.0 * sim_pi; + } + + // Axis is normalized qv + vec::fixed<3> axis = qv / sin_half; + + if (degrees) { + angle *= 180.0 / sim_pi; + } + + return axis * angle; +} + +mat::fixed<6,6> Rotation::as_QS(bool active) const { + mat R = as_matrix(); + mat QS_dyn = fillQS(R, active); + mat::fixed<6,6> QS; + QS = QS_dyn; + return QS; +} + +mat::fixed<6,6> Rotation::as_QE(bool active) const { + mat R = as_matrix(); + mat QE_dyn = fillQE(R, active); + mat::fixed<6,6> QE; + QE = QE_dyn; + return QE; +} + +// ============================================================================= +// Apply Methods (3D objects) +// ============================================================================= + +vec::fixed<3> Rotation::apply(const vec::fixed<3>& v, bool inverse) const { + // Direct quaternion-vector rotation using Rodrigues formula + // v' = v + 2*qw*(qv × v) + 2*(qv × (qv × v)) + // This is equivalent to q * v * q^(-1) but more efficient + + double qx = _quat(0), qy = _quat(1), qz = _quat(2), qw = _quat(3); + + if (inverse) { + // For inverse, negate the vector part of quaternion + qx = -qx; qy = -qy; qz = -qz; + } + + vec::fixed<3> qv = {qx, qy, qz}; + vec::fixed<3> uv = cross(qv, v); + vec::fixed<3> uuv = cross(qv, uv); + + return v + 2.0 * (qw * uv + uuv); +} + +vec Rotation::apply(const vec& v, bool inverse) const { + if (v.n_elem != 3) { + throw invalid_argument("Vector must have 3 elements"); + } + vec::fixed<3> v_fixed = {v(0), v(1), v(2)}; + vec::fixed<3> result = apply(v_fixed, inverse); + return vec(result); +} + +mat::fixed<3,3> Rotation::apply_tensor(const mat::fixed<3,3>& m, bool inverse) const { + mat::fixed<3,3> R = as_matrix(); + if (inverse) { + return R.t() * m * R; + } else { + return R * m * R.t(); + } +} + +mat Rotation::apply_tensor(const mat& m, bool inverse) const { + if (m.n_rows != 3 || m.n_cols != 3) { + throw invalid_argument("Tensor must be 3x3"); + } + mat::fixed<3,3> m_fixed; + m_fixed = m; + mat::fixed<3,3> result = apply_tensor(m_fixed, inverse); + return mat(result); +} + +// ============================================================================= +// Apply Methods (Voigt notation) +// ============================================================================= + +vec::fixed<6> Rotation::apply_stress(const vec::fixed<6>& sigma, bool active) const { + mat::fixed<6,6> QS = as_QS(active); + vec::fixed<6> result; + result = QS * sigma; + return result; +} + +vec Rotation::apply_stress(const vec& sigma, bool active) const { + if (sigma.n_elem != 6) { + throw invalid_argument("Stress vector must have 6 elements"); + } + mat R = as_matrix(); + return rotate_stress(sigma, R, active); +} + +vec::fixed<6> Rotation::apply_strain(const vec::fixed<6>& epsilon, bool active) const { + mat::fixed<6,6> QE = as_QE(active); + vec::fixed<6> result; + result = QE * epsilon; + return result; +} + +vec Rotation::apply_strain(const vec& epsilon, bool active) const { + if (epsilon.n_elem != 6) { + throw invalid_argument("Strain vector must have 6 elements"); + } + mat R = as_matrix(); + return rotate_strain(epsilon, R, active); +} + +mat::fixed<6,6> Rotation::apply_stiffness(const mat::fixed<6,6>& L, bool active) const { + mat::fixed<6,6> QS = as_QS(active); + mat::fixed<6,6> result; + result = QS * L * QS.t(); + return result; +} + +mat Rotation::apply_stiffness(const mat& L, bool active) const { + if (L.n_rows != 6 || L.n_cols != 6) { + throw invalid_argument("Stiffness matrix must be 6x6"); + } + mat R = as_matrix(); + return rotateL(L, R, active); +} + +mat::fixed<6,6> Rotation::apply_compliance(const mat::fixed<6,6>& M, bool active) const { + mat::fixed<6,6> QE = as_QE(active); + mat::fixed<6,6> result; + result = QE * M * QE.t(); + return result; +} + +mat Rotation::apply_compliance(const mat& M, bool active) const { + if (M.n_rows != 6 || M.n_cols != 6) { + throw invalid_argument("Compliance matrix must be 6x6"); + } + mat R = as_matrix(); + return rotateM(M, R, active); +} + +// ============================================================================= +// Operations +// ============================================================================= + +Rotation Rotation::inv() const { + return Rotation(quat_conjugate(_quat)); +} + +double Rotation::magnitude(bool degrees) const { + // Rotation angle = 2 * acos(|qw|) + double angle = 2.0 * acos(min(abs(_quat(3)), 1.0)); + + if (degrees) { + angle *= 180.0 / sim_pi; + } + + return angle; +} + +Rotation Rotation::operator*(const Rotation& other) const { + return Rotation(normalize_quat(quat_multiply(_quat, other._quat))); +} + +Rotation& Rotation::operator*=(const Rotation& other) { + _quat = normalize_quat(quat_multiply(_quat, other._quat)); + return *this; +} + +Rotation Rotation::slerp(const Rotation& other, double t) const { + // Spherical linear interpolation + // Reference: K. Shoemake, "Animating rotation with quaternion curves", SIGGRAPH 1985 + + vec::fixed<4> q1 = _quat; + vec::fixed<4> q2 = other._quat; + + // Compute dot product + double dot = arma::dot(q1, q2); + + // If dot is negative, negate one quaternion to take shorter path + if (dot < 0.0) { + q2 = -q2; + dot = -dot; + } + + // Clamp dot to valid range + dot = min(dot, 1.0); + + vec::fixed<4> result; + + if (dot > 0.9995) { + // Quaternions are very close, use linear interpolation + result = q1 + t * (q2 - q1); + } else { + // Standard SLERP + double theta_0 = acos(dot); + double theta = theta_0 * t; + double sin_theta = sin(theta); + double sin_theta_0 = sin(theta_0); + + double s0 = cos(theta) - dot * sin_theta / sin_theta_0; + double s1 = sin_theta / sin_theta_0; + + result = s0 * q1 + s1 * q2; + } + + return Rotation(normalize_quat(result)); +} + +} // namespace simcoon diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ef6bf848d..d219e5242 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,6 +27,7 @@ set(TEST_SRCS test/Libraries/Maths/Tlagrange.cpp test/Libraries/Maths/Tnum_solve.cpp test/Libraries/Maths/Trotation.cpp + test/Libraries/Maths/TRotationClass.cpp test/Libraries/Maths/Tstats.cpp # Libraries - Phase diff --git a/test/Libraries/Maths/TRotationClass.cpp b/test/Libraries/Maths/TRotationClass.cpp new file mode 100644 index 000000000..25dd86d86 --- /dev/null +++ b/test/Libraries/Maths/TRotationClass.cpp @@ -0,0 +1,451 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file TRotationClass.cpp +///@brief Test for the Rotation class +///@version 1.0 + +#include +#include + +#include +#include +#include + +using namespace std; +using namespace arma; +using namespace simcoon; + +// ============================================================================= +// Identity and Basic Tests +// ============================================================================= + +TEST(TRotationClass, identity) +{ + Rotation r = Rotation::identity(); + + EXPECT_TRUE(r.is_identity()); + + // Identity quaternion should be [0, 0, 0, 1] + vec::fixed<4> q = r.as_quat(); + EXPECT_NEAR(q(0), 0.0, 1.E-12); + EXPECT_NEAR(q(1), 0.0, 1.E-12); + EXPECT_NEAR(q(2), 0.0, 1.E-12); + EXPECT_NEAR(q(3), 1.0, 1.E-12); + + // Identity matrix should be eye(3,3) + mat::fixed<3,3> R = r.as_matrix(); + EXPECT_LT(norm(R - eye(3,3), "fro"), 1.E-12); +} + +TEST(TRotationClass, default_constructor) +{ + Rotation r; + EXPECT_TRUE(r.is_identity()); +} + +// ============================================================================= +// Factory Methods +// ============================================================================= + +TEST(TRotationClass, from_quat) +{ + // Test with a known quaternion (90 degrees around z-axis) + double angle = sim_pi / 2.0; + double s = sin(angle / 2.0); + double c = cos(angle / 2.0); + vec::fixed<4> q = {0, 0, s, c}; + + Rotation r = Rotation::from_quat(q); + vec::fixed<4> q_out = r.as_quat(); + + // Should be normalized and equal + EXPECT_NEAR(norm(q_out), 1.0, 1.E-12); + EXPECT_LT(norm(q - q_out), 1.E-12); +} + +TEST(TRotationClass, from_matrix) +{ + // Create a rotation matrix using existing fillR function + double psi = 0.5; + double theta = 0.3; + double phi = 0.7; + + mat R_expected = fillR(psi, theta, phi, true, "zxz"); + + mat::fixed<3,3> R_fixed; + R_fixed = R_expected; + + Rotation r = Rotation::from_matrix(R_fixed); + mat::fixed<3,3> R_actual = r.as_matrix(); + + EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); +} + +TEST(TRotationClass, from_euler_zxz) +{ + double psi = 23.0 * (sim_pi / 180.0); + double theta = 42.0 * (sim_pi / 180.0); + double phi = 165.0 * (sim_pi / 180.0); + + // Use existing fillR to get expected result + mat R_expected = fillR(psi, theta, phi, true, "zxz"); + + // Create rotation using Rotation class + Rotation r = Rotation::from_euler(psi, theta, phi, "zxz", true, false); + mat::fixed<3,3> R_actual = r.as_matrix(); + + EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); +} + +TEST(TRotationClass, from_euler_zyz) +{ + double psi = 23.0 * (sim_pi / 180.0); + double theta = 42.0 * (sim_pi / 180.0); + double phi = 165.0 * (sim_pi / 180.0); + + mat R_expected = fillR(psi, theta, phi, true, "zyz"); + + Rotation r = Rotation::from_euler(psi, theta, phi, "zyz", true, false); + mat::fixed<3,3> R_actual = r.as_matrix(); + + EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); +} + +TEST(TRotationClass, from_euler_degrees) +{ + // Test with degrees flag + Rotation r1 = Rotation::from_euler(90.0, 0.0, 0.0, "zxz", true, true); + Rotation r2 = Rotation::from_euler(sim_pi/2.0, 0.0, 0.0, "zxz", true, false); + + EXPECT_TRUE(r1.equals(r2, 1.E-9)); +} + +TEST(TRotationClass, from_axis_angle) +{ + // Test rotation around z-axis + double angle = sim_pi / 4.0; // 45 degrees + + Rotation r = Rotation::from_axis_angle(angle, 3, false); + mat::fixed<3,3> R_actual = r.as_matrix(); + + mat R_expected = fillR(angle, 3, true); + + EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); +} + +TEST(TRotationClass, from_rotvec) +{ + // Rotation vector: 45 degrees around z-axis + double angle = sim_pi / 4.0; + vec::fixed<3> rotvec = {0, 0, angle}; + + Rotation r = Rotation::from_rotvec(rotvec, false); + mat::fixed<3,3> R_actual = r.as_matrix(); + + mat R_expected = fillR(angle, 3, true); + + EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); +} + +// ============================================================================= +// Conversion Methods +// ============================================================================= + +TEST(TRotationClass, as_euler_roundtrip) +{ + // Test round-trip: euler -> rotation -> euler + double psi_in = 0.5; + double theta_in = 0.3; + double phi_in = 0.7; + + Rotation r = Rotation::from_euler(psi_in, theta_in, phi_in, "zxz", true, false); + vec::fixed<3> euler_out = r.as_euler("zxz", true, false); + + // Reconstruct rotation from output euler angles + Rotation r2 = Rotation::from_euler(euler_out(0), euler_out(1), euler_out(2), "zxz", true, false); + + // The two rotations should be equal + EXPECT_TRUE(r.equals(r2, 1.E-9)); +} + +TEST(TRotationClass, as_rotvec_roundtrip) +{ + // Create a rotation + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + + // Convert to rotvec and back + vec::fixed<3> rotvec = r.as_rotvec(); + Rotation r2 = Rotation::from_rotvec(rotvec); + + EXPECT_TRUE(r.equals(r2, 1.E-9)); +} + +TEST(TRotationClass, as_QS_consistency) +{ + // Test that as_QS matches existing fillQS function + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + mat::fixed<6,6> QS_class = r.as_QS(true); + mat QS_func = fillQS(mat(R), true); + + EXPECT_LT(norm(mat(QS_class) - QS_func, "fro"), 1.E-9); +} + +TEST(TRotationClass, as_QE_consistency) +{ + // Test that as_QE matches existing fillQE function + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + mat::fixed<6,6> QE_class = r.as_QE(true); + mat QE_func = fillQE(mat(R), true); + + EXPECT_LT(norm(mat(QE_class) - QE_func, "fro"), 1.E-9); +} + +// ============================================================================= +// Apply Methods +// ============================================================================= + +TEST(TRotationClass, apply_vector) +{ + // 90 degree rotation around z-axis + Rotation r = Rotation::from_axis_angle(sim_pi / 2.0, 3); + + vec::fixed<3> v = {1, 0, 0}; + vec::fixed<3> v_rot = r.apply(v); + + // x-axis should rotate to y-axis + EXPECT_NEAR(v_rot(0), 0.0, 1.E-9); + EXPECT_NEAR(v_rot(1), 1.0, 1.E-9); + EXPECT_NEAR(v_rot(2), 0.0, 1.E-9); +} + +TEST(TRotationClass, apply_vector_inverse) +{ + Rotation r = Rotation::from_axis_angle(sim_pi / 2.0, 3); + + vec::fixed<3> v = {1, 0, 0}; + vec::fixed<3> v_rot = r.apply(v, false); + vec::fixed<3> v_back = r.apply(v_rot, true); + + EXPECT_LT(norm(v - v_back), 1.E-9); +} + +TEST(TRotationClass, apply_stress_consistency) +{ + // Test that apply_stress matches existing rotate_stress function + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + vec::fixed<6> sigma = {100, 50, 0, 25, 0, 0}; + + vec::fixed<6> sigma_class = r.apply_stress(sigma, true); + vec sigma_func = rotate_stress(vec(sigma), mat(R), true); + + EXPECT_LT(norm(vec(sigma_class) - sigma_func), 1.E-9); +} + +TEST(TRotationClass, apply_strain_consistency) +{ + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + vec::fixed<6> epsilon = {0.01, -0.005, -0.005, 0.002, 0.001, 0.003}; + + vec::fixed<6> epsilon_class = r.apply_strain(epsilon, true); + vec epsilon_func = rotate_strain(vec(epsilon), mat(R), true); + + EXPECT_LT(norm(vec(epsilon_class) - epsilon_func), 1.E-9); +} + +TEST(TRotationClass, apply_stiffness_consistency) +{ + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + // Create a simple stiffness matrix (isotropic) + mat L = eye(6, 6) * 100.0; + L(0,1) = L(1,0) = 30.0; + L(0,2) = L(2,0) = 30.0; + L(1,2) = L(2,1) = 30.0; + + mat::fixed<6,6> L_fixed; + L_fixed = L; + + mat::fixed<6,6> L_class = r.apply_stiffness(L_fixed, true); + mat L_func = rotateL(L, mat(R), true); + + EXPECT_LT(norm(mat(L_class) - L_func, "fro"), 1.E-9); +} + +// ============================================================================= +// Operations +// ============================================================================= + +TEST(TRotationClass, inverse) +{ + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + Rotation r_inv = r.inv(); + + // r * r_inv should be identity + Rotation product = r * r_inv; + EXPECT_TRUE(product.is_identity(1.E-9)); +} + +TEST(TRotationClass, composition) +{ + // Test that (r1 * r2).apply(v) == r1.apply(r2.apply(v)) + Rotation r1 = Rotation::from_axis_angle(0.5, 1); + Rotation r2 = Rotation::from_axis_angle(0.3, 2); + + vec::fixed<3> v = {1, 2, 3}; + + // Method 1: compose then apply + Rotation r_composed = r1 * r2; + vec::fixed<3> v1 = r_composed.apply(v); + + // Method 2: apply sequentially (r2 first, then r1) + vec::fixed<3> v2 = r1.apply(r2.apply(v)); + + EXPECT_LT(norm(v1 - v2), 1.E-9); +} + +TEST(TRotationClass, composition_inplace) +{ + Rotation r1 = Rotation::from_axis_angle(0.5, 1); + Rotation r2 = Rotation::from_axis_angle(0.3, 2); + + Rotation r_composed = r1 * r2; + + Rotation r1_copy = r1; + r1_copy *= r2; + + EXPECT_TRUE(r_composed.equals(r1_copy, 1.E-9)); +} + +TEST(TRotationClass, magnitude) +{ + // 45 degree rotation + double angle = sim_pi / 4.0; + Rotation r = Rotation::from_axis_angle(angle, 3); + + EXPECT_NEAR(r.magnitude(), angle, 1.E-9); + EXPECT_NEAR(r.magnitude(true), 45.0, 1.E-9); // degrees +} + +TEST(TRotationClass, slerp) +{ + Rotation r1 = Rotation::identity(); + Rotation r2 = Rotation::from_axis_angle(sim_pi / 2.0, 3); + + // t=0 should give r1 + Rotation r_t0 = r1.slerp(r2, 0.0); + EXPECT_TRUE(r_t0.equals(r1, 1.E-9)); + + // t=1 should give r2 + Rotation r_t1 = r1.slerp(r2, 1.0); + EXPECT_TRUE(r_t1.equals(r2, 1.E-9)); + + // t=0.5 should give 45 degrees rotation + Rotation r_t5 = r1.slerp(r2, 0.5); + EXPECT_NEAR(r_t5.magnitude(), sim_pi / 4.0, 1.E-9); +} + +TEST(TRotationClass, equals) +{ + Rotation r1 = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + Rotation r2 = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + Rotation r3 = Rotation::from_euler(0.6, 0.3, 0.7, "zxz"); + + EXPECT_TRUE(r1.equals(r2, 1.E-9)); + EXPECT_FALSE(r1.equals(r3, 1.E-9)); +} + +// ============================================================================= +// Edge Cases +// ============================================================================= + +TEST(TRotationClass, small_angle) +{ + // Very small rotation should not cause numerical issues + double small_angle = 1.E-10; + Rotation r = Rotation::from_axis_angle(small_angle, 3); + + vec::fixed<3> v = {1, 0, 0}; + vec::fixed<3> v_rot = r.apply(v); + + // Should be nearly unchanged + EXPECT_LT(norm(v - v_rot), 1.E-8); +} + +TEST(TRotationClass, rotation_180_degrees) +{ + // 180 degree rotation around z-axis + Rotation r = Rotation::from_axis_angle(sim_pi, 3); + + vec::fixed<3> v = {1, 0, 0}; + vec::fixed<3> v_rot = r.apply(v); + + // x should become -x + EXPECT_NEAR(v_rot(0), -1.0, 1.E-9); + EXPECT_NEAR(v_rot(1), 0.0, 1.E-9); + EXPECT_NEAR(v_rot(2), 0.0, 1.E-9); +} + +TEST(TRotationClass, random_rotation_properties) +{ + // Random rotation should produce a valid rotation matrix + Rotation r = Rotation::random(); + mat::fixed<3,3> R = r.as_matrix(); + + // R * R^T should be identity + EXPECT_LT(norm(R * R.t() - eye(3,3), "fro"), 1.E-9); + + // det(R) should be 1 + EXPECT_NEAR(det(R), 1.0, 1.E-9); +} + +// ============================================================================= +// Matrix Round-trip +// ============================================================================= + +TEST(TRotationClass, matrix_roundtrip) +{ + // Create rotation, convert to matrix, convert back + Rotation r1 = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r1.as_matrix(); + Rotation r2 = Rotation::from_matrix(R); + + EXPECT_TRUE(r1.equals(r2, 1.E-9)); +} + +TEST(TRotationClass, quat_matrix_quat_roundtrip) +{ + // quat -> matrix -> quat round-trip + vec::fixed<4> q1 = {0.5, 0.5, 0.5, 0.5}; // This is already unit quaternion + q1 /= norm(q1); // Ensure normalization + + Rotation r1 = Rotation::from_quat(q1); + mat::fixed<3,3> R = r1.as_matrix(); + Rotation r2 = Rotation::from_matrix(R); + + // Note: q and -q represent the same rotation + EXPECT_TRUE(r1.equals(r2, 1.E-9)); +} From ac9334562145dfd7834e84e8fd71119b9e5f487b Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Thu, 5 Feb 2026 16:28:33 +0100 Subject: [PATCH 02/31] Add Python validation & SciPy interop; use simcoon Add input validation helpers in the Python wrapper (validate_vector_size, validate_matrix_size) and apply them to rotation bindings to ensure correct array shapes. Add SciPy interoperability: Rotation.from_scipy and to_scipy methods with docs and conversions via quaternions. Replace uses of legacy sim_pi/sim_iota with simcoon::pi and simcoon::iota across Rotation implementation and update tests accordingly to use the centralized constants. This improves robustness of the Python API and unifies constant usage in the core code and tests. --- .../Libraries/Maths/rotation_class.cpp | 95 +++++++++++++++++++ src/Simulation/Maths/rotation_class.cpp | 40 ++++---- test/Libraries/Maths/TRotationClass.cpp | 32 +++---- 3 files changed, 131 insertions(+), 36 deletions(-) diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp index 0d9bfac76..cd219ed91 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp @@ -15,6 +15,31 @@ namespace py = pybind11; namespace simpy { +namespace { + // Validation helper functions for professional solver integration + + void validate_vector_size(const py::array_t& arr, size_t expected, const string& name) { + if (arr.ndim() != 1) { + throw invalid_argument(name + " must be a 1D array, got " + to_string(arr.ndim()) + "D"); + } + if (static_cast(arr.size()) != expected) { + throw invalid_argument(name + " must have " + to_string(expected) + + " elements, got " + to_string(arr.size())); + } + } + + void validate_matrix_size(const py::array_t& arr, size_t rows, size_t cols, const string& name) { + if (arr.ndim() != 2) { + throw invalid_argument(name + " must be a 2D array, got " + to_string(arr.ndim()) + "D"); + } + auto shape = arr.shape(); + if (static_cast(shape[0]) != rows || static_cast(shape[1]) != cols) { + throw invalid_argument(name + " must have shape (" + to_string(rows) + ", " + + to_string(cols) + "), got (" + to_string(shape[0]) + ", " + to_string(shape[1]) + ")"); + } + } +} // anonymous namespace + void register_rotation_class(py::module_& m) { py::class_(m, "Rotation", R"doc( @@ -57,6 +82,7 @@ void register_rotation_class(py::module_& m) { .def_static("from_quat", [](py::array_t quat) { + validate_vector_size(quat, 4, "quat"); vec q = carma::arr_to_col(quat); return simcoon::Rotation::from_quat(q); }, @@ -78,6 +104,7 @@ void register_rotation_class(py::module_& m) { .def_static("from_matrix", [](py::array_t R) { + validate_matrix_size(R, 3, 3, "R"); mat R_mat = carma::arr_to_mat(R); return simcoon::Rotation::from_matrix(R_mat); }, @@ -129,6 +156,7 @@ void register_rotation_class(py::module_& m) { .def_static("from_rotvec", [](py::array_t rotvec, bool degrees) { + validate_vector_size(rotvec, 3, "rotvec"); vec rv = carma::arr_to_col(rotvec); return simcoon::Rotation::from_rotvec(rv, degrees); }, @@ -172,6 +200,67 @@ void register_rotation_class(py::module_& m) { .def_static("random", &simcoon::Rotation::random, "Create a uniformly distributed random rotation") + .def_static("from_scipy", + [](py::object scipy_rot) { + py::array_t q = scipy_rot.attr("as_quat")().cast>(); + validate_vector_size(q, 4, "scipy_rot.as_quat()"); + vec qv = carma::arr_to_col(q); + return simcoon::Rotation::from_quat(qv); + }, + py::arg("scipy_rot"), + R"doc( + Create rotation from a scipy.spatial.transform.Rotation object. + + Converts via unit quaternion (scalar-last convention shared by both + libraries), so there is no trigonometric or matrix conversion overhead. + + Parameters + ---------- + scipy_rot : scipy.spatial.transform.Rotation + A scipy Rotation object + + Returns + ------- + Rotation + Rotation object + + Example + ------- + >>> from scipy.spatial.transform import Rotation as R + >>> scipy_rot = R.from_euler('z', 45, degrees=True) + >>> r = smc.Rotation.from_scipy(scipy_rot) + )doc") + + .def("to_scipy", + [](const simcoon::Rotation& self) { + py::module_ sp_rot = py::module_::import("scipy.spatial.transform"); + py::object R_class = sp_rot.attr("Rotation"); + vec q(self.as_quat()); + py::array_t q_arr = carma::col_to_arr(q); + // scipy expects shape (4,), carma returns (4,1) + q_arr = q_arr.attr("flatten")().cast>(); + return R_class.attr("from_quat")(q_arr); + }, + R"doc( + Convert to a scipy.spatial.transform.Rotation object. + + Converts via unit quaternion (scalar-last convention shared by both + libraries), so there is no trigonometric or matrix conversion overhead. + + Requires scipy to be installed. + + Returns + ------- + scipy.spatial.transform.Rotation + Equivalent scipy Rotation object + + Example + ------- + >>> r = smc.Rotation.from_axis_angle(np.pi/4, 3) + >>> scipy_rot = r.to_scipy() + >>> scipy_rot.as_euler('zxz', degrees=True) + )doc") + // Conversion methods .def("as_quat", [](const simcoon::Rotation& self) { @@ -282,6 +371,7 @@ void register_rotation_class(py::module_& m) { // Apply methods .def("apply", [](const simcoon::Rotation& self, py::array_t v, bool inverse) { + validate_vector_size(v, 3, "v"); vec v_cpp = carma::arr_to_col(v); vec result = self.apply(v_cpp, inverse); return carma::col_to_arr(result); @@ -305,6 +395,7 @@ void register_rotation_class(py::module_& m) { .def("apply_tensor", [](const simcoon::Rotation& self, py::array_t m, bool inverse) { + validate_matrix_size(m, 3, 3, "m"); mat m_cpp = carma::arr_to_mat(m); mat result = self.apply_tensor(m_cpp, inverse); return carma::mat_to_arr(result); @@ -328,6 +419,7 @@ void register_rotation_class(py::module_& m) { .def("apply_stress", [](const simcoon::Rotation& self, py::array_t sigma, bool active) { + validate_vector_size(sigma, 6, "sigma"); vec sigma_cpp = carma::arr_to_col(sigma); vec result = self.apply_stress(sigma_cpp, active); return carma::col_to_arr(result); @@ -351,6 +443,7 @@ void register_rotation_class(py::module_& m) { .def("apply_strain", [](const simcoon::Rotation& self, py::array_t epsilon, bool active) { + validate_vector_size(epsilon, 6, "epsilon"); vec epsilon_cpp = carma::arr_to_col(epsilon); vec result = self.apply_strain(epsilon_cpp, active); return carma::col_to_arr(result); @@ -374,6 +467,7 @@ void register_rotation_class(py::module_& m) { .def("apply_stiffness", [](const simcoon::Rotation& self, py::array_t L, bool active) { + validate_matrix_size(L, 6, 6, "L"); mat L_cpp = carma::arr_to_mat(L); mat result = self.apply_stiffness(L_cpp, active); return carma::mat_to_arr(result); @@ -397,6 +491,7 @@ void register_rotation_class(py::module_& m) { .def("apply_compliance", [](const simcoon::Rotation& self, py::array_t M, bool active) { + validate_matrix_size(M, 6, 6, "M"); mat M_cpp = carma::arr_to_mat(M); mat result = self.apply_compliance(M_cpp, active); return carma::mat_to_arr(result); diff --git a/src/Simulation/Maths/rotation_class.cpp b/src/Simulation/Maths/rotation_class.cpp index 9a7e96212..2022f25b1 100644 --- a/src/Simulation/Maths/rotation_class.cpp +++ b/src/Simulation/Maths/rotation_class.cpp @@ -70,7 +70,7 @@ void parse_euler_convention(const string& conv, int& axis1, int& axis2, int& axi // Normalize a quaternion to unit length vec::fixed<4> normalize_quat(const vec::fixed<4>& q) { double n = norm(q); - if (n < sim_iota) { + if (n < simcoon::iota) { // Return identity quaternion if input is near zero return {0.0, 0.0, 0.0, 1.0}; } @@ -180,9 +180,9 @@ Rotation Rotation::from_euler(double psi, double theta, double phi, const string& conv, bool intrinsic, bool degrees) { // Convert to radians if needed if (degrees) { - psi *= sim_pi / 180.0; - theta *= sim_pi / 180.0; - phi *= sim_pi / 180.0; + psi *= simcoon::pi / 180.0; + theta *= simcoon::pi / 180.0; + phi *= simcoon::pi / 180.0; } // Handle "user" convention using the existing fillR function @@ -235,14 +235,14 @@ Rotation Rotation::from_rotvec(const vec::fixed<3>& rotvec, bool degrees) { double angle = norm(rotvec); if (degrees) { - angle *= sim_pi / 180.0; + angle *= simcoon::pi / 180.0; } - if (angle < sim_iota) { + if (angle < simcoon::iota) { return identity(); } - vec::fixed<3> axis = rotvec / (degrees ? norm(rotvec) * 180.0 / sim_pi : norm(rotvec)); + vec::fixed<3> axis = rotvec / (degrees ? norm(rotvec) * 180.0 / simcoon::pi : norm(rotvec)); double half_angle = angle / 2.0; double s = sin(half_angle); double c = cos(half_angle); @@ -260,7 +260,7 @@ Rotation Rotation::from_rotvec(const vec& rotvec, bool degrees) { Rotation Rotation::from_axis_angle(double angle, int axis, bool degrees) { if (degrees) { - angle *= sim_pi / 180.0; + angle *= simcoon::pi / 180.0; } int ax_idx = axis_to_index(axis); @@ -282,8 +282,8 @@ Rotation Rotation::random() { double sqrt1_u0 = sqrt(1.0 - u(0)); double sqrt_u0 = sqrt(u(0)); - double two_pi_u1 = 2.0 * sim_pi * u(1); - double two_pi_u2 = 2.0 * sim_pi * u(2); + double two_pi_u1 = 2.0 * simcoon::pi * u(1); + double two_pi_u2 = 2.0 * simcoon::pi * u(2); vec::fixed<4> q = { sqrt1_u0 * sin(two_pi_u1), @@ -361,7 +361,7 @@ vec::fixed<3> Rotation::as_euler(const string& conv, bool intrinsic, bool degree // Proper Euler angles (e.g., zxz, zyz) double sy = sqrt(R_work(i,j)*R_work(i,j) + R_work(i,k)*R_work(i,k)); - if (sy > sim_iota) { + if (sy > simcoon::iota) { angles(0) = atan2(R_work(i,j), sign*R_work(i,k)); angles(1) = atan2(sy, R_work(i,i)); angles(2) = atan2(R_work(j,i), -sign*R_work(k,i)); @@ -375,7 +375,7 @@ vec::fixed<3> Rotation::as_euler(const string& conv, bool intrinsic, bool degree // Tait-Bryan angles (e.g., xyz, zyx) double cy = sqrt(R_work(i,i)*R_work(i,i) + R_work(j,i)*R_work(j,i)); - if (cy > sim_iota) { + if (cy > simcoon::iota) { angles(0) = atan2(sign*R_work(k,j), R_work(k,k)); angles(1) = atan2(-sign*R_work(k,i), cy); angles(2) = atan2(sign*R_work(j,i), R_work(i,i)); @@ -393,7 +393,7 @@ vec::fixed<3> Rotation::as_euler(const string& conv, bool intrinsic, bool degree } if (degrees) { - angles *= 180.0 / sim_pi; + angles *= 180.0 / simcoon::pi; } return angles; @@ -406,7 +406,7 @@ vec::fixed<3> Rotation::as_rotvec(bool degrees) const { double sin_half = norm(qv); - if (sin_half < sim_iota) { + if (sin_half < simcoon::iota) { // Near identity rotation vec::fixed<3> result = {0.0, 0.0, 0.0}; return result; @@ -416,17 +416,17 @@ vec::fixed<3> Rotation::as_rotvec(bool degrees) const { double angle = 2.0 * atan2(sin_half, qw); // Normalize to [-pi, pi] - if (angle > sim_pi) { - angle -= 2.0 * sim_pi; - } else if (angle < -sim_pi) { - angle += 2.0 * sim_pi; + if (angle > simcoon::pi) { + angle -= 2.0 * simcoon::pi; + } else if (angle < -simcoon::pi) { + angle += 2.0 * simcoon::pi; } // Axis is normalized qv vec::fixed<3> axis = qv / sin_half; if (degrees) { - angle *= 180.0 / sim_pi; + angle *= 180.0 / simcoon::pi; } return axis * angle; @@ -576,7 +576,7 @@ double Rotation::magnitude(bool degrees) const { double angle = 2.0 * acos(min(abs(_quat(3)), 1.0)); if (degrees) { - angle *= 180.0 / sim_pi; + angle *= 180.0 / simcoon::pi; } return angle; diff --git a/test/Libraries/Maths/TRotationClass.cpp b/test/Libraries/Maths/TRotationClass.cpp index 25dd86d86..1ca1865e9 100644 --- a/test/Libraries/Maths/TRotationClass.cpp +++ b/test/Libraries/Maths/TRotationClass.cpp @@ -65,7 +65,7 @@ TEST(TRotationClass, default_constructor) TEST(TRotationClass, from_quat) { // Test with a known quaternion (90 degrees around z-axis) - double angle = sim_pi / 2.0; + double angle = simcoon::pi / 2.0; double s = sin(angle / 2.0); double c = cos(angle / 2.0); vec::fixed<4> q = {0, 0, s, c}; @@ -98,9 +98,9 @@ TEST(TRotationClass, from_matrix) TEST(TRotationClass, from_euler_zxz) { - double psi = 23.0 * (sim_pi / 180.0); - double theta = 42.0 * (sim_pi / 180.0); - double phi = 165.0 * (sim_pi / 180.0); + double psi = 23.0 * (simcoon::pi / 180.0); + double theta = 42.0 * (simcoon::pi / 180.0); + double phi = 165.0 * (simcoon::pi / 180.0); // Use existing fillR to get expected result mat R_expected = fillR(psi, theta, phi, true, "zxz"); @@ -114,9 +114,9 @@ TEST(TRotationClass, from_euler_zxz) TEST(TRotationClass, from_euler_zyz) { - double psi = 23.0 * (sim_pi / 180.0); - double theta = 42.0 * (sim_pi / 180.0); - double phi = 165.0 * (sim_pi / 180.0); + double psi = 23.0 * (simcoon::pi / 180.0); + double theta = 42.0 * (simcoon::pi / 180.0); + double phi = 165.0 * (simcoon::pi / 180.0); mat R_expected = fillR(psi, theta, phi, true, "zyz"); @@ -130,7 +130,7 @@ TEST(TRotationClass, from_euler_degrees) { // Test with degrees flag Rotation r1 = Rotation::from_euler(90.0, 0.0, 0.0, "zxz", true, true); - Rotation r2 = Rotation::from_euler(sim_pi/2.0, 0.0, 0.0, "zxz", true, false); + Rotation r2 = Rotation::from_euler(simcoon::pi/2.0, 0.0, 0.0, "zxz", true, false); EXPECT_TRUE(r1.equals(r2, 1.E-9)); } @@ -138,7 +138,7 @@ TEST(TRotationClass, from_euler_degrees) TEST(TRotationClass, from_axis_angle) { // Test rotation around z-axis - double angle = sim_pi / 4.0; // 45 degrees + double angle = simcoon::pi / 4.0; // 45 degrees Rotation r = Rotation::from_axis_angle(angle, 3, false); mat::fixed<3,3> R_actual = r.as_matrix(); @@ -151,7 +151,7 @@ TEST(TRotationClass, from_axis_angle) TEST(TRotationClass, from_rotvec) { // Rotation vector: 45 degrees around z-axis - double angle = sim_pi / 4.0; + double angle = simcoon::pi / 4.0; vec::fixed<3> rotvec = {0, 0, angle}; Rotation r = Rotation::from_rotvec(rotvec, false); @@ -226,7 +226,7 @@ TEST(TRotationClass, as_QE_consistency) TEST(TRotationClass, apply_vector) { // 90 degree rotation around z-axis - Rotation r = Rotation::from_axis_angle(sim_pi / 2.0, 3); + Rotation r = Rotation::from_axis_angle(simcoon::pi / 2.0, 3); vec::fixed<3> v = {1, 0, 0}; vec::fixed<3> v_rot = r.apply(v); @@ -239,7 +239,7 @@ TEST(TRotationClass, apply_vector) TEST(TRotationClass, apply_vector_inverse) { - Rotation r = Rotation::from_axis_angle(sim_pi / 2.0, 3); + Rotation r = Rotation::from_axis_angle(simcoon::pi / 2.0, 3); vec::fixed<3> v = {1, 0, 0}; vec::fixed<3> v_rot = r.apply(v, false); @@ -343,7 +343,7 @@ TEST(TRotationClass, composition_inplace) TEST(TRotationClass, magnitude) { // 45 degree rotation - double angle = sim_pi / 4.0; + double angle = simcoon::pi / 4.0; Rotation r = Rotation::from_axis_angle(angle, 3); EXPECT_NEAR(r.magnitude(), angle, 1.E-9); @@ -353,7 +353,7 @@ TEST(TRotationClass, magnitude) TEST(TRotationClass, slerp) { Rotation r1 = Rotation::identity(); - Rotation r2 = Rotation::from_axis_angle(sim_pi / 2.0, 3); + Rotation r2 = Rotation::from_axis_angle(simcoon::pi / 2.0, 3); // t=0 should give r1 Rotation r_t0 = r1.slerp(r2, 0.0); @@ -365,7 +365,7 @@ TEST(TRotationClass, slerp) // t=0.5 should give 45 degrees rotation Rotation r_t5 = r1.slerp(r2, 0.5); - EXPECT_NEAR(r_t5.magnitude(), sim_pi / 4.0, 1.E-9); + EXPECT_NEAR(r_t5.magnitude(), simcoon::pi / 4.0, 1.E-9); } TEST(TRotationClass, equals) @@ -398,7 +398,7 @@ TEST(TRotationClass, small_angle) TEST(TRotationClass, rotation_180_degrees) { // 180 degree rotation around z-axis - Rotation r = Rotation::from_axis_angle(sim_pi, 3); + Rotation r = Rotation::from_axis_angle(simcoon::pi, 3); vec::fixed<3> v = {1, 0, 0}; vec::fixed<3> v_rot = r.apply(v); From 0f6ad8f3235517b2991f7f297bf36672b172c93d Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Thu, 5 Feb 2026 16:56:49 +0100 Subject: [PATCH 03/31] Enhance rotation docs and add rotation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand rotation Python docs with Scipy interoperability, active vs passive rotation conventions, input validation guidance, and notes on quaternion equivalence. Fix cross‑references in C++ API docs to point to the correct continuum_mechanics doc pages. Rename test file to Trotation_class.cpp, update CMakeLists, and add comprehensive Rotation unit tests (tensor/stiffness/compliance behavior, factory methods, Euler conventions, round‑trip and gimbal‑lock cases) to improve coverage and verify correctness. --- .../functions/doc_rotation.rst | 92 +++++++++ docs/cpp_api/simulation/rotation.rst | 7 +- test/CMakeLists.txt | 2 +- ...TRotationClass.cpp => Trotation_class.cpp} | 187 +++++++++++++++++- 4 files changed, 283 insertions(+), 5 deletions(-) rename test/Libraries/Maths/{TRotationClass.cpp => Trotation_class.cpp} (67%) diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index 809094630..a6172c755 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -49,6 +49,11 @@ Creating Rotations # Random rotation (uniform distribution) r = smc.Rotation.random() + # From a scipy.spatial.transform.Rotation (requires scipy) + from scipy.spatial.transform import Rotation as R + scipy_rot = R.from_euler('z', 45, degrees=True) + r = smc.Rotation.from_scipy(scipy_rot) + Converting Between Representations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -74,6 +79,9 @@ Converting Between Representations QS = r.as_QS() # 6×6 for stress QE = r.as_QE() # 6×6 for strain + # To scipy.spatial.transform.Rotation (requires scipy) + scipy_rot = r.to_scipy() + Applying Rotations ~~~~~~~~~~~~~~~~~~ @@ -174,6 +182,90 @@ Utility Methods r2 = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") are_equal = r.equals(r2, tol=1e-12) +Scipy Interoperability +~~~~~~~~~~~~~~~~~~~~~ + +Both simcoon and scipy use the scalar-last quaternion convention ``[qx, qy, qz, qw]``, +so conversion between the two is done via quaternion transfer with no trigonometric +or matrix computation overhead. Scipy is an **optional** dependency — it is only +imported when ``to_scipy()`` is called. + +.. code-block:: python + + from scipy.spatial.transform import Rotation as R + + # scipy → simcoon + scipy_rot = R.from_euler('z', 45, degrees=True) + r = smc.Rotation.from_scipy(scipy_rot) + + # simcoon → scipy + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + scipy_rot = r.to_scipy() + + # Round-trip is lossless + q_original = scipy_rot.as_quat() + q_roundtrip = smc.Rotation.from_scipy(scipy_rot).to_scipy().as_quat() + # q_original == q_roundtrip + +Active vs Passive Rotations +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``active`` parameter on Voigt methods (``apply_stress``, ``apply_strain``, +``apply_stiffness``, ``apply_compliance``, ``as_QS``, ``as_QE``) controls the +rotation convention: + +- **active=True** (default): **Alibi** rotation — rotates the physical object + while the coordinate system stays fixed. +- **active=False**: **Alias** rotation — rotates the coordinate system while the + object stays fixed. This is equivalent to the inverse active rotation. + +.. code-block:: python + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + + # Active: rotate the stress tensor itself + sigma_active = r.apply_stress(sigma, active=True) + + # Passive: express the same stress in a rotated coordinate system + sigma_passive = r.apply_stress(sigma, active=False) + +Input Validation +~~~~~~~~~~~~~~~~ + +All methods that accept NumPy arrays validate the input shape and raise +``ValueError`` with a descriptive message if the dimensions are wrong: + +.. code-block:: python + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + + r.apply(np.array([1.0, 2.0])) + # ValueError: v must have 3 elements, got 2 + + r.apply_stress(np.array([1.0, 2.0, 3.0])) + # ValueError: sigma must have 6 elements, got 3 + + smc.Rotation.from_matrix(np.eye(4)) + # ValueError: R must have shape (3, 3), got (4, 4) + + r.apply_stiffness(np.eye(3)) + # ValueError: L must have shape (6, 6), got (3, 3) + +Expected input shapes: + +- ``from_quat``: 1D array, 4 elements +- ``from_matrix``: 2D array, shape (3, 3) +- ``from_rotvec``: 1D array, 3 elements +- ``apply``: 1D array, 3 elements +- ``apply_tensor``: 2D array, shape (3, 3) +- ``apply_stress``, ``apply_strain``: 1D array, 6 elements +- ``apply_stiffness``, ``apply_compliance``: 2D array, shape (6, 6) + +.. note:: + + Quaternions ``q`` and ``-q`` represent the same rotation. The ``equals()`` + method accounts for this antipodal equivalence. + Euler Angle Conventions ----------------------- diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst index d9e3b5cf2..bbaedf956 100644 --- a/docs/cpp_api/simulation/rotation.rst +++ b/docs/cpp_api/simulation/rotation.rst @@ -548,6 +548,7 @@ where :math:`\Omega = \arccos(q_0 \cdot q_1)` is the angle between the quaternio See Also ======== -- :doc:`../continuum_mechanics/functions/kinematics` - Finite strain kinematics -- :doc:`../continuum_mechanics/functions/constitutive` - Constitutive matrices -- :doc:`../continuum_mechanics/functions/transfer` - Voigt/tensor conversions +- :doc:`/continuum_mechanics/functions/doc_kinematics` - Finite strain kinematics +- :doc:`/continuum_mechanics/functions/doc_constitutive` - Constitutive matrices +- :doc:`/continuum_mechanics/functions/doc_transfer` - Voigt/tensor conversions +- :doc:`/continuum_mechanics/functions/doc_rotation` - Python user documentation diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d219e5242..42fb4fb1a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,7 +27,7 @@ set(TEST_SRCS test/Libraries/Maths/Tlagrange.cpp test/Libraries/Maths/Tnum_solve.cpp test/Libraries/Maths/Trotation.cpp - test/Libraries/Maths/TRotationClass.cpp + test/Libraries/Maths/Trotation_class.cpp test/Libraries/Maths/Tstats.cpp # Libraries - Phase diff --git a/test/Libraries/Maths/TRotationClass.cpp b/test/Libraries/Maths/Trotation_class.cpp similarity index 67% rename from test/Libraries/Maths/TRotationClass.cpp rename to test/Libraries/Maths/Trotation_class.cpp index 1ca1865e9..45cd812cc 100644 --- a/test/Libraries/Maths/TRotationClass.cpp +++ b/test/Libraries/Maths/Trotation_class.cpp @@ -15,7 +15,7 @@ */ -///@file TRotationClass.cpp +///@file Trotation_class.cpp ///@brief Test for the Rotation class ///@version 1.0 @@ -378,6 +378,191 @@ TEST(TRotationClass, equals) EXPECT_FALSE(r1.equals(r3, 1.E-9)); } +TEST(TRotationClass, apply_tensor) +{ + // Rotate a symmetric 3x3 tensor + Rotation r = Rotation::from_axis_angle(simcoon::pi / 2.0, 3); + mat::fixed<3,3> R = r.as_matrix(); + + // Diagonal tensor + mat::fixed<3,3> T = {{100, 10, 0}, {10, 50, 0}, {0, 0, 25}}; + + mat::fixed<3,3> T_rot = r.apply_tensor(T); + + // Verify R * T * R^T + mat::fixed<3,3> T_expected; + T_expected = R * T * R.t(); + EXPECT_LT(norm(T_rot - T_expected, "fro"), 1.E-9); + + // Inverse should recover original + mat::fixed<3,3> T_back = r.apply_tensor(T_rot, true); + EXPECT_LT(norm(T - T_back, "fro"), 1.E-9); +} + +TEST(TRotationClass, apply_compliance_consistency) +{ + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + // Create a compliance matrix + mat M = eye(6, 6) * 0.01; + M(0,1) = M(1,0) = -0.003; + M(0,2) = M(2,0) = -0.003; + M(1,2) = M(2,1) = -0.003; + + mat::fixed<6,6> M_fixed; + M_fixed = M; + + mat::fixed<6,6> M_class = r.apply_compliance(M_fixed, true); + mat M_func = rotateM(M, mat(R), true); + + EXPECT_LT(norm(mat(M_class) - M_func, "fro"), 1.E-9); +} + +// ============================================================================= +// Additional Factory Method Tests +// ============================================================================= + +TEST(TRotationClass, from_axis_angle_x) +{ + double angle = simcoon::pi / 3.0; // 60 degrees around x + Rotation r = Rotation::from_axis_angle(angle, 1); + mat::fixed<3,3> R_actual = r.as_matrix(); + mat R_expected = fillR(angle, 1, true); + + EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); +} + +TEST(TRotationClass, from_axis_angle_y) +{ + double angle = simcoon::pi / 5.0; // 36 degrees around y + Rotation r = Rotation::from_axis_angle(angle, 2); + mat::fixed<3,3> R_actual = r.as_matrix(); + mat R_expected = fillR(angle, 2, true); + + EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); +} + +// ============================================================================= +// Additional Euler Convention Tests +// ============================================================================= + +TEST(TRotationClass, from_euler_xyz) +{ + double psi = 0.4, theta = 0.6, phi = 0.8; + + // Build reference from individual axis rotations (extrinsic xyz = intrinsic zyx) + Rotation rx = Rotation::from_axis_angle(psi, 1); + Rotation ry = Rotation::from_axis_angle(theta, 2); + Rotation rz = Rotation::from_axis_angle(phi, 3); + + // Intrinsic xyz: apply x, then y', then z'' + // Quaternion order: rz * ry * rx + Rotation r_ref = rz * ry * rx; + Rotation r_xyz = Rotation::from_euler(psi, theta, phi, "xyz", true, false); + + EXPECT_TRUE(r_ref.equals(r_xyz, 1.E-9)); +} + +TEST(TRotationClass, from_euler_zyx) +{ + double psi = 0.3, theta = 0.5, phi = 0.2; + + Rotation rz = Rotation::from_axis_angle(psi, 3); + Rotation ry = Rotation::from_axis_angle(theta, 2); + Rotation rx = Rotation::from_axis_angle(phi, 1); + + // Intrinsic zyx: apply z, then y', then x'' + Rotation r_ref = rx * ry * rz; + Rotation r_zyx = Rotation::from_euler(psi, theta, phi, "zyx", true, false); + + EXPECT_TRUE(r_ref.equals(r_zyx, 1.E-9)); +} + +TEST(TRotationClass, from_euler_xyx) +{ + double psi = 0.4, theta = 0.7, phi = 0.9; + + Rotation rx1 = Rotation::from_axis_angle(psi, 1); + Rotation ry = Rotation::from_axis_angle(theta, 2); + Rotation rx2 = Rotation::from_axis_angle(phi, 1); + + // Intrinsic xyx: apply x, then y', then x'' + Rotation r_ref = rx2 * ry * rx1; + Rotation r_xyx = Rotation::from_euler(psi, theta, phi, "xyx", true, false); + + EXPECT_TRUE(r_ref.equals(r_xyx, 1.E-9)); +} + +TEST(TRotationClass, euler_roundtrip_xyz) +{ + double psi_in = 0.3, theta_in = 0.5, phi_in = 0.2; + Rotation r = Rotation::from_euler(psi_in, theta_in, phi_in, "xyz", true, false); + vec::fixed<3> angles = r.as_euler("xyz", true, false); + + Rotation r2 = Rotation::from_euler(angles(0), angles(1), angles(2), "xyz", true, false); + EXPECT_TRUE(r.equals(r2, 1.E-9)); +} + +TEST(TRotationClass, euler_extrinsic_vs_intrinsic) +{ + double a = 0.4, b = 0.6, c = 0.8; + + // Extrinsic XYZ = Intrinsic ZYX (reversed) + Rotation r_ext = Rotation::from_euler(a, b, c, "xyz", false, false); + Rotation r_int = Rotation::from_euler(c, b, a, "zyx", true, false); + + EXPECT_TRUE(r_ext.equals(r_int, 1.E-9)); +} + +// ============================================================================= +// Gimbal Lock Tests +// ============================================================================= + +TEST(TRotationClass, gimbal_lock_zxz_theta_zero) +{ + // theta=0 for zxz: psi and phi are not individually recoverable + double psi = 0.5, theta = 0.0, phi = 0.3; + Rotation r = Rotation::from_euler(psi, theta, phi, "zxz", true, false); + + // Round-trip through euler should still give the same rotation + vec::fixed<3> angles = r.as_euler("zxz", true, false); + Rotation r2 = Rotation::from_euler(angles(0), angles(1), angles(2), "zxz", true, false); + + EXPECT_TRUE(r.equals(r2, 1.E-9)); +} + +TEST(TRotationClass, gimbal_lock_zxz_theta_pi) +{ + // theta=pi for zxz: another singular configuration + double psi = 0.5, theta = simcoon::pi, phi = 0.3; + Rotation r = Rotation::from_euler(psi, theta, phi, "zxz", true, false); + + // The rotation itself must be valid + mat::fixed<3,3> R = r.as_matrix(); + EXPECT_LT(norm(R * R.t() - eye(3,3), "fro"), 1.E-9); + EXPECT_NEAR(det(R), 1.0, 1.E-9); + + // Round-trip should preserve the rotation + vec::fixed<3> angles = r.as_euler("zxz", true, false); + Rotation r2 = Rotation::from_euler(angles(0), angles(1), angles(2), "zxz", true, false); + EXPECT_TRUE(r.equals(r2, 1.E-9)); +} + +TEST(TRotationClass, gimbal_lock_xyz_theta_pi_half) +{ + // theta=pi/2 for xyz (Tait-Bryan): gimbal lock + double psi = 0.4, theta = simcoon::pi / 2.0, phi = 0.6; + Rotation r = Rotation::from_euler(psi, theta, phi, "xyz", true, false); + + mat::fixed<3,3> R = r.as_matrix(); + EXPECT_LT(norm(R * R.t() - eye(3,3), "fro"), 1.E-9); + + vec::fixed<3> angles = r.as_euler("xyz", true, false); + Rotation r2 = Rotation::from_euler(angles(0), angles(1), angles(2), "xyz", true, false); + EXPECT_TRUE(r.equals(r2, 1.E-9)); +} + // ============================================================================= // Edge Cases // ============================================================================= From 24ba6a1061490c5f889a7e89a6f6ae8ec428b3a0 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Thu, 5 Feb 2026 22:16:23 +0100 Subject: [PATCH 04/31] Rename Voigt rotation methods and add localization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename ambiguous as_QS/as_QE to explicit as_voigt_stress_rotation/as_voigt_strain_rotation and add apply_localization_strain/apply_localization_stress to Rotation API. Update implementation, header, Python bindings, documentation and tests to use the new names and to expose rotation of localization tensors (QE·A·QS^T and QS·B·QE^T). Refactor callers (ellipsoid_multi, state_variables*, etc.) to use Rotation::from_euler and the new apply_* methods instead of ad-hoc rotate_* helpers; add necessary includes and input validation for 6×6 tensors. Add unit tests verifying consistency with existing fillQS/fillQE/rotateA/rotateB functions and update docs to reflect the new API and examples. --- .../functions/doc_rotation.rst | 12 +- docs/cpp_api/simulation/rotation.rst | 8 +- .../Simulation/Maths/rotation_class.hpp | 36 +++- .../Libraries/Maths/rotation_class.cpp | 56 +++++- .../Homogenization/ellipsoid_multi.cpp | 46 ++--- src/Simulation/Maths/rotation_class.cpp | 44 ++++- src/Simulation/Phase/state_variables.cpp | 163 +++++------------- src/Simulation/Phase/state_variables_M.cpp | 69 +++----- src/Simulation/Phase/state_variables_T.cpp | 82 +++------ test/Libraries/Maths/Trotation_class.cpp | 48 +++++- 10 files changed, 306 insertions(+), 258 deletions(-) diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index a6172c755..6cf599c8b 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -76,8 +76,8 @@ Converting Between Representations rotvec_deg = r.as_rotvec(degrees=True) # To Voigt rotation matrices - QS = r.as_QS() # 6×6 for stress - QE = r.as_QE() # 6×6 for strain + QS = r.as_voigt_stress_rotation() # 6×6 for stress + QE = r.as_voigt_strain_rotation() # 6×6 for strain # To scipy.spatial.transform.Rotation (requires scipy) scipy_rot = r.to_scipy() @@ -125,6 +125,12 @@ Applying Rotations M = smc.M_iso([210e9, 0.3], "Enu") M_rot = r.apply_compliance(M) + # Strain localization tensor (6×6): A' = QE * A * QS^T + A_global = r.apply_localization_strain(A_local) + + # Stress localization tensor (6×6): B' = QS * B * QE^T + B_global = r.apply_localization_stress(B_local) + Composing Rotations ~~~~~~~~~~~~~~~~~~~ @@ -211,7 +217,7 @@ Active vs Passive Rotations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``active`` parameter on Voigt methods (``apply_stress``, ``apply_strain``, -``apply_stiffness``, ``apply_compliance``, ``as_QS``, ``as_QE``) controls the +``apply_stiffness``, ``apply_compliance``, ``as_voigt_stress_rotation``, ``as_voigt_strain_rotation``) controls the rotation convention: - **active=True** (default): **Alibi** rotation — rotates the physical object diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst index bbaedf956..8f89541b7 100644 --- a/docs/cpp_api/simulation/rotation.rst +++ b/docs/cpp_api/simulation/rotation.rst @@ -165,9 +165,9 @@ Conversion Methods - Return Euler angles [psi, theta, phi] * - ``as_rotvec(degrees)`` - Return rotation vector (axis × angle) - * - ``as_QS(active)`` + * - ``as_voigt_stress_rotation(active)`` - Return 6×6 stress rotation matrix - * - ``as_QE(active)`` + * - ``as_voigt_strain_rotation(active)`` - Return 6×6 strain rotation matrix Apply Methods @@ -202,6 +202,10 @@ Apply Methods - Rotate 6×6 stiffness matrix: QS·L·QS\ :sup:`T` * - ``apply_compliance(M, active)`` - Rotate 6×6 compliance matrix: QE·M·QE\ :sup:`T` + * - ``apply_localization_strain(A, active)`` + - Rotate 6×6 strain localization tensor: QE·A·QS\ :sup:`T` + * - ``apply_localization_stress(B, active)`` + - Rotate 6×6 stress localization tensor: QS·B·QE\ :sup:`T` The ``active`` parameter controls whether the rotation is **active** (alibi, rotating the object) or **passive** (alias, rotating the coordinate system). diff --git a/include/simcoon/Simulation/Maths/rotation_class.hpp b/include/simcoon/Simulation/Maths/rotation_class.hpp index adc024f8c..e2846a70e 100644 --- a/include/simcoon/Simulation/Maths/rotation_class.hpp +++ b/include/simcoon/Simulation/Maths/rotation_class.hpp @@ -229,14 +229,14 @@ class Rotation { * @param active If true (default), active rotation; if false, passive * @return 6x6 stress rotation matrix (QS) */ - arma::mat::fixed<6,6> as_QS(bool active = true) const; + arma::mat::fixed<6,6> as_voigt_stress_rotation(bool active = true) const; /** * @brief Get 6x6 rotation matrix for strain tensors in Voigt notation. * @param active If true (default), active rotation; if false, passive * @return 6x6 strain rotation matrix (QE) */ - arma::mat::fixed<6,6> as_QE(bool active = true) const; + arma::mat::fixed<6,6> as_voigt_strain_rotation(bool active = true) const; // ========================================================================= // Apply Methods (3D objects) @@ -343,6 +343,38 @@ class Rotation { */ arma::mat apply_compliance(const arma::mat& M, bool active = true) const; + /** + * @brief Apply rotation to a 6x6 strain localization tensor. + * @param A 6x6 strain localization tensor in Voigt notation + * @param active If true (default), active rotation + * @return Rotated strain localization tensor: QE * A * QS^T + */ + arma::mat::fixed<6,6> apply_localization_strain(const arma::mat::fixed<6,6>& A, bool active = true) const; + + /** + * @brief Apply rotation to a strain localization tensor (dynamic size). + * @param A 6x6 strain localization tensor + * @param active If true, active rotation + * @return Rotated strain localization tensor + */ + arma::mat apply_localization_strain(const arma::mat& A, bool active = true) const; + + /** + * @brief Apply rotation to a 6x6 stress localization tensor. + * @param B 6x6 stress localization tensor in Voigt notation + * @param active If true (default), active rotation + * @return Rotated stress localization tensor: QS * B * QE^T + */ + arma::mat::fixed<6,6> apply_localization_stress(const arma::mat::fixed<6,6>& B, bool active = true) const; + + /** + * @brief Apply rotation to a stress localization tensor (dynamic size). + * @param B 6x6 stress localization tensor + * @param active If true, active rotation + * @return Rotated stress localization tensor + */ + arma::mat apply_localization_stress(const arma::mat& B, bool active = true) const; + // ========================================================================= // Operations // ========================================================================= diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp index cd219ed91..3238d7757 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp @@ -330,9 +330,9 @@ void register_rotation_class(py::module_& m) { Rotation vector (axis * angle) )doc") - .def("as_QS", + .def("as_voigt_stress_rotation", [](const simcoon::Rotation& self, bool active) { - return carma::mat_to_arr(mat(self.as_QS(active))); + return carma::mat_to_arr(mat(self.as_voigt_stress_rotation(active))); }, py::arg("active") = true, R"doc( @@ -349,9 +349,9 @@ void register_rotation_class(py::module_& m) { 6x6 stress rotation matrix (QS) )doc") - .def("as_QE", + .def("as_voigt_strain_rotation", [](const simcoon::Rotation& self, bool active) { - return carma::mat_to_arr(mat(self.as_QE(active))); + return carma::mat_to_arr(mat(self.as_voigt_strain_rotation(active))); }, py::arg("active") = true, R"doc( @@ -513,6 +513,54 @@ void register_rotation_class(py::module_& m) { Rotated compliance matrix: QE * M * QE^T )doc") + .def("apply_localization_strain", + [](const simcoon::Rotation& self, py::array_t A, bool active) { + validate_matrix_size(A, 6, 6, "A"); + mat A_cpp = carma::arr_to_mat(A); + mat result = self.apply_localization_strain(A_cpp, active); + return carma::mat_to_arr(result); + }, + py::arg("A"), py::arg("active") = true, + R"doc( + Apply rotation to a 6x6 strain localization tensor. + + Parameters + ---------- + A : array_like + 6x6 strain localization tensor in Voigt notation + active : bool, optional + If True (default), active rotation + + Returns + ------- + ndarray + Rotated strain localization tensor: QE * A * QS^T + )doc") + + .def("apply_localization_stress", + [](const simcoon::Rotation& self, py::array_t B, bool active) { + validate_matrix_size(B, 6, 6, "B"); + mat B_cpp = carma::arr_to_mat(B); + mat result = self.apply_localization_stress(B_cpp, active); + return carma::mat_to_arr(result); + }, + py::arg("B"), py::arg("active") = true, + R"doc( + Apply rotation to a 6x6 stress localization tensor. + + Parameters + ---------- + B : array_like + 6x6 stress localization tensor in Voigt notation + active : bool, optional + If True (default), active rotation + + Returns + ------- + ndarray + Rotated stress localization tensor: QS * B * QE^T + )doc") + // Operations .def("inv", &simcoon::Rotation::inv, "Get the inverse (conjugate) rotation") diff --git a/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp b/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp index d8d2fdc3f..8bfeffe32 100755 --- a/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp +++ b/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -112,35 +113,38 @@ ellipsoid_multi::~ellipsoid_multi() {} void ellipsoid_multi::fillS_loc(const mat& Lt_m, const ellipsoid &ell) //------------------------------------- { - mat Ltm_local_geom = rotate_g2l_L(Lt_m, ell.psi_geom, ell.theta_geom, ell.phi_geom); + Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); + mat Ltm_local_geom = rot.apply_stiffness(Lt_m, false); S_loc = Eshelby(Ltm_local_geom, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); } - + //------------------------------------- void ellipsoid_multi::fillP_loc(const mat& Lt_m, const ellipsoid &ell) //------------------------------------- { - mat Ltm_local_geom = rotate_g2l_L(Lt_m, ell.psi_geom, ell.theta_geom, ell.phi_geom); + Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); + mat Ltm_local_geom = rot.apply_stiffness(Lt_m, false); P_loc = T_II(Ltm_local_geom, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); } - + //------------------------------------- void ellipsoid_multi::fillT(const mat& Lt_m, const mat& Lt, const ellipsoid &ell) //This method correspond to the classical Eshelby method //------------------------------------- { - mat Lt_m_local_geom = rotate_g2l_L(Lt_m, ell.psi_geom, ell.theta_geom, ell.phi_geom); + Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); + mat Lt_m_local_geom = rot.apply_stiffness(Lt_m, false); S_loc = Eshelby(Lt_m_local_geom, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); - mat Lt_local_geom = rotate_g2l_L(Lt, ell.psi_geom, ell.theta_geom, ell.phi_geom); - + mat Lt_local_geom = rot.apply_stiffness(Lt, false); + try { T_loc = inv(eye(6,6) + S_loc*inv(Lt_m_local_geom)*(Lt_local_geom - Lt_m_local_geom)); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside ellipsoid_multi::fillT."); - } - T = rotate_l2g_A(T_loc, ell.psi_geom, ell.theta_geom, ell.phi_geom); + } + T = rot.apply_localization_strain(T_loc, true); } //------------------------------------- @@ -148,18 +152,19 @@ void ellipsoid_multi::fillT_iso(const mat& Lt_m, const mat& Lt, const ellipsoid //This method corresponf to the isotropization method //------------------------------------- { + Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); mat Lt_m_iso = Isotropize(Lt_m); S_loc = Eshelby(Lt_m_iso, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); - mat Lt_local_geom = rotate_g2l_L(Lt, ell.psi_geom, ell.theta_geom, ell.phi_geom); - + mat Lt_local_geom = rot.apply_stiffness(Lt, false); + try { T_loc = inv(eye(6,6) + S_loc*inv(Lt_m_iso)*(Lt_local_geom - Lt_m_iso)); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside ellipsoid_multi::fillT_is."); - } - - T = rotate_l2g_A(T_loc, ell.psi_geom, ell.theta_geom, ell.phi_geom); + } + + T = rot.apply_localization_strain(T_loc, true); } //------------------------------------- @@ -168,20 +173,21 @@ void ellipsoid_multi::fillT_mec_in(const mat& L_m, const mat& L, const ellipsoid //the interaction tensors T for the elastic and the inelastic part. //------------------------------------- { - mat L_m_local_geom = rotate_g2l_L(L_m, ell.psi_geom, ell.theta_geom, ell.phi_geom); + Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); + mat L_m_local_geom = rot.apply_stiffness(L_m, false); S_loc = Eshelby(L_m_local_geom, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); - mat L_local_geom = rotate_g2l_L(L, ell.psi_geom, ell.theta_geom, ell.phi_geom); - + mat L_local_geom = rot.apply_stiffness(L, false); + try { T_loc = inv(eye(6,6) + S_loc*inv(L_m_local_geom)*(L_local_geom - L_m_local_geom)); - T_in_loc = (eye(6,6)-T_loc)*inv(L_m_local_geom - L_local_geom); + T_in_loc = (eye(6,6)-T_loc)*inv(L_m_local_geom - L_local_geom); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside ellipsoid_multi::fillT_mec_in."); } - T = rotate_l2g_A(T_loc, ell.psi_geom, ell.theta_geom, ell.phi_geom); - T_in = rotate_l2g_M(T_in_loc, ell.psi_geom, ell.theta_geom, ell.phi_geom); + T = rot.apply_localization_strain(T_loc, true); + T_in = rot.apply_compliance(T_in_loc, true); } //---------------------------------------------------------------------- diff --git a/src/Simulation/Maths/rotation_class.cpp b/src/Simulation/Maths/rotation_class.cpp index 2022f25b1..dbe05b241 100644 --- a/src/Simulation/Maths/rotation_class.cpp +++ b/src/Simulation/Maths/rotation_class.cpp @@ -432,7 +432,7 @@ vec::fixed<3> Rotation::as_rotvec(bool degrees) const { return axis * angle; } -mat::fixed<6,6> Rotation::as_QS(bool active) const { +mat::fixed<6,6> Rotation::as_voigt_stress_rotation(bool active) const { mat R = as_matrix(); mat QS_dyn = fillQS(R, active); mat::fixed<6,6> QS; @@ -440,7 +440,7 @@ mat::fixed<6,6> Rotation::as_QS(bool active) const { return QS; } -mat::fixed<6,6> Rotation::as_QE(bool active) const { +mat::fixed<6,6> Rotation::as_voigt_strain_rotation(bool active) const { mat R = as_matrix(); mat QE_dyn = fillQE(R, active); mat::fixed<6,6> QE; @@ -504,7 +504,7 @@ mat Rotation::apply_tensor(const mat& m, bool inverse) const { // ============================================================================= vec::fixed<6> Rotation::apply_stress(const vec::fixed<6>& sigma, bool active) const { - mat::fixed<6,6> QS = as_QS(active); + mat::fixed<6,6> QS = as_voigt_stress_rotation(active); vec::fixed<6> result; result = QS * sigma; return result; @@ -519,7 +519,7 @@ vec Rotation::apply_stress(const vec& sigma, bool active) const { } vec::fixed<6> Rotation::apply_strain(const vec::fixed<6>& epsilon, bool active) const { - mat::fixed<6,6> QE = as_QE(active); + mat::fixed<6,6> QE = as_voigt_strain_rotation(active); vec::fixed<6> result; result = QE * epsilon; return result; @@ -534,7 +534,7 @@ vec Rotation::apply_strain(const vec& epsilon, bool active) const { } mat::fixed<6,6> Rotation::apply_stiffness(const mat::fixed<6,6>& L, bool active) const { - mat::fixed<6,6> QS = as_QS(active); + mat::fixed<6,6> QS = as_voigt_stress_rotation(active); mat::fixed<6,6> result; result = QS * L * QS.t(); return result; @@ -549,7 +549,7 @@ mat Rotation::apply_stiffness(const mat& L, bool active) const { } mat::fixed<6,6> Rotation::apply_compliance(const mat::fixed<6,6>& M, bool active) const { - mat::fixed<6,6> QE = as_QE(active); + mat::fixed<6,6> QE = as_voigt_strain_rotation(active); mat::fixed<6,6> result; result = QE * M * QE.t(); return result; @@ -563,6 +563,38 @@ mat Rotation::apply_compliance(const mat& M, bool active) const { return rotateM(M, R, active); } +mat::fixed<6,6> Rotation::apply_localization_strain(const mat::fixed<6,6>& A, bool active) const { + mat R = as_matrix(); + mat A_dyn = rotateA(mat(A), R, active); + mat::fixed<6,6> result; + result = A_dyn; + return result; +} + +mat Rotation::apply_localization_strain(const mat& A, bool active) const { + if (A.n_rows != 6 || A.n_cols != 6) { + throw invalid_argument("Strain localization tensor must be 6x6"); + } + mat R = as_matrix(); + return rotateA(A, R, active); +} + +mat::fixed<6,6> Rotation::apply_localization_stress(const mat::fixed<6,6>& B, bool active) const { + mat R = as_matrix(); + mat B_dyn = rotateB(mat(B), R, active); + mat::fixed<6,6> result; + result = B_dyn; + return result; +} + +mat Rotation::apply_localization_stress(const mat& B, bool active) const { + if (B.n_rows != 6 || B.n_cols != 6) { + throw invalid_argument("Stress localization tensor must be 6x6"); + } + mat R = as_matrix(); + return rotateB(B, R, active); +} + // ============================================================================= // Operations // ============================================================================= diff --git a/src/Simulation/Phase/state_variables.cpp b/src/Simulation/Phase/state_variables.cpp index 5481c5d38..83c809d2d 100755 --- a/src/Simulation/Phase/state_variables.cpp +++ b/src/Simulation/Phase/state_variables.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -474,7 +475,7 @@ arma::mat state_variables::Biot_stress_start() state_variables& state_variables::rotate_l2g(const state_variables& sv, const double &psi, const double &theta, const double &phi) //---------------------------------------------------------------------- { - + Etot = sv.Etot; DEtot = sv.DEtot; etot = sv.etot; @@ -493,66 +494,31 @@ state_variables& state_variables::rotate_l2g(const state_variables& sv, const do DR = sv.DR; T = sv.T; DT = sv.DT; - + nstatev = sv.nstatev; statev = sv.statev; statev_start = sv.statev_start; - - if(fabs(phi) > simcoon::iota) { - Etot = rotate_strain(Etot, -phi, simcoon::axis_phi); - DEtot = rotate_strain(DEtot, -phi, simcoon::axis_phi); - etot = rotate_strain(etot, -phi, simcoon::axis_phi); - Detot = rotate_strain(Detot, -phi, simcoon::axis_phi); - PKII = rotate_stress(PKII, -phi, simcoon::axis_phi); - PKII_start = rotate_stress(PKII_start, -phi, simcoon::axis_phi); - tau = rotate_stress(tau, -phi, simcoon::axis_phi); - tau_start = rotate_stress(tau_start, -phi, simcoon::axis_phi); - sigma = rotate_stress(sigma, -phi, simcoon::axis_phi); - sigma_start = rotate_stress(sigma_start, -phi, simcoon::axis_phi); - F0 = rotate_mat(F0, -phi, simcoon::axis_phi); - F1 = rotate_mat(F1, -phi, simcoon::axis_phi); - U0 = rotate_mat(U0, -phi, simcoon::axis_phi); - U1 = rotate_mat(U1, -phi, simcoon::axis_phi); - R = rotate_mat(R, -phi, simcoon::axis_phi); - DR = rotate_mat(DR, -phi, simcoon::axis_phi); - } - if(fabs(theta) > simcoon::iota) { - Etot = rotate_strain(Etot, -theta, simcoon::axis_theta); - DEtot = rotate_strain(DEtot, -theta, simcoon::axis_theta); - etot = rotate_strain(etot, -theta, simcoon::axis_theta); - Detot = rotate_strain(Detot, -theta, simcoon::axis_theta); - PKII = rotate_stress(PKII, -theta, simcoon::axis_theta); - PKII_start = rotate_stress(PKII_start, -theta, simcoon::axis_theta); - tau = rotate_stress(tau, -theta, simcoon::axis_theta); - tau_start = rotate_stress(tau_start, -theta, simcoon::axis_theta); - sigma = rotate_stress(sigma, -theta, simcoon::axis_theta); - sigma_start = rotate_stress(sigma_start, -theta, simcoon::axis_theta); - F0 = rotate_mat(F0, -theta, simcoon::axis_theta); - F1 = rotate_mat(F1, -theta, simcoon::axis_theta); - U0 = rotate_mat(U0, -theta, simcoon::axis_theta); - U1 = rotate_mat(U1, -theta, simcoon::axis_theta); - R = rotate_mat(R, -theta, simcoon::axis_theta); - DR = rotate_mat(DR, -theta, simcoon::axis_theta); - } - if(fabs(psi) > simcoon::iota) { - Etot = rotate_strain(Etot, -psi, simcoon::axis_psi); - DEtot = rotate_strain(DEtot, -psi, simcoon::axis_psi); - etot = rotate_strain(etot, -psi, simcoon::axis_psi); - Detot = rotate_strain(Detot, -psi, simcoon::axis_psi); - PKII = rotate_stress(PKII, -psi, simcoon::axis_psi); - PKII_start = rotate_stress(PKII_start, -psi, simcoon::axis_psi); - tau = rotate_stress(tau, -psi, simcoon::axis_psi); - tau_start = rotate_stress(tau_start, -psi, simcoon::axis_psi); - sigma = rotate_stress(sigma, -psi, simcoon::axis_psi); - sigma_start = rotate_stress(sigma_start, -psi, simcoon::axis_psi); - F0 = rotate_mat(F0, -psi, simcoon::axis_psi); - F1 = rotate_mat(F1, -psi, simcoon::axis_psi); - U0 = rotate_mat(U0, -psi, simcoon::axis_psi); - U1 = rotate_mat(U1, -psi, simcoon::axis_psi); - R = rotate_mat(R, -psi, simcoon::axis_psi); - DR = rotate_mat(DR, -psi, simcoon::axis_psi); - } - + + Rotation rot = Rotation::from_euler(psi, theta, phi, "zxz"); + if (!rot.is_identity()) { + Etot = rot.apply_strain(Etot); + DEtot = rot.apply_strain(DEtot); + etot = rot.apply_strain(etot); + Detot = rot.apply_strain(Detot); + PKII = rot.apply_stress(PKII); + PKII_start = rot.apply_stress(PKII_start); + tau = rot.apply_stress(tau); + tau_start = rot.apply_stress(tau_start); + sigma = rot.apply_stress(sigma); + sigma_start = rot.apply_stress(sigma_start); + F0 = rot.apply_tensor(F0); + F1 = rot.apply_tensor(F1); + U0 = rot.apply_tensor(U0); + U1 = rot.apply_tensor(U1); + R = rot.apply_tensor(R); + DR = rot.apply_tensor(DR); + } + return *this; } @@ -575,69 +541,34 @@ state_variables& state_variables::rotate_g2l(const state_variables& sv, const do R = sv.R; DR = sv.DR; U0 = sv.U0; - U1 = sv.U1; + U1 = sv.U1; T = sv.T; DT = sv.DT; - + nstatev = sv.nstatev; statev = sv.statev; - statev_start = sv.statev_start; - - if(fabs(psi) > simcoon::iota) { - Etot = rotate_strain(Etot, psi, simcoon::axis_psi); - DEtot = rotate_strain(DEtot, psi, simcoon::axis_psi); - etot = rotate_strain(etot, psi, simcoon::axis_psi); - Detot = rotate_strain(Detot, psi, simcoon::axis_psi); - PKII = rotate_stress(PKII, psi, simcoon::axis_psi); - PKII_start = rotate_stress(PKII_start, psi, simcoon::axis_psi); - tau = rotate_stress(tau, psi, simcoon::axis_psi); - tau_start = rotate_stress(tau_start, psi, simcoon::axis_psi); - sigma = rotate_stress(sigma, psi, simcoon::axis_psi); - sigma_start = rotate_stress(sigma_start, psi, simcoon::axis_psi); - F0 = rotate_mat(F0, psi, simcoon::axis_psi); - F1 = rotate_mat(F1, psi, simcoon::axis_psi); - U0 = rotate_mat(U0, psi, simcoon::axis_psi); - U1 = rotate_mat(U1, psi, simcoon::axis_psi); - R = rotate_mat(R, psi, simcoon::axis_psi); - DR = rotate_mat(DR, psi, simcoon::axis_psi); - } - if(fabs(theta) > simcoon::iota) { - Etot = rotate_strain(Etot, theta, simcoon::axis_theta); - DEtot = rotate_strain(DEtot, theta, simcoon::axis_theta); - etot = rotate_strain(etot, theta, simcoon::axis_theta); - Detot = rotate_strain(Detot, theta, simcoon::axis_theta); - PKII = rotate_stress(PKII, theta, simcoon::axis_theta); - PKII_start = rotate_stress(PKII_start, theta, simcoon::axis_theta); - tau = rotate_stress(tau, theta, simcoon::axis_theta); - tau_start = rotate_stress(tau_start, theta, simcoon::axis_theta); - sigma = rotate_stress(sigma, theta, simcoon::axis_theta); - sigma_start = rotate_stress(sigma_start, theta, simcoon::axis_theta); - F0 = rotate_mat(F0, theta, simcoon::axis_theta); - F1 = rotate_mat(F1, theta, simcoon::axis_theta); - U0 = rotate_mat(U0, theta, simcoon::axis_theta); - U1 = rotate_mat(U1, theta, simcoon::axis_theta); - R = rotate_mat(R, theta, simcoon::axis_theta); - DR = rotate_mat(DR, theta, simcoon::axis_theta); - } - if(fabs(phi) > simcoon::iota) { - Etot = rotate_strain(Etot, phi, simcoon::axis_phi); - DEtot = rotate_strain(DEtot, phi, simcoon::axis_phi); - etot = rotate_strain(etot, phi, simcoon::axis_phi); - Detot = rotate_strain(Detot, phi, simcoon::axis_phi); - PKII = rotate_stress(PKII, phi, simcoon::axis_phi); - PKII_start = rotate_stress(PKII_start, phi, simcoon::axis_phi); - tau = rotate_stress(tau, phi, simcoon::axis_phi); - tau_start = rotate_stress(tau_start, phi, simcoon::axis_phi); - sigma = rotate_stress(sigma, phi, simcoon::axis_phi); - sigma_start = rotate_stress(sigma_start, phi, simcoon::axis_phi); - F0 = rotate_mat(F0, phi, simcoon::axis_phi); - F1 = rotate_mat(F1, phi, simcoon::axis_phi); - U0 = rotate_mat(U0, phi, simcoon::axis_phi); - U1 = rotate_mat(U1, phi, simcoon::axis_phi); - R = rotate_mat(R, phi, simcoon::axis_phi); - DR = rotate_mat(DR, phi, simcoon::axis_phi); + statev_start = sv.statev_start; + + Rotation rot = Rotation::from_euler(psi, theta, phi, "zxz").inv(); + if (!rot.is_identity()) { + Etot = rot.apply_strain(Etot); + DEtot = rot.apply_strain(DEtot); + etot = rot.apply_strain(etot); + Detot = rot.apply_strain(Detot); + PKII = rot.apply_stress(PKII); + PKII_start = rot.apply_stress(PKII_start); + tau = rot.apply_stress(tau); + tau_start = rot.apply_stress(tau_start); + sigma = rot.apply_stress(sigma); + sigma_start = rot.apply_stress(sigma_start); + F0 = rot.apply_tensor(F0); + F1 = rot.apply_tensor(F1); + U0 = rot.apply_tensor(U0); + U1 = rot.apply_tensor(U1); + R = rot.apply_tensor(R); + DR = rot.apply_tensor(DR); } - + return *this; } diff --git a/src/Simulation/Phase/state_variables_M.cpp b/src/Simulation/Phase/state_variables_M.cpp index 996947c44..ea554ebfa 100755 --- a/src/Simulation/Phase/state_variables_M.cpp +++ b/src/Simulation/Phase/state_variables_M.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -237,37 +238,26 @@ void state_variables_M::set_start(const int &corate_type) state_variables_M& state_variables_M::rotate_l2g(const state_variables_M& sv, const double &psi, const double &theta, const double &phi) //---------------------------------------------------------------------- { - + state_variables::rotate_l2g(sv, psi, theta, phi); - + sigma_in = sv.sigma_in; sigma_in_start = sv.sigma_in_start; - + Wm = sv.Wm; Wm_start = sv.Wm_start; - + L = sv.L; Lt = sv.Lt; - - if(fabs(phi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, -phi, simcoon::axis_phi); - sigma_in_start = rotate_stress(sigma_in_start, -phi, simcoon::axis_phi); - L = rotateL(L, -phi, simcoon::axis_phi); - Lt = rotateL(Lt, -phi, simcoon::axis_phi); - } - if(fabs(theta) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, -theta, simcoon::axis_theta); - sigma_in_start = rotate_stress(sigma_in_start, -theta, simcoon::axis_theta); - Lt = rotateL(Lt, -theta, simcoon::axis_theta); - L = rotateL(L, -theta, simcoon::axis_theta); - } - if(fabs(psi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, -psi, simcoon::axis_psi); - sigma_in_start = rotate_stress(sigma_in_start, -psi, simcoon::axis_psi); - Lt = rotateL(Lt, -psi, simcoon::axis_psi); - L = rotateL(L, -psi, simcoon::axis_psi); - } - + + Rotation rot = Rotation::from_euler(psi, theta, phi, "zxz"); + if (!rot.is_identity()) { + sigma_in = rot.apply_stress(sigma_in); + sigma_in_start = rot.apply_stress(sigma_in_start); + L = rot.apply_stiffness(L); + Lt = rot.apply_stiffness(Lt); + } + return *this; } @@ -277,35 +267,24 @@ state_variables_M& state_variables_M::rotate_g2l(const state_variables_M& sv, co { state_variables::rotate_g2l(sv, psi, theta, phi); - + sigma_in = sv.sigma_in; sigma_in_start = sv.sigma_in_start; - + Wm = sv.Wm; Wm_start = sv.Wm_start; L = sv.L; Lt = sv.Lt; - - if(fabs(psi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, psi, simcoon::axis_psi); - sigma_in_start = rotate_stress(sigma_in_start, psi, simcoon::axis_psi); - L = rotateL(L, psi, simcoon::axis_psi); - Lt = rotateL(Lt, psi, simcoon::axis_psi); - } - if(fabs(theta) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, theta, simcoon::axis_theta); - sigma_in_start = rotate_stress(sigma_in_start, theta, simcoon::axis_theta); - L = rotateL(L, theta, simcoon::axis_theta); - Lt = rotateL(Lt, theta, simcoon::axis_theta); - } - if(fabs(phi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, phi, simcoon::axis_phi); - sigma_in_start = rotate_stress(sigma_in_start, phi, simcoon::axis_phi); - L = rotateL(L, phi, simcoon::axis_phi); - Lt = rotateL(Lt, phi, simcoon::axis_phi); + + Rotation rot = Rotation::from_euler(psi, theta, phi, "zxz").inv(); + if (!rot.is_identity()) { + sigma_in = rot.apply_stress(sigma_in); + sigma_in_start = rot.apply_stress(sigma_in_start); + L = rot.apply_stiffness(L); + Lt = rot.apply_stiffness(Lt); } - + return *this; } diff --git a/src/Simulation/Phase/state_variables_T.cpp b/src/Simulation/Phase/state_variables_T.cpp index f19b9912a..825485af2 100755 --- a/src/Simulation/Phase/state_variables_T.cpp +++ b/src/Simulation/Phase/state_variables_T.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include using namespace std; @@ -320,32 +321,19 @@ state_variables_T& state_variables_T::rotate_l2g(const state_variables_T& sv, co Wt = sv.Wt; Wm_start = sv.Wm_start; Wt_start = sv.Wt_start; - - if(fabs(phi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, -phi, simcoon::axis_phi); - sigma_in_start = rotate_stress(sigma_in_start, -phi, simcoon::axis_phi); - dSdE = rotateL(dSdE, -phi, simcoon::axis_phi); - dSdEt = rotateL(dSdEt, -phi, simcoon::axis_phi); - dSdT = rotate_stress(dSdT, -phi, simcoon::axis_phi); - drdE = rotate_strain(drdE, -phi, simcoon::axis_phi); - } - if(fabs(theta) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, -theta, simcoon::axis_theta); - sigma_in_start = rotate_stress(sigma_in_start, -theta, simcoon::axis_theta); - dSdE = rotateL(dSdE, -theta, simcoon::axis_theta); - dSdEt = rotateL(dSdEt, -theta, simcoon::axis_theta); - dSdT = rotate_stress(dSdT, -theta, simcoon::axis_theta); - drdE = rotate_strain(drdE, -theta, simcoon::axis_theta); - } - if(fabs(psi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, -psi, simcoon::axis_psi); - sigma_in_start = rotate_stress(sigma_in_start, -psi, simcoon::axis_psi); - dSdE = rotateL(dSdE, -psi, simcoon::axis_psi); - dSdEt = rotateL(dSdEt, -psi, simcoon::axis_psi); - dSdT = rotate_stress(dSdT, -psi, simcoon::axis_psi); - drdE = rotate_strain(drdE, -psi, simcoon::axis_psi); - } - + + Rotation rot = Rotation::from_euler(psi, theta, phi, "zxz"); + if (!rot.is_identity()) { + sigma_in = rot.apply_stress(sigma_in); + sigma_in_start = rot.apply_stress(sigma_in_start); + dSdE = rot.apply_stiffness(dSdE); + dSdEt = rot.apply_stiffness(dSdEt); + vec dSdT_v = rot.apply_stress(vec(dSdT.as_col())); + dSdT = dSdT_v.as_row(); + vec drdE_v = rot.apply_strain(vec(drdE.as_col())); + drdE = drdE_v.as_row(); + } + return *this; } @@ -355,10 +343,10 @@ state_variables_T& state_variables_T::rotate_g2l(const state_variables_T& sv, co { state_variables::rotate_g2l(sv, psi, theta, phi); - + sigma_in = sv.sigma_in; sigma_in_start = sv.sigma_in_start; - + dSdE = sv.dSdE; dSdEt = sv.dSdEt; dSdT = sv.dSdT; @@ -370,33 +358,19 @@ state_variables_T& state_variables_T::rotate_g2l(const state_variables_T& sv, co Wt = sv.Wt; Wm_start = sv.Wm_start; Wt_start = sv.Wt_start; - - if(fabs(psi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, psi, simcoon::axis_psi); - sigma_in_start = rotate_stress(sigma_in_start, psi, simcoon::axis_psi); - dSdE = rotateL(dSdE, psi, simcoon::axis_psi); - dSdEt = rotateL(dSdEt, psi, simcoon::axis_psi); - dSdT = rotate_stress(dSdT, psi, simcoon::axis_psi); - drdE = rotate_strain(drdE, psi, simcoon::axis_psi); - - } - if(fabs(theta) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, theta, simcoon::axis_theta); - sigma_in_start = rotate_stress(sigma_in_start, theta, simcoon::axis_theta); - dSdE = rotateL(dSdE, theta, simcoon::axis_theta); - dSdEt = rotateL(dSdEt, theta, simcoon::axis_theta); - dSdT = rotate_stress(dSdT, theta, simcoon::axis_theta); - drdE = rotate_strain(drdE, theta, simcoon::axis_theta); - } - if(fabs(phi) > simcoon::iota) { - sigma_in = rotate_stress(sigma_in, phi, simcoon::axis_phi); - sigma_in_start = rotate_stress(sigma_in_start, phi, simcoon::axis_phi); - dSdE = rotateL(dSdE, phi, simcoon::axis_phi); - dSdEt = rotateL(dSdEt, phi, simcoon::axis_phi); - dSdT = rotate_stress(dSdT, phi, simcoon::axis_phi); - drdE = rotate_strain(drdE, phi, simcoon::axis_phi); + + Rotation rot = Rotation::from_euler(psi, theta, phi, "zxz").inv(); + if (!rot.is_identity()) { + sigma_in = rot.apply_stress(sigma_in); + sigma_in_start = rot.apply_stress(sigma_in_start); + dSdE = rot.apply_stiffness(dSdE); + dSdEt = rot.apply_stiffness(dSdEt); + vec dSdT_v = rot.apply_stress(vec(dSdT.as_col())); + dSdT = dSdT_v.as_row(); + vec drdE_v = rot.apply_strain(vec(drdE.as_col())); + drdE = drdE_v.as_row(); } - + return *this; } diff --git a/test/Libraries/Maths/Trotation_class.cpp b/test/Libraries/Maths/Trotation_class.cpp index 45cd812cc..f1fe02f07 100644 --- a/test/Libraries/Maths/Trotation_class.cpp +++ b/test/Libraries/Maths/Trotation_class.cpp @@ -195,25 +195,25 @@ TEST(TRotationClass, as_rotvec_roundtrip) EXPECT_TRUE(r.equals(r2, 1.E-9)); } -TEST(TRotationClass, as_QS_consistency) +TEST(TRotationClass, as_voigt_stress_rotation_vs_fillQS) { - // Test that as_QS matches existing fillQS function + // Test that as_voigt_stress_rotation matches existing fillQS function Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); mat::fixed<3,3> R = r.as_matrix(); - mat::fixed<6,6> QS_class = r.as_QS(true); + mat::fixed<6,6> QS_class = r.as_voigt_stress_rotation(true); mat QS_func = fillQS(mat(R), true); EXPECT_LT(norm(mat(QS_class) - QS_func, "fro"), 1.E-9); } -TEST(TRotationClass, as_QE_consistency) +TEST(TRotationClass, as_voigt_strain_rotation_vs_fillQE) { - // Test that as_QE matches existing fillQE function + // Test that as_voigt_strain_rotation matches existing fillQE function Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); mat::fixed<3,3> R = r.as_matrix(); - mat::fixed<6,6> QE_class = r.as_QE(true); + mat::fixed<6,6> QE_class = r.as_voigt_strain_rotation(true); mat QE_func = fillQE(mat(R), true); EXPECT_LT(norm(mat(QE_class) - QE_func, "fro"), 1.E-9); @@ -419,6 +419,42 @@ TEST(TRotationClass, apply_compliance_consistency) EXPECT_LT(norm(mat(M_class) - M_func, "fro"), 1.E-9); } +// ============================================================================= +// Apply Methods (Localization Tensors) +// ============================================================================= + +TEST(TRotationClass, apply_localization_strain_consistency) +{ + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + // Create a strain localization tensor + mat A = eye(6, 6) + 0.1 * randu(6, 6); + mat::fixed<6,6> A_fixed; + A_fixed = A; + + mat::fixed<6,6> A_class = r.apply_localization_strain(A_fixed, true); + mat A_func = rotateA(A, mat(R), true); + + EXPECT_LT(norm(mat(A_class) - A_func, "fro"), 1.E-9); +} + +TEST(TRotationClass, apply_localization_stress_consistency) +{ + Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + mat::fixed<3,3> R = r.as_matrix(); + + // Create a stress localization tensor + mat B = eye(6, 6) + 0.1 * randu(6, 6); + mat::fixed<6,6> B_fixed; + B_fixed = B; + + mat::fixed<6,6> B_class = r.apply_localization_stress(B_fixed, true); + mat B_func = rotateB(B, mat(R), true); + + EXPECT_LT(norm(mat(B_class) - B_func, "fro"), 1.E-9); +} + // ============================================================================= // Additional Factory Method Tests // ============================================================================= From aac463365dcc6dc87084d9c885e27d0aec930c63 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 09:45:46 +0100 Subject: [PATCH 05/31] Require explicit Euler conv and active args Remove default values for Euler convention and active/passive flags across rotation APIs and pybind bindings to force callers to specify convention and mode. Updated C++ headers (rotation.hpp, rotation_class.hpp) and Python wrappers to make conv and active mandatory, adjusted pybind registrations in python_module.cpp and rotation_class.cpp, and updated tests (Trotation.cpp) to pass explicit arguments. Also add simcoon_docs rotation docstrings (doc_rotation.hpp) and hook them into module definitions for improved Python documentation. --- include/simcoon/Simulation/Maths/rotation.hpp | 7 +- .../Simulation/Maths/rotation_class.hpp | 6 +- .../docs/Libraries/Maths/doc_rotation.hpp | 321 ++++++++++++++++++ .../Libraries/Maths/rotation.hpp | 4 +- .../Libraries/Maths/rotation_class.cpp | 13 +- .../src/python_wrappers/python_module.cpp | 21 +- test/Libraries/Maths/Trotation.cpp | 2 +- 7 files changed, 349 insertions(+), 25 deletions(-) create mode 100644 simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp diff --git a/include/simcoon/Simulation/Maths/rotation.hpp b/include/simcoon/Simulation/Maths/rotation.hpp index ec3983ec0..4acb00075 100755 --- a/include/simcoon/Simulation/Maths/rotation.hpp +++ b/include/simcoon/Simulation/Maths/rotation.hpp @@ -103,15 +103,16 @@ arma::mat fillR(const double &angle, const int &axis, const bool &active = true) * @param psi First Euler angle in radians (double) * @param theta Second Euler angle in radians (double) * @param phi Third Euler angle in radians (double) - * @param active If true (default), active (alibi) rotation; if false, passive (alias) rotation (bool) - * @param conv The Euler angle convention: "zxz" (default), "zyz", "xyz", etc. Use "user" for custom convention (std::string) + * @param active If true, active (alibi) rotation; if false, passive (alias) rotation (bool) + * @param conv The Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", + * "xyx", "xzx", "yxy", "yzy", or "user" (std::string) * @return The 3x3 rotation matrix (arma::mat) * @details Example: * @code * mat R = fillR(M_PI/4., M_PI/6., M_PI/3., true, "zxz"); * @endcode */ -arma::mat fillR(const double &psi, const double &theta, const double &phi, const bool &active = true, const std::string &conv = "zxz"); +arma::mat fillR(const double &psi, const double &theta, const double &phi, const bool &active, const std::string &conv); /** * @brief Generates a 6x6 rotation matrix for stress tensors in Voigt notation. diff --git a/include/simcoon/Simulation/Maths/rotation_class.hpp b/include/simcoon/Simulation/Maths/rotation_class.hpp index e2846a70e..ac40b5ac0 100644 --- a/include/simcoon/Simulation/Maths/rotation_class.hpp +++ b/include/simcoon/Simulation/Maths/rotation_class.hpp @@ -145,7 +145,7 @@ class Rotation { * @return Rotation object */ static Rotation from_euler(double psi, double theta, double phi, - const std::string& conv = "zxz", + const std::string& conv, bool intrinsic = true, bool degrees = false); /** @@ -157,7 +157,7 @@ class Rotation { * @return Rotation object */ static Rotation from_euler(const arma::vec::fixed<3>& angles, - const std::string& conv = "zxz", + const std::string& conv, bool intrinsic = true, bool degrees = false); /** @@ -214,7 +214,7 @@ class Rotation { * @param degrees If true, return angles in degrees * @return 3-element vector of Euler angles [psi, theta, phi] */ - arma::vec::fixed<3> as_euler(const std::string& conv = "zxz", + arma::vec::fixed<3> as_euler(const std::string& conv, bool intrinsic = true, bool degrees = false) const; /** diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp new file mode 100644 index 000000000..fe54a4e6d --- /dev/null +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp @@ -0,0 +1,321 @@ +#pragma once + +namespace simcoon_docs { + +constexpr auto fillR_angle = R"pbdoc( + Generate a 3x3 rotation matrix for rotation around a single axis. + + Parameters + ---------- + angle : float + Rotation angle in radians. + axis : int + Axis of rotation: 1=x, 2=y, 3=z. + active : bool, optional + If True (default), active (alibi) rotation; if False, passive (alias) rotation. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + 3x3 rotation matrix. + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + # 45 degree rotation around z-axis + R = sim.fillR_angle(np.pi/4, 3) + print(R) +)pbdoc"; + +constexpr auto fillR_euler = R"pbdoc( + Generate a 3x3 rotation matrix from Euler angles. + + Parameters + ---------- + psi : float + First Euler angle in radians. + theta : float + Second Euler angle in radians. + phi : float + Third Euler angle in radians. + active : bool + If True, active (alibi) rotation; if False, passive (alias) rotation. + conv : str + Euler angle convention. Supported conventions: + - Proper Euler angles: "zxz", "xyx", "yzy", "zyz", "xzx", "yxy" + - Tait-Bryan angles: "xyz", "yzx", "zxy", "xzy", "zyx", "yxz" + - Custom: "user" (uses axis_psi, axis_theta, axis_phi from parameter.hpp) + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + 3x3 rotation matrix. + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + # Rotation using ZXZ Euler angles + psi, theta, phi = np.pi/6, np.pi/4, np.pi/3 + R = sim.fillR_euler(psi, theta, phi, True, "zxz") + print(R) + + # Rotation using XYZ Tait-Bryan angles + R_xyz = sim.fillR_euler(0.1, 0.2, 0.3, True, "xyz") + print(R_xyz) +)pbdoc"; + +constexpr auto fillQS_angle = R"pbdoc( + Generate a 6x6 rotation matrix for stress tensors in Voigt notation. + + Parameters + ---------- + angle : float + Rotation angle in radians. + axis : int + Axis of rotation: 1=x, 2=y, 3=z. + active : bool, optional + If True (default), active rotation; if False, passive rotation. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + 6x6 stress rotation matrix (QS). + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + # 45 degree rotation matrix for stress + QS = sim.fillQS_angle(np.pi/4, 3) + print(QS) +)pbdoc"; + +constexpr auto fillQS_R = R"pbdoc( + Generate a 6x6 rotation matrix for stress tensors from a 3x3 rotation matrix. + + Parameters + ---------- + R : ndarray + 3x3 rotation matrix. + active : bool, optional + If True (default), active rotation; if False, passive rotation. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + 6x6 stress rotation matrix (QS). + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + R = sim.fillR_angle(np.pi/4, 3) + QS = sim.fillQS_R(R) + print(QS) +)pbdoc"; + +constexpr auto fillQE_angle = R"pbdoc( + Generate a 6x6 rotation matrix for strain tensors in Voigt notation. + + Parameters + ---------- + angle : float + Rotation angle in radians. + axis : int + Axis of rotation: 1=x, 2=y, 3=z. + active : bool, optional + If True (default), active rotation; if False, passive rotation. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + 6x6 strain rotation matrix (QE). + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + # 45 degree rotation matrix for strain + QE = sim.fillQE_angle(np.pi/4, 3) + print(QE) +)pbdoc"; + +constexpr auto fillQE_R = R"pbdoc( + Generate a 6x6 rotation matrix for strain tensors from a 3x3 rotation matrix. + + Parameters + ---------- + R : ndarray + 3x3 rotation matrix. + active : bool, optional + If True (default), active rotation; if False, passive rotation. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + 6x6 strain rotation matrix (QE). + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + R = sim.fillR_angle(np.pi/4, 3) + QE = sim.fillQE_R(R) + print(QE) +)pbdoc"; + +constexpr auto rotate_vec_R = R"pbdoc( + Rotate a 3D vector using a rotation matrix. + + Parameters + ---------- + input : ndarray + 3D vector to rotate. + R : ndarray + 3x3 rotation matrix. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + Rotated 3D vector. + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + v = np.array([1.0, 0.0, 0.0]) + R = sim.fillR_angle(np.pi/4, 3) + v_rot = sim.rotate_vec_R(v, R) + print(v_rot) +)pbdoc"; + +constexpr auto rotate_vec_angle = R"pbdoc( + Rotate a 3D vector by an angle around a given axis. + + Parameters + ---------- + input : ndarray + 3D vector to rotate. + angle : float + Rotation angle in radians. + axis : int + Axis of rotation: 1=x, 2=y, 3=z. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + Rotated 3D vector. + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + v = np.array([1.0, 0.0, 0.0]) + v_rot = sim.rotate_vec_angle(v, np.pi/4, 3) + print(v_rot) +)pbdoc"; + +constexpr auto rotate_mat_R = R"pbdoc( + Rotate a 3x3 matrix using a rotation matrix. + + Parameters + ---------- + input : ndarray + 3x3 matrix to rotate. + R : ndarray + 3x3 rotation matrix. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + Rotated 3x3 matrix: R * input * R^T. + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + m = np.eye(3) + R = sim.fillR_angle(np.pi/4, 3) + m_rot = sim.rotate_mat_R(m, R) + print(m_rot) +)pbdoc"; + +constexpr auto rotate_mat_angle = R"pbdoc( + Rotate a 3x3 matrix by an angle around a given axis. + + Parameters + ---------- + input : ndarray + 3x3 matrix to rotate. + angle : float + Rotation angle in radians. + axis : int + Axis of rotation: 1=x, 2=y, 3=z. + copy : bool, optional + If True (default), return a copy of the result. + + Returns + ------- + ndarray + Rotated 3x3 matrix. + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as sim + + m = np.eye(3) + m_rot = sim.rotate_mat_angle(m, np.pi/4, 3) + print(m_rot) +)pbdoc"; + +} // namespace simcoon_docs diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp index f97f961a8..b2077dbfc 100755 --- a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp +++ b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp @@ -19,8 +19,8 @@ pybind11::array_t rotate_mat_angle(const pybind11::array_t &inpu //This function returns the 3*3 rotation matrix according to an angle, an axis and depending if it is active or passive rotation pybind11::array_t fillR_angle(const double &angle, const int &axis, const bool &active=true, const bool ©=true); -//This function returns the 3*3 rotation matrix according to the three Euler angles, depending if it is active or passive rotation and the Euler convention (ex :"zxz") -pybind11::array_t fillR_euler(const double &psi, const double &theta, const double &phi, const bool &active=true, const std::string &conv="zxz", const bool ©=true); +//This function returns the 3*3 rotation matrix according to the three Euler angles, depending if it is active or passive rotation and the Euler convention (ex: "zxz", "xyz", "zyz", etc.) +pybind11::array_t fillR_euler(const double &psi, const double &theta, const double &phi, const bool &active, const std::string &conv, const bool ©=true); //This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'stress' from an angle and an axis pybind11::array_t fillQS_angle(const double &angle, const int &axis, const bool &active=true, const bool ©=true); diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp index 3238d7757..93091410f 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp @@ -127,7 +127,7 @@ void register_rotation_class(py::module_& m) { py::overload_cast( &simcoon::Rotation::from_euler), py::arg("psi"), py::arg("theta"), py::arg("phi"), - py::arg("conv") = "zxz", py::arg("intrinsic") = true, py::arg("degrees") = false, + py::arg("conv"), py::arg("intrinsic") = true, py::arg("degrees") = false, R"doc( Create rotation from Euler angles. @@ -139,9 +139,9 @@ void register_rotation_class(py::module_& m) { Second Euler angle phi : float Third Euler angle - conv : str, optional + conv : str Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", - "xyx", "xzx", "yxy", "yzy", or "user". Default is "zxz". + "xyx", "xzx", "yxy", "yzy", or "user". intrinsic : bool, optional If True (default), intrinsic rotations (rotating frame); if False, extrinsic rotations (fixed frame) @@ -292,14 +292,15 @@ void register_rotation_class(py::module_& m) { [](const simcoon::Rotation& self, const string& conv, bool intrinsic, bool degrees) { return carma::col_to_arr(vec(self.as_euler(conv, intrinsic, degrees))); }, - py::arg("conv") = "zxz", py::arg("intrinsic") = true, py::arg("degrees") = false, + py::arg("conv"), py::arg("intrinsic") = true, py::arg("degrees") = false, R"doc( Get rotation as Euler angles. Parameters ---------- - conv : str, optional - Euler angle convention. Default is "zxz". + conv : str + Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", + "xyx", "xzx", "yxy", "yzy", or "user". intrinsic : bool, optional If True (default), return intrinsic angles; if False, extrinsic degrees : bool, optional diff --git a/simcoon-python-builder/src/python_wrappers/python_module.cpp b/simcoon-python-builder/src/python_wrappers/python_module.cpp index ab850fd72..5cb4c74e0 100755 --- a/simcoon-python-builder/src/python_wrappers/python_module.cpp +++ b/simcoon-python-builder/src/python_wrappers/python_module.cpp @@ -46,6 +46,7 @@ #include #include +#include using namespace std; using namespace arma; @@ -210,16 +211,16 @@ PYBIND11_MODULE(_core, m) m.def("T_II", &T_II, "L"_a, "a1"_a = 1., "a2"_a = 1., "a3"_a = 1., "mp"_a = 50, "np"_a = 50, "copy"_a = true, simcoon_docs::T_II); // Register the rotation library - m.def("rotate_vec_R", &rotate_vec_R, "input"_a, "R"_a, "copy"_a = true, "This function returns a rotated vector (3) according to a rotation matrix"); - m.def("rotate_vec_angle", &rotate_vec_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, "This function returns a rotated vector (3) according to an angle and an axis"); - m.def("rotate_mat_R", &rotate_mat_R, "input"_a, "R"_a, "copy"_a = true, "This function returns a rotated matrix (3x3) according to a rotation matrix"); - m.def("rotate_mat_angle", &rotate_mat_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, "This function returns a rotated matrix (3x3) according to an angle and an axis"); - m.def("fillR_angle", &fillR_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "This function returns the 3*3 rotation matrix according to an angle, an axis and depending if it is active or passive rotation"); - m.def("fillR_euler", &fillR_euler, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "conv"_a = "zxz", "copy"_a = true, "This function returns the 3*3 rotation matrix according to the three Euler angles, depending if it is active or passive rotation and the Euler convention (ex :zxz)"); - m.def("fillQS_angle", &fillQS_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'stress' from an angle and an axis"); - m.def("fillQS_R", &fillQS_R, "R"_a, "active"_a = true, "copy"_a = true, "This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'stress' from a rotation matrix"); - m.def("fillQE_angle", &fillQE_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'strain' from an angle and an axis"); - m.def("fillQE_R", &fillQE_R, "R"_a, "active"_a = true, "copy"_a = true, "This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'strain' from a rotation matrix"); + m.def("rotate_vec_R", &rotate_vec_R, "input"_a, "R"_a, "copy"_a = true, simcoon_docs::rotate_vec_R); + m.def("rotate_vec_angle", &rotate_vec_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, simcoon_docs::rotate_vec_angle); + m.def("rotate_mat_R", &rotate_mat_R, "input"_a, "R"_a, "copy"_a = true, simcoon_docs::rotate_mat_R); + m.def("rotate_mat_angle", &rotate_mat_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, simcoon_docs::rotate_mat_angle); + m.def("fillR_angle", &fillR_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillR_angle); + m.def("fillR_euler", &fillR_euler, "psi"_a, "theta"_a, "phi"_a, "active"_a, "conv"_a, "copy"_a = true, simcoon_docs::fillR_euler); + m.def("fillQS_angle", &fillQS_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQS_angle); + m.def("fillQS_R", &fillQS_R, "R"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQS_R); + m.def("fillQE_angle", &fillQE_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQE_angle); + m.def("fillQE_R", &fillQE_R, "R"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQE_R); m.def("rotateL_angle", &rotateL_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 stiffness matrix according to an angle and an axis"); m.def("rotateL_R", &rotateL_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 stiffness matrix according to a rotation matrix"); m.def("rotate_l2g_L", &rotate_l2g_L, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 stiffness matrix from local to global frame"); diff --git a/test/Libraries/Maths/Trotation.cpp b/test/Libraries/Maths/Trotation.cpp index 4d8e3bceb..d0c0b55c4 100755 --- a/test/Libraries/Maths/Trotation.cpp +++ b/test/Libraries/Maths/Trotation.cpp @@ -129,7 +129,7 @@ TEST(Trotation, rotation) assert(axis_theta == 1); assert(axis_phi == 3); - mat Rp2 = fillR(psi,theta,phi); + mat Rp2 = fillR(psi,theta,phi, true, "user"); mat Rp3 = fillR(psi,theta,phi, true, "zxz"); mat a1 = rotate_mat(a, R); From 3538ce5a281318f597bcab506900e51b03700def Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 10:07:43 +0100 Subject: [PATCH 06/31] Throw on invalid Euler convention Docs updated to remove references to a "default" Euler convention and require the convention to be specified in examples. In rotation.cpp, the previous behavior of printing an error and falling back to default axes has been replaced with throwing std::invalid_argument when an invalid Euler convention is supplied (message lists supported conventions). This enforces explicit, valid conventions and prevents silent fallback. Affected files: docs/continuum_mechanics/functions/doc_rotation.rst, docs/cpp_api/simulation/rotation.rst, src/Simulation/Maths/rotation.cpp. --- docs/continuum_mechanics/functions/doc_rotation.rst | 8 ++++---- docs/cpp_api/simulation/rotation.rst | 2 +- src/Simulation/Maths/rotation.cpp | 7 +------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index 6cf599c8b..a8dc02956 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -25,8 +25,8 @@ Creating Rotations # Identity rotation (no rotation) r = smc.Rotation.identity() - # From Euler angles (default: zxz convention, intrinsic, radians) - r = smc.Rotation.from_euler(psi, theta, phi) + # From Euler angles (convention must be specified) + r = smc.Rotation.from_euler(psi, theta, phi, "zxz") # From Euler angles with options r = smc.Rotation.from_euler(psi, theta, phi, conv="xyz", intrinsic=False, degrees=True) @@ -279,7 +279,7 @@ Simcoon supports multiple Euler angle conventions: **Proper Euler angles** (axis sequence where first = last): -- ``zxz`` (default in simcoon) +- ``zxz`` - ``zyz`` - ``xyx``, ``xzx`` - ``yxy``, ``yzy`` @@ -296,7 +296,7 @@ Simcoon supports multiple Euler angle conventions: .. code-block:: python - # Intrinsic ZXZ (simcoon default) + # Intrinsic ZXZ r1 = smc.Rotation.from_euler(psi, theta, phi, "zxz", intrinsic=True) # Extrinsic XYZ (aerospace convention) diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst index 8f89541b7..29a8645c0 100644 --- a/docs/cpp_api/simulation/rotation.rst +++ b/docs/cpp_api/simulation/rotation.rst @@ -142,7 +142,7 @@ The ``from_euler`` and ``as_euler`` methods support multiple conventions: .. code-block:: cpp - // Intrinsic ZXZ Euler angles (default in simcoon) + // Intrinsic ZXZ Euler angles Rotation r1 = Rotation::from_euler(psi, theta, phi, "zxz", true, false); // Extrinsic XYZ (aerospace convention) diff --git a/src/Simulation/Maths/rotation.cpp b/src/Simulation/Maths/rotation.cpp index 333e3be01..39fd85adf 100755 --- a/src/Simulation/Maths/rotation.cpp +++ b/src/Simulation/Maths/rotation.cpp @@ -120,12 +120,7 @@ mat fillR(const double &psi, const double &theta, const double &phi, const bool R = R3*R2*R1; } else { - cout << "error in Simulation/Maths/rotation.cpp: please provide a consistent convention for Euler rotation, i.e. zxz or zyz. The by-default convention has been utilized (as defined in parameter.hpp)" << endl; - mat R1 = fillR(psi, simcoon::axis_psi, active); - mat R2 = fillR(theta, simcoon::axis_theta, active); - mat R3 = fillR(phi, simcoon::axis_phi, active); - - R = R3*R2*R1; + throw std::invalid_argument("error in Simulation/Maths/rotation.cpp: invalid Euler convention '" + conv + "'. Supported conventions: zxz, zyz, xyz, xzy, yxz, yzx, zxy, zyx, xyx, xzx, yxy, yzy, user"); } return R; From 78437288791fda8a11640d6243efa41d44cbe6c0 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 15:09:24 +0100 Subject: [PATCH 07/31] Update rotation API names in docs Rename and reorganize rotation-related documentation to match updated API names and types. Key changes: - Replace old function names with clearer ones: rotateL/rotateM -> rotate_stiffness/rotate_compliance; apply_localization_* -> apply_*_concentration; rotate_l2g_*/g2l_* functions replaced by *_R and *_angle variants. - Update free-function autodoc/doxygen entries and Python autofunction names accordingly. - Change C++ include references from rotation_class.hpp to rotation.hpp and document new Rotation construction functions (Rotation::from_euler, Rotation::from_axis_angle). - Update examples to use Rotation::from_euler or pass a Rotation matrix to free functions. These edits align the docs with the refactored rotation API and clarify naming (strain/stress concentration vs localization). --- .../functions/doc_rotation.rst | 58 +++------- docs/cpp_api/simulation/rotation.rst | 105 ++++++------------ docs/cpp_api/simulation_overview.md | 8 +- 3 files changed, 51 insertions(+), 120 deletions(-) diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index a8dc02956..0e585fe03 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -125,11 +125,11 @@ Applying Rotations M = smc.M_iso([210e9, 0.3], "Enu") M_rot = r.apply_compliance(M) - # Strain localization tensor (6×6): A' = QE * A * QS^T - A_global = r.apply_localization_strain(A_local) + # Strain concentration tensor (6×6): A' = QE * A * QS^T + A_global = r.apply_strain_concentration(A_local) - # Stress localization tensor (6×6): B' = QS * B * QE^T - B_global = r.apply_localization_stress(B_local) + # Stress concentration tensor (6×6): B' = QS * B * QE^T + B_global = r.apply_stress_concentration(B_local) Composing Rotations ~~~~~~~~~~~~~~~~~~~ @@ -302,29 +302,11 @@ Simcoon supports multiple Euler angle conventions: # Extrinsic XYZ (aerospace convention) r2 = smc.Rotation.from_euler(roll, pitch, yaw, "xyz", intrinsic=False) -Rotation Functions ------------------- +Rotation Free Functions +----------------------- The following functions provide direct rotation operations without creating a ``Rotation`` object. -Rotation Matrix Generation -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: simcoon.fillR_angle - -.. autofunction:: simcoon.fillR_euler - -Voigt Rotation Matrices -~~~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: simcoon.fillQS_angle - -.. autofunction:: simcoon.fillQS_R - -.. autofunction:: simcoon.fillQE_angle - -.. autofunction:: simcoon.fillQE_R - Vector and Matrix Rotation ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -350,32 +332,24 @@ Stress and Strain Rotation Stiffness and Compliance Rotation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autofunction:: simcoon.rotateL_angle - -.. autofunction:: simcoon.rotateL_R - -.. autofunction:: simcoon.rotateM_angle - -.. autofunction:: simcoon.rotateM_R - -Local-Global Transformations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. autofunction:: simcoon.rotate_stiffness_angle -.. autofunction:: simcoon.rotate_l2g_L +.. autofunction:: simcoon.rotate_stiffness_R -.. autofunction:: simcoon.rotate_g2l_L +.. autofunction:: simcoon.rotate_compliance_angle -.. autofunction:: simcoon.rotate_l2g_M +.. autofunction:: simcoon.rotate_compliance_R -.. autofunction:: simcoon.rotate_g2l_M +Strain and Stress Concentration Tensor Rotation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autofunction:: simcoon.rotate_l2g_strain +.. autofunction:: simcoon.rotate_strain_concentration_angle -.. autofunction:: simcoon.rotate_g2l_strain +.. autofunction:: simcoon.rotate_strain_concentration_R -.. autofunction:: simcoon.rotate_l2g_stress +.. autofunction:: simcoon.rotate_stress_concentration_angle -.. autofunction:: simcoon.rotate_g2l_stress +.. autofunction:: simcoon.rotate_stress_concentration_R Practical Examples ------------------ diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst index 29a8645c0..1620addee 100644 --- a/docs/cpp_api/simulation/rotation.rst +++ b/docs/cpp_api/simulation/rotation.rst @@ -35,7 +35,7 @@ C++ API .. code-block:: cpp - #include + #include using namespace simcoon; @@ -202,10 +202,10 @@ Apply Methods - Rotate 6×6 stiffness matrix: QS·L·QS\ :sup:`T` * - ``apply_compliance(M, active)`` - Rotate 6×6 compliance matrix: QE·M·QE\ :sup:`T` - * - ``apply_localization_strain(A, active)`` - - Rotate 6×6 strain localization tensor: QE·A·QS\ :sup:`T` - * - ``apply_localization_stress(B, active)`` - - Rotate 6×6 stress localization tensor: QS·B·QE\ :sup:`T` + * - ``apply_strain_concentration(A, active)`` + - Rotate 6×6 strain concentration tensor: QE·A·QS\ :sup:`T` + * - ``apply_stress_concentration(B, active)`` + - Rotate 6×6 stress concentration tensor: QS·B·QE\ :sup:`T` The ``active`` parameter controls whether the rotation is **active** (alibi, rotating the object) or **passive** (alias, rotating the coordinate system). @@ -240,32 +240,6 @@ Rotation Free Functions The following free functions provide direct rotation operations without creating a ``Rotation`` object. They are preserved for backward compatibility and direct matrix operations. -Rotation Matrix Generation --------------------------- - -.. doxygenfunction:: simcoon::fillR(const double&, const int&, const bool&) - :project: simcoon - -.. doxygenfunction:: simcoon::fillR(const double&, const double&, const double&, const bool&, const std::string&) - :project: simcoon - -Voigt Rotation Matrices ------------------------ - -The 6×6 rotation matrices for Voigt notation stress and strain tensors: - -.. doxygenfunction:: simcoon::fillQS(const double&, const int&, const bool&) - :project: simcoon - -.. doxygenfunction:: simcoon::fillQS(const arma::mat&, const bool&) - :project: simcoon - -.. doxygenfunction:: simcoon::fillQE(const double&, const int&, const bool&) - :project: simcoon - -.. doxygenfunction:: simcoon::fillQE(const arma::mat&, const bool&) - :project: simcoon - Vector and Matrix Rotation -------------------------- @@ -299,45 +273,31 @@ Stress and Strain Rotation Stiffness and Compliance Rotation --------------------------------- -.. doxygenfunction:: simcoon::rotateL(const arma::mat&, const double&, const int&, const bool&) - :project: simcoon - -.. doxygenfunction:: simcoon::rotateL(const arma::mat&, const arma::mat&, const bool&) +.. doxygenfunction:: simcoon::rotate_stiffness(const arma::mat&, const double&, const int&, const bool&) :project: simcoon -.. doxygenfunction:: simcoon::rotateM(const arma::mat&, const double&, const int&, const bool&) +.. doxygenfunction:: simcoon::rotate_stiffness(const arma::mat&, const arma::mat&, const bool&) :project: simcoon -.. doxygenfunction:: simcoon::rotateM(const arma::mat&, const arma::mat&, const bool&) +.. doxygenfunction:: simcoon::rotate_compliance(const arma::mat&, const double&, const int&, const bool&) :project: simcoon -Local-to-Global and Global-to-Local Transformations ---------------------------------------------------- - -These functions use Euler angles to transform between local and global coordinate systems: - -.. doxygenfunction:: simcoon::rotate_l2g_strain +.. doxygenfunction:: simcoon::rotate_compliance(const arma::mat&, const arma::mat&, const bool&) :project: simcoon -.. doxygenfunction:: simcoon::rotate_g2l_strain - :project: simcoon +Strain and Stress Concentration Tensor Rotation +------------------------------------------------ -.. doxygenfunction:: simcoon::rotate_l2g_stress +.. doxygenfunction:: simcoon::rotate_strain_concentration(const arma::mat&, const double&, const int&, const bool&) :project: simcoon -.. doxygenfunction:: simcoon::rotate_g2l_stress +.. doxygenfunction:: simcoon::rotate_strain_concentration(const arma::mat&, const arma::mat&, const bool&) :project: simcoon -.. doxygenfunction:: simcoon::rotate_l2g_L +.. doxygenfunction:: simcoon::rotate_stress_concentration(const arma::mat&, const double&, const int&, const bool&) :project: simcoon -.. doxygenfunction:: simcoon::rotate_g2l_L - :project: simcoon - -.. doxygenfunction:: simcoon::rotate_l2g_M - :project: simcoon - -.. doxygenfunction:: simcoon::rotate_g2l_M +.. doxygenfunction:: simcoon::rotate_stress_concentration(const arma::mat&, const arma::mat&, const bool&) :project: simcoon Python Functions @@ -345,18 +305,6 @@ Python Functions The following functions are available in Python for direct rotation operations: -.. autofunction:: simcoon.fillR_angle - -.. autofunction:: simcoon.fillR_euler - -.. autofunction:: simcoon.fillQS_angle - -.. autofunction:: simcoon.fillQS_R - -.. autofunction:: simcoon.fillQE_angle - -.. autofunction:: simcoon.fillQE_R - .. autofunction:: simcoon.rotate_vec_R .. autofunction:: simcoon.rotate_vec_angle @@ -373,13 +321,21 @@ The following functions are available in Python for direct rotation operations: .. autofunction:: simcoon.rotate_strain_R -.. autofunction:: simcoon.rotateL_angle +.. autofunction:: simcoon.rotate_stiffness_angle + +.. autofunction:: simcoon.rotate_stiffness_R + +.. autofunction:: simcoon.rotate_compliance_angle + +.. autofunction:: simcoon.rotate_compliance_R + +.. autofunction:: simcoon.rotate_strain_concentration_angle -.. autofunction:: simcoon.rotateL_R +.. autofunction:: simcoon.rotate_strain_concentration_R -.. autofunction:: simcoon.rotateM_angle +.. autofunction:: simcoon.rotate_stress_concentration_angle -.. autofunction:: simcoon.rotateM_R +.. autofunction:: simcoon.rotate_stress_concentration_R Examples ======== @@ -404,8 +360,9 @@ Rotating material properties from local to global coordinates: r = smc.Rotation.from_euler(psi, theta, phi, "zxz") L_global = r.apply_stiffness(L_local) - # Method 2: Using free function - L_global = smc.rotate_l2g_L(L_local, psi, theta, phi) + # Method 2: Using free function with rotation matrix + R = smc.Rotation.from_euler(psi, theta, phi, "zxz").as_matrix() + L_global = smc.rotate_stiffness_R(L_local, R) Example 2: Stress Transformation -------------------------------- @@ -433,7 +390,7 @@ Combining multiple rotations: .. code-block:: cpp - #include + #include using namespace simcoon; diff --git a/docs/cpp_api/simulation_overview.md b/docs/cpp_api/simulation_overview.md index 2d8378512..033d4550d 100644 --- a/docs/cpp_api/simulation_overview.md +++ b/docs/cpp_api/simulation_overview.md @@ -195,12 +195,12 @@ Rotation operations for objective stress integration: - Active/passive rotation conventions **Key Functions:** -- `fillR_euler()` - Construct rotation matrix from Euler angles (ZXZ, ZYZ conventions) -- `fillR_angle()` - Rotation matrix from axis-angle representation +- `Rotation::from_euler()` - Construct rotation from Euler angles (ZXZ, ZYZ, XYZ, etc.) +- `Rotation::from_axis_angle()` - Rotation from axis-angle representation - `rotate_strain()` - Rotate strain tensor (Voigt notation) - `rotate_stress()` - Rotate stress tensor (Voigt notation) -- `rotateL()` - Rotate stiffness matrix -- `rotateM()` - Rotate compliance matrix +- `rotate_stiffness()` - Rotate stiffness matrix +- `rotate_compliance()` - Rotate compliance matrix #### **lagrange.hpp** Lagrange multiplier methods for constrained problems: From ac809458918701368c6ef1311a5d19ed3e29bd33 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 15:09:49 +0100 Subject: [PATCH 08/31] Refactor rotation API and rename wrappers Rename and reorganize rotation-related Python wrappers and docs for clarity and vectorized usage. Replaced legacy fillR/fillQS/fillQE and rotateL/M/A/B/l2g/g2l APIs with clearer functions (rotate_stiffness_*, rotate_compliance_*, rotate_strain_concentration_*, rotate_stress_concentration_*, rotate_strain_*, rotate_stress_*), updated wrapper implementations to use descriptive variable names and support batched (ndim==2) inputs, and adjusted Rotation class method names (apply_localization_* -> apply_*_concentration). Also updated example docs to use simcoon.Rotation.from_axis_angle(...). Modified files: include/docs/Libraries/Maths/doc_rotation.hpp, include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp, src/python_wrappers/Libraries/Maths/rotation.cpp, src/python_wrappers/Libraries/Maths/rotation_class.cpp, src/python_wrappers/python_module.cpp. --- .../docs/Libraries/Maths/doc_rotation.hpp | 198 +------------ .../Libraries/Maths/rotation.hpp | 66 ++--- .../Libraries/Maths/rotation.cpp | 279 +++++------------- .../Libraries/Maths/rotation_class.cpp | 22 +- .../src/python_wrappers/python_module.cpp | 42 +-- 5 files changed, 122 insertions(+), 485 deletions(-) diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp index fe54a4e6d..b88c2235d 100644 --- a/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp @@ -2,200 +2,6 @@ namespace simcoon_docs { -constexpr auto fillR_angle = R"pbdoc( - Generate a 3x3 rotation matrix for rotation around a single axis. - - Parameters - ---------- - angle : float - Rotation angle in radians. - axis : int - Axis of rotation: 1=x, 2=y, 3=z. - active : bool, optional - If True (default), active (alibi) rotation; if False, passive (alias) rotation. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - 3x3 rotation matrix. - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - # 45 degree rotation around z-axis - R = sim.fillR_angle(np.pi/4, 3) - print(R) -)pbdoc"; - -constexpr auto fillR_euler = R"pbdoc( - Generate a 3x3 rotation matrix from Euler angles. - - Parameters - ---------- - psi : float - First Euler angle in radians. - theta : float - Second Euler angle in radians. - phi : float - Third Euler angle in radians. - active : bool - If True, active (alibi) rotation; if False, passive (alias) rotation. - conv : str - Euler angle convention. Supported conventions: - - Proper Euler angles: "zxz", "xyx", "yzy", "zyz", "xzx", "yxy" - - Tait-Bryan angles: "xyz", "yzx", "zxy", "xzy", "zyx", "yxz" - - Custom: "user" (uses axis_psi, axis_theta, axis_phi from parameter.hpp) - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - 3x3 rotation matrix. - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - # Rotation using ZXZ Euler angles - psi, theta, phi = np.pi/6, np.pi/4, np.pi/3 - R = sim.fillR_euler(psi, theta, phi, True, "zxz") - print(R) - - # Rotation using XYZ Tait-Bryan angles - R_xyz = sim.fillR_euler(0.1, 0.2, 0.3, True, "xyz") - print(R_xyz) -)pbdoc"; - -constexpr auto fillQS_angle = R"pbdoc( - Generate a 6x6 rotation matrix for stress tensors in Voigt notation. - - Parameters - ---------- - angle : float - Rotation angle in radians. - axis : int - Axis of rotation: 1=x, 2=y, 3=z. - active : bool, optional - If True (default), active rotation; if False, passive rotation. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - 6x6 stress rotation matrix (QS). - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - # 45 degree rotation matrix for stress - QS = sim.fillQS_angle(np.pi/4, 3) - print(QS) -)pbdoc"; - -constexpr auto fillQS_R = R"pbdoc( - Generate a 6x6 rotation matrix for stress tensors from a 3x3 rotation matrix. - - Parameters - ---------- - R : ndarray - 3x3 rotation matrix. - active : bool, optional - If True (default), active rotation; if False, passive rotation. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - 6x6 stress rotation matrix (QS). - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - R = sim.fillR_angle(np.pi/4, 3) - QS = sim.fillQS_R(R) - print(QS) -)pbdoc"; - -constexpr auto fillQE_angle = R"pbdoc( - Generate a 6x6 rotation matrix for strain tensors in Voigt notation. - - Parameters - ---------- - angle : float - Rotation angle in radians. - axis : int - Axis of rotation: 1=x, 2=y, 3=z. - active : bool, optional - If True (default), active rotation; if False, passive rotation. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - 6x6 strain rotation matrix (QE). - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - # 45 degree rotation matrix for strain - QE = sim.fillQE_angle(np.pi/4, 3) - print(QE) -)pbdoc"; - -constexpr auto fillQE_R = R"pbdoc( - Generate a 6x6 rotation matrix for strain tensors from a 3x3 rotation matrix. - - Parameters - ---------- - R : ndarray - 3x3 rotation matrix. - active : bool, optional - If True (default), active rotation; if False, passive rotation. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - 6x6 strain rotation matrix (QE). - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - R = sim.fillR_angle(np.pi/4, 3) - QE = sim.fillQE_R(R) - print(QE) -)pbdoc"; - constexpr auto rotate_vec_R = R"pbdoc( Rotate a 3D vector using a rotation matrix. @@ -221,7 +27,7 @@ constexpr auto rotate_vec_R = R"pbdoc( import simcoon as sim v = np.array([1.0, 0.0, 0.0]) - R = sim.fillR_angle(np.pi/4, 3) + R = sim.Rotation.from_axis_angle(np.pi/4, 3).as_matrix() v_rot = sim.rotate_vec_R(v, R) print(v_rot) )pbdoc"; @@ -282,7 +88,7 @@ constexpr auto rotate_mat_R = R"pbdoc( import simcoon as sim m = np.eye(3) - R = sim.fillR_angle(np.pi/4, 3) + R = sim.Rotation.from_axis_angle(np.pi/4, 3).as_matrix() m_rot = sim.rotate_mat_R(m, R) print(m_rot) )pbdoc"; diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp index b2077dbfc..83673e532 100755 --- a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp +++ b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp @@ -15,59 +15,29 @@ pybind11::array_t rotate_mat_R(const pybind11::array_t &input, c //This function returns a rotated matrix (3x3) according to an angle and an axis pybind11::array_t rotate_mat_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool ©=true); - -//This function returns the 3*3 rotation matrix according to an angle, an axis and depending if it is active or passive rotation -pybind11::array_t fillR_angle(const double &angle, const int &axis, const bool &active=true, const bool ©=true); - -//This function returns the 3*3 rotation matrix according to the three Euler angles, depending if it is active or passive rotation and the Euler convention (ex: "zxz", "xyz", "zyz", etc.) -pybind11::array_t fillR_euler(const double &psi, const double &theta, const double &phi, const bool &active, const std::string &conv, const bool ©=true); - -//This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'stress' from an angle and an axis -pybind11::array_t fillQS_angle(const double &angle, const int &axis, const bool &active=true, const bool ©=true); -//This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'stress' from a rotation matrix -pybind11::array_t fillQS_R(const pybind11::array_t &R, const bool &active=true, const bool ©=true); +//These functions rotate a 6*6 stiffness matrix +pybind11::array_t rotate_stiffness_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); +pybind11::array_t rotate_stiffness_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -//This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'strain' from an angle and an axis -pybind11::array_t fillQE_angle(const double &angle, const int &axis, const bool &active=true, const bool ©=true); - -//This function returns the 6*6 rotation arma::matrix of a arma::vector of type 'strain' from a rotation matrix -pybind11::array_t fillQE_R(const pybind11::array_t &R, const bool &active=true, const bool ©=true); +//These functions rotate a 6*6 compliance matrix +pybind11::array_t rotate_compliance_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); +pybind11::array_t rotate_compliance_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -//These functions rotates a 6*6 stiffness arma::matrix (L) -pybind11::array_t rotateL_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotateL_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_l2g_L(const pybind11::array_t &input, const double &, const double &, const double &, const bool ©=true); -pybind11::array_t rotate_g2l_L(const pybind11::array_t &input, const double &, const double &, const double &, const bool ©=true); - -//These functions rotates a 6*6 compliance arma::matrix (M) -pybind11::array_t rotateM_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotateM_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_l2g_M(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); -pybind11::array_t rotate_g2l_M(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); - -//These functions rotates a 6*6 strain concentration (A) -pybind11::array_t rotateA_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotateA_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_l2g_A(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); -pybind11::array_t rotate_g2l_A(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); - -//These functions rotates a 6*6 stress concentration (B) -pybind11::array_t rotateB_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotateB_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_l2g_B(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); -pybind11::array_t rotate_g2l_B(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); - -//These functions rotates strain arma::vectors +//These functions rotate a 6*6 strain concentration tensor (A) +pybind11::array_t rotate_strain_concentration_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); +pybind11::array_t rotate_strain_concentration_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); + +//These functions rotate a 6*6 stress concentration tensor (B) +pybind11::array_t rotate_stress_concentration_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); +pybind11::array_t rotate_stress_concentration_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); + +//These functions rotate strain vectors - Can be used with stack of arrays (vectorized) pybind11::array_t rotate_strain_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); pybind11::array_t rotate_strain_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_l2g_strain(const pybind11::array_t &, const double &psi, const double &theta, const double &phi, const bool ©=true); -pybind11::array_t rotate_g2l_strain(const pybind11::array_t &, const double &psi, const double &theta, const double &phi, const bool ©=true); -//These functions rotates stress arma::vectors +//These functions rotate stress vectors - Can be used with stack of arrays (vectorized) pybind11::array_t rotate_stress_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); pybind11::array_t rotate_stress_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_l2g_stress(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); -pybind11::array_t rotate_g2l_stress(const pybind11::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©=true); - -} //namespace simpy \ No newline at end of file + +} //namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp index efa223b56..05ec65e8f 100755 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp @@ -17,140 +17,103 @@ namespace simpy { py::array_t rotate_vec_R(const py::array_t &input, const py::array_t &R, const bool ©) { vec v_cpp = carma::arr_to_col(input); mat R_cpp = carma::arr_to_mat(R); - vec rotated_v = simcoon::rotate_vec(v_cpp,R_cpp); - return carma::col_to_arr(rotated_v, copy); + vec rotated_vec = simcoon::rotate_vec(v_cpp, R_cpp); + return carma::col_to_arr(rotated_vec, copy); } py::array_t rotate_vec_angle(const py::array_t &input, const double &angle, const int &axis, const bool ©) { vec v_cpp = carma::arr_to_col(input); - vec rotated_v = simcoon::rotate_vec(v_cpp,angle,axis); - return carma::col_to_arr(rotated_v, copy); + vec rotated_vec = simcoon::rotate_vec(v_cpp, angle, axis); + return carma::col_to_arr(rotated_vec, copy); } py::array_t rotate_mat_R(const py::array_t &input, const py::array_t &R, const bool ©) { mat m_cpp = carma::arr_to_mat(input); mat R_cpp = carma::arr_to_mat(R); - mat rotated_m = simcoon::rotate_mat(m_cpp,R_cpp); - return carma::mat_to_arr(rotated_m, copy); + mat rotated_mat = simcoon::rotate_mat(m_cpp, R_cpp); + return carma::mat_to_arr(rotated_mat, copy); } -py::array_t rotate_mat_angle(const py::array_t &input, const double &angle, const int &axis, const bool ©) { +py::array_t rotate_mat_angle(const py::array_t &input, const double &angle, const int &axis, const bool ©) { mat m_cpp = carma::arr_to_mat(input); - mat rotated_m = simcoon::rotate_mat(m_cpp,angle,axis); - return carma::mat_to_arr(rotated_m, copy); + mat rotated_mat = simcoon::rotate_mat(m_cpp, angle, axis); + return carma::mat_to_arr(rotated_mat, copy); } -py::array_t fillR_angle(const double &angle, const int &axis, const bool &active, const bool ©) { - mat R = simcoon::fillR(angle,axis,active); - return carma::mat_to_arr(R, copy); -} - -//This function returns the 3*3 rotation matrix -py::array_t fillR_euler(const double &psi, const double &theta, const double &phi, const bool &active, const string &conv, const bool ©) { - mat R = simcoon::fillR(psi,theta,phi,active,conv); - return carma::mat_to_arr(R, copy); -} - -//This function returns the 6*6 rotation matrix of a vector of type 'stress' -py::array_t fillQS_angle(const double &angle, const int &axis, const bool &active, const bool ©) { - mat QS = simcoon::fillQS(angle,axis,active); - return carma::mat_to_arr(QS, copy); -} - -//This function returns the 6*6 rotation matrix of a vector of type 'stress' -py::array_t fillQS_R(const py::array_t &R, const bool &active, const bool ©) { - mat R_cpp = carma::arr_to_mat(R); - mat QS = simcoon::fillQS(R_cpp,active); - return carma::mat_to_arr(QS, copy); -} - -//This function returns the 6*6 rotation matrix of a vector of type 'strain' -py::array_t fillQE_angle(const double &angle, const int &axis, const bool &active, const bool ©) { - mat QE = simcoon::fillQE(angle,axis,active); - return carma::mat_to_arr(QE, copy); -} - -//This function returns the 6*6 rotation matrix of a vector of type 'strain' -py::array_t fillQE_R(const py::array_t &R, const bool &active, const bool ©) { - mat R_cpp = carma::arr_to_mat(R); - mat QE = simcoon::fillQE(R_cpp,active); - return carma::mat_to_arr(QE, copy); -} - -//This function rotates a 6*6 stiffness matrix (L) -py::array_t rotateL_angle(const py::array_t &input, const double &angle, const int & axis, const bool &active, const bool ©) { - mat L_cpp = carma::arr_to_mat(input); - mat rotated_L = simcoon::rotateL(L_cpp,angle,axis,active); - return carma::mat_to_arr(rotated_L, copy); +//This function rotates a 6x6 stiffness matrix +py::array_t rotate_stiffness_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { + mat stiffness = carma::arr_to_mat(input); + mat rotated_stiffness = simcoon::rotate_stiffness(stiffness, angle, axis, active); + return carma::mat_to_arr(rotated_stiffness, copy); } -//This function rotates a 6*6 stiffness matrix (L) -py::array_t rotateL_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat L_cpp = carma::arr_to_mat(input); +//This function rotates a 6x6 stiffness matrix +py::array_t rotate_stiffness_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { + mat stiffness = carma::arr_to_mat(input); mat R_cpp = carma::arr_to_mat(R); - mat rotated_L = simcoon::rotateL(L_cpp,R_cpp,active); - return carma::mat_to_arr(rotated_L, copy); + mat rotated_stiffness = simcoon::rotate_stiffness(stiffness, R_cpp, active); + return carma::mat_to_arr(rotated_stiffness, copy); } -//This function rotates a 6*6 compliance matrix (M) -py::array_t rotateM_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - mat M_cpp = carma::arr_to_mat(input); - mat rotated_M = simcoon::rotateM(M_cpp,angle,axis,active); - return carma::mat_to_arr(rotated_M, copy); +//This function rotates a 6x6 compliance matrix +py::array_t rotate_compliance_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { + mat compliance = carma::arr_to_mat(input); + mat rotated_compliance = simcoon::rotate_compliance(compliance, angle, axis, active); + return carma::mat_to_arr(rotated_compliance, copy); } -//This function rotates a 6*6 compliance matrix (M) -py::array_t rotateM_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat M_cpp = carma::arr_to_mat(input); +//This function rotates a 6x6 compliance matrix +py::array_t rotate_compliance_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { + mat compliance = carma::arr_to_mat(input); mat R_cpp = carma::arr_to_mat(R); - mat rotated_M = simcoon::rotateM(M_cpp,R_cpp,active); - return carma::mat_to_arr(rotated_M); + mat rotated_compliance = simcoon::rotate_compliance(compliance, R_cpp, active); + return carma::mat_to_arr(rotated_compliance, copy); } -//This function rotates a 6*6 strain concentration (A) -py::array_t rotateA_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - mat A_cpp = carma::arr_to_mat(input); - mat rotated_A = simcoon::rotateA(A_cpp,angle,axis,active); - return carma::mat_to_arr(rotated_A, copy); +//This function rotates a 6x6 strain concentration tensor +py::array_t rotate_strain_concentration_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { + mat strain_concentration = carma::arr_to_mat(input); + mat rotated_strain_concentration = simcoon::rotate_strain_concentration(strain_concentration, angle, axis, active); + return carma::mat_to_arr(rotated_strain_concentration, copy); } -//This function rotates a 6*6 strain concentration (A) -py::array_t rotateA_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat A_cpp = carma::arr_to_mat(input); +//This function rotates a 6x6 strain concentration tensor +py::array_t rotate_strain_concentration_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { + mat strain_concentration = carma::arr_to_mat(input); mat R_cpp = carma::arr_to_mat(R); - mat rotated_A = simcoon::rotateA(A_cpp,R_cpp,active); - return carma::mat_to_arr(rotated_A, copy); + mat rotated_strain_concentration = simcoon::rotate_strain_concentration(strain_concentration, R_cpp, active); + return carma::mat_to_arr(rotated_strain_concentration, copy); } -//This function rotates a 6*6 stress concentration (B) -py::array_t rotateB_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - mat B_cpp = carma::arr_to_mat(input); - mat rotated_B = simcoon::rotateB(B_cpp,angle,axis,active); - return carma::mat_to_arr(rotated_B, copy); +//This function rotates a 6x6 stress concentration tensor +py::array_t rotate_stress_concentration_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { + mat stress_concentration = carma::arr_to_mat(input); + mat rotated_stress_concentration = simcoon::rotate_stress_concentration(stress_concentration, angle, axis, active); + return carma::mat_to_arr(rotated_stress_concentration, copy); } -//This function rotates a 6*6 stress concentration (B) -py::array_t rotateB_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat B_cpp = carma::arr_to_mat(input); +//This function rotates a 6x6 stress concentration tensor +py::array_t rotate_stress_concentration_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { + mat stress_concentration = carma::arr_to_mat(input); mat R_cpp = carma::arr_to_mat(R); - mat rotated_B = simcoon::rotateB(B_cpp,R_cpp,active); - return carma::mat_to_arr(rotated_B, copy); + mat rotated_stress_concentration = simcoon::rotate_stress_concentration(stress_concentration, R_cpp, active); + return carma::mat_to_arr(rotated_stress_concentration, copy); } //This function rotates stress vectors - Can be used with stack of arrays (vectorized) py::array_t rotate_stress_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { if (input.ndim()==1){ - vec v = carma::arr_to_col(input); - vec rotated_stress = simcoon::rotate_stress(v,angle,axis,active); + vec stress = carma::arr_to_col(input); + vec rotated_stress = simcoon::rotate_stress(stress, angle, axis, active); return carma::col_to_arr(rotated_stress, copy); } else if (input.ndim() == 2){ - mat m = carma::arr_to_mat_view(input); - int nb_points = m.n_cols; - mat rotated_stress(6,nb_points); + mat stress_batch = carma::arr_to_mat_view(input); + int nb_points = stress_batch.n_cols; + mat rotated_stress(6, nb_points); for (int pt = 0; pt < nb_points; pt++) { - vec v = m.unsafe_col(pt); - rotated_stress.col(pt) = simcoon::rotate_stress(v,angle,axis,active); + vec stress = stress_batch.unsafe_col(pt); + rotated_stress.col(pt) = simcoon::rotate_stress(stress, angle, axis, active); } return carma::mat_to_arr(rotated_stress, copy); } @@ -158,23 +121,23 @@ py::array_t rotate_stress_angle(const py::array_t &input, const throw std::invalid_argument("input.ndim should be 1 or 2"); } } - + //This function rotates stress vectors - Can be used with stack of arrays (vectorized) py::array_t rotate_stress_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { if (input.ndim()==1){ - vec v = carma::arr_to_col(input); + vec stress = carma::arr_to_col(input); mat R_cpp = carma::arr_to_mat(R); - vec rotated_stress = simcoon::rotate_stress(v,R_cpp,active); + vec rotated_stress = simcoon::rotate_stress(stress, R_cpp, active); return carma::col_to_arr(rotated_stress, copy); } else if (input.ndim() == 2){ - mat m = carma::arr_to_mat_view(input); + mat stress_batch = carma::arr_to_mat_view(input); cube R_cpp = carma::arr_to_cube_view(R); - int nb_points = m.n_cols; - mat rotated_stress(6,nb_points); + int nb_points = stress_batch.n_cols; + mat rotated_stress(6, nb_points); for (int pt = 0; pt < nb_points; pt++) { - vec v = m.unsafe_col(pt); - rotated_stress.col(pt) = simcoon::rotate_stress(v,R_cpp.slice(pt),active); + vec stress = stress_batch.unsafe_col(pt); + rotated_stress.col(pt) = simcoon::rotate_stress(stress, R_cpp.slice(pt), active); } return carma::mat_to_arr(rotated_stress, copy); } @@ -186,17 +149,17 @@ py::array_t rotate_stress_R(const py::array_t &input, const py:: //This function rotates strain vectors - Can be used with stack of arrays (vectorized) py::array_t rotate_strain_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { if (input.ndim()==1){ - vec v = carma::arr_to_col(input); - vec rotated_strain = simcoon::rotate_strain(v,angle,axis,active); + vec strain = carma::arr_to_col(input); + vec rotated_strain = simcoon::rotate_strain(strain, angle, axis, active); return carma::col_to_arr(rotated_strain, copy); } else if (input.ndim() == 2){ - mat m = carma::arr_to_mat_view(input); - int nb_points = m.n_cols; - mat rotated_strain(6,nb_points); + mat strain_batch = carma::arr_to_mat_view(input); + int nb_points = strain_batch.n_cols; + mat rotated_strain(6, nb_points); for (int pt = 0; pt < nb_points; pt++) { - vec v = m.unsafe_col(pt); - rotated_strain.col(pt) = simcoon::rotate_strain(v,angle,axis,active); + vec strain = strain_batch.unsafe_col(pt); + rotated_strain.col(pt) = simcoon::rotate_strain(strain, angle, axis, active); } return carma::mat_to_arr(rotated_strain, copy); } @@ -208,19 +171,19 @@ py::array_t rotate_strain_angle(const py::array_t &input, const //This function rotates strain vectors - Can be used with stack of arrays (vectorized) py::array_t rotate_strain_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { if (input.ndim()==1){ - vec v = carma::arr_to_col(input); + vec strain = carma::arr_to_col(input); mat R_cpp = carma::arr_to_mat(R); - vec rotated_strain = simcoon::rotate_strain(v,R_cpp,active); + vec rotated_strain = simcoon::rotate_strain(strain, R_cpp, active); return carma::col_to_arr(rotated_strain, copy); } else if (input.ndim() == 2){ - mat m = carma::arr_to_mat_view(input); + mat strain_batch = carma::arr_to_mat_view(input); cube R_cpp = carma::arr_to_cube_view(R); - int nb_points = m.n_cols; - mat rotated_strain(6,nb_points); + int nb_points = strain_batch.n_cols; + mat rotated_strain(6, nb_points); for (int pt = 0; pt < nb_points; pt++) { - vec v = m.unsafe_col(pt); - rotated_strain.col(pt) = simcoon::rotate_strain(v,R_cpp.slice(pt),active); + vec strain = strain_batch.unsafe_col(pt); + rotated_strain.col(pt) = simcoon::rotate_strain(strain, R_cpp.slice(pt), active); } return carma::mat_to_arr(rotated_strain, copy); } @@ -229,88 +192,4 @@ py::array_t rotate_strain_R(const py::array_t &input, const py:: } } -//This function rotates strain vectors from a local to global set of coordinates (using Euler angles) -py::array_t rotate_l2g_strain(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - vec E_cpp = carma::arr_to_col(input); - vec rotated_E = simcoon::rotate_l2g_strain(E_cpp,psi,theta,phi); - return carma::col_to_arr(rotated_E, copy); -} - -//This function rotates strain vectors from a global to local set of coordinates (using Euler angles) -py::array_t rotate_g2l_strain(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - vec E_cpp = carma::arr_to_col(input); - vec rotated_E = simcoon::rotate_g2l_strain(E_cpp,psi,theta,phi); - return carma::col_to_arr(rotated_E, copy); -} - -//This function rotates stress vectors from a local to global set of coordinates (using Euler angles) -py::array_t rotate_l2g_stress(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - vec S_cpp = carma::arr_to_col(input); - vec rotated_S = simcoon::rotate_l2g_strain(S_cpp,psi,theta,phi); - return carma::col_to_arr(rotated_S, copy); -} - -//This function rotates stress vectors from a global to local set of coordinates (using Euler angles) -py::array_t rotate_g2l_stress(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - vec S_cpp = carma::arr_to_col(input); - vec rotated_S = simcoon::rotate_g2l_stress(S_cpp,psi,theta,phi); - return carma::col_to_arr(rotated_S, copy); -} - -//This function rotates stiffness matrices (L) from a local to global set of coordinates (using Euler angles) -py::array_t rotate_l2g_L(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat L_cpp = carma::arr_to_mat(input); - mat rotated_L = simcoon::rotate_l2g_L(L_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_L, copy); -} - -//This function rotates stiffness matrices (L) from a global to local set of coordinates (using Euler angles) -py::array_t rotate_g2l_L(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat L_cpp = carma::arr_to_mat(input); - mat rotated_L = simcoon::rotate_g2l_L(L_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_L, copy); -} - -//This function rotates compliance matrices (M) from a local to global set of coordinates (using Euler angles) -py::array_t rotate_l2g_M(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat M_cpp = carma::arr_to_mat(input); - mat rotated_M = simcoon::rotate_l2g_M(M_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_M, copy); -} - -//This function rotates compliance matrices (M) from a global to local set of coordinates (using Euler angles) -py::array_t rotate_g2l_M(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat M_cpp = carma::arr_to_mat(input); - mat rotated_M = simcoon::rotate_g2l_M(M_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_M, copy); -} - -//This function rotates strain concentration matrices (A) from a local to global set of coordinates (using Euler angles) -py::array_t rotate_l2g_A(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat A_cpp = carma::arr_to_mat(input); - mat rotated_A = simcoon::rotate_l2g_A(A_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_A, copy); -} - -//This function rotates strain concentration matrices (A) from a global to local set of coordinates (using Euler angles) -py::array_t rotate_g2l_A(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat A_cpp = carma::arr_to_mat(input); - mat rotated_A = simcoon::rotate_g2l_A(A_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_A, copy); -} - -//This function rotates stress concentration matrices (B) from a local to global set of coordinates (using Euler angles) -py::array_t rotate_l2g_B(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat B_cpp = carma::arr_to_mat(input); - mat rotated_B = simcoon::rotate_l2g_B(B_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_B, copy); -} - -//This function rotates stress concentration matrices (B) from a global to local set of coordinates (using Euler angles) -py::array_t rotate_g2l_B(const py::array_t &input, const double &psi, const double &theta, const double &phi, const bool ©) { - mat B_cpp = carma::arr_to_mat(input); - mat rotated_B = simcoon::rotate_g2l_B(B_cpp,psi,theta,phi); - return carma::mat_to_arr(rotated_B, copy); -} - -} //namepsace simpy +} //namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp index 93091410f..30bcdfef1 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include using namespace std; @@ -514,52 +514,52 @@ void register_rotation_class(py::module_& m) { Rotated compliance matrix: QE * M * QE^T )doc") - .def("apply_localization_strain", + .def("apply_strain_concentration", [](const simcoon::Rotation& self, py::array_t A, bool active) { validate_matrix_size(A, 6, 6, "A"); mat A_cpp = carma::arr_to_mat(A); - mat result = self.apply_localization_strain(A_cpp, active); + mat result = self.apply_strain_concentration(A_cpp, active); return carma::mat_to_arr(result); }, py::arg("A"), py::arg("active") = true, R"doc( - Apply rotation to a 6x6 strain localization tensor. + Apply rotation to a 6x6 strain concentration tensor. Parameters ---------- A : array_like - 6x6 strain localization tensor in Voigt notation + 6x6 strain concentration tensor in Voigt notation active : bool, optional If True (default), active rotation Returns ------- ndarray - Rotated strain localization tensor: QE * A * QS^T + Rotated strain concentration tensor: QE * A * QS^T )doc") - .def("apply_localization_stress", + .def("apply_stress_concentration", [](const simcoon::Rotation& self, py::array_t B, bool active) { validate_matrix_size(B, 6, 6, "B"); mat B_cpp = carma::arr_to_mat(B); - mat result = self.apply_localization_stress(B_cpp, active); + mat result = self.apply_stress_concentration(B_cpp, active); return carma::mat_to_arr(result); }, py::arg("B"), py::arg("active") = true, R"doc( - Apply rotation to a 6x6 stress localization tensor. + Apply rotation to a 6x6 stress concentration tensor. Parameters ---------- B : array_like - 6x6 stress localization tensor in Voigt notation + 6x6 stress concentration tensor in Voigt notation active : bool, optional If True (default), active rotation Returns ------- ndarray - Rotated stress localization tensor: QS * B * QE^T + Rotated stress concentration tensor: QS * B * QE^T )doc") // Operations diff --git a/simcoon-python-builder/src/python_wrappers/python_module.cpp b/simcoon-python-builder/src/python_wrappers/python_module.cpp index 5cb4c74e0..7aaec49e6 100755 --- a/simcoon-python-builder/src/python_wrappers/python_module.cpp +++ b/simcoon-python-builder/src/python_wrappers/python_module.cpp @@ -215,36 +215,18 @@ PYBIND11_MODULE(_core, m) m.def("rotate_vec_angle", &rotate_vec_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, simcoon_docs::rotate_vec_angle); m.def("rotate_mat_R", &rotate_mat_R, "input"_a, "R"_a, "copy"_a = true, simcoon_docs::rotate_mat_R); m.def("rotate_mat_angle", &rotate_mat_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, simcoon_docs::rotate_mat_angle); - m.def("fillR_angle", &fillR_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillR_angle); - m.def("fillR_euler", &fillR_euler, "psi"_a, "theta"_a, "phi"_a, "active"_a, "conv"_a, "copy"_a = true, simcoon_docs::fillR_euler); - m.def("fillQS_angle", &fillQS_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQS_angle); - m.def("fillQS_R", &fillQS_R, "R"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQS_R); - m.def("fillQE_angle", &fillQE_angle, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQE_angle); - m.def("fillQE_R", &fillQE_R, "R"_a, "active"_a = true, "copy"_a = true, simcoon_docs::fillQE_R); - m.def("rotateL_angle", &rotateL_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 stiffness matrix according to an angle and an axis"); - m.def("rotateL_R", &rotateL_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 stiffness matrix according to a rotation matrix"); - m.def("rotate_l2g_L", &rotate_l2g_L, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 stiffness matrix from local to global frame"); - m.def("rotate_g2l_L", &rotate_g2l_L, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 stiffness matrix from global to local frame"); - m.def("rotateM_angle", &rotateM_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 compliance matrix according to an angle and an axis"); - m.def("rotateM_R", &rotateM_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 compliance matrix according to a rotation matrix"); - m.def("rotate_l2g_M", &rotate_l2g_M, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 compliance matrix from local to global frame"); - m.def("rotate_g2l_M", &rotate_g2l_M, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 compliance matrix from global to local frame"); - m.def("rotateA_angle", &rotateA_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 strain concentration matrix according to an angle and an axis"); - m.def("rotateA_R", &rotateA_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 strain concentration matrix according to a rotation matrix"); - m.def("rotate_l2g_A", &rotate_l2g_A, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 strain concentration matrix from local to global frame"); - m.def("rotate_g2l_A", &rotate_g2l_A, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 strain concentration matrix from global to local frame"); - m.def("rotateB_angle", &rotateB_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 stress concentration matrix according to an angle and an axis"); - m.def("rotateB_R", &rotateB_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6*6 stress concentration matrix according to a rotation matrix"); - m.def("rotate_l2g_B", &rotate_l2g_B, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 stress concentration matrix from local to global frame"); - m.def("rotate_g2l_B", &rotate_g2l_B, "input"_a, "psi"_a, "theta"_a, "phi"_a, "active"_a = true, "Return the rotated 6*6 stress concentration matrix from global to local frame"); - m.def("rotate_strain_angle", &rotate_strain_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = false, "Return the rotated strain matrix using voigt notations according to an angle and an axis"); - m.def("rotate_strain_R", &rotate_strain_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = false, "Return the rotated strain matrix using voigt notations according to a rotation matrix"); - m.def("rotate_l2g_strain", &rotate_l2g_strain, "input"_a, "psi"_a, "theta"_a, "phi"_a, "copy"_a = true, "Return the rotated strain matrix using voigt notations from local to global frame"); - m.def("rotate_g2l_strain", &rotate_g2l_strain, "input"_a, "psi"_a, "theta"_a, "phi"_a, "copy"_a = true, "Return the rotated strain matrix using voigt notations from global to local frame"); - m.def("rotate_stress_angle", &rotate_stress_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = false, "Return the rotated stress matrix using voigt notations according to an angle and an axis"); - m.def("rotate_stress_R", &rotate_stress_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = false, "Return the rotated stress matrix using voigt notations according to a rotation matrix"); - m.def("rotate_l2g_stress", &rotate_l2g_stress, "input"_a, "psi"_a, "theta"_a, "phi"_a, "copy"_a = true, "Return the rotated stress matrix using voigt notations from local to global frame"); - m.def("rotate_g2l_stress", &rotate_g2l_stress, "input"_a, "psi"_a, "theta"_a, "phi"_a, "copy"_a = true, "Return the rotated stress matrix using voigt notations from global to local frame"); + m.def("rotate_stiffness_angle", &rotate_stiffness_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stiffness matrix according to an angle and an axis"); + m.def("rotate_stiffness_R", &rotate_stiffness_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stiffness matrix according to a rotation matrix"); + m.def("rotate_compliance_angle", &rotate_compliance_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 compliance matrix according to an angle and an axis"); + m.def("rotate_compliance_R", &rotate_compliance_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 compliance matrix according to a rotation matrix"); + m.def("rotate_strain_concentration_angle", &rotate_strain_concentration_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 strain concentration tensor according to an angle and an axis"); + m.def("rotate_strain_concentration_R", &rotate_strain_concentration_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 strain concentration tensor according to a rotation matrix"); + m.def("rotate_stress_concentration_angle", &rotate_stress_concentration_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stress concentration tensor according to an angle and an axis"); + m.def("rotate_stress_concentration_R", &rotate_stress_concentration_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stress concentration tensor according to a rotation matrix"); + m.def("rotate_strain_angle", &rotate_strain_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = false, "Return the rotated strain vector using Voigt notation according to an angle and an axis"); + m.def("rotate_strain_R", &rotate_strain_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = false, "Return the rotated strain vector using Voigt notation according to a rotation matrix"); + m.def("rotate_stress_angle", &rotate_stress_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = false, "Return the rotated stress vector using Voigt notation according to an angle and an axis"); + m.def("rotate_stress_R", &rotate_stress_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = false, "Return the rotated stress vector using Voigt notation according to a rotation matrix"); // Register the Rotation class register_rotation_class(m); From f9581650a06cbaabf88ab7dbec33156df0e9ef7a Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 15:09:52 +0100 Subject: [PATCH 09/31] Update CMakeLists.txt --- src/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6cd448d21..439b5125f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -127,7 +127,6 @@ target_sources(simcoon PRIVATE Simulation/Maths/num_solve.cpp Simulation/Maths/random.cpp Simulation/Maths/rotation.cpp - Simulation/Maths/rotation_class.cpp Simulation/Maths/solve.cpp Simulation/Maths/stats.cpp From 7c6eb963838f2f3195569341a1d9f78724242f8b Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 15:10:16 +0100 Subject: [PATCH 10/31] Add Rotation class and rename rotation APIs Introduce a unified Rotation class into rotation.hpp (merged from the removed rotation_class.hpp) providing quaternion-based factories, conversions, and apply methods for vectors, tensors and Voigt-notation quantities. Rename and refactor many legacy free functions (fillR / fillQS / fillQE / rotateL / rotateM / rotateA / rotateB / etc.) to clearer APIs (rotate_vec/rotate_mat, rotate_stress, rotate_strain, rotate_stiffness, rotate_compliance, rotate_strain_concentration, rotate_stress_concentration, etc.). Update the Python example to use sim.Rotation.from_axis_angle / from_euler and the new rotate_* function names. Remove the standalone rotation_class header and corresponding legacy sources to consolidate rotation functionality; this is a breaking API change that modernizes rotation handling and exposes richer functionality (quaternions, Euler, rotvec, Voigt helpers, slerp, composition, etc.). --- examples/continuum_mechanics/rotation.py | 55 +- include/simcoon/Simulation/Maths/rotation.hpp | 726 +++++--- .../Simulation/Maths/rotation_class.hpp | 442 ----- src/Simulation/Maths/rotation.cpp | 1625 +++++++++-------- src/Simulation/Maths/rotation_class.cpp | 666 ------- 5 files changed, 1367 insertions(+), 2147 deletions(-) delete mode 100644 include/simcoon/Simulation/Maths/rotation_class.hpp delete mode 100644 src/Simulation/Maths/rotation_class.cpp diff --git a/examples/continuum_mechanics/rotation.py b/examples/continuum_mechanics/rotation.py index 44a4c16ba..b6b90c9b2 100644 --- a/examples/continuum_mechanics/rotation.py +++ b/examples/continuum_mechanics/rotation.py @@ -26,11 +26,13 @@ # %% # 1. Rotate a vector using a rotation matrix # -# This example shows how to build a rotation matrix and rotate a vector. -# It uses :func:`simcoon.fillR_angle` and :func:`simcoon.rotate_vec_R`. +# This example shows how to build a rotation matrix using the Rotation class +# and rotate a vector. +# It uses :class:`simcoon.Rotation` and :func:`simcoon.rotate_vec_R`. -Rmat = sim.fillR_angle(angle, axis, active, copy) +rot = sim.Rotation.from_axis_angle(angle, axis) +Rmat = rot.as_matrix() v_rot1 = sim.rotate_vec_R(v, Rmat, copy) print("Rotated vector (using R):", v_rot1) print("Rotation matrix (using R):", Rmat) @@ -72,11 +74,11 @@ # %% # 5. Build a rotation matrix from Euler angles # -# This example shows how to create a rotation matrix from Euler angles. -# It uses :func:`simcoon.fillR_euler`. +# This example shows how to create a rotation from Euler angles using the Rotation class. psi, theta, phi = np.pi / 6, np.pi / 4, np.pi / 3 -R_euler = sim.fillR_euler(psi, theta, phi, active, "zxz", copy) +r_euler = sim.Rotation.from_euler(psi, theta, phi, "zxz") +R_euler = r_euler.as_matrix() print("Rotation matrix from Euler angles (zxz):\n", R_euler) @@ -116,12 +118,12 @@ # %% # 8. Rotate a stiffness matrix (L) # -# This example uses both :func:`simcoon.rotateL_angle` and -# :func:`simcoon.rotateL_R`. +# This example uses both :func:`simcoon.rotate_stiffness_angle` and +# :func:`simcoon.rotate_stiffness_R`. L6 = np.eye(6) -rotL1 = sim.rotateL_angle(L6, angle, axis, active, copy) -rotL2 = sim.rotateL_R(L6, Rmat, active, copy) +rotL1 = sim.rotate_stiffness_angle(L6, angle, axis, active, copy) +rotL2 = sim.rotate_stiffness_R(L6, Rmat, active, copy) print("Rotated L (angle):\n", rotL1) print("Rotated L (R):\n", rotL2) @@ -129,38 +131,38 @@ # %% # 9. Rotate a compliance matrix (M) # -# This example uses both :func:`simcoon.rotateM_angle` and -# :func:`simcoon.rotateM_R`. +# This example uses both :func:`simcoon.rotate_compliance_angle` and +# :func:`simcoon.rotate_compliance_R`. M6 = np.eye(6) -rotM1 = sim.rotateM_angle(M6, angle, axis, active, copy) -rotM2 = sim.rotateM_R(M6, Rmat, active, copy) +rotM1 = sim.rotate_compliance_angle(M6, angle, axis, active, copy) +rotM2 = sim.rotate_compliance_R(M6, Rmat, active, copy) print("Rotated M (angle):\n", rotM1) print("Rotated M (R):\n", rotM2) # %% -# 10. Rotate a strain concentration matrix (A) +# 10. Rotate a strain concentration tensor (A) # -# This example uses both :func:`simcoon.rotateA_angle` and -# :func:`simcoon.rotateA_R`. +# This example uses both :func:`simcoon.rotate_strain_concentration_angle` and +# :func:`simcoon.rotate_strain_concentration_R`. A6 = np.eye(6) -rotA1 = sim.rotateA_angle(A6, angle, axis, active, copy) -rotA2 = sim.rotateA_R(A6, Rmat, active, copy) +rotA1 = sim.rotate_strain_concentration_angle(A6, angle, axis, active, copy) +rotA2 = sim.rotate_strain_concentration_R(A6, Rmat, active, copy) print("Rotated A (angle):\n", rotA1) print("Rotated A (R):\n", rotA2) # %% -# 11. Rotate a stress concentration matrix (B) +# 11. Rotate a stress concentration tensor (B) # -# This example uses both :func:`simcoon.rotateB_angle` and -# :func:`simcoon.rotateB_R`. +# This example uses both :func:`simcoon.rotate_stress_concentration_angle` and +# :func:`simcoon.rotate_stress_concentration_R`. B6 = np.eye(6) -rotB1 = sim.rotateB_angle(B6, angle, axis, active, copy) -rotB2 = sim.rotateB_R(B6, Rmat, active, copy) +rotB1 = sim.rotate_stress_concentration_angle(B6, angle, axis, active, copy) +rotB2 = sim.rotate_stress_concentration_R(B6, Rmat, active, copy) print("Rotated B (angle):\n", rotB1) print("Rotated B (R):\n", rotB2) @@ -188,10 +190,9 @@ rot_matrix = np.array( [[np.cos(alpha), -np.sin(alpha), 0.0], [np.sin(alpha), np.cos(alpha), 0], [0, 0, 1]] ) -# print(R) -L_rotate = sim.rotateL_R(L, rot_matrix) -L_rotate_angle = sim.rotateL_angle(L, alpha, axis=3) +L_rotate = sim.rotate_stiffness_R(L, rot_matrix) +L_rotate_angle = sim.rotate_stiffness_angle(L, alpha, axis=3) print(np.array_str(L_rotate, suppress_small=True)) print(np.array_str(L_rotate_angle, suppress_small=True)) diff --git a/include/simcoon/Simulation/Maths/rotation.hpp b/include/simcoon/Simulation/Maths/rotation.hpp index 4acb00075..022745998 100755 --- a/include/simcoon/Simulation/Maths/rotation.hpp +++ b/include/simcoon/Simulation/Maths/rotation.hpp @@ -1,50 +1,466 @@ /* This file is part of simcoon. - + simcoon is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + simcoon 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 General Public License for more details. - + You should have received a copy of the GNU General Public License along with simcoon. If not, see . - + */ ///@file rotation.hpp -///@brief rotation of a Voigt tensor -///@version 1.0 +///@brief Rotation class and Voigt tensor rotation utilities. +///@version 2.0 #pragma once #include +#include namespace simcoon{ /** * @file rotation.hpp - * @brief Mathematical utility functions. + * @brief Rotation class and convenience free functions for rotating + * vectors, tensors, and Voigt-notation quantities. */ /** @addtogroup maths * @{ */ +// ========================================================================= +// Rotation Class +// ========================================================================= + +/** + * @class Rotation + * @brief A class representing 3D rotations using unit quaternions. + * + * Inspired by scipy.spatial.transform.Rotation, this class provides a unified + * interface for working with rotations in various representations (quaternion, + * rotation matrix, Euler angles, rotation vector) while maintaining high + * numerical precision and performance. + * + * Internal representation uses unit quaternions in scalar-last convention [qx, qy, qz, qw]. + * + * @details Example usage: + * @code + * // Create rotation from Euler angles + * Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); + * + * // Apply to a vector + * arma::vec::fixed<3> v = {1, 0, 0}; + * arma::vec::fixed<3> v_rot = r.apply(v); + * + * // Compose rotations + * Rotation r2 = Rotation::from_axis_angle(M_PI/4, 3); + * Rotation r_combined = r * r2; + * + * // Apply to Voigt stress tensor + * arma::vec::fixed<6> sigma = {100, 50, 0, 25, 0, 0}; + * arma::vec::fixed<6> sigma_rot = r.apply_stress(sigma); + * @endcode + */ +class Rotation { +private: + arma::vec::fixed<4> _quat; // [qx, qy, qz, qw] - scalar-last convention, stack allocated + + // Private constructor from quaternion (assumes already normalized) + explicit Rotation(const arma::vec::fixed<4>& quat); + +public: + /** + * @brief Default constructor creating identity rotation. + */ + Rotation(); + + /** + * @brief Copy constructor. + */ + Rotation(const Rotation& other) = default; + + /** + * @brief Move constructor. + */ + Rotation(Rotation&& other) noexcept = default; + + /** + * @brief Copy assignment operator. + */ + Rotation& operator=(const Rotation& other) = default; + + /** + * @brief Move assignment operator. + */ + Rotation& operator=(Rotation&& other) noexcept = default; + + /** + * @brief Destructor. + */ + ~Rotation() = default; + + // ========================================================================= + // Factory Methods + // ========================================================================= + + /** + * @brief Create an identity rotation. + * @return Identity rotation (no rotation) + */ + static Rotation identity(); + + /** + * @brief Create rotation from a unit quaternion. + * @param quat Quaternion in [qx, qy, qz, qw] format (scalar-last) + * @return Rotation object + * @note The quaternion will be normalized if not already unit length + */ + static Rotation from_quat(const arma::vec::fixed<4>& quat); + + /** + * @brief Create rotation from a unit quaternion. + * @param quat Quaternion in [qx, qy, qz, qw] format (scalar-last), dynamic size + * @return Rotation object + */ + static Rotation from_quat(const arma::vec& quat); + + /** + * @brief Create rotation from a 3x3 rotation matrix. + * @param R 3x3 rotation matrix (must be orthogonal with det=1) + * @return Rotation object + */ + static Rotation from_matrix(const arma::mat::fixed<3,3>& R); + + /** + * @brief Create rotation from a 3x3 rotation matrix. + * @param R 3x3 rotation matrix (dynamic size) + * @return Rotation object + */ + static Rotation from_matrix(const arma::mat& R); + + /** + * @brief Create rotation from Euler angles. + * @param psi First Euler angle in radians (or degrees if degrees=true) + * @param theta Second Euler angle in radians (or degrees if degrees=true) + * @param phi Third Euler angle in radians (or degrees if degrees=true) + * @param conv Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", + * "xyx", "xzx", "yxy", "yzy", or "user" + * @param intrinsic If true (default), intrinsic rotations (rotating frame); + * if false, extrinsic rotations (fixed frame) + * @param degrees If true, angles are in degrees; if false (default), radians + * @return Rotation object + */ + static Rotation from_euler(double psi, double theta, double phi, + const std::string& conv, + bool intrinsic = true, bool degrees = false); + + /** + * @brief Create rotation from Euler angles (vector input). + * @param angles 3-element vector of Euler angles + * @param conv Euler angle convention + * @param intrinsic If true, intrinsic rotations; if false, extrinsic rotations + * @param degrees If true, angles are in degrees + * @return Rotation object + */ + static Rotation from_euler(const arma::vec::fixed<3>& angles, + const std::string& conv, + bool intrinsic = true, bool degrees = false); + + /** + * @brief Create rotation from a rotation vector (axis-angle representation). + * @param rotvec Rotation vector where direction is axis and magnitude is angle + * @param degrees If true, magnitude is in degrees; if false (default), radians + * @return Rotation object + */ + static Rotation from_rotvec(const arma::vec::fixed<3>& rotvec, bool degrees = false); + + /** + * @brief Create rotation from a rotation vector (dynamic size). + * @param rotvec Rotation vector (3 elements) + * @param degrees If true, magnitude is in degrees + * @return Rotation object + */ + static Rotation from_rotvec(const arma::vec& rotvec, bool degrees = false); + + /** + * @brief Create rotation around a single axis. + * @param angle Rotation angle in radians (or degrees if degrees=true) + * @param axis Axis of rotation: 1=x, 2=y, 3=z + * @param degrees If true, angle is in degrees + * @return Rotation object + */ + static Rotation from_axis_angle(double angle, int axis, bool degrees = false); + + /** + * @brief Create a random rotation (uniformly distributed). + * @return Random rotation object + */ + static Rotation random(); + + // ========================================================================= + // Conversion Methods + // ========================================================================= + + /** + * @brief Get rotation as a unit quaternion. + * @return Quaternion in [qx, qy, qz, qw] format (scalar-last) + */ + inline arma::vec::fixed<4> as_quat() const { return _quat; } + + /** + * @brief Get rotation as a 3x3 rotation matrix. + * @return 3x3 orthogonal rotation matrix + */ + arma::mat::fixed<3,3> as_matrix() const; + + /** + * @brief Get rotation as Euler angles. + * @param conv Euler angle convention + * @param intrinsic If true, return intrinsic angles; if false, extrinsic + * @param degrees If true, return angles in degrees + * @return 3-element vector of Euler angles [psi, theta, phi] + */ + arma::vec::fixed<3> as_euler(const std::string& conv, + bool intrinsic = true, bool degrees = false) const; + + /** + * @brief Get rotation as a rotation vector. + * @param degrees If true, magnitude is in degrees + * @return Rotation vector (axis * angle) + */ + arma::vec::fixed<3> as_rotvec(bool degrees = false) const; + + /** + * @brief Get 6x6 rotation matrix for stress tensors in Voigt notation. + * @param active If true (default), active rotation; if false, passive + * @return 6x6 stress rotation matrix + */ + arma::mat::fixed<6,6> as_voigt_stress_rotation(bool active = true) const; + + /** + * @brief Get 6x6 rotation matrix for strain tensors in Voigt notation. + * @param active If true (default), active rotation; if false, passive + * @return 6x6 strain rotation matrix + */ + arma::mat::fixed<6,6> as_voigt_strain_rotation(bool active = true) const; + + // ========================================================================= + // Apply Methods (3D objects) + // ========================================================================= + + /** + * @brief Apply rotation to a 3D vector. + * @param v 3D vector to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated vector + */ + arma::vec::fixed<3> apply(const arma::vec::fixed<3>& v, bool inverse = false) const; + + /** + * @brief Apply rotation to a 3D vector (dynamic size). + * @param v 3D vector to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated vector + */ + arma::vec apply(const arma::vec& v, bool inverse = false) const; + + /** + * @brief Apply rotation to a 3x3 tensor (matrix). + * @param m 3x3 tensor to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated tensor: R * m * R^T (or R^T * m * R for inverse) + */ + arma::mat::fixed<3,3> apply_tensor(const arma::mat::fixed<3,3>& m, bool inverse = false) const; + + /** + * @brief Apply rotation to a 3x3 tensor (dynamic size). + * @param m 3x3 tensor to rotate + * @param inverse If true, apply inverse rotation + * @return Rotated tensor + */ + arma::mat apply_tensor(const arma::mat& m, bool inverse = false) const; + + // ========================================================================= + // Apply Methods (Voigt notation) + // ========================================================================= + + /** + * @brief Apply rotation to a stress vector in Voigt notation. + * @param sigma 6-component stress vector [s11, s22, s33, s12, s13, s23] + * @param active If true (default), active rotation + * @return Rotated stress vector + */ + arma::vec::fixed<6> apply_stress(const arma::vec::fixed<6>& sigma, bool active = true) const; + + /** + * @brief Apply rotation to a stress vector (dynamic size). + * @param sigma 6-component stress vector + * @param active If true, active rotation + * @return Rotated stress vector + */ + arma::vec apply_stress(const arma::vec& sigma, bool active = true) const; + + /** + * @brief Apply rotation to a strain vector in Voigt notation. + * @param epsilon 6-component strain vector [e11, e22, e33, 2*e12, 2*e13, 2*e23] + * @param active If true (default), active rotation + * @return Rotated strain vector + */ + arma::vec::fixed<6> apply_strain(const arma::vec::fixed<6>& epsilon, bool active = true) const; + + /** + * @brief Apply rotation to a strain vector (dynamic size). + * @param epsilon 6-component strain vector + * @param active If true, active rotation + * @return Rotated strain vector + */ + arma::vec apply_strain(const arma::vec& epsilon, bool active = true) const; + + /** + * @brief Apply rotation to a 6x6 stiffness matrix. + * @param L 6x6 stiffness matrix in Voigt notation + * @param active If true (default), active rotation + * @return Rotated stiffness matrix: voigt_stress * L * voigt_stress^T + */ + arma::mat::fixed<6,6> apply_stiffness(const arma::mat::fixed<6,6>& L, bool active = true) const; + + /** + * @brief Apply rotation to a stiffness matrix (dynamic size). + * @param L 6x6 stiffness matrix + * @param active If true, active rotation + * @return Rotated stiffness matrix + */ + arma::mat apply_stiffness(const arma::mat& L, bool active = true) const; + + /** + * @brief Apply rotation to a 6x6 compliance matrix. + * @param M 6x6 compliance matrix in Voigt notation + * @param active If true (default), active rotation + * @return Rotated compliance matrix: voigt_strain * M * voigt_strain^T + */ + arma::mat::fixed<6,6> apply_compliance(const arma::mat::fixed<6,6>& M, bool active = true) const; + + /** + * @brief Apply rotation to a compliance matrix (dynamic size). + * @param M 6x6 compliance matrix + * @param active If true, active rotation + * @return Rotated compliance matrix + */ + arma::mat apply_compliance(const arma::mat& M, bool active = true) const; + + /** + * @brief Apply rotation to a 6x6 strain concentration tensor. + * @param A 6x6 strain concentration tensor in Voigt notation + * @param active If true (default), active rotation + * @return Rotated strain concentration tensor: voigt_strain * A * voigt_stress^T + */ + arma::mat::fixed<6,6> apply_strain_concentration(const arma::mat::fixed<6,6>& A, bool active = true) const; + + /** + * @brief Apply rotation to a strain concentration tensor (dynamic size). + * @param A 6x6 strain concentration tensor + * @param active If true, active rotation + * @return Rotated strain concentration tensor + */ + arma::mat apply_strain_concentration(const arma::mat& A, bool active = true) const; + + /** + * @brief Apply rotation to a 6x6 stress concentration tensor. + * @param B 6x6 stress concentration tensor in Voigt notation + * @param active If true (default), active rotation + * @return Rotated stress concentration tensor: voigt_stress * B * voigt_strain^T + */ + arma::mat::fixed<6,6> apply_stress_concentration(const arma::mat::fixed<6,6>& B, bool active = true) const; + + /** + * @brief Apply rotation to a stress concentration tensor (dynamic size). + * @param B 6x6 stress concentration tensor + * @param active If true, active rotation + * @return Rotated stress concentration tensor + */ + arma::mat apply_stress_concentration(const arma::mat& B, bool active = true) const; + + // ========================================================================= + // Operations + // ========================================================================= + + /** + * @brief Get the inverse (conjugate) rotation. + * @return Inverse rotation + */ + Rotation inv() const; + + /** + * @brief Get the magnitude (rotation angle) of this rotation. + * @param degrees If true, return in degrees + * @return Rotation angle in radians (or degrees) + */ + double magnitude(bool degrees = false) const; + + /** + * @brief Compose this rotation with another (this * other). + * @param other Rotation to compose with + * @return Composed rotation + * @details The composition represents applying 'other' first, then 'this' + */ + Rotation operator*(const Rotation& other) const; + + /** + * @brief Compose this rotation with another in-place. + * @param other Rotation to compose with + * @return Reference to this rotation + */ + Rotation& operator*=(const Rotation& other); + + /** + * @brief Spherical linear interpolation between this rotation and another. + * @param other Target rotation + * @param t Interpolation parameter in [0, 1] (0 = this, 1 = other) + * @return Interpolated rotation + */ + Rotation slerp(const Rotation& other, double t) const; + + /** + * @brief Check if this rotation equals another within tolerance. + * @param other Rotation to compare with + * @param tol Tolerance for comparison + * @return True if rotations are equal (or antipodal quaternions) + */ + inline bool equals(const Rotation& other, double tol = 1e-12) const { + // Quaternions q and -q represent the same rotation + double diff1 = arma::norm(_quat - other._quat); + double diff2 = arma::norm(_quat + other._quat); + return (diff1 < tol) || (diff2 < tol); + } + + /** + * @brief Check if this rotation is identity (no rotation). + * @param tol Tolerance for comparison + * @return True if this is the identity rotation + */ + inline bool is_identity(double tol = 1e-12) const { + // Identity quaternion is [0, 0, 0, 1] or [0, 0, 0, -1] + return std::abs(std::abs(_quat(3)) - 1.0) < tol; + } +}; + +// ========================================================================= +// Convenience Free Functions +// ========================================================================= + /** * @brief Rotates a 3D vector using a rotation matrix. * @param v The input 3D vector (arma::vec) * @param R The 3x3 rotation matrix (arma::mat) * @return The rotated vector (arma::vec) - * @details Example: - * @code - * vec v = {1., 0., 0.}; - * mat R = fillR(M_PI/4., 3); - * vec v_rot = rotate_vec(v, R); - * @endcode */ arma::vec rotate_vec(const arma::vec &v, const arma::mat &R); @@ -54,11 +470,6 @@ arma::vec rotate_vec(const arma::vec &v, const arma::mat &R); * @param angle The rotation angle in radians (double) * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) * @return The rotated vector (arma::vec) - * @details Example: - * @code - * vec v = {1., 0., 0.}; - * vec v_rot = rotate_vec(v, M_PI/4., 3); // Rotate 45° around z-axis - * @endcode */ arma::vec rotate_vec(const arma::vec &v, const double &angle, const int &axis); @@ -67,12 +478,6 @@ arma::vec rotate_vec(const arma::vec &v, const double &angle, const int &axis); * @param m The input 3x3 matrix (arma::mat) * @param R The 3x3 rotation matrix (arma::mat) * @return The rotated matrix (arma::mat) - * @details The rotation is performed as \f$ \mathbf{m}' = \mathbf{R} \cdot \mathbf{m} \cdot \mathbf{R}^T \f$ - * @code - * mat m = eye(3,3); - * mat R = fillR(M_PI/4., 3); - * mat m_rot = rotate_mat(m, R); - * @endcode */ arma::mat rotate_mat(const arma::mat &m, const arma::mat &R); @@ -86,82 +491,52 @@ arma::mat rotate_mat(const arma::mat &m, const arma::mat &R); arma::mat rotate_mat(const arma::mat &m, const double &angle, const int &axis); /** - * @brief Generates a 3x3 rotation matrix for rotation around a single axis. - * @param angle The rotation angle in radians (double) - * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) - * @param active If true (default), active (alibi) rotation; if false, passive (alias) rotation (bool) - * @return The 3x3 rotation matrix (arma::mat) - * @details Example: - * @code - * mat R = fillR(M_PI/4., 3); // 45° rotation around z-axis - * @endcode - */ -arma::mat fillR(const double &angle, const int &axis, const bool &active = true); - -/** - * @brief Generates a 3x3 rotation matrix using Euler angles. - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @param active If true, active (alibi) rotation; if false, passive (alias) rotation (bool) - * @param conv The Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", - * "xyx", "xzx", "yxy", "yzy", or "user" (std::string) - * @return The 3x3 rotation matrix (arma::mat) - * @details Example: - * @code - * mat R = fillR(M_PI/4., M_PI/6., M_PI/3., true, "zxz"); - * @endcode - */ -arma::mat fillR(const double &psi, const double &theta, const double &phi, const bool &active, const std::string &conv); - -/** - * @brief Generates a 6x6 rotation matrix for stress tensors in Voigt notation. + * @brief Rotates a stress vector in Voigt notation. + * @param sigma The 6-component stress vector (arma::vec) * @param angle The rotation angle in radians (double) * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) * @param active If true (default), active rotation (bool) - * @return The 6x6 rotation matrix for stress (arma::mat) + * @return The rotated stress vector (arma::vec) */ -arma::mat fillQS(const double &angle, const int &axis, const bool &active = true); +arma::vec rotate_stress(const arma::vec &sigma, const double &angle, const int &axis, const bool &active = true); /** - * @brief Generates a 6x6 rotation matrix for stress tensors from a 3x3 rotation matrix. + * @brief Rotates a stress vector using a 3x3 rotation matrix. + * @param sigma The 6-component stress vector (arma::vec) * @param R The 3x3 rotation matrix (arma::mat) * @param active If true (default), active rotation (bool) - * @return The 6x6 rotation matrix for stress (arma::mat) + * @return The rotated stress vector (arma::vec) */ -arma::mat fillQS(const arma::mat &R, const bool &active = true); - +arma::vec rotate_stress(const arma::vec &sigma, const arma::mat &R, const bool &active = true); + /** - * @brief Generates a 6x6 rotation matrix for strain tensors in Voigt notation. + * @brief Rotates a strain vector in Voigt notation. + * @param epsilon The 6-component strain vector (arma::vec) * @param angle The rotation angle in radians (double) * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) * @param active If true (default), active rotation (bool) - * @return The 6x6 rotation matrix for strain (arma::mat) + * @return The rotated strain vector (arma::vec) */ -arma::mat fillQE(const double &angle, const int &axis, const bool &active = true); +arma::vec rotate_strain(const arma::vec &epsilon, const double &angle, const int &axis, const bool &active = true); /** - * @brief Generates a 6x6 rotation matrix for strain tensors from a 3x3 rotation matrix. + * @brief Rotates a strain vector using a 3x3 rotation matrix. + * @param epsilon The 6-component strain vector (arma::vec) * @param R The 3x3 rotation matrix (arma::mat) * @param active If true (default), active rotation (bool) - * @return The 6x6 rotation matrix for strain (arma::mat) + * @return The rotated strain vector (arma::vec) */ -arma::mat fillQE(const arma::mat &R, const bool &active = true); +arma::vec rotate_strain(const arma::vec &epsilon, const arma::mat &R, const bool &active = true); /** - * @brief Rotates a 6x6 stiffness matrix. + * @brief Rotates a 6x6 stiffness matrix by an angle around a given axis. * @param L The 6x6 stiffness matrix in Voigt notation (arma::mat) * @param angle The rotation angle in radians (double) * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) * @param active If true (default), active rotation (bool) * @return The rotated 6x6 stiffness matrix (arma::mat) - * @details Example: - * @code - * mat L = L_iso(E, nu, "EnuG"); - * mat L_rot = rotateL(L, M_PI/4., 3); - * @endcode */ -arma::mat rotateL(const arma::mat &L, const double &angle, const int &axis, const bool &active = true); +arma::mat rotate_stiffness(const arma::mat &L, const double &angle, const int &axis, const bool &active = true); /** * @brief Rotates a 6x6 stiffness matrix using a 3x3 rotation matrix. @@ -170,17 +545,17 @@ arma::mat rotateL(const arma::mat &L, const double &angle, const int &axis, cons * @param active If true (default), active rotation (bool) * @return The rotated 6x6 stiffness matrix (arma::mat) */ -arma::mat rotateL(const arma::mat &L, const arma::mat &R, const bool &active = true); +arma::mat rotate_stiffness(const arma::mat &L, const arma::mat &R, const bool &active = true); /** - * @brief Rotates a 6x6 compliance matrix. + * @brief Rotates a 6x6 compliance matrix by an angle around a given axis. * @param M The 6x6 compliance matrix in Voigt notation (arma::mat) * @param angle The rotation angle in radians (double) * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) * @param active If true (default), active rotation (bool) * @return The rotated 6x6 compliance matrix (arma::mat) */ -arma::mat rotateM(const arma::mat &M, const double &angle, const int &axis, const bool &active = true); +arma::mat rotate_compliance(const arma::mat &M, const double &angle, const int &axis, const bool &active = true); /** * @brief Rotates a 6x6 compliance matrix using a 3x3 rotation matrix. @@ -189,210 +564,45 @@ arma::mat rotateM(const arma::mat &M, const double &angle, const int &axis, cons * @param active If true (default), active rotation (bool) * @return The rotated 6x6 compliance matrix (arma::mat) */ -arma::mat rotateM(const arma::mat &M, const arma::mat &R, const bool &active = true); - -/** - * @brief Rotates a 6x6 strain localization matrix A. - * @param A The 6x6 strain localization matrix (arma::mat) - * @param angle The rotation angle in radians (double) - * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) - * @param active If true (default), active rotation (bool) - * @return The rotated 6x6 strain localization matrix (arma::mat) - */ -arma::mat rotateA(const arma::mat &A, const double &angle, const int &axis, const bool &active = true); - -/** - * @brief Rotates a 6x6 strain localization matrix A using a 3x3 rotation matrix. - * @param A The 6x6 strain localization matrix (arma::mat) - * @param R The 3x3 rotation matrix (arma::mat) - * @param active If true (default), active rotation (bool) - * @return The rotated 6x6 strain localization matrix (arma::mat) - */ -arma::mat rotateA(const arma::mat &A, const arma::mat &R, const bool &active = true); +arma::mat rotate_compliance(const arma::mat &M, const arma::mat &R, const bool &active = true); /** - * @brief Rotates a 6x6 stress localization matrix B. - * @param B The 6x6 stress localization matrix (arma::mat) + * @brief Rotates a 6x6 strain concentration matrix by an angle around a given axis. + * @param A The 6x6 strain concentration matrix (arma::mat) * @param angle The rotation angle in radians (double) * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) * @param active If true (default), active rotation (bool) - * @return The rotated 6x6 stress localization matrix (arma::mat) + * @return The rotated 6x6 strain concentration matrix (arma::mat) */ -arma::mat rotateB(const arma::mat &B, const double &angle, const int &axis, const bool &active = true); +arma::mat rotate_strain_concentration(const arma::mat &A, const double &angle, const int &axis, const bool &active = true); /** - * @brief Rotates a 6x6 stress localization matrix B using a 3x3 rotation matrix. - * @param B The 6x6 stress localization matrix (arma::mat) + * @brief Rotates a 6x6 strain concentration matrix using a 3x3 rotation matrix. + * @param A The 6x6 strain concentration matrix (arma::mat) * @param R The 3x3 rotation matrix (arma::mat) * @param active If true (default), active rotation (bool) - * @return The rotated 6x6 stress localization matrix (arma::mat) - */ -arma::mat rotateB(const arma::mat &B, const arma::mat &R, const bool &active = true); - -/** - * @brief Rotates a stress vector in Voigt notation. - * @param sigma The 6-component stress vector (arma::vec) - * @param angle The rotation angle in radians (double) - * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) - * @param active If true (default), active rotation (bool) - * @return The rotated stress vector (arma::vec) - * @details Example: - * @code - * vec sigma = {100., 50., 0., 25., 0., 0.}; - * vec sigma_rot = rotate_stress(sigma, M_PI/4., 3); - * @endcode + * @return The rotated 6x6 strain concentration matrix (arma::mat) */ -arma::vec rotate_stress(const arma::vec &sigma, const double &angle, const int &axis, const bool &active = true); +arma::mat rotate_strain_concentration(const arma::mat &A, const arma::mat &R, const bool &active = true); /** - * @brief Rotates a stress vector using a 3x3 rotation matrix. - * @param sigma The 6-component stress vector (arma::vec) - * @param R The 3x3 rotation matrix (arma::mat) - * @param active If true (default), active rotation (bool) - * @return The rotated stress vector (arma::vec) - */ -arma::vec rotate_stress(const arma::vec &sigma, const arma::mat &R, const bool &active = true); - -/** - * @brief Rotates a strain vector in Voigt notation. - * @param epsilon The 6-component strain vector (arma::vec) + * @brief Rotates a 6x6 stress concentration matrix by an angle around a given axis. + * @param B The 6x6 stress concentration matrix (arma::mat) * @param angle The rotation angle in radians (double) * @param axis The axis of rotation: 1=x, 2=y, 3=z (int) * @param active If true (default), active rotation (bool) - * @return The rotated strain vector (arma::vec) + * @return The rotated 6x6 stress concentration matrix (arma::mat) */ -arma::vec rotate_strain(const arma::vec &epsilon, const double &angle, const int &axis, const bool &active = true); +arma::mat rotate_stress_concentration(const arma::mat &B, const double &angle, const int &axis, const bool &active = true); /** - * @brief Rotates a strain vector using a 3x3 rotation matrix. - * @param epsilon The 6-component strain vector (arma::vec) + * @brief Rotates a 6x6 stress concentration matrix using a 3x3 rotation matrix. + * @param B The 6x6 stress concentration matrix (arma::mat) * @param R The 3x3 rotation matrix (arma::mat) * @param active If true (default), active rotation (bool) - * @return The rotated strain vector (arma::vec) - */ -arma::vec rotate_strain(const arma::vec &epsilon, const arma::mat &R, const bool &active = true); - -/** - * @brief Rotates a strain tensor from local to global coordinates using Euler angles. - * @param E The 6-component strain vector in local coordinates (arma::vec) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 3x3 strain tensor in global coordinates (arma::mat) - */ -arma::mat rotate_l2g_strain(const arma::vec &E, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a strain tensor from global to local coordinates using Euler angles. - * @param E The 6-component strain vector in global coordinates (arma::vec) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 3x3 strain tensor in local coordinates (arma::mat) - */ -arma::mat rotate_g2l_strain(const arma::vec &E, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a stress tensor from local to global coordinates using Euler angles. - * @param sigma The 6-component stress vector in local coordinates (arma::vec) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 3x3 stress tensor in global coordinates (arma::mat) - */ -arma::mat rotate_l2g_stress(const arma::vec &sigma, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a stress tensor from global to local coordinates using Euler angles. - * @param sigma The 6-component stress vector in global coordinates (arma::vec) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 3x3 stress tensor in local coordinates (arma::mat) - */ -arma::mat rotate_g2l_stress(const arma::vec &sigma, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a stiffness matrix from local to global coordinates using Euler angles. - * @param L The 6x6 stiffness matrix in local coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 stiffness matrix in global coordinates (arma::mat) - */ -arma::mat rotate_l2g_L(const arma::mat &L, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a stiffness matrix from global to local coordinates using Euler angles. - * @param L The 6x6 stiffness matrix in global coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 stiffness matrix in local coordinates (arma::mat) - */ -arma::mat rotate_g2l_L(const arma::mat &L, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a strain localization matrix from local to global coordinates using Euler angles. - * @param A The 6x6 strain localization matrix in local coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 strain localization matrix in global coordinates (arma::mat) - */ -arma::mat rotate_l2g_A(const arma::mat &A, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a strain localization matrix from global to local coordinates using Euler angles. - * @param A The 6x6 strain localization matrix in global coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 strain localization matrix in local coordinates (arma::mat) - */ -arma::mat rotate_g2l_A(const arma::mat &A, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a stress localization matrix from local to global coordinates using Euler angles. - * @param B The 6x6 stress localization matrix in local coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 stress localization matrix in global coordinates (arma::mat) - */ -arma::mat rotate_l2g_B(const arma::mat &B, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a stress localization matrix from global to local coordinates using Euler angles. - * @param B The 6x6 stress localization matrix in global coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 stress localization matrix in local coordinates (arma::mat) - */ -arma::mat rotate_g2l_B(const arma::mat &B, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a compliance matrix from local to global coordinates using Euler angles. - * @param M The 6x6 compliance matrix in local coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 compliance matrix in global coordinates (arma::mat) - */ -arma::mat rotate_l2g_M(const arma::mat &M, const double &psi, const double &theta, const double &phi); - -/** - * @brief Rotates a compliance matrix from global to local coordinates using Euler angles. - * @param M The 6x6 compliance matrix in global coordinates (arma::mat) - * @param psi First Euler angle in radians (double) - * @param theta Second Euler angle in radians (double) - * @param phi Third Euler angle in radians (double) - * @return The 6x6 compliance matrix in local coordinates (arma::mat) + * @return The rotated 6x6 stress concentration matrix (arma::mat) */ -arma::mat rotate_g2l_M(const arma::mat &M, const double &psi, const double &theta, const double &phi); - - +arma::mat rotate_stress_concentration(const arma::mat &B, const arma::mat &R, const bool &active = true); /** @} */ // end of maths group diff --git a/include/simcoon/Simulation/Maths/rotation_class.hpp b/include/simcoon/Simulation/Maths/rotation_class.hpp deleted file mode 100644 index ac40b5ac0..000000000 --- a/include/simcoon/Simulation/Maths/rotation_class.hpp +++ /dev/null @@ -1,442 +0,0 @@ -/* This file is part of simcoon. - - simcoon is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - simcoon 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with simcoon. If not, see . - - */ - -///@file rotation_class.hpp -///@brief Rotation class inspired by scipy.spatial.transform.Rotation -///@version 1.0 - -#pragma once - -#include -#include - -namespace simcoon { - -/** - * @class Rotation - * @brief A class representing 3D rotations using unit quaternions. - * - * Inspired by scipy.spatial.transform.Rotation, this class provides a unified - * interface for working with rotations in various representations (quaternion, - * rotation matrix, Euler angles, rotation vector) while maintaining high - * numerical precision and performance. - * - * Internal representation uses unit quaternions in scalar-last convention [qx, qy, qz, qw]. - * - * @details Example usage: - * @code - * // Create rotation from Euler angles - * Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); - * - * // Apply to a vector - * arma::vec::fixed<3> v = {1, 0, 0}; - * arma::vec::fixed<3> v_rot = r.apply(v); - * - * // Compose rotations - * Rotation r2 = Rotation::from_axis_angle(M_PI/4, 3); - * Rotation r_combined = r * r2; - * - * // Apply to Voigt stress tensor - * arma::vec::fixed<6> sigma = {100, 50, 0, 25, 0, 0}; - * arma::vec::fixed<6> sigma_rot = r.apply_stress(sigma); - * @endcode - */ -class Rotation { -private: - arma::vec::fixed<4> _quat; // [qx, qy, qz, qw] - scalar-last convention, stack allocated - - // Private constructor from quaternion (assumes already normalized) - explicit Rotation(const arma::vec::fixed<4>& quat); - -public: - /** - * @brief Default constructor creating identity rotation. - */ - Rotation(); - - /** - * @brief Copy constructor. - */ - Rotation(const Rotation& other) = default; - - /** - * @brief Move constructor. - */ - Rotation(Rotation&& other) noexcept = default; - - /** - * @brief Copy assignment operator. - */ - Rotation& operator=(const Rotation& other) = default; - - /** - * @brief Move assignment operator. - */ - Rotation& operator=(Rotation&& other) noexcept = default; - - /** - * @brief Destructor. - */ - ~Rotation() = default; - - // ========================================================================= - // Factory Methods - // ========================================================================= - - /** - * @brief Create an identity rotation. - * @return Identity rotation (no rotation) - */ - static Rotation identity(); - - /** - * @brief Create rotation from a unit quaternion. - * @param quat Quaternion in [qx, qy, qz, qw] format (scalar-last) - * @return Rotation object - * @note The quaternion will be normalized if not already unit length - */ - static Rotation from_quat(const arma::vec::fixed<4>& quat); - - /** - * @brief Create rotation from a unit quaternion. - * @param quat Quaternion in [qx, qy, qz, qw] format (scalar-last), dynamic size - * @return Rotation object - */ - static Rotation from_quat(const arma::vec& quat); - - /** - * @brief Create rotation from a 3x3 rotation matrix. - * @param R 3x3 rotation matrix (must be orthogonal with det=1) - * @return Rotation object - */ - static Rotation from_matrix(const arma::mat::fixed<3,3>& R); - - /** - * @brief Create rotation from a 3x3 rotation matrix. - * @param R 3x3 rotation matrix (dynamic size) - * @return Rotation object - */ - static Rotation from_matrix(const arma::mat& R); - - /** - * @brief Create rotation from Euler angles. - * @param psi First Euler angle in radians (or degrees if degrees=true) - * @param theta Second Euler angle in radians (or degrees if degrees=true) - * @param phi Third Euler angle in radians (or degrees if degrees=true) - * @param conv Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", - * "xyx", "xzx", "yxy", "yzy", or "user" - * @param intrinsic If true (default), intrinsic rotations (rotating frame); - * if false, extrinsic rotations (fixed frame) - * @param degrees If true, angles are in degrees; if false (default), radians - * @return Rotation object - */ - static Rotation from_euler(double psi, double theta, double phi, - const std::string& conv, - bool intrinsic = true, bool degrees = false); - - /** - * @brief Create rotation from Euler angles (vector input). - * @param angles 3-element vector of Euler angles - * @param conv Euler angle convention - * @param intrinsic If true, intrinsic rotations; if false, extrinsic rotations - * @param degrees If true, angles are in degrees - * @return Rotation object - */ - static Rotation from_euler(const arma::vec::fixed<3>& angles, - const std::string& conv, - bool intrinsic = true, bool degrees = false); - - /** - * @brief Create rotation from a rotation vector (axis-angle representation). - * @param rotvec Rotation vector where direction is axis and magnitude is angle - * @param degrees If true, magnitude is in degrees; if false (default), radians - * @return Rotation object - */ - static Rotation from_rotvec(const arma::vec::fixed<3>& rotvec, bool degrees = false); - - /** - * @brief Create rotation from a rotation vector (dynamic size). - * @param rotvec Rotation vector (3 elements) - * @param degrees If true, magnitude is in degrees - * @return Rotation object - */ - static Rotation from_rotvec(const arma::vec& rotvec, bool degrees = false); - - /** - * @brief Create rotation around a single axis. - * @param angle Rotation angle in radians (or degrees if degrees=true) - * @param axis Axis of rotation: 1=x, 2=y, 3=z - * @param degrees If true, angle is in degrees - * @return Rotation object - */ - static Rotation from_axis_angle(double angle, int axis, bool degrees = false); - - /** - * @brief Create a random rotation (uniformly distributed). - * @return Random rotation object - */ - static Rotation random(); - - // ========================================================================= - // Conversion Methods - // ========================================================================= - - /** - * @brief Get rotation as a unit quaternion. - * @return Quaternion in [qx, qy, qz, qw] format (scalar-last) - */ - inline arma::vec::fixed<4> as_quat() const { return _quat; } - - /** - * @brief Get rotation as a 3x3 rotation matrix. - * @return 3x3 orthogonal rotation matrix - */ - arma::mat::fixed<3,3> as_matrix() const; - - /** - * @brief Get rotation as Euler angles. - * @param conv Euler angle convention - * @param intrinsic If true, return intrinsic angles; if false, extrinsic - * @param degrees If true, return angles in degrees - * @return 3-element vector of Euler angles [psi, theta, phi] - */ - arma::vec::fixed<3> as_euler(const std::string& conv, - bool intrinsic = true, bool degrees = false) const; - - /** - * @brief Get rotation as a rotation vector. - * @param degrees If true, magnitude is in degrees - * @return Rotation vector (axis * angle) - */ - arma::vec::fixed<3> as_rotvec(bool degrees = false) const; - - /** - * @brief Get 6x6 rotation matrix for stress tensors in Voigt notation. - * @param active If true (default), active rotation; if false, passive - * @return 6x6 stress rotation matrix (QS) - */ - arma::mat::fixed<6,6> as_voigt_stress_rotation(bool active = true) const; - - /** - * @brief Get 6x6 rotation matrix for strain tensors in Voigt notation. - * @param active If true (default), active rotation; if false, passive - * @return 6x6 strain rotation matrix (QE) - */ - arma::mat::fixed<6,6> as_voigt_strain_rotation(bool active = true) const; - - // ========================================================================= - // Apply Methods (3D objects) - // ========================================================================= - - /** - * @brief Apply rotation to a 3D vector. - * @param v 3D vector to rotate - * @param inverse If true, apply inverse rotation - * @return Rotated vector - * @details Uses direct quaternion-vector rotation (faster than matrix multiplication) - */ - arma::vec::fixed<3> apply(const arma::vec::fixed<3>& v, bool inverse = false) const; - - /** - * @brief Apply rotation to a 3D vector (dynamic size). - * @param v 3D vector to rotate - * @param inverse If true, apply inverse rotation - * @return Rotated vector - */ - arma::vec apply(const arma::vec& v, bool inverse = false) const; - - /** - * @brief Apply rotation to a 3x3 tensor (matrix). - * @param m 3x3 tensor to rotate - * @param inverse If true, apply inverse rotation - * @return Rotated tensor: R * m * R^T (or R^T * m * R for inverse) - */ - arma::mat::fixed<3,3> apply_tensor(const arma::mat::fixed<3,3>& m, bool inverse = false) const; - - /** - * @brief Apply rotation to a 3x3 tensor (dynamic size). - * @param m 3x3 tensor to rotate - * @param inverse If true, apply inverse rotation - * @return Rotated tensor - */ - arma::mat apply_tensor(const arma::mat& m, bool inverse = false) const; - - // ========================================================================= - // Apply Methods (Voigt notation) - // ========================================================================= - - /** - * @brief Apply rotation to a stress vector in Voigt notation. - * @param sigma 6-component stress vector [s11, s22, s33, s12, s13, s23] - * @param active If true (default), active rotation - * @return Rotated stress vector - */ - arma::vec::fixed<6> apply_stress(const arma::vec::fixed<6>& sigma, bool active = true) const; - - /** - * @brief Apply rotation to a stress vector (dynamic size). - * @param sigma 6-component stress vector - * @param active If true, active rotation - * @return Rotated stress vector - */ - arma::vec apply_stress(const arma::vec& sigma, bool active = true) const; - - /** - * @brief Apply rotation to a strain vector in Voigt notation. - * @param epsilon 6-component strain vector [e11, e22, e33, 2*e12, 2*e13, 2*e23] - * @param active If true (default), active rotation - * @return Rotated strain vector - */ - arma::vec::fixed<6> apply_strain(const arma::vec::fixed<6>& epsilon, bool active = true) const; - - /** - * @brief Apply rotation to a strain vector (dynamic size). - * @param epsilon 6-component strain vector - * @param active If true, active rotation - * @return Rotated strain vector - */ - arma::vec apply_strain(const arma::vec& epsilon, bool active = true) const; - - /** - * @brief Apply rotation to a 6x6 stiffness matrix. - * @param L 6x6 stiffness matrix in Voigt notation - * @param active If true (default), active rotation - * @return Rotated stiffness matrix: QS * L * QS^T - */ - arma::mat::fixed<6,6> apply_stiffness(const arma::mat::fixed<6,6>& L, bool active = true) const; - - /** - * @brief Apply rotation to a stiffness matrix (dynamic size). - * @param L 6x6 stiffness matrix - * @param active If true, active rotation - * @return Rotated stiffness matrix - */ - arma::mat apply_stiffness(const arma::mat& L, bool active = true) const; - - /** - * @brief Apply rotation to a 6x6 compliance matrix. - * @param M 6x6 compliance matrix in Voigt notation - * @param active If true (default), active rotation - * @return Rotated compliance matrix: QE * M * QE^T - */ - arma::mat::fixed<6,6> apply_compliance(const arma::mat::fixed<6,6>& M, bool active = true) const; - - /** - * @brief Apply rotation to a compliance matrix (dynamic size). - * @param M 6x6 compliance matrix - * @param active If true, active rotation - * @return Rotated compliance matrix - */ - arma::mat apply_compliance(const arma::mat& M, bool active = true) const; - - /** - * @brief Apply rotation to a 6x6 strain localization tensor. - * @param A 6x6 strain localization tensor in Voigt notation - * @param active If true (default), active rotation - * @return Rotated strain localization tensor: QE * A * QS^T - */ - arma::mat::fixed<6,6> apply_localization_strain(const arma::mat::fixed<6,6>& A, bool active = true) const; - - /** - * @brief Apply rotation to a strain localization tensor (dynamic size). - * @param A 6x6 strain localization tensor - * @param active If true, active rotation - * @return Rotated strain localization tensor - */ - arma::mat apply_localization_strain(const arma::mat& A, bool active = true) const; - - /** - * @brief Apply rotation to a 6x6 stress localization tensor. - * @param B 6x6 stress localization tensor in Voigt notation - * @param active If true (default), active rotation - * @return Rotated stress localization tensor: QS * B * QE^T - */ - arma::mat::fixed<6,6> apply_localization_stress(const arma::mat::fixed<6,6>& B, bool active = true) const; - - /** - * @brief Apply rotation to a stress localization tensor (dynamic size). - * @param B 6x6 stress localization tensor - * @param active If true, active rotation - * @return Rotated stress localization tensor - */ - arma::mat apply_localization_stress(const arma::mat& B, bool active = true) const; - - // ========================================================================= - // Operations - // ========================================================================= - - /** - * @brief Get the inverse (conjugate) rotation. - * @return Inverse rotation - */ - Rotation inv() const; - - /** - * @brief Get the magnitude (rotation angle) of this rotation. - * @param degrees If true, return in degrees - * @return Rotation angle in radians (or degrees) - */ - double magnitude(bool degrees = false) const; - - /** - * @brief Compose this rotation with another (this * other). - * @param other Rotation to compose with - * @return Composed rotation - * @details The composition represents applying 'other' first, then 'this' - */ - Rotation operator*(const Rotation& other) const; - - /** - * @brief Compose this rotation with another in-place. - * @param other Rotation to compose with - * @return Reference to this rotation - */ - Rotation& operator*=(const Rotation& other); - - /** - * @brief Spherical linear interpolation between this rotation and another. - * @param other Target rotation - * @param t Interpolation parameter in [0, 1] (0 = this, 1 = other) - * @return Interpolated rotation - */ - Rotation slerp(const Rotation& other, double t) const; - - /** - * @brief Check if this rotation equals another within tolerance. - * @param other Rotation to compare with - * @param tol Tolerance for comparison - * @return True if rotations are equal (or antipodal quaternions) - */ - inline bool equals(const Rotation& other, double tol = 1e-12) const { - // Quaternions q and -q represent the same rotation - double diff1 = arma::norm(_quat - other._quat); - double diff2 = arma::norm(_quat + other._quat); - return (diff1 < tol) || (diff2 < tol); - } - - /** - * @brief Check if this rotation is identity (no rotation). - * @param tol Tolerance for comparison - * @return True if this is the identity rotation - */ - inline bool is_identity(double tol = 1e-12) const { - // Identity quaternion is [0, 0, 0, 1] or [0, 0, 0, -1] - return std::abs(std::abs(_quat(3)) - 1.0) < tol; - } -}; - -} // namespace simcoon diff --git a/src/Simulation/Maths/rotation.cpp b/src/Simulation/Maths/rotation.cpp index 39fd85adf..c349efed0 100755 --- a/src/Simulation/Maths/rotation.cpp +++ b/src/Simulation/Maths/rotation.cpp @@ -1,25 +1,26 @@ /* This file is part of simcoon. - + simcoon is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + simcoon 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 General Public License for more details. - + You should have received a copy of the GNU General Public License along with simcoon. If not, see . */ ///@file rotation.cpp -///@brief rotation of a Voigt tensor -///@version 1.0 +///@brief Rotation class and Voigt tensor rotation utilities. +///@version 2.0 -#include +#include #include +#include #include #include #include @@ -30,37 +31,77 @@ using namespace arma; namespace simcoon{ -vec rotate_vec(const vec &v, const mat &DR) { - return DR*v; +// ============================================================================= +// Anonymous namespace: internal helpers +// ============================================================================= + +namespace { + +// Convert axis number (1,2,3) to axis index (0,1,2) +inline int axis_to_index(int axis) { + if (axis < 1 || axis > 3) { + throw invalid_argument("Axis must be 1 (x), 2 (y), or 3 (z)"); + } + return axis - 1; } - -vec rotate_vec(const vec &v, const double &alpha, const int &axis) { - assert(axis>0); - mat DR = fillR(alpha, axis); - return rotate_vec(v, DR); + +// Parse Euler convention string to get axis sequence +// Returns axes as 0=x, 1=y, 2=z +void parse_euler_convention(const string& conv, int& axis1, int& axis2, int& axis3) { + if (conv.length() != 3) { + throw invalid_argument("Euler convention must be 3 characters (e.g., 'zxz', 'xyz')"); + } + + auto char_to_axis = [](char c) -> int { + switch (c) { + case 'x': case 'X': return 0; + case 'y': case 'Y': return 1; + case 'z': case 'Z': return 2; + default: + throw invalid_argument("Invalid axis character in Euler convention"); + } + }; + + axis1 = char_to_axis(conv[0]); + axis2 = char_to_axis(conv[1]); + axis3 = char_to_axis(conv[2]); } -mat rotate_mat(const mat &m, const mat &DR) { - return trans(DR)*m*DR; +// Normalize a quaternion to unit length +vec::fixed<4> normalize_quat(const vec::fixed<4>& q) { + double n = norm(q); + if (n < simcoon::iota) { + return {0.0, 0.0, 0.0, 1.0}; + } + return q / n; } -mat rotate_mat(const mat &m, const double &alpha, const int &axis) { - mat DR = fillR(alpha, axis); - return rotate_mat(m, DR); -} - -mat fillR(const double &alpha, const int &axis, const bool &active) { - - double act = 0; - if (active) - act = 1.; - else - act = -1.; - +// Hamilton product of two quaternions (scalar-last convention) +vec::fixed<4> quat_multiply(const vec::fixed<4>& q1, const vec::fixed<4>& q2) { + double x1 = q1(0), y1 = q1(1), z1 = q1(2), w1 = q1(3); + double x2 = q2(0), y2 = q2(1), z2 = q2(2), w2 = q2(3); + + return { + w1*x2 + x1*w2 + y1*z2 - z1*y2, + w1*y2 - x1*z2 + y1*w2 + z1*x2, + w1*z2 + x1*y2 - y1*x2 + z1*w2, + w1*w2 - x1*x2 - y1*y2 - z1*z2 + }; +} + +// Quaternion conjugate (inverse for unit quaternion) +vec::fixed<4> quat_conjugate(const vec::fixed<4>& q) { + return {-q(0), -q(1), -q(2), q(3)}; +} + +// Internal: Generate a 3x3 rotation matrix for rotation around a single axis +mat fillR_internal(const double &alpha, const int &axis, const bool &active) { + double act = active ? 1. : -1.; + mat R = zeros(3,3); double c = cos(alpha); double s = act*sin(alpha); - + switch(axis) { case 1: { R = { {1,0,0}, {0,c,-s}, {0,s,c} }; @@ -75,16 +116,16 @@ mat fillR(const double &alpha, const int &axis, const bool &active) { break; } default: { - cout << "Please choose the axis 1,2 or 3\n"; + throw invalid_argument("Please choose the axis 1, 2 or 3"); } } return R; } -mat fillR(const double &psi, const double &theta, const double &phi, const bool &active, const std::string &conv) { - +// Internal: Generate a 3x3 rotation matrix using Euler angles +mat fillR_euler_internal(const double &psi, const double &theta, const double &phi, const bool &active, const std::string &conv) { mat R = zeros(3,3); - + if(conv == "zxz") { double c1 = cos(psi); double s1 = sin(psi); @@ -112,747 +153,823 @@ mat fillR(const double &psi, const double &theta, const double &phi, const bool R = { {c3*c2*c1-s3*s1,c3*s1+c2*c1*s3,-c1*s2}, {-c1*s3-c3*c2*s1,c3*c1-c2*s3*s1,s2*s1}, {c3*s2,s3*s2,c2} }; } else if(conv == "user") { - - mat R1 = fillR(psi, simcoon::axis_psi, active); - mat R2 = fillR(theta, simcoon::axis_theta, active); - mat R3 = fillR(phi, simcoon::axis_phi, active); - + mat R1 = fillR_internal(psi, simcoon::axis_psi, active); + mat R2 = fillR_internal(theta, simcoon::axis_theta, active); + mat R3 = fillR_internal(phi, simcoon::axis_phi, active); R = R3*R2*R1; } else { throw std::invalid_argument("error in Simulation/Maths/rotation.cpp: invalid Euler convention '" + conv + "'. Supported conventions: zxz, zyz, xyz, xzy, yxz, yzx, zxy, zyx, xyx, xzx, yxy, yzy, user"); } - + return R; } - -mat fillQS(const double &alpha, const int &axis, const bool &active) { - double act = 0; - if (active) { - act = 1.; +// Internal: Generate 6x6 stress Voigt rotation matrix from angle/axis +mat fill_voigt_stress(const double &alpha, const int &axis, const bool &active) { + double act = active ? 1. : -1.; + + mat voigt_stress = zeros(6,6); + double c = cos(alpha); + double s = act*sin(alpha); + + switch(axis) { + case 1: { + voigt_stress(0,0) = 1.; + voigt_stress(1,1) = c*c; + voigt_stress(1,2) = s*s; + voigt_stress(1,5) = -2.*s*c; + voigt_stress(2,1) = s*s; + voigt_stress(2,2) = c*c; + voigt_stress(2,5) = 2.*s*c; + voigt_stress(3,3) = c; + voigt_stress(3,4) = -s; + voigt_stress(4,3) = s; + voigt_stress(4,4) = c; + voigt_stress(5,1) = s*c; + voigt_stress(5,2) = -s*c; + voigt_stress(5,5) = c*c-s*s; + break; + } + case 2: { + voigt_stress(0,0) = c*c; + voigt_stress(0,2) = s*s; + voigt_stress(0,4) = 2*c*s; + voigt_stress(1,1) = 1.; + voigt_stress(2,0) = s*s; + voigt_stress(2,2) = c*c; + voigt_stress(2,4) = -2*c*s; + voigt_stress(3,3) = c; + voigt_stress(3,5) = s; + voigt_stress(4,0) = -c*s; + voigt_stress(4,2) = c*s; + voigt_stress(4,4) = c*c-s*s; + voigt_stress(5,3) = -s; + voigt_stress(5,5) = c; + break; + } + case 3: { + voigt_stress(0,0) = c*c; + voigt_stress(0,1) = s*s; + voigt_stress(0,3) = -2*s*c; + voigt_stress(1,0) = s*s; + voigt_stress(1,1) = c*c; + voigt_stress(1,3) = 2*s*c; + voigt_stress(2,2) = 1.; + voigt_stress(3,0) = s*c; + voigt_stress(3,1) = -s*c; + voigt_stress(3,3) = c*c-s*s; + voigt_stress(4,4) = c; + voigt_stress(4,5) = -s; + voigt_stress(5,4) = s; + voigt_stress(5,5) = c; + break; + } + default: { + throw invalid_argument("Please choose the axis 1, 2 or 3"); + } } - else { - act = -1.; - } - - mat QS = zeros(6,6); - double c = cos(alpha); - double s = act*sin(alpha); - - switch(axis) { - case 1: { - QS(0,0) = 1.; - QS(0,1) = 0.; - QS(0,2) = 0.; - QS(0,3) = 0.; - QS(0,4) = 0.; - QS(0,5) = 0.; - QS(1,0) = 0.; - QS(1,1) = c*c; - QS(1,2) = s*s; - QS(1,3) = 0.; - QS(1,4) = 0.; - QS(1,5) = -2.*s*c; - QS(2,0) = 0.; - QS(2,1) = s*s; - QS(2,2) = c*c; - QS(2,3) = 0.; - QS(2,4) = 0.; - QS(2,5) = 2.*s*c; - QS(3,0) = 0.; - QS(3,1) = 0.; - QS(3,2) = 0.; - QS(3,3) = c; - QS(3,4) = -s; - QS(3,5) = 0.; - QS(4,0) = 0.; - QS(4,1) = 0.; - QS(4,2) = 0.; - QS(4,3) = s; - QS(4,4) = c; - QS(4,5) = 0.; - QS(5,0) = 0.; - QS(5,1) = s*c; - QS(5,2) = -s*c; - QS(5,3) = 0.; - QS(5,4) = 0.; - QS(5,5) = c*c-s*s; - - break; - } - case 2: { - QS(0,0) = c*c; - QS(0,1) = 0.; - QS(0,2) = s*s; - QS(0,3) = 0.; - QS(0,4) = 2*c*s; - QS(0,5) = 0.; - QS(1,0) = 0.; - QS(1,1) = 1.; - QS(1,2) = 0.; - QS(1,3) = 0.; - QS(1,4) = 0.; - QS(1,5) = 0.; - QS(2,0) = s*s; - QS(2,1) = 0.; - QS(2,2) = c*c; - QS(2,3) = 0.; - QS(2,4) = -2*c*s; - QS(2,5) = 0.; - QS(3,0) = 0.; - QS(3,1) = 0.; - QS(3,2) = 0.; - QS(3,3) = c; - QS(3,4) = 0.; - QS(3,5) = s; - QS(4,0) = -c*s; - QS(4,1) = 0.; - QS(4,2) = c*s; - QS(4,3) = 0.; - QS(4,4) = c*c-s*s; - QS(4,5) = 0.; - QS(5,0) = 0.; - QS(5,1) = 0.; - QS(5,2) = 0.; - QS(5,3) = -s; - QS(5,4) = 0.; - QS(5,5) = c; - - break; - - } - case 3: { - QS(0,0) = c*c; - QS(0,1) = s*s; - QS(0,2) = 0.; - QS(0,3) = -2*s*c; - QS(0,4) = 0.; - QS(0,5) = 0.; - QS(1,0) = s*s; - QS(1,1) = c*c; - QS(1,2) = 0.; - QS(1,3) = 2*s*c; - QS(1,4) = 0.; - QS(1,5) = 0.; - QS(2,0) = 0.; - QS(2,1) = 0.; - QS(2,2) = 1.; - QS(2,3) = 0.; - QS(2,4) = 0.; - QS(2,5) = 0.; - QS(3,0) = s*c; - QS(3,1) = -s*c; - QS(3,2) = 0.; - QS(3,3) = c*c-s*s; - QS(3,4) = 0.; - QS(3,5) = 0.; - QS(4,0) = 0.; - QS(4,1) = 0.; - QS(4,2) = 0.; - QS(4,3) = 0.; - QS(4,4) = c; - QS(4,5) = -s; - QS(5,0) = 0.; - QS(5,1) = 0.; - QS(5,2) = 0.; - QS(5,3) = 0.; - QS(5,4) = s; - QS(5,5) = c; - - break; - } - default: { - cout << "Please choose the axis 1,2 or 3\n"; - } - } - return QS; -} - -mat fillQS(const mat &DR, const bool &active) { - - double a = 0.; - double d = 0.; - double g = 0.; - double b = 0.; - double e = 0.; - double h = 0.; - double c = 0.; - double f = 0.; - double i = 0.; - + return voigt_stress; +} + +// Internal: Generate 6x6 stress Voigt rotation matrix from rotation matrix +mat fill_voigt_stress(const mat &DR, const bool &active) { + double a, b, c, d, e, f, g, h, i; + if(active) { - a = DR(0,0); - b = DR(0,1); - c = DR(0,2); - d = DR(1,0); - e = DR(1,1); - f = DR(1,2); - g = DR(2,0); - h = DR(2,1); - i = DR(2,2); - } - else{ - a = DR(0,0); - d = DR(0,1); - g = DR(0,2); - b = DR(1,0); - e = DR(1,1); - h = DR(1,2); - c = DR(2,0); - f = DR(2,1); - i = DR(2,2); - } - - mat QS= zeros(6,6); - QS(0,0) = a*a; - QS(0,1) = b*b; - QS(0,2) = c*c; - QS(0,3) = 2.*a*b; - QS(0,4) = 2.*a*c; - QS(0,5) = 2.*b*c; - QS(1,0) = d*d; - QS(1,1) = e*e; - QS(1,2) = f*f; - QS(1,3) = 2.*d*e; - QS(1,4) = 2.*d*f; - QS(1,5) = 2.*e*f; - QS(2,0) = g*g; - QS(2,1) = h*h; - QS(2,2) = i*i; - QS(2,3) = 2.*g*h; - QS(2,4) = 2.*g*i; - QS(2,5) = 2.*h*i; - QS(3,0) = a*d; - QS(3,1) = b*e; - QS(3,2) = c*f; - QS(3,3) = d*b+a*e; - QS(3,4) = d*c+a*f; - QS(3,5) = e*c+b*f; - QS(4,0) = a*g; - QS(4,1) = b*h; - QS(4,2) = c*i; - QS(4,3) = g*b+a*h; - QS(4,4) = g*c+a*i; - QS(4,5) = h*c+b*i; - QS(5,0) = d*g; - QS(5,1) = e*h; - QS(5,2) = f*i; - QS(5,3) = g*e+d*h; - QS(5,4) = g*f+d*i; - QS(5,5) = h*f+e*i; - - return QS; -} - -mat fillQE(const double &alpha, const int &axis, const bool &active) { - - double act = 0; - if (active) { - act = 1.; - } - else { - act = -1.; - } - - mat QE = zeros(6,6); + a = DR(0,0); b = DR(0,1); c = DR(0,2); + d = DR(1,0); e = DR(1,1); f = DR(1,2); + g = DR(2,0); h = DR(2,1); i = DR(2,2); + } else { + a = DR(0,0); d = DR(0,1); g = DR(0,2); + b = DR(1,0); e = DR(1,1); h = DR(1,2); + c = DR(2,0); f = DR(2,1); i = DR(2,2); + } + + mat voigt_stress = zeros(6,6); + voigt_stress(0,0) = a*a; voigt_stress(0,1) = b*b; voigt_stress(0,2) = c*c; + voigt_stress(0,3) = 2.*a*b; voigt_stress(0,4) = 2.*a*c; voigt_stress(0,5) = 2.*b*c; + voigt_stress(1,0) = d*d; voigt_stress(1,1) = e*e; voigt_stress(1,2) = f*f; + voigt_stress(1,3) = 2.*d*e; voigt_stress(1,4) = 2.*d*f; voigt_stress(1,5) = 2.*e*f; + voigt_stress(2,0) = g*g; voigt_stress(2,1) = h*h; voigt_stress(2,2) = i*i; + voigt_stress(2,3) = 2.*g*h; voigt_stress(2,4) = 2.*g*i; voigt_stress(2,5) = 2.*h*i; + voigt_stress(3,0) = a*d; voigt_stress(3,1) = b*e; voigt_stress(3,2) = c*f; + voigt_stress(3,3) = d*b+a*e; voigt_stress(3,4) = d*c+a*f; voigt_stress(3,5) = e*c+b*f; + voigt_stress(4,0) = a*g; voigt_stress(4,1) = b*h; voigt_stress(4,2) = c*i; + voigt_stress(4,3) = g*b+a*h; voigt_stress(4,4) = g*c+a*i; voigt_stress(4,5) = h*c+b*i; + voigt_stress(5,0) = d*g; voigt_stress(5,1) = e*h; voigt_stress(5,2) = f*i; + voigt_stress(5,3) = g*e+d*h; voigt_stress(5,4) = g*f+d*i; voigt_stress(5,5) = h*f+e*i; + + return voigt_stress; +} + +// Internal: Generate 6x6 strain Voigt rotation matrix from angle/axis +mat fill_voigt_strain(const double &alpha, const int &axis, const bool &active) { + double act = active ? 1. : -1.; + + mat voigt_strain = zeros(6,6); double c = cos(alpha); double s = act*sin(alpha); - - switch(axis) { - case 1: { - QE(0,0) = 1.; - QE(0,1) = 0.; - QE(0,2) = 0.; - QE(0,3) = 0.; - QE(0,4) = 0.; - QE(0,5) = 0.; - QE(1,0) = 0.; - QE(1,1) = c*c; - QE(1,2) = s*s; - QE(1,3) = 0.; - QE(1,4) = 0.; - QE(1,5) = -s*c; - QE(2,0) = 0.; - QE(2,1) = s*s; - QE(2,2) = c*c; - QE(2,3) = 0.; - QE(2,4) = 0.; - QE(2,5) = s*c; - QE(3,0) = 0.; - QE(3,1) = 0.; - QE(3,2) = 0.; - QE(3,3) = c; - QE(3,4) = -s; - QE(3,5) = 0.; - QE(4,0) = 0.; - QE(4,1) = 0.; - QE(4,2) = 0.; - QE(4,3) = s; - QE(4,4) = c; - QE(4,5) = 0.; - QE(5,0) = 0.; - QE(5,1) = 2.*s*c; - QE(5,2) = -2.*s*c; - QE(5,3) = 0.; - QE(5,4) = 0.; - QE(5,5) = c*c-s*s; - break; - } - case 2: { - QE(0,0) = c*c; - QE(0,1) = 0.; - QE(0,2) = s*s; - QE(0,3) = 0.; - QE(0,4) = c*s; - QE(0,5) = 0.; - QE(1,0) = 0.; - QE(1,1) = 1.; - QE(1,2) = 0.; - QE(1,3) = 0.; - QE(1,4) = 0.; - QE(1,5) = 0.; - QE(2,0) = s*s; - QE(2,1) = 0.; - QE(2,2) = c*c; - QE(2,3) = 0.; - QE(2,4) = -c*s; - QE(2,5) = 0.; - QE(3,0) = 0.; - QE(3,1) = 0.; - QE(3,2) = 0.; - QE(3,3) = c; - QE(3,4) = 0.; - QE(3,5) = s; - QE(4,0) = -2.*c*s; - QE(4,1) = 0.; - QE(4,2) = 2.*c*s; - QE(4,3) = 0.; - QE(4,4) = c*c-s*s; - QE(4,5) = 0.; - QE(5,0) = 0.; - QE(5,1) = 0.; - QE(5,2) = 0.; - QE(5,3) = -s; - QE(5,4) = 0.; - QE(5,5) = c; - break; - } - case 3: { - QE(0,0) = c*c; - QE(0,1) = s*s; - QE(0,2) = 0.; - QE(0,3) = -s*c; - QE(0,4) = 0.; - QE(0,5) = 0.; - QE(1,0) = s*s; - QE(1,1) = c*c; - QE(1,2) = 0.; - QE(1,3) = s*c; - QE(1,4) = 0.; - QE(1,5) = 0.; - QE(2,0) = 0.; - QE(2,1) = 0.; - QE(2,2) = 1.; - QE(2,3) = 0.; - QE(2,4) = 0.; - QE(2,5) = 0.; - QE(3,0) = 2.*s*c; - QE(3,1) = -2.*s*c; - QE(3,2) = 0.; - QE(3,3) = c*c-s*s; - QE(3,4) = 0.; - QE(3,5) = 0.; - QE(4,0) = 0.; - QE(4,1) = 0.; - QE(4,2) = 0.; - QE(4,3) = 0.; - QE(4,4) = c; - QE(4,5) = -s; - QE(5,0) = 0.; - QE(5,1) = 0.; - QE(5,2) = 0.; - QE(5,3) = 0.; - QE(5,4) = s; - QE(5,5) = c; - break; - } - default: { - cout << "Please choose the axis 1,2 or 3\n"; - } - } - return QE; -} - -mat fillQE(const mat &DR, const bool &active) { - - double a = 0.; - double d = 0.; - double g = 0.; - double b = 0.; - double e = 0.; - double h = 0.; - double c = 0.; - double f = 0.; - double i = 0.; - + + switch(axis) { + case 1: { + voigt_strain(0,0) = 1.; + voigt_strain(1,1) = c*c; + voigt_strain(1,2) = s*s; + voigt_strain(1,5) = -s*c; + voigt_strain(2,1) = s*s; + voigt_strain(2,2) = c*c; + voigt_strain(2,5) = s*c; + voigt_strain(3,3) = c; + voigt_strain(3,4) = -s; + voigt_strain(4,3) = s; + voigt_strain(4,4) = c; + voigt_strain(5,1) = 2.*s*c; + voigt_strain(5,2) = -2.*s*c; + voigt_strain(5,5) = c*c-s*s; + break; + } + case 2: { + voigt_strain(0,0) = c*c; + voigt_strain(0,2) = s*s; + voigt_strain(0,4) = c*s; + voigt_strain(1,1) = 1.; + voigt_strain(2,0) = s*s; + voigt_strain(2,2) = c*c; + voigt_strain(2,4) = -c*s; + voigt_strain(3,3) = c; + voigt_strain(3,5) = s; + voigt_strain(4,0) = -2.*c*s; + voigt_strain(4,2) = 2.*c*s; + voigt_strain(4,4) = c*c-s*s; + voigt_strain(5,3) = -s; + voigt_strain(5,5) = c; + break; + } + case 3: { + voigt_strain(0,0) = c*c; + voigt_strain(0,1) = s*s; + voigt_strain(0,3) = -s*c; + voigt_strain(1,0) = s*s; + voigt_strain(1,1) = c*c; + voigt_strain(1,3) = s*c; + voigt_strain(2,2) = 1.; + voigt_strain(3,0) = 2.*s*c; + voigt_strain(3,1) = -2.*s*c; + voigt_strain(3,3) = c*c-s*s; + voigt_strain(4,4) = c; + voigt_strain(4,5) = -s; + voigt_strain(5,4) = s; + voigt_strain(5,5) = c; + break; + } + default: { + throw invalid_argument("Please choose the axis 1, 2 or 3"); + } + } + return voigt_strain; +} + +// Internal: Generate 6x6 strain Voigt rotation matrix from rotation matrix +mat fill_voigt_strain(const mat &DR, const bool &active) { + double a, b, c, d, e, f, g, h, i; + if(active) { - a = DR(0,0); - b = DR(0,1); - c = DR(0,2); - d = DR(1,0); - e = DR(1,1); - f = DR(1,2); - g = DR(2,0); - h = DR(2,1); - i = DR(2,2); - } - else{ - a = DR(0,0); - d = DR(0,1); - g = DR(0,2); - b = DR(1,0); - e = DR(1,1); - h = DR(1,2); - c = DR(2,0); - f = DR(2,1); - i = DR(2,2); - } - - mat QE= zeros(6,6); - QE(0,0) = a*a; - QE(0,1) = b*b; - QE(0,2) = c*c; - QE(0,3) = a*b; - QE(0,4) = a*c; - QE(0,5) = b*c; - QE(1,0) = d*d; - QE(1,1) = e*e; - QE(1,2) = f*f; - QE(1,3) = d*e; - QE(1,4) = d*f; - QE(1,5) = e*f; - QE(2,0) = g*g; - QE(2,1) = h*h; - QE(2,2) = i*i; - QE(2,3) = g*h; - QE(2,4) = g*i; - QE(2,5) = h*i; - QE(3,0) = 2.*a*d; - QE(3,1) = 2.*b*e; - QE(3,2) = 2.*c*f; - QE(3,3) = d*b+a*e; - QE(3,4) = d*c+a*f; - QE(3,5) = e*c+b*f; - QE(4,0) = 2.*a*g; - QE(4,1) = 2.*b*h; - QE(4,2) = 2.*c*i; - QE(4,3) = g*b+a*h; - QE(4,4) = g*c+a*i; - QE(4,5) = h*c+b*i; - QE(5,0) = 2.*d*g; - QE(5,1) = 2.*e*h; - QE(5,2) = 2.*f*i; - QE(5,3) = g*e+d*h; - QE(5,4) = g*f+d*i; - QE(5,5) = h*f+e*i; - - return QE; -} - - -//To rotate a stiffness matrix (6,6) -mat rotateL(const mat &L, const double &alpha, const int &axis, const bool &active) { - - mat QS = fillQS(alpha, axis, active); - return QS*(L*trans(QS)); -} - -mat rotateL(const mat &L, const mat &DR, const bool &active) { - - mat QS = fillQS(DR, active); - return QS*(L*trans(QS)); -} - - -//To rotate a compliance matrix (6,6) -mat rotateM(const mat &M, const double &alpha, const int &axis, const bool &active) { - - mat QE = fillQE(alpha, axis, active); - return QE*(M*trans(QE)); -} - -mat rotateM(const mat &M, const mat &DR, const bool &active) { - - mat QE = fillQE(DR, active); - return QE*(M*trans(QE)); -} - - -//To rotate an interaction matrix of type A (strain) (6,6) -mat rotateA(const mat &A, const double &alpha, const int &axis, const bool &active) { - - mat QE = fillQE(alpha, axis, active); - mat QS = fillQS(alpha, axis, active); - - return QE*(A*trans(QS)); -} - -mat rotateA(const mat &A, const mat &DR, const bool &active) { - - mat QE = fillQE(DR, active); - mat QS = fillQS(DR, active); - - return QE*(A*trans(QS)); -} - -//To rotate an interaction matrix of type B (stress) (6,6) -mat rotateB(const mat &B, const double &alpha, const int &axis, const bool &active) { - - mat QE = fillQE(alpha, axis, active); - mat QS = fillQS(alpha, axis, active); - - return QS*(B*trans(QE)); -} - -mat rotateB(const mat &B, const mat &DR, const bool &active) { - - mat QE = fillQE(DR, active); - mat QS = fillQS(DR, active); - - return QS*(B*trans(QE)); -} - -//To rotate a stress vector (6) -vec rotate_stress(const vec &V, const double &alpha, const int &axis, const bool &active) { + a = DR(0,0); b = DR(0,1); c = DR(0,2); + d = DR(1,0); e = DR(1,1); f = DR(1,2); + g = DR(2,0); h = DR(2,1); i = DR(2,2); + } else { + a = DR(0,0); d = DR(0,1); g = DR(0,2); + b = DR(1,0); e = DR(1,1); h = DR(1,2); + c = DR(2,0); f = DR(2,1); i = DR(2,2); + } + + mat voigt_strain = zeros(6,6); + voigt_strain(0,0) = a*a; voigt_strain(0,1) = b*b; voigt_strain(0,2) = c*c; + voigt_strain(0,3) = a*b; voigt_strain(0,4) = a*c; voigt_strain(0,5) = b*c; + voigt_strain(1,0) = d*d; voigt_strain(1,1) = e*e; voigt_strain(1,2) = f*f; + voigt_strain(1,3) = d*e; voigt_strain(1,4) = d*f; voigt_strain(1,5) = e*f; + voigt_strain(2,0) = g*g; voigt_strain(2,1) = h*h; voigt_strain(2,2) = i*i; + voigt_strain(2,3) = g*h; voigt_strain(2,4) = g*i; voigt_strain(2,5) = h*i; + voigt_strain(3,0) = 2.*a*d; voigt_strain(3,1) = 2.*b*e; voigt_strain(3,2) = 2.*c*f; + voigt_strain(3,3) = d*b+a*e; voigt_strain(3,4) = d*c+a*f; voigt_strain(3,5) = e*c+b*f; + voigt_strain(4,0) = 2.*a*g; voigt_strain(4,1) = 2.*b*h; voigt_strain(4,2) = 2.*c*i; + voigt_strain(4,3) = g*b+a*h; voigt_strain(4,4) = g*c+a*i; voigt_strain(4,5) = h*c+b*i; + voigt_strain(5,0) = 2.*d*g; voigt_strain(5,1) = 2.*e*h; voigt_strain(5,2) = 2.*f*i; + voigt_strain(5,3) = g*e+d*h; voigt_strain(5,4) = g*f+d*i; voigt_strain(5,5) = h*f+e*i; + + return voigt_strain; +} + +} // anonymous namespace + +// ============================================================================= +// Rotation Class: Constructors +// ============================================================================= + +Rotation::Rotation() : _quat({0.0, 0.0, 0.0, 1.0}) { +} + +Rotation::Rotation(const vec::fixed<4>& quat) : _quat(quat) { +} + +// ============================================================================= +// Rotation Class: Factory Methods +// ============================================================================= + +Rotation Rotation::identity() { + return Rotation(); +} + +Rotation Rotation::from_quat(const vec::fixed<4>& quat) { + return Rotation(normalize_quat(quat)); +} + +Rotation Rotation::from_quat(const vec& quat) { + if (quat.n_elem != 4) { + throw invalid_argument("Quaternion must have 4 elements"); + } + vec::fixed<4> q_fixed = {quat(0), quat(1), quat(2), quat(3)}; + return from_quat(q_fixed); +} + +Rotation Rotation::from_matrix(const mat::fixed<3,3>& R) { + double trace = R(0,0) + R(1,1) + R(2,2); + vec::fixed<4> q; + + if (trace > 0) { + double s = 0.5 / sqrt(trace + 1.0); + q(3) = 0.25 / s; + q(0) = (R(2,1) - R(1,2)) * s; + q(1) = (R(0,2) - R(2,0)) * s; + q(2) = (R(1,0) - R(0,1)) * s; + } else if (R(0,0) > R(1,1) && R(0,0) > R(2,2)) { + double s = 2.0 * sqrt(1.0 + R(0,0) - R(1,1) - R(2,2)); + q(3) = (R(2,1) - R(1,2)) / s; + q(0) = 0.25 * s; + q(1) = (R(0,1) + R(1,0)) / s; + q(2) = (R(0,2) + R(2,0)) / s; + } else if (R(1,1) > R(2,2)) { + double s = 2.0 * sqrt(1.0 + R(1,1) - R(0,0) - R(2,2)); + q(3) = (R(0,2) - R(2,0)) / s; + q(0) = (R(0,1) + R(1,0)) / s; + q(1) = 0.25 * s; + q(2) = (R(1,2) + R(2,1)) / s; + } else { + double s = 2.0 * sqrt(1.0 + R(2,2) - R(0,0) - R(1,1)); + q(3) = (R(1,0) - R(0,1)) / s; + q(0) = (R(0,2) + R(2,0)) / s; + q(1) = (R(1,2) + R(2,1)) / s; + q(2) = 0.25 * s; + } + + return Rotation(normalize_quat(q)); +} + +Rotation Rotation::from_matrix(const mat& R) { + if (R.n_rows != 3 || R.n_cols != 3) { + throw invalid_argument("Rotation matrix must be 3x3"); + } + mat::fixed<3,3> R_fixed; + R_fixed = R; + return from_matrix(R_fixed); +} + +Rotation Rotation::from_euler(double psi, double theta, double phi, + const string& conv, bool intrinsic, bool degrees) { + if (degrees) { + psi *= simcoon::pi / 180.0; + theta *= simcoon::pi / 180.0; + phi *= simcoon::pi / 180.0; + } + + if (conv == "user") { + mat R = fillR_euler_internal(psi, theta, phi, true, "user"); + mat::fixed<3,3> R_fixed; + R_fixed = R; + return from_matrix(R_fixed); + } + + int axis1, axis2, axis3; + parse_euler_convention(conv, axis1, axis2, axis3); + + auto axis_quat = [](double angle, int axis) -> vec::fixed<4> { + double half_angle = angle / 2.0; + double s = sin(half_angle); + double c = cos(half_angle); + vec::fixed<4> q = {0, 0, 0, c}; + q(axis) = s; + return q; + }; + + vec::fixed<4> q1 = axis_quat(psi, axis1); + vec::fixed<4> q2 = axis_quat(theta, axis2); + vec::fixed<4> q3 = axis_quat(phi, axis3); + + vec::fixed<4> result; + if (intrinsic) { + result = quat_multiply(quat_multiply(q3, q2), q1); + } else { + result = quat_multiply(quat_multiply(q1, q2), q3); + } + + return Rotation(normalize_quat(result)); +} + +Rotation Rotation::from_euler(const vec::fixed<3>& angles, const string& conv, + bool intrinsic, bool degrees) { + return from_euler(angles(0), angles(1), angles(2), conv, intrinsic, degrees); +} + +Rotation Rotation::from_rotvec(const vec::fixed<3>& rotvec, bool degrees) { + double angle = norm(rotvec); + + if (degrees) { + angle *= simcoon::pi / 180.0; + } + + if (angle < simcoon::iota) { + return identity(); + } + + vec::fixed<3> axis = rotvec / (degrees ? norm(rotvec) * 180.0 / simcoon::pi : norm(rotvec)); + double half_angle = angle / 2.0; + double s = sin(half_angle); + double c = cos(half_angle); + + return Rotation({axis(0)*s, axis(1)*s, axis(2)*s, c}); +} + +Rotation Rotation::from_rotvec(const vec& rotvec, bool degrees) { + if (rotvec.n_elem != 3) { + throw invalid_argument("Rotation vector must have 3 elements"); + } + vec::fixed<3> rv_fixed = {rotvec(0), rotvec(1), rotvec(2)}; + return from_rotvec(rv_fixed, degrees); +} + +Rotation Rotation::from_axis_angle(double angle, int axis, bool degrees) { + if (degrees) { + angle *= simcoon::pi / 180.0; + } + + int ax_idx = axis_to_index(axis); + double half_angle = angle / 2.0; + double s = sin(half_angle); + double c = cos(half_angle); + + vec::fixed<4> q = {0, 0, 0, c}; + q(ax_idx) = s; + + return Rotation(q); +} + +Rotation Rotation::random() { + vec u = randu(3); + + double sqrt1_u0 = sqrt(1.0 - u(0)); + double sqrt_u0 = sqrt(u(0)); + double two_pi_u1 = 2.0 * simcoon::pi * u(1); + double two_pi_u2 = 2.0 * simcoon::pi * u(2); + + vec::fixed<4> q = { + sqrt1_u0 * sin(two_pi_u1), + sqrt1_u0 * cos(two_pi_u1), + sqrt_u0 * sin(two_pi_u2), + sqrt_u0 * cos(two_pi_u2) + }; + + return Rotation(q); +} + +// ============================================================================= +// Rotation Class: Conversion Methods +// ============================================================================= + +mat::fixed<3,3> Rotation::as_matrix() const { + double qx = _quat(0), qy = _quat(1), qz = _quat(2), qw = _quat(3); + + double qx2 = qx * qx, qy2 = qy * qy, qz2 = qz * qz; + double qxqy = qx * qy, qxqz = qx * qz, qxqw = qx * qw; + double qyqz = qy * qz, qyqw = qy * qw, qzqw = qz * qw; + + mat::fixed<3,3> R; + R(0,0) = 1.0 - 2.0*(qy2 + qz2); + R(0,1) = 2.0*(qxqy - qzqw); + R(0,2) = 2.0*(qxqz + qyqw); + R(1,0) = 2.0*(qxqy + qzqw); + R(1,1) = 1.0 - 2.0*(qx2 + qz2); + R(1,2) = 2.0*(qyqz - qxqw); + R(2,0) = 2.0*(qxqz - qyqw); + R(2,1) = 2.0*(qyqz + qxqw); + R(2,2) = 1.0 - 2.0*(qx2 + qy2); + + return R; +} + +vec::fixed<3> Rotation::as_euler(const string& conv, bool intrinsic, bool degrees) const { + mat::fixed<3,3> R = as_matrix(); + + int axis1, axis2, axis3; + + if (conv == "user") { + axis1 = axis_psi - 1; + axis2 = axis_theta - 1; + axis3 = axis_phi - 1; + } else { + parse_euler_convention(conv, axis1, axis2, axis3); + } - mat QS = fillQS(alpha, axis, active); - return QS*V; + mat::fixed<3,3> R_work = intrinsic ? R : R.t(); + if (!intrinsic) { + swap(axis1, axis3); + } + + vec::fixed<3> angles; + + bool proper_euler = (axis1 == axis3); + + int i = axis1; + int j = axis2; + int k = proper_euler ? (3 - i - j) : axis3; + + double sign = ((j - i + 3) % 3 == 1) ? 1.0 : -1.0; + + if (proper_euler) { + double sy = sqrt(R_work(i,j)*R_work(i,j) + R_work(i,k)*R_work(i,k)); + + if (sy > simcoon::iota) { + angles(0) = atan2(R_work(i,j), sign*R_work(i,k)); + angles(1) = atan2(sy, R_work(i,i)); + angles(2) = atan2(R_work(j,i), -sign*R_work(k,i)); + } else { + angles(0) = atan2(-sign*R_work(j,k), R_work(j,j)); + angles(1) = atan2(sy, R_work(i,i)); + angles(2) = 0.0; + } + } else { + double cy = sqrt(R_work(i,i)*R_work(i,i) + R_work(j,i)*R_work(j,i)); + + if (cy > simcoon::iota) { + angles(0) = atan2(sign*R_work(k,j), R_work(k,k)); + angles(1) = atan2(-sign*R_work(k,i), cy); + angles(2) = atan2(sign*R_work(j,i), R_work(i,i)); + } else { + angles(0) = atan2(-sign*R_work(j,k), R_work(j,j)); + angles(1) = atan2(-sign*R_work(k,i), cy); + angles(2) = 0.0; + } + } + + if (!intrinsic) { + swap(angles(0), angles(2)); + } + + if (degrees) { + angles *= 180.0 / simcoon::pi; + } + + return angles; +} + +vec::fixed<3> Rotation::as_rotvec(bool degrees) const { + double qw = _quat(3); + vec::fixed<3> qv = {_quat(0), _quat(1), _quat(2)}; + + double sin_half = norm(qv); + + if (sin_half < simcoon::iota) { + vec::fixed<3> result = {0.0, 0.0, 0.0}; + return result; + } + + double angle = 2.0 * atan2(sin_half, qw); + + if (angle > simcoon::pi) { + angle -= 2.0 * simcoon::pi; + } else if (angle < -simcoon::pi) { + angle += 2.0 * simcoon::pi; + } + + vec::fixed<3> axis = qv / sin_half; + + if (degrees) { + angle *= 180.0 / simcoon::pi; + } + + return axis * angle; +} + +mat::fixed<6,6> Rotation::as_voigt_stress_rotation(bool active) const { + mat R = as_matrix(); + mat vs_dyn = fill_voigt_stress(R, active); + mat::fixed<6,6> vs; + vs = vs_dyn; + return vs; +} + +mat::fixed<6,6> Rotation::as_voigt_strain_rotation(bool active) const { + mat R = as_matrix(); + mat ve_dyn = fill_voigt_strain(R, active); + mat::fixed<6,6> ve; + ve = ve_dyn; + return ve; +} + +// ============================================================================= +// Rotation Class: Apply Methods (3D objects) +// ============================================================================= + +vec::fixed<3> Rotation::apply(const vec::fixed<3>& v, bool inverse) const { + double qx = _quat(0), qy = _quat(1), qz = _quat(2), qw = _quat(3); + + if (inverse) { + qx = -qx; qy = -qy; qz = -qz; + } + + vec::fixed<3> qv = {qx, qy, qz}; + vec::fixed<3> uv = cross(qv, v); + vec::fixed<3> uuv = cross(qv, uv); + + return v + 2.0 * (qw * uv + uuv); +} + +vec Rotation::apply(const vec& v, bool inverse) const { + if (v.n_elem != 3) { + throw invalid_argument("Vector must have 3 elements"); + } + vec::fixed<3> v_fixed = {v(0), v(1), v(2)}; + vec::fixed<3> result = apply(v_fixed, inverse); + return vec(result); +} + +mat::fixed<3,3> Rotation::apply_tensor(const mat::fixed<3,3>& m, bool inverse) const { + mat::fixed<3,3> R = as_matrix(); + if (inverse) { + return R.t() * m * R; + } else { + return R * m * R.t(); + } +} + +mat Rotation::apply_tensor(const mat& m, bool inverse) const { + if (m.n_rows != 3 || m.n_cols != 3) { + throw invalid_argument("Tensor must be 3x3"); + } + mat::fixed<3,3> m_fixed; + m_fixed = m; + mat::fixed<3,3> result = apply_tensor(m_fixed, inverse); + return mat(result); +} + +// ============================================================================= +// Rotation Class: Apply Methods (Voigt notation) +// ============================================================================= + +vec::fixed<6> Rotation::apply_stress(const vec::fixed<6>& sigma, bool active) const { + mat::fixed<6,6> vs = as_voigt_stress_rotation(active); + vec::fixed<6> result; + result = vs * sigma; + return result; +} + +vec Rotation::apply_stress(const vec& sigma, bool active) const { + if (sigma.n_elem != 6) { + throw invalid_argument("Stress vector must have 6 elements"); + } + mat R = as_matrix(); + mat vs = fill_voigt_stress(R, active); + return vs * sigma; +} + +vec::fixed<6> Rotation::apply_strain(const vec::fixed<6>& epsilon, bool active) const { + mat::fixed<6,6> ve = as_voigt_strain_rotation(active); + vec::fixed<6> result; + result = ve * epsilon; + return result; +} + +vec Rotation::apply_strain(const vec& epsilon, bool active) const { + if (epsilon.n_elem != 6) { + throw invalid_argument("Strain vector must have 6 elements"); + } + mat R = as_matrix(); + mat ve = fill_voigt_strain(R, active); + return ve * epsilon; +} + +mat::fixed<6,6> Rotation::apply_stiffness(const mat::fixed<6,6>& L, bool active) const { + mat::fixed<6,6> vs = as_voigt_stress_rotation(active); + mat::fixed<6,6> result; + result = vs * L * vs.t(); + return result; +} + +mat Rotation::apply_stiffness(const mat& L, bool active) const { + if (L.n_rows != 6 || L.n_cols != 6) { + throw invalid_argument("Stiffness matrix must be 6x6"); + } + mat R = as_matrix(); + mat vs = fill_voigt_stress(R, active); + return vs * (L * trans(vs)); +} + +mat::fixed<6,6> Rotation::apply_compliance(const mat::fixed<6,6>& M, bool active) const { + mat::fixed<6,6> ve = as_voigt_strain_rotation(active); + mat::fixed<6,6> result; + result = ve * M * ve.t(); + return result; +} + +mat Rotation::apply_compliance(const mat& M, bool active) const { + if (M.n_rows != 6 || M.n_cols != 6) { + throw invalid_argument("Compliance matrix must be 6x6"); + } + mat R = as_matrix(); + mat ve = fill_voigt_strain(R, active); + return ve * (M * trans(ve)); +} + +mat::fixed<6,6> Rotation::apply_strain_concentration(const mat::fixed<6,6>& A, bool active) const { + mat::fixed<6,6> ve = as_voigt_strain_rotation(active); + mat::fixed<6,6> vs = as_voigt_stress_rotation(active); + mat::fixed<6,6> result; + result = ve * mat(A) * vs.t(); + return result; +} + +mat Rotation::apply_strain_concentration(const mat& A, bool active) const { + if (A.n_rows != 6 || A.n_cols != 6) { + throw invalid_argument("Strain concentration tensor must be 6x6"); + } + mat R = as_matrix(); + mat ve = fill_voigt_strain(R, active); + mat vs = fill_voigt_stress(R, active); + return ve * (A * trans(vs)); +} + +mat::fixed<6,6> Rotation::apply_stress_concentration(const mat::fixed<6,6>& B, bool active) const { + mat::fixed<6,6> vs = as_voigt_stress_rotation(active); + mat::fixed<6,6> ve = as_voigt_strain_rotation(active); + mat::fixed<6,6> result; + result = vs * mat(B) * ve.t(); + return result; +} + +mat Rotation::apply_stress_concentration(const mat& B, bool active) const { + if (B.n_rows != 6 || B.n_cols != 6) { + throw invalid_argument("Stress concentration tensor must be 6x6"); + } + mat R = as_matrix(); + mat vs = fill_voigt_stress(R, active); + mat ve = fill_voigt_strain(R, active); + return vs * (B * trans(ve)); +} + +// ============================================================================= +// Rotation Class: Operations +// ============================================================================= + +Rotation Rotation::inv() const { + return Rotation(quat_conjugate(_quat)); +} + +double Rotation::magnitude(bool degrees) const { + double angle = 2.0 * acos(min(abs(_quat(3)), 1.0)); + + if (degrees) { + angle *= 180.0 / simcoon::pi; + } + + return angle; +} + +Rotation Rotation::operator*(const Rotation& other) const { + return Rotation(normalize_quat(quat_multiply(_quat, other._quat))); +} + +Rotation& Rotation::operator*=(const Rotation& other) { + _quat = normalize_quat(quat_multiply(_quat, other._quat)); + return *this; +} + +Rotation Rotation::slerp(const Rotation& other, double t) const { + vec::fixed<4> q1 = _quat; + vec::fixed<4> q2 = other._quat; + + double dot = arma::dot(q1, q2); + + if (dot < 0.0) { + q2 = -q2; + dot = -dot; + } + + dot = min(dot, 1.0); + + vec::fixed<4> result; + + if (dot > 0.9995) { + result = q1 + t * (q2 - q1); + } else { + double theta_0 = acos(dot); + double theta = theta_0 * t; + double sin_theta = sin(theta); + double sin_theta_0 = sin(theta_0); + + double s0 = cos(theta) - dot * sin_theta / sin_theta_0; + double s1 = sin_theta / sin_theta_0; + + result = s0 * q1 + s1 * q2; + } + + return Rotation(normalize_quat(result)); +} + +// ============================================================================= +// Convenience Free Functions +// ============================================================================= + +vec rotate_vec(const vec &v, const mat &DR) { + return DR*v; +} + +vec rotate_vec(const vec &v, const double &alpha, const int &axis) { + assert(axis>0); + Rotation rot = Rotation::from_axis_angle(alpha, axis); + return rot.apply(v); +} + +mat rotate_mat(const mat &m, const mat &DR) { + return trans(DR)*m*DR; +} + +mat rotate_mat(const mat &m, const double &alpha, const int &axis) { + Rotation rot = Rotation::from_axis_angle(alpha, axis); + return rot.apply_tensor(m); +} + +vec rotate_stress(const vec &V, const double &alpha, const int &axis, const bool &active) { + mat vs = fill_voigt_stress(alpha, axis, active); + return vs*V; } -//To rotate a stress vector (6) vec rotate_stress(const vec &V, const mat &DR, const bool &active) { - - mat QS = fillQS(DR, active); - return QS*V; + mat vs = fill_voigt_stress(DR, active); + return vs*V; } - -//To rotate a strain vector (6) -vec rotate_strain(const vec &V, const double &alpha, const int &axis, const bool &active) { - mat QE = fillQE(alpha, axis, active); - return QE*V; +vec rotate_strain(const vec &V, const double &alpha, const int &axis, const bool &active) { + mat ve = fill_voigt_strain(alpha, axis, active); + return ve*V; } vec rotate_strain(const vec &V, const mat &DR, const bool &active) { - - mat QE = fillQE(DR, active); - return QE*V; -} - - -//To rotate from local to global a strain tensor (6) using Euler angles -mat rotate_l2g_strain(const vec &E, const double &psi, const double &theta, const double &phi) { - - mat E_temp = E; - if(fabs(phi) > simcoon::iota) { - E_temp = rotate_strain(E_temp, -phi, simcoon::axis_phi, false); - } - if(fabs(theta) > simcoon::iota) { - E_temp = rotate_strain(E_temp, -theta, simcoon::axis_theta, false); - } - if(fabs(psi) > simcoon::iota) { - E_temp = rotate_strain(E_temp, -psi, simcoon::axis_psi, false); - } - - return E_temp; -} - -//To rotate from global to local a strain tensor (6) using Euler angles -mat rotate_g2l_strain(const vec &E, const double &psi, const double &theta, const double &phi) { - - mat E_temp = E; - if(fabs(psi) > simcoon::iota) { - E_temp = rotate_strain(E_temp, psi, simcoon::axis_psi, false); - } - if(fabs(theta) > simcoon::iota) { - E_temp = rotate_strain(E_temp, theta, simcoon::axis_theta, false); - } - if(fabs(phi) > simcoon::iota) { - E_temp = rotate_strain(E_temp, phi, simcoon::axis_phi, false); - } - - return E_temp; -} - -//To rotate from local to global a stress tensor (6) -mat rotate_l2g_stress(const vec &S, const double &psi, const double &theta, const double &phi) { - - mat S_temp = S; - if(fabs(phi) > simcoon::iota) { - S_temp = rotate_stress(S_temp, -phi, simcoon::axis_phi, false); - } - if(fabs(theta) > simcoon::iota) { - S_temp = rotate_stress(S_temp, -theta, simcoon::axis_theta, false); - } - if(fabs(psi) > simcoon::iota) { - S_temp = rotate_stress(S_temp, -psi, simcoon::axis_psi, false); - } - - return S_temp; -} - -//To rotate from global to local a stress tensor (6) -mat rotate_g2l_stress(const vec &S, const double &psi, const double &theta, const double &phi) { - - mat S_temp = S; - if(fabs(psi) > simcoon::iota) { - S_temp = rotate_stress(S_temp, psi, simcoon::axis_psi, false); - } - if(fabs(theta) > simcoon::iota) { - S_temp = rotate_stress(S_temp, theta, simcoon::axis_theta, false); - } - if(fabs(phi) > simcoon::iota) { - S_temp = rotate_stress(S_temp, phi, simcoon::axis_phi, false); - } - - return S_temp; -} - -//To rotate from local to global a stiffness matrix (6,6) -mat rotate_l2g_L(const mat &Lt, const double &psi, const double &theta, const double &phi) { - - mat Lt_temp = Lt; - if(fabs(phi) > simcoon::iota) { - Lt_temp = rotateL(Lt_temp, -phi, simcoon::axis_phi, false); - } - if(fabs(theta) > simcoon::iota) { - Lt_temp = rotateL(Lt_temp, -theta, simcoon::axis_theta, false); - } - if(fabs(psi) > simcoon::iota) { - Lt_temp = rotateL(Lt_temp, -psi, simcoon::axis_psi, false); - } - - return Lt_temp; -} - -//To rotate from global to local a stiffness matrix (6,6) -mat rotate_g2l_L(const mat &Lt, const double &psi, const double &theta, const double &phi) { - - mat Lt_temp = Lt; - if(fabs(psi) > simcoon::iota) { - Lt_temp = rotateL(Lt_temp, psi, simcoon::axis_psi, false); - } - if(fabs(theta) > simcoon::iota) { - Lt_temp = rotateL(Lt_temp, theta, simcoon::axis_theta, false); - } - if(fabs(phi) > simcoon::iota) { - Lt_temp = rotateL(Lt_temp, phi, simcoon::axis_phi, false); - } - - return Lt_temp; -} - -//To rotate from local to global a localisation matrix (6,6) -mat rotate_l2g_M(const mat &M, const double &psi, const double &theta, const double &phi) { - - mat M_temp = M; - if(fabs(phi) > simcoon::iota) { - M_temp = rotateM(M_temp, -phi, simcoon::axis_phi, false); - } - if(fabs(theta) > simcoon::iota) { - M_temp = rotateM(M_temp, -theta, simcoon::axis_theta, false); - } - if(fabs(psi) > simcoon::iota) { - M_temp = rotateM(M_temp, -psi, simcoon::axis_psi, false); - } - - return M_temp; -} - -//To rotate from global to local a localisation matrix (6,6) -mat rotate_g2l_M(const mat &M, const double &psi, const double &theta, const double &phi) { - - mat M_temp = M; - if(fabs(psi) > simcoon::iota) { - M_temp = rotateM(M_temp, psi, simcoon::axis_psi, false); - } - if(fabs(theta) > simcoon::iota) { - M_temp = rotateM(M_temp, theta, simcoon::axis_theta, false); - } - if(fabs(phi) > simcoon::iota) { - M_temp = rotateM(M_temp, phi, simcoon::axis_phi, false); - } - - return M_temp; -} - -//To rotate from local to global a strain localisation matrix (6,6) -mat rotate_l2g_A(const mat &A, const double &psi, const double &theta, const double &phi) { - - mat A_temp = A; - if(fabs(phi) > simcoon::iota) { - A_temp = rotateA(A_temp, -phi, simcoon::axis_phi, false); - } - if(fabs(theta) > simcoon::iota) { - A_temp = rotateA(A_temp, -theta, simcoon::axis_theta, false); - } - if(fabs(psi) > simcoon::iota) { - A_temp = rotateA(A_temp, -psi, simcoon::axis_psi, false); - } - - return A_temp; -} - -//To rotate from global to local a strain localisation matrix (6,6) -mat rotate_g2l_A(const mat &A, const double &psi, const double &theta, const double &phi) { - - mat A_temp = A; - if(fabs(psi) > simcoon::iota) { - A_temp = rotateA(A_temp, psi, simcoon::axis_psi, false); - } - if(fabs(theta) > simcoon::iota) { - A_temp = rotateA(A_temp, theta, simcoon::axis_theta, false); - } - if(fabs(phi) > simcoon::iota) { - A_temp = rotateA(A_temp, phi, simcoon::axis_phi, false); - } - - return A_temp; -} - -//To rotate from local to global a stress localisation matrix (6,6) -mat rotate_l2g_B(const mat &B, const double &psi, const double &theta, const double &phi) { - - mat B_temp = B; - if(fabs(phi) > simcoon::iota) { - B_temp = rotateB(B_temp, -phi, simcoon::axis_phi, false); - } - if(fabs(theta) > simcoon::iota) { - B_temp = rotateB(B_temp, -theta, simcoon::axis_theta, false); - } - if(fabs(psi) > simcoon::iota) { - B_temp = rotateB(B_temp, -psi, simcoon::axis_psi, false); - } - - return B_temp; -} - -//To rotate from global to local a stress localisation matrix (6,6) -mat rotate_g2l_B(const mat &B, const double &psi, const double &theta, const double &phi) { - - mat B_temp = B; - if(fabs(psi) > simcoon::iota) { - B_temp = rotateB(B_temp, psi, simcoon::axis_psi, false); - } - if(fabs(theta) > simcoon::iota) { - B_temp = rotateB(B_temp, theta, simcoon::axis_theta, false); - } - if(fabs(phi) > simcoon::iota) { - B_temp = rotateB(B_temp, phi, simcoon::axis_phi, false); - } - - return B_temp; -} - + mat ve = fill_voigt_strain(DR, active); + return ve*V; +} + +mat rotate_stiffness(const mat &L, const double &alpha, const int &axis, const bool &active) { + mat vs = fill_voigt_stress(alpha, axis, active); + return vs*(L*trans(vs)); +} + +mat rotate_stiffness(const mat &L, const mat &DR, const bool &active) { + mat vs = fill_voigt_stress(DR, active); + return vs*(L*trans(vs)); +} + +mat rotate_compliance(const mat &M, const double &alpha, const int &axis, const bool &active) { + mat ve = fill_voigt_strain(alpha, axis, active); + return ve*(M*trans(ve)); +} + +mat rotate_compliance(const mat &M, const mat &DR, const bool &active) { + mat ve = fill_voigt_strain(DR, active); + return ve*(M*trans(ve)); +} + +mat rotate_strain_concentration(const mat &A, const double &alpha, const int &axis, const bool &active) { + mat ve = fill_voigt_strain(alpha, axis, active); + mat vs = fill_voigt_stress(alpha, axis, active); + return ve*(A*trans(vs)); +} + +mat rotate_strain_concentration(const mat &A, const mat &DR, const bool &active) { + mat ve = fill_voigt_strain(DR, active); + mat vs = fill_voigt_stress(DR, active); + return ve*(A*trans(vs)); +} + +mat rotate_stress_concentration(const mat &B, const double &alpha, const int &axis, const bool &active) { + mat ve = fill_voigt_strain(alpha, axis, active); + mat vs = fill_voigt_stress(alpha, axis, active); + return vs*(B*trans(ve)); +} + +mat rotate_stress_concentration(const mat &B, const mat &DR, const bool &active) { + mat ve = fill_voigt_strain(DR, active); + mat vs = fill_voigt_stress(DR, active); + return vs*(B*trans(ve)); +} + } //namespace simcoon diff --git a/src/Simulation/Maths/rotation_class.cpp b/src/Simulation/Maths/rotation_class.cpp deleted file mode 100644 index dbe05b241..000000000 --- a/src/Simulation/Maths/rotation_class.cpp +++ /dev/null @@ -1,666 +0,0 @@ -/* This file is part of simcoon. - - simcoon is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - simcoon 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with simcoon. If not, see . - - */ - -///@file rotation_class.cpp -///@brief Implementation of the Rotation class -///@version 1.0 - -#include -#include -#include -#include -#include -#include - -using namespace std; -using namespace arma; - -namespace simcoon { - -// ============================================================================= -// Helper functions (internal) -// ============================================================================= - -namespace { - -// Convert axis number (1,2,3) to axis index (0,1,2) -inline int axis_to_index(int axis) { - if (axis < 1 || axis > 3) { - throw invalid_argument("Axis must be 1 (x), 2 (y), or 3 (z)"); - } - return axis - 1; -} - -// Parse Euler convention string to get axis sequence -// Returns axes as 0=x, 1=y, 2=z -void parse_euler_convention(const string& conv, int& axis1, int& axis2, int& axis3) { - if (conv.length() != 3) { - throw invalid_argument("Euler convention must be 3 characters (e.g., 'zxz', 'xyz')"); - } - - auto char_to_axis = [](char c) -> int { - switch (c) { - case 'x': case 'X': return 0; - case 'y': case 'Y': return 1; - case 'z': case 'Z': return 2; - default: - throw invalid_argument("Invalid axis character in Euler convention"); - } - }; - - axis1 = char_to_axis(conv[0]); - axis2 = char_to_axis(conv[1]); - axis3 = char_to_axis(conv[2]); -} - -// Normalize a quaternion to unit length -vec::fixed<4> normalize_quat(const vec::fixed<4>& q) { - double n = norm(q); - if (n < simcoon::iota) { - // Return identity quaternion if input is near zero - return {0.0, 0.0, 0.0, 1.0}; - } - return q / n; -} - -// Hamilton product of two quaternions (scalar-last convention) -// q = [qx, qy, qz, qw] -vec::fixed<4> quat_multiply(const vec::fixed<4>& q1, const vec::fixed<4>& q2) { - double x1 = q1(0), y1 = q1(1), z1 = q1(2), w1 = q1(3); - double x2 = q2(0), y2 = q2(1), z2 = q2(2), w2 = q2(3); - - return { - w1*x2 + x1*w2 + y1*z2 - z1*y2, // x - w1*y2 - x1*z2 + y1*w2 + z1*x2, // y - w1*z2 + x1*y2 - y1*x2 + z1*w2, // z - w1*w2 - x1*x2 - y1*y2 - z1*z2 // w - }; -} - -// Quaternion conjugate (inverse for unit quaternion) -vec::fixed<4> quat_conjugate(const vec::fixed<4>& q) { - return {-q(0), -q(1), -q(2), q(3)}; -} - -} // anonymous namespace - -// ============================================================================= -// Constructors -// ============================================================================= - -Rotation::Rotation() : _quat({0.0, 0.0, 0.0, 1.0}) { - // Identity rotation -} - -Rotation::Rotation(const vec::fixed<4>& quat) : _quat(quat) { - // Private constructor - assumes quaternion is already normalized -} - -// ============================================================================= -// Factory Methods -// ============================================================================= - -Rotation Rotation::identity() { - return Rotation(); -} - -Rotation Rotation::from_quat(const vec::fixed<4>& quat) { - return Rotation(normalize_quat(quat)); -} - -Rotation Rotation::from_quat(const vec& quat) { - if (quat.n_elem != 4) { - throw invalid_argument("Quaternion must have 4 elements"); - } - vec::fixed<4> q_fixed = {quat(0), quat(1), quat(2), quat(3)}; - return from_quat(q_fixed); -} - -Rotation Rotation::from_matrix(const mat::fixed<3,3>& R) { - // Shepperd's method for numerical stability - // Reference: S.W. Shepperd, "Quaternion from Rotation Matrix" - // Journal of Guidance and Control, Vol. 1, No. 3, 1978 - - double trace = R(0,0) + R(1,1) + R(2,2); - vec::fixed<4> q; - - if (trace > 0) { - double s = 0.5 / sqrt(trace + 1.0); - q(3) = 0.25 / s; - q(0) = (R(2,1) - R(1,2)) * s; - q(1) = (R(0,2) - R(2,0)) * s; - q(2) = (R(1,0) - R(0,1)) * s; - } else if (R(0,0) > R(1,1) && R(0,0) > R(2,2)) { - double s = 2.0 * sqrt(1.0 + R(0,0) - R(1,1) - R(2,2)); - q(3) = (R(2,1) - R(1,2)) / s; - q(0) = 0.25 * s; - q(1) = (R(0,1) + R(1,0)) / s; - q(2) = (R(0,2) + R(2,0)) / s; - } else if (R(1,1) > R(2,2)) { - double s = 2.0 * sqrt(1.0 + R(1,1) - R(0,0) - R(2,2)); - q(3) = (R(0,2) - R(2,0)) / s; - q(0) = (R(0,1) + R(1,0)) / s; - q(1) = 0.25 * s; - q(2) = (R(1,2) + R(2,1)) / s; - } else { - double s = 2.0 * sqrt(1.0 + R(2,2) - R(0,0) - R(1,1)); - q(3) = (R(1,0) - R(0,1)) / s; - q(0) = (R(0,2) + R(2,0)) / s; - q(1) = (R(1,2) + R(2,1)) / s; - q(2) = 0.25 * s; - } - - return Rotation(normalize_quat(q)); -} - -Rotation Rotation::from_matrix(const mat& R) { - if (R.n_rows != 3 || R.n_cols != 3) { - throw invalid_argument("Rotation matrix must be 3x3"); - } - mat::fixed<3,3> R_fixed; - R_fixed = R; - return from_matrix(R_fixed); -} - -Rotation Rotation::from_euler(double psi, double theta, double phi, - const string& conv, bool intrinsic, bool degrees) { - // Convert to radians if needed - if (degrees) { - psi *= simcoon::pi / 180.0; - theta *= simcoon::pi / 180.0; - phi *= simcoon::pi / 180.0; - } - - // Handle "user" convention using the existing fillR function - if (conv == "user") { - mat R = fillR(psi, theta, phi, true, "user"); - mat::fixed<3,3> R_fixed; - R_fixed = R; - return from_matrix(R_fixed); - } - - // Parse the convention - int axis1, axis2, axis3; - parse_euler_convention(conv, axis1, axis2, axis3); - - // Build quaternions for each elementary rotation - auto axis_quat = [](double angle, int axis) -> vec::fixed<4> { - double half_angle = angle / 2.0; - double s = sin(half_angle); - double c = cos(half_angle); - vec::fixed<4> q = {0, 0, 0, c}; - q(axis) = s; - return q; - }; - - vec::fixed<4> q1 = axis_quat(psi, axis1); - vec::fixed<4> q2 = axis_quat(theta, axis2); - vec::fixed<4> q3 = axis_quat(phi, axis3); - - // Compose quaternions - vec::fixed<4> result; - if (intrinsic) { - // Intrinsic: rotations about axes of the rotating frame - // Order: q3 * q2 * q1 (applied right-to-left) - result = quat_multiply(quat_multiply(q3, q2), q1); - } else { - // Extrinsic: rotations about fixed axes - // Order: q1 * q2 * q3 (applied left-to-right) - result = quat_multiply(quat_multiply(q1, q2), q3); - } - - return Rotation(normalize_quat(result)); -} - -Rotation Rotation::from_euler(const vec::fixed<3>& angles, const string& conv, - bool intrinsic, bool degrees) { - return from_euler(angles(0), angles(1), angles(2), conv, intrinsic, degrees); -} - -Rotation Rotation::from_rotvec(const vec::fixed<3>& rotvec, bool degrees) { - double angle = norm(rotvec); - - if (degrees) { - angle *= simcoon::pi / 180.0; - } - - if (angle < simcoon::iota) { - return identity(); - } - - vec::fixed<3> axis = rotvec / (degrees ? norm(rotvec) * 180.0 / simcoon::pi : norm(rotvec)); - double half_angle = angle / 2.0; - double s = sin(half_angle); - double c = cos(half_angle); - - return Rotation({axis(0)*s, axis(1)*s, axis(2)*s, c}); -} - -Rotation Rotation::from_rotvec(const vec& rotvec, bool degrees) { - if (rotvec.n_elem != 3) { - throw invalid_argument("Rotation vector must have 3 elements"); - } - vec::fixed<3> rv_fixed = {rotvec(0), rotvec(1), rotvec(2)}; - return from_rotvec(rv_fixed, degrees); -} - -Rotation Rotation::from_axis_angle(double angle, int axis, bool degrees) { - if (degrees) { - angle *= simcoon::pi / 180.0; - } - - int ax_idx = axis_to_index(axis); - double half_angle = angle / 2.0; - double s = sin(half_angle); - double c = cos(half_angle); - - vec::fixed<4> q = {0, 0, 0, c}; - q(ax_idx) = s; - - return Rotation(q); -} - -Rotation Rotation::random() { - // Generate uniformly distributed random rotation using Shoemake's method - // Reference: K. Shoemake, "Uniform random rotations", Graphics Gems III, 1992 - - vec u = randu(3); - - double sqrt1_u0 = sqrt(1.0 - u(0)); - double sqrt_u0 = sqrt(u(0)); - double two_pi_u1 = 2.0 * simcoon::pi * u(1); - double two_pi_u2 = 2.0 * simcoon::pi * u(2); - - vec::fixed<4> q = { - sqrt1_u0 * sin(two_pi_u1), - sqrt1_u0 * cos(two_pi_u1), - sqrt_u0 * sin(two_pi_u2), - sqrt_u0 * cos(two_pi_u2) - }; - - return Rotation(q); -} - -// ============================================================================= -// Conversion Methods -// ============================================================================= - -mat::fixed<3,3> Rotation::as_matrix() const { - double qx = _quat(0), qy = _quat(1), qz = _quat(2), qw = _quat(3); - - // Precompute products - double qx2 = qx * qx, qy2 = qy * qy, qz2 = qz * qz; - double qxqy = qx * qy, qxqz = qx * qz, qxqw = qx * qw; - double qyqz = qy * qz, qyqw = qy * qw, qzqw = qz * qw; - - mat::fixed<3,3> R; - R(0,0) = 1.0 - 2.0*(qy2 + qz2); - R(0,1) = 2.0*(qxqy - qzqw); - R(0,2) = 2.0*(qxqz + qyqw); - R(1,0) = 2.0*(qxqy + qzqw); - R(1,1) = 1.0 - 2.0*(qx2 + qz2); - R(1,2) = 2.0*(qyqz - qxqw); - R(2,0) = 2.0*(qxqz - qyqw); - R(2,1) = 2.0*(qyqz + qxqw); - R(2,2) = 1.0 - 2.0*(qx2 + qy2); - - return R; -} - -vec::fixed<3> Rotation::as_euler(const string& conv, bool intrinsic, bool degrees) const { - mat::fixed<3,3> R = as_matrix(); - - // Parse the convention - int axis1, axis2, axis3; - - if (conv == "user") { - // Use default axes from parameter.hpp - axis1 = axis_psi - 1; // Convert 1-indexed to 0-indexed - axis2 = axis_theta - 1; - axis3 = axis_phi - 1; - } else { - parse_euler_convention(conv, axis1, axis2, axis3); - } - - // For extrinsic, we transpose R and swap axis order - mat::fixed<3,3> R_work = intrinsic ? R : R.t(); - if (!intrinsic) { - swap(axis1, axis3); - } - - vec::fixed<3> angles; - - // Determine if proper Euler (axis1 == axis3) or Tait-Bryan (axis1 != axis3) - bool proper_euler = (axis1 == axis3); - - // Find the "other" axis for proper Euler, or compute for Tait-Bryan - int i = axis1; - int j = axis2; - int k = proper_euler ? (3 - i - j) : axis3; // For proper Euler, k is the third axis - - // Sign based on cyclic permutation - // Cyclic: (0,1,2), (1,2,0), (2,0,1) give +1 - // Anti-cyclic: (0,2,1), (2,1,0), (1,0,2) give -1 - double sign = ((j - i + 3) % 3 == 1) ? 1.0 : -1.0; - - if (proper_euler) { - // Proper Euler angles (e.g., zxz, zyz) - double sy = sqrt(R_work(i,j)*R_work(i,j) + R_work(i,k)*R_work(i,k)); - - if (sy > simcoon::iota) { - angles(0) = atan2(R_work(i,j), sign*R_work(i,k)); - angles(1) = atan2(sy, R_work(i,i)); - angles(2) = atan2(R_work(j,i), -sign*R_work(k,i)); - } else { - // Gimbal lock - angles(0) = atan2(-sign*R_work(j,k), R_work(j,j)); - angles(1) = atan2(sy, R_work(i,i)); - angles(2) = 0.0; - } - } else { - // Tait-Bryan angles (e.g., xyz, zyx) - double cy = sqrt(R_work(i,i)*R_work(i,i) + R_work(j,i)*R_work(j,i)); - - if (cy > simcoon::iota) { - angles(0) = atan2(sign*R_work(k,j), R_work(k,k)); - angles(1) = atan2(-sign*R_work(k,i), cy); - angles(2) = atan2(sign*R_work(j,i), R_work(i,i)); - } else { - // Gimbal lock - angles(0) = atan2(-sign*R_work(j,k), R_work(j,j)); - angles(1) = atan2(-sign*R_work(k,i), cy); - angles(2) = 0.0; - } - } - - // For extrinsic, swap back - if (!intrinsic) { - swap(angles(0), angles(2)); - } - - if (degrees) { - angles *= 180.0 / simcoon::pi; - } - - return angles; -} - -vec::fixed<3> Rotation::as_rotvec(bool degrees) const { - // Get rotation angle from quaternion - double qw = _quat(3); - vec::fixed<3> qv = {_quat(0), _quat(1), _quat(2)}; - - double sin_half = norm(qv); - - if (sin_half < simcoon::iota) { - // Near identity rotation - vec::fixed<3> result = {0.0, 0.0, 0.0}; - return result; - } - - // Angle = 2 * atan2(||qv||, qw) - double angle = 2.0 * atan2(sin_half, qw); - - // Normalize to [-pi, pi] - if (angle > simcoon::pi) { - angle -= 2.0 * simcoon::pi; - } else if (angle < -simcoon::pi) { - angle += 2.0 * simcoon::pi; - } - - // Axis is normalized qv - vec::fixed<3> axis = qv / sin_half; - - if (degrees) { - angle *= 180.0 / simcoon::pi; - } - - return axis * angle; -} - -mat::fixed<6,6> Rotation::as_voigt_stress_rotation(bool active) const { - mat R = as_matrix(); - mat QS_dyn = fillQS(R, active); - mat::fixed<6,6> QS; - QS = QS_dyn; - return QS; -} - -mat::fixed<6,6> Rotation::as_voigt_strain_rotation(bool active) const { - mat R = as_matrix(); - mat QE_dyn = fillQE(R, active); - mat::fixed<6,6> QE; - QE = QE_dyn; - return QE; -} - -// ============================================================================= -// Apply Methods (3D objects) -// ============================================================================= - -vec::fixed<3> Rotation::apply(const vec::fixed<3>& v, bool inverse) const { - // Direct quaternion-vector rotation using Rodrigues formula - // v' = v + 2*qw*(qv × v) + 2*(qv × (qv × v)) - // This is equivalent to q * v * q^(-1) but more efficient - - double qx = _quat(0), qy = _quat(1), qz = _quat(2), qw = _quat(3); - - if (inverse) { - // For inverse, negate the vector part of quaternion - qx = -qx; qy = -qy; qz = -qz; - } - - vec::fixed<3> qv = {qx, qy, qz}; - vec::fixed<3> uv = cross(qv, v); - vec::fixed<3> uuv = cross(qv, uv); - - return v + 2.0 * (qw * uv + uuv); -} - -vec Rotation::apply(const vec& v, bool inverse) const { - if (v.n_elem != 3) { - throw invalid_argument("Vector must have 3 elements"); - } - vec::fixed<3> v_fixed = {v(0), v(1), v(2)}; - vec::fixed<3> result = apply(v_fixed, inverse); - return vec(result); -} - -mat::fixed<3,3> Rotation::apply_tensor(const mat::fixed<3,3>& m, bool inverse) const { - mat::fixed<3,3> R = as_matrix(); - if (inverse) { - return R.t() * m * R; - } else { - return R * m * R.t(); - } -} - -mat Rotation::apply_tensor(const mat& m, bool inverse) const { - if (m.n_rows != 3 || m.n_cols != 3) { - throw invalid_argument("Tensor must be 3x3"); - } - mat::fixed<3,3> m_fixed; - m_fixed = m; - mat::fixed<3,3> result = apply_tensor(m_fixed, inverse); - return mat(result); -} - -// ============================================================================= -// Apply Methods (Voigt notation) -// ============================================================================= - -vec::fixed<6> Rotation::apply_stress(const vec::fixed<6>& sigma, bool active) const { - mat::fixed<6,6> QS = as_voigt_stress_rotation(active); - vec::fixed<6> result; - result = QS * sigma; - return result; -} - -vec Rotation::apply_stress(const vec& sigma, bool active) const { - if (sigma.n_elem != 6) { - throw invalid_argument("Stress vector must have 6 elements"); - } - mat R = as_matrix(); - return rotate_stress(sigma, R, active); -} - -vec::fixed<6> Rotation::apply_strain(const vec::fixed<6>& epsilon, bool active) const { - mat::fixed<6,6> QE = as_voigt_strain_rotation(active); - vec::fixed<6> result; - result = QE * epsilon; - return result; -} - -vec Rotation::apply_strain(const vec& epsilon, bool active) const { - if (epsilon.n_elem != 6) { - throw invalid_argument("Strain vector must have 6 elements"); - } - mat R = as_matrix(); - return rotate_strain(epsilon, R, active); -} - -mat::fixed<6,6> Rotation::apply_stiffness(const mat::fixed<6,6>& L, bool active) const { - mat::fixed<6,6> QS = as_voigt_stress_rotation(active); - mat::fixed<6,6> result; - result = QS * L * QS.t(); - return result; -} - -mat Rotation::apply_stiffness(const mat& L, bool active) const { - if (L.n_rows != 6 || L.n_cols != 6) { - throw invalid_argument("Stiffness matrix must be 6x6"); - } - mat R = as_matrix(); - return rotateL(L, R, active); -} - -mat::fixed<6,6> Rotation::apply_compliance(const mat::fixed<6,6>& M, bool active) const { - mat::fixed<6,6> QE = as_voigt_strain_rotation(active); - mat::fixed<6,6> result; - result = QE * M * QE.t(); - return result; -} - -mat Rotation::apply_compliance(const mat& M, bool active) const { - if (M.n_rows != 6 || M.n_cols != 6) { - throw invalid_argument("Compliance matrix must be 6x6"); - } - mat R = as_matrix(); - return rotateM(M, R, active); -} - -mat::fixed<6,6> Rotation::apply_localization_strain(const mat::fixed<6,6>& A, bool active) const { - mat R = as_matrix(); - mat A_dyn = rotateA(mat(A), R, active); - mat::fixed<6,6> result; - result = A_dyn; - return result; -} - -mat Rotation::apply_localization_strain(const mat& A, bool active) const { - if (A.n_rows != 6 || A.n_cols != 6) { - throw invalid_argument("Strain localization tensor must be 6x6"); - } - mat R = as_matrix(); - return rotateA(A, R, active); -} - -mat::fixed<6,6> Rotation::apply_localization_stress(const mat::fixed<6,6>& B, bool active) const { - mat R = as_matrix(); - mat B_dyn = rotateB(mat(B), R, active); - mat::fixed<6,6> result; - result = B_dyn; - return result; -} - -mat Rotation::apply_localization_stress(const mat& B, bool active) const { - if (B.n_rows != 6 || B.n_cols != 6) { - throw invalid_argument("Stress localization tensor must be 6x6"); - } - mat R = as_matrix(); - return rotateB(B, R, active); -} - -// ============================================================================= -// Operations -// ============================================================================= - -Rotation Rotation::inv() const { - return Rotation(quat_conjugate(_quat)); -} - -double Rotation::magnitude(bool degrees) const { - // Rotation angle = 2 * acos(|qw|) - double angle = 2.0 * acos(min(abs(_quat(3)), 1.0)); - - if (degrees) { - angle *= 180.0 / simcoon::pi; - } - - return angle; -} - -Rotation Rotation::operator*(const Rotation& other) const { - return Rotation(normalize_quat(quat_multiply(_quat, other._quat))); -} - -Rotation& Rotation::operator*=(const Rotation& other) { - _quat = normalize_quat(quat_multiply(_quat, other._quat)); - return *this; -} - -Rotation Rotation::slerp(const Rotation& other, double t) const { - // Spherical linear interpolation - // Reference: K. Shoemake, "Animating rotation with quaternion curves", SIGGRAPH 1985 - - vec::fixed<4> q1 = _quat; - vec::fixed<4> q2 = other._quat; - - // Compute dot product - double dot = arma::dot(q1, q2); - - // If dot is negative, negate one quaternion to take shorter path - if (dot < 0.0) { - q2 = -q2; - dot = -dot; - } - - // Clamp dot to valid range - dot = min(dot, 1.0); - - vec::fixed<4> result; - - if (dot > 0.9995) { - // Quaternions are very close, use linear interpolation - result = q1 + t * (q2 - q1); - } else { - // Standard SLERP - double theta_0 = acos(dot); - double theta = theta_0 * t; - double sin_theta = sin(theta); - double sin_theta_0 = sin(theta_0); - - double s0 = cos(theta) - dot * sin_theta / sin_theta_0; - double s1 = sin_theta / sin_theta_0; - - result = s0 * q1 + s1 * q2; - } - - return Rotation(normalize_quat(result)); -} - -} // namespace simcoon From 96c71ebdc00b5e356abeaaf4061d9ac2c54c1a5f Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 15:10:33 +0100 Subject: [PATCH 11/31] Migrate to Rotation class API for transforms Replace legacy rotation helpers with the new Rotation class methods across the codebase and tests. Updates include: using Rotation::from_euler / from_matrix and its apply_* helpers (apply_stiffness, apply_compliance, apply_strain/apply_stress, apply_strain_concentration/apply_stress_concentration) instead of free functions like rotateL, rotateM, rotateA, rotate_g2l_L, rotate_g2l_stress, rotate_l2g_A, fillR, fillQE/QS, etc. Remove includes of rotation_class.hpp where no longer needed. Adjusted unit tests to construct expected rotations via the Rotation API and to validate transformations by applying rotations to vectors/matrices rather than calling the old utility functions. Changes touch continuum mechanics, homogenization, micromechanics schemes, phase state variables, and math tests to unify on the new rotation interface and improve consistency. --- .../Functions/recovery_props.cpp | 12 +-- .../Homogenization/ellipsoid_multi.cpp | 7 +- .../Micromechanics/schemes.cpp | 24 +++-- src/Simulation/Phase/state_variables.cpp | 1 - src/Simulation/Phase/state_variables_M.cpp | 1 - src/Simulation/Phase/state_variables_T.cpp | 1 - test/Libraries/Maths/Trotation.cpp | 32 +++--- test/Libraries/Maths/Trotation_class.cpp | 101 ++++++++++-------- 8 files changed, 98 insertions(+), 81 deletions(-) diff --git a/src/Continuum_mechanics/Functions/recovery_props.cpp b/src/Continuum_mechanics/Functions/recovery_props.cpp index c77041af4..41bf049ee 100755 --- a/src/Continuum_mechanics/Functions/recovery_props.cpp +++ b/src/Continuum_mechanics/Functions/recovery_props.cpp @@ -58,29 +58,29 @@ void check_symetries(const mat &L, std::string &umat_type, int &axis, vec &props //Check reflexions //reflexion around the z axis - symmetry plane xy Q = { {1,0,0}, {0,1,0}, {0,0,-1} }; - L_test = rotateL(L_sym, Q); + L_test = rotate_stiffness(L_sym, Q); check_sym(2,0) = norm(L_sym - L_test,2); //reflexion around the y axis - symmetry plane xz Q = { {1,0,0}, {0,-1,0}, {0,0,1} }; - L_test = rotateL(L_sym, Q); + L_test = rotate_stiffness(L_sym, Q); check_sym(1,0) = norm(L_sym - L_test,2); //reflexion around the x axis - symmetry plane yz Q = { {-1,0,0}, {0,1,0}, {0,0,1} }; - L_test = rotateL(L_sym, Q); + L_test = rotate_stiffness(L_sym, Q); check_sym(0,0) = norm(L_sym - L_test,2); //Check 90deg rotations //90 rotation around the z axis Q = { {0,1,0}, {-1,0,0}, {0,0,1} }; - L_test = rotateL(L_sym, Q); + L_test = rotate_stiffness(L_sym, Q); check_sym(2,1) = norm(L_sym - L_test,2); //90 rotation around the y axis Q = { {0,0,1}, {0,1,0}, {-1,0,0} }; - L_test = rotateL(L_sym, Q); + L_test = rotate_stiffness(L_sym, Q); check_sym(1,1) = norm(L_sym - L_test,2); //90 rotation around the x axis Q = { {1,0,0}, {0,0,1}, {0,-1,0} }; - L_test = rotateL(L_sym, Q); + L_test = rotate_stiffness(L_sym, Q); check_sym(0,1) = norm(L_sym - L_test,2); //Check equality between constants diff --git a/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp b/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp index 8bfeffe32..3928b1964 100755 --- a/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp +++ b/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -144,7 +143,7 @@ void ellipsoid_multi::fillT(const mat& Lt_m, const mat& Lt, const ellipsoid &ell cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside ellipsoid_multi::fillT."); } - T = rot.apply_localization_strain(T_loc, true); + T = rot.apply_strain_concentration(T_loc, true); } //------------------------------------- @@ -164,7 +163,7 @@ void ellipsoid_multi::fillT_iso(const mat& Lt_m, const mat& Lt, const ellipsoid throw simcoon::exception_inv("Error in inv function inside ellipsoid_multi::fillT_is."); } - T = rot.apply_localization_strain(T_loc, true); + T = rot.apply_strain_concentration(T_loc, true); } //------------------------------------- @@ -186,7 +185,7 @@ void ellipsoid_multi::fillT_mec_in(const mat& L_m, const mat& L, const ellipsoid throw simcoon::exception_inv("Error in inv function inside ellipsoid_multi::fillT_mec_in."); } - T = rot.apply_localization_strain(T_loc, true); + T = rot.apply_strain_concentration(T_loc, true); T_in = rot.apply_compliance(T_in_loc, true); } diff --git a/src/Continuum_mechanics/Micromechanics/schemes.cpp b/src/Continuum_mechanics/Micromechanics/schemes.cpp index e91c7c86d..eaab628d3 100755 --- a/src/Continuum_mechanics/Micromechanics/schemes.cpp +++ b/src/Continuum_mechanics/Micromechanics/schemes.cpp @@ -386,8 +386,9 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); lay_multi = std::dynamic_pointer_cast(r.sptr_multi); lay = std::dynamic_pointer_cast(r.sptr_shape); - Lt_loc = rotate_g2l_L(sv_r->Lt, lay->psi_geom, lay->theta_geom, lay->phi_geom); - + Rotation rot_layer = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); + Lt_loc = rot_layer.inv().apply_stiffness(sv_r->Lt); + lay_multi->Dnn(0,0) = Lt_loc(0,0); lay_multi->Dnn(0,1) = Lt_loc(0,3); lay_multi->Dnn(0,2) = Lt_loc(0,4); @@ -397,8 +398,8 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { lay_multi->Dnn(2,0) = Lt_loc(4,0); lay_multi->Dnn(2,1) = Lt_loc(4,3); lay_multi->Dnn(2,2) = Lt_loc(4,4); - - mat sigma_local = rotate_g2l_stress(sv_r->sigma, lay->psi_geom, lay->theta_geom, lay->phi_geom); + + vec sigma_local = rot_layer.inv().apply_stress(sv_r->sigma); lay_multi->sigma_hat(0) = sigma_local(0); lay_multi->sigma_hat(1) = sigma_local(3); lay_multi->sigma_hat(2) = sigma_local(4); @@ -449,11 +450,12 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { } lay = std::dynamic_pointer_cast(r.sptr_shape); - mat dEtot_local = zeros(6); + vec dEtot_local = zeros(6); dEtot_local(0) = lay_multi->dzdx1(0); dEtot_local(3) = lay_multi->dzdx1(1); dEtot_local(4) = lay_multi->dzdx1(2); - mat dEtot_global = rotate_l2g_strain(dEtot_local, lay->psi_geom, lay->theta_geom, lay->phi_geom); + Rotation rot_l2g = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); + vec dEtot_global = rot_l2g.apply_strain(dEtot_local); sv_r->DEtot += dEtot_global; } @@ -475,7 +477,10 @@ void Lt_Periodic_Layer(phase_characteristics &phase) { sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); lay_multi = std::dynamic_pointer_cast(r.sptr_multi); lay = std::dynamic_pointer_cast(r.sptr_shape); - Lt_loc = rotate_g2l_L(sv_r->Lt, lay->psi_geom, lay->theta_geom, lay->phi_geom); + { + Rotation rot = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); + Lt_loc = rot.inv().apply_stiffness(sv_r->Lt); + } lay_multi->Dnn(0,0) = Lt_loc(0,0); lay_multi->Dnn(0,1) = Lt_loc(0,3); @@ -558,7 +563,10 @@ void Lt_Periodic_Layer(phase_characteristics &phase) { A_loc(4,4) += lay_multi->dXn(2,2); A_loc(4,5) += lay_multi->dXt(2,2); - lay_multi->A = rotate_l2g_A(A_loc, lay->psi_geom, lay->theta_geom, lay->phi_geom); + { + Rotation rot = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); + lay_multi->A = rot.apply_strain_concentration(A_loc); + } } } diff --git a/src/Simulation/Phase/state_variables.cpp b/src/Simulation/Phase/state_variables.cpp index 83c809d2d..7b8730357 100755 --- a/src/Simulation/Phase/state_variables.cpp +++ b/src/Simulation/Phase/state_variables.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include diff --git a/src/Simulation/Phase/state_variables_M.cpp b/src/Simulation/Phase/state_variables_M.cpp index ea554ebfa..e4f00ba0a 100755 --- a/src/Simulation/Phase/state_variables_M.cpp +++ b/src/Simulation/Phase/state_variables_M.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include diff --git a/src/Simulation/Phase/state_variables_T.cpp b/src/Simulation/Phase/state_variables_T.cpp index 825485af2..d86b414c1 100755 --- a/src/Simulation/Phase/state_variables_T.cpp +++ b/src/Simulation/Phase/state_variables_T.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include using namespace std; diff --git a/test/Libraries/Maths/Trotation.cpp b/test/Libraries/Maths/Trotation.cpp index d0c0b55c4..f888a7a34 100755 --- a/test/Libraries/Maths/Trotation.cpp +++ b/test/Libraries/Maths/Trotation.cpp @@ -46,10 +46,10 @@ TEST(Trotation, basic_rotation) mat R_zxz = R3*R2*R1; mat R_zyz = R3*R2b*R1; - mat R_zxz_a = fillR(psi, theta, phi, true, "zxz"); - mat R_zxz_p = fillR(psi, theta, phi, false, "zxz"); - mat R_zyz_a = fillR(psi, theta, phi, true, "zyz"); - mat R_zyz_p = fillR(psi, theta, phi, false, "zyz"); + mat R_zxz_a = mat(Rotation::from_euler(psi, theta, phi, "zxz", true).as_matrix()); + mat R_zxz_p = mat(Rotation::from_euler(psi, theta, phi, "zxz", true).as_matrix()).t(); + mat R_zyz_a = mat(Rotation::from_euler(psi, theta, phi, "zyz", true).as_matrix()); + mat R_zyz_p = mat(Rotation::from_euler(psi, theta, phi, "zyz", true).as_matrix()).t(); cout << "R_zxz" << R_zxz << endl; cout << "R_zxz_a" << R_zxz_a << endl; @@ -100,19 +100,19 @@ TEST(Trotation, rotation) //test of rotate A mat S_c = Eshelby_cylinder(0.12); - mat S_c1 = rotateA(S_c, psi, 3); - mat S_c2 = rotateA(S_c, theta, 1); - mat S_c3 = rotateA(S_c, phi, 3); + mat S_c1 = rotate_strain_concentration(S_c, psi, 3); + mat S_c2 = rotate_strain_concentration(S_c, theta, 1); + mat S_c3 = rotate_strain_concentration(S_c, phi, 3); - mat S_c11 = rotateA(S_c,R1); - mat S_c22 = rotateA(S_c,R2); - mat S_c33 = rotateA(S_c,R3); + mat S_c11 = rotate_strain_concentration(S_c,R1); + mat S_c22 = rotate_strain_concentration(S_c,R2); + mat S_c33 = rotate_strain_concentration(S_c,R3); - mat S_cR = rotateA(S_c, R1); - S_cR = rotateA(S_cR, R2); - S_cR = rotateA(S_cR, R3); + mat S_cR = rotate_strain_concentration(S_c, R1); + S_cR = rotate_strain_concentration(S_cR, R2); + S_cR = rotate_strain_concentration(S_cR, R3); - mat S_cRR = rotateA(S_c,R); + mat S_cRR = rotate_strain_concentration(S_c,R); EXPECT_LT( norm(S_c33-S_c3,2),1.E-9); EXPECT_LT( norm(S_c22-S_c2,2),1.E-9); @@ -129,8 +129,8 @@ TEST(Trotation, rotation) assert(axis_theta == 1); assert(axis_phi == 3); - mat Rp2 = fillR(psi,theta,phi, true, "user"); - mat Rp3 = fillR(psi,theta,phi, true, "zxz"); + mat Rp2 = mat(Rotation::from_euler(psi,theta,phi, "user", true).as_matrix()); + mat Rp3 = mat(Rotation::from_euler(psi,theta,phi, "zxz", true).as_matrix()); mat a1 = rotate_mat(a, R); mat a2 = rotate_mat(a, Rp2); diff --git a/test/Libraries/Maths/Trotation_class.cpp b/test/Libraries/Maths/Trotation_class.cpp index f1fe02f07..0a7cd4cb1 100644 --- a/test/Libraries/Maths/Trotation_class.cpp +++ b/test/Libraries/Maths/Trotation_class.cpp @@ -23,7 +23,6 @@ #include #include -#include #include using namespace std; @@ -80,17 +79,15 @@ TEST(TRotationClass, from_quat) TEST(TRotationClass, from_matrix) { - // Create a rotation matrix using existing fillR function + // Create a rotation matrix using Rotation class from_euler double psi = 0.5; double theta = 0.3; double phi = 0.7; - mat R_expected = fillR(psi, theta, phi, true, "zxz"); + Rotation r_ref = Rotation::from_euler(psi, theta, phi, "zxz"); + mat::fixed<3,3> R_expected = r_ref.as_matrix(); - mat::fixed<3,3> R_fixed; - R_fixed = R_expected; - - Rotation r = Rotation::from_matrix(R_fixed); + Rotation r = Rotation::from_matrix(R_expected); mat::fixed<3,3> R_actual = r.as_matrix(); EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); @@ -102,14 +99,16 @@ TEST(TRotationClass, from_euler_zxz) double theta = 42.0 * (simcoon::pi / 180.0); double phi = 165.0 * (simcoon::pi / 180.0); - // Use existing fillR to get expected result - mat R_expected = fillR(psi, theta, phi, true, "zxz"); + // Build reference from individual axis rotations (intrinsic zxz) + Rotation rz1 = Rotation::from_axis_angle(psi, 3); + Rotation rx = Rotation::from_axis_angle(theta, 1); + Rotation rz2 = Rotation::from_axis_angle(phi, 3); + Rotation r_ref = rz2 * rx * rz1; - // Create rotation using Rotation class + // Create rotation using from_euler Rotation r = Rotation::from_euler(psi, theta, phi, "zxz", true, false); - mat::fixed<3,3> R_actual = r.as_matrix(); - EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); + EXPECT_TRUE(r.equals(r_ref, 1.E-9)); } TEST(TRotationClass, from_euler_zyz) @@ -118,12 +117,15 @@ TEST(TRotationClass, from_euler_zyz) double theta = 42.0 * (simcoon::pi / 180.0); double phi = 165.0 * (simcoon::pi / 180.0); - mat R_expected = fillR(psi, theta, phi, true, "zyz"); + // Build reference from individual axis rotations (intrinsic zyz) + Rotation rz1 = Rotation::from_axis_angle(psi, 3); + Rotation ry = Rotation::from_axis_angle(theta, 2); + Rotation rz2 = Rotation::from_axis_angle(phi, 3); + Rotation r_ref = rz2 * ry * rz1; Rotation r = Rotation::from_euler(psi, theta, phi, "zyz", true, false); - mat::fixed<3,3> R_actual = r.as_matrix(); - EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); + EXPECT_TRUE(r.equals(r_ref, 1.E-9)); } TEST(TRotationClass, from_euler_degrees) @@ -143,7 +145,8 @@ TEST(TRotationClass, from_axis_angle) Rotation r = Rotation::from_axis_angle(angle, 3, false); mat::fixed<3,3> R_actual = r.as_matrix(); - mat R_expected = fillR(angle, 3, true); + // Build expected rotation matrix for z-axis rotation + mat R_expected = {{cos(angle), -sin(angle), 0}, {sin(angle), cos(angle), 0}, {0, 0, 1}}; EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); } @@ -155,11 +158,11 @@ TEST(TRotationClass, from_rotvec) vec::fixed<3> rotvec = {0, 0, angle}; Rotation r = Rotation::from_rotvec(rotvec, false); - mat::fixed<3,3> R_actual = r.as_matrix(); - mat R_expected = fillR(angle, 3, true); + // Should match axis-angle construction + Rotation r_expected = Rotation::from_axis_angle(angle, 3); - EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); + EXPECT_TRUE(r.equals(r_expected, 1.E-9)); } // ============================================================================= @@ -195,28 +198,34 @@ TEST(TRotationClass, as_rotvec_roundtrip) EXPECT_TRUE(r.equals(r2, 1.E-9)); } -TEST(TRotationClass, as_voigt_stress_rotation_vs_fillQS) +TEST(TRotationClass, as_voigt_stress_rotation) { - // Test that as_voigt_stress_rotation matches existing fillQS function + // Test that voigt stress rotation matrix correctly transforms stress vectors Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); - mat::fixed<3,3> R = r.as_matrix(); - mat::fixed<6,6> QS_class = r.as_voigt_stress_rotation(true); - mat QS_func = fillQS(mat(R), true); + mat::fixed<6,6> QS = r.as_voigt_stress_rotation(true); + vec::fixed<6> sigma = {100, 50, 0, 25, 0, 0}; + + // QS applied to stress should match apply_stress + vec::fixed<6> sigma_QS = QS * sigma; + vec::fixed<6> sigma_apply = r.apply_stress(sigma, true); - EXPECT_LT(norm(mat(QS_class) - QS_func, "fro"), 1.E-9); + EXPECT_LT(norm(sigma_QS - sigma_apply), 1.E-9); } -TEST(TRotationClass, as_voigt_strain_rotation_vs_fillQE) +TEST(TRotationClass, as_voigt_strain_rotation) { - // Test that as_voigt_strain_rotation matches existing fillQE function + // Test that voigt strain rotation matrix correctly transforms strain vectors Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); - mat::fixed<3,3> R = r.as_matrix(); - mat::fixed<6,6> QE_class = r.as_voigt_strain_rotation(true); - mat QE_func = fillQE(mat(R), true); + mat::fixed<6,6> QE = r.as_voigt_strain_rotation(true); + vec::fixed<6> epsilon = {0.01, -0.005, -0.005, 0.002, 0.001, 0.003}; + + // QE applied to strain should match apply_strain + vec::fixed<6> epsilon_QE = QE * epsilon; + vec::fixed<6> epsilon_apply = r.apply_strain(epsilon, true); - EXPECT_LT(norm(mat(QE_class) - QE_func, "fro"), 1.E-9); + EXPECT_LT(norm(epsilon_QE - epsilon_apply), 1.E-9); } // ============================================================================= @@ -290,7 +299,7 @@ TEST(TRotationClass, apply_stiffness_consistency) L_fixed = L; mat::fixed<6,6> L_class = r.apply_stiffness(L_fixed, true); - mat L_func = rotateL(L, mat(R), true); + mat L_func = rotate_stiffness(L, mat(R), true); EXPECT_LT(norm(mat(L_class) - L_func, "fro"), 1.E-9); } @@ -414,43 +423,43 @@ TEST(TRotationClass, apply_compliance_consistency) M_fixed = M; mat::fixed<6,6> M_class = r.apply_compliance(M_fixed, true); - mat M_func = rotateM(M, mat(R), true); + mat M_func = rotate_compliance(M, mat(R), true); EXPECT_LT(norm(mat(M_class) - M_func, "fro"), 1.E-9); } // ============================================================================= -// Apply Methods (Localization Tensors) +// Apply Methods (Concentration Tensors) // ============================================================================= -TEST(TRotationClass, apply_localization_strain_consistency) +TEST(TRotationClass, apply_strain_concentration_consistency) { Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); mat::fixed<3,3> R = r.as_matrix(); - // Create a strain localization tensor + // Create a strain concentration tensor mat A = eye(6, 6) + 0.1 * randu(6, 6); mat::fixed<6,6> A_fixed; A_fixed = A; - mat::fixed<6,6> A_class = r.apply_localization_strain(A_fixed, true); - mat A_func = rotateA(A, mat(R), true); + mat::fixed<6,6> A_class = r.apply_strain_concentration(A_fixed, true); + mat A_func = rotate_strain_concentration(A, mat(R), true); EXPECT_LT(norm(mat(A_class) - A_func, "fro"), 1.E-9); } -TEST(TRotationClass, apply_localization_stress_consistency) +TEST(TRotationClass, apply_stress_concentration_consistency) { Rotation r = Rotation::from_euler(0.5, 0.3, 0.7, "zxz"); mat::fixed<3,3> R = r.as_matrix(); - // Create a stress localization tensor + // Create a stress concentration tensor mat B = eye(6, 6) + 0.1 * randu(6, 6); mat::fixed<6,6> B_fixed; B_fixed = B; - mat::fixed<6,6> B_class = r.apply_localization_stress(B_fixed, true); - mat B_func = rotateB(B, mat(R), true); + mat::fixed<6,6> B_class = r.apply_stress_concentration(B_fixed, true); + mat B_func = rotate_stress_concentration(B, mat(R), true); EXPECT_LT(norm(mat(B_class) - B_func, "fro"), 1.E-9); } @@ -464,7 +473,9 @@ TEST(TRotationClass, from_axis_angle_x) double angle = simcoon::pi / 3.0; // 60 degrees around x Rotation r = Rotation::from_axis_angle(angle, 1); mat::fixed<3,3> R_actual = r.as_matrix(); - mat R_expected = fillR(angle, 1, true); + + // Expected: rotation around x-axis + mat R_expected = {{1, 0, 0}, {0, cos(angle), -sin(angle)}, {0, sin(angle), cos(angle)}}; EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); } @@ -474,7 +485,9 @@ TEST(TRotationClass, from_axis_angle_y) double angle = simcoon::pi / 5.0; // 36 degrees around y Rotation r = Rotation::from_axis_angle(angle, 2); mat::fixed<3,3> R_actual = r.as_matrix(); - mat R_expected = fillR(angle, 2, true); + + // Expected: rotation around y-axis + mat R_expected = {{cos(angle), 0, sin(angle)}, {0, 1, 0}, {-sin(angle), 0, cos(angle)}}; EXPECT_LT(norm(R_expected - R_actual, "fro"), 1.E-9); } From cde44c464789aa9d68eaed46f7b23024f24ea64d Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 16:53:52 +0100 Subject: [PATCH 12/31] Remove legacy rotation free-functions (Python & docs) Remove the legacy Python bindings and documentation for the standalone rotation free-functions and migrate examples/docs to use the Rotation class API. Changes: - Docs: updated docs/continuum_mechanics/functions/doc_rotation.rst and docs/cpp_api/simulation/rotation.rst to remove free-function sections and emphasize the Rotation class. - Examples: examples/continuum_mechanics/rotation.py rewritten to use simcoon.Rotation (from_axis_angle/from_euler and apply/apply_tensor/apply_stress/etc.) instead of the rotate_* free functions. - Python bindings: deleted simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp, include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp and src/python_wrappers/Libraries/Maths/rotation.cpp; removed their registration from python_module.cpp. - Build: simcoon-python-builder/CMakeLists.txt no longer compiles src/python_wrappers/Libraries/Maths/rotation.cpp. Rationale: consolidate the public Python API around the object-oriented Rotation class and remove duplicated/free-function Python wrappers while keeping the Rotation class binding and C++ API intact. --- .../functions/doc_rotation.rst | 51 +---- docs/cpp_api/simulation/rotation.rst | 54 +---- examples/continuum_mechanics/rotation.py | 112 ++++------ simcoon-python-builder/CMakeLists.txt | 1 - .../docs/Libraries/Maths/doc_rotation.hpp | 127 ------------ .../Libraries/Maths/rotation.hpp | 43 ---- .../Libraries/Maths/rotation.cpp | 195 ------------------ .../src/python_wrappers/python_module.cpp | 20 -- 8 files changed, 52 insertions(+), 551 deletions(-) delete mode 100644 simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp delete mode 100755 simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp delete mode 100755 simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index 0e585fe03..8066824db 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -2,7 +2,7 @@ The Rotation Library ==================== The rotation library provides comprehensive tools for 3D rotations in continuum mechanics. -Simcoon 2.0 introduces a powerful ``Rotation`` class alongside the existing rotation functions. +The ``Rotation`` class provides a powerful object-oriented interface for all rotation operations. .. contents:: Contents :local: @@ -302,55 +302,6 @@ Simcoon supports multiple Euler angle conventions: # Extrinsic XYZ (aerospace convention) r2 = smc.Rotation.from_euler(roll, pitch, yaw, "xyz", intrinsic=False) -Rotation Free Functions ------------------------ - -The following functions provide direct rotation operations without creating a ``Rotation`` object. - -Vector and Matrix Rotation -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: simcoon.rotate_vec_R - -.. autofunction:: simcoon.rotate_vec_angle - -.. autofunction:: simcoon.rotate_mat_R - -.. autofunction:: simcoon.rotate_mat_angle - -Stress and Strain Rotation -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: simcoon.rotate_stress_angle - -.. autofunction:: simcoon.rotate_stress_R - -.. autofunction:: simcoon.rotate_strain_angle - -.. autofunction:: simcoon.rotate_strain_R - -Stiffness and Compliance Rotation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: simcoon.rotate_stiffness_angle - -.. autofunction:: simcoon.rotate_stiffness_R - -.. autofunction:: simcoon.rotate_compliance_angle - -.. autofunction:: simcoon.rotate_compliance_R - -Strain and Stress Concentration Tensor Rotation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: simcoon.rotate_strain_concentration_angle - -.. autofunction:: simcoon.rotate_strain_concentration_R - -.. autofunction:: simcoon.rotate_stress_concentration_angle - -.. autofunction:: simcoon.rotate_stress_concentration_R - Practical Examples ------------------ diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst index 1620addee..c4d508204 100644 --- a/docs/cpp_api/simulation/rotation.rst +++ b/docs/cpp_api/simulation/rotation.rst @@ -3,8 +3,8 @@ The Rotation Module ================== The Rotation module provides comprehensive tools for 3D rotations in continuum mechanics -applications. It includes both a modern **Rotation class** (inspired by ``scipy.spatial.transform.Rotation``) -and **free functions** for tensor rotation in Voigt notation. +applications. It provides a modern **Rotation class** (inspired by ``scipy.spatial.transform.Rotation``) +for all rotation operations including tensor rotation in Voigt notation. .. contents:: Table of Contents :local: @@ -21,8 +21,7 @@ unit quaternions as the internal representation. This design provides: - **Multiple representations**: Easy conversion between quaternions, matrices, Euler angles, and rotation vectors - **Voigt notation support**: Direct application to stress/strain tensors and stiffness/compliance matrices -The existing free functions are preserved for backward compatibility and provide direct -operations on matrices and vectors. +The C++ free functions remain available for direct operations on matrices and vectors. The Rotation Class ================== @@ -237,8 +236,8 @@ Operations Rotation Free Functions ======================= -The following free functions provide direct rotation operations without creating a ``Rotation`` -object. They are preserved for backward compatibility and direct matrix operations. +The following C++ free functions provide direct rotation operations without creating a ``Rotation`` +object. They are used internally and available in the C++ API. Vector and Matrix Rotation -------------------------- @@ -300,43 +299,6 @@ Strain and Stress Concentration Tensor Rotation .. doxygenfunction:: simcoon::rotate_stress_concentration(const arma::mat&, const arma::mat&, const bool&) :project: simcoon -Python Functions -================ - -The following functions are available in Python for direct rotation operations: - -.. autofunction:: simcoon.rotate_vec_R - -.. autofunction:: simcoon.rotate_vec_angle - -.. autofunction:: simcoon.rotate_mat_R - -.. autofunction:: simcoon.rotate_mat_angle - -.. autofunction:: simcoon.rotate_stress_angle - -.. autofunction:: simcoon.rotate_stress_R - -.. autofunction:: simcoon.rotate_strain_angle - -.. autofunction:: simcoon.rotate_strain_R - -.. autofunction:: simcoon.rotate_stiffness_angle - -.. autofunction:: simcoon.rotate_stiffness_R - -.. autofunction:: simcoon.rotate_compliance_angle - -.. autofunction:: simcoon.rotate_compliance_R - -.. autofunction:: simcoon.rotate_strain_concentration_angle - -.. autofunction:: simcoon.rotate_strain_concentration_R - -.. autofunction:: simcoon.rotate_stress_concentration_angle - -.. autofunction:: simcoon.rotate_stress_concentration_R - Examples ======== @@ -356,14 +318,10 @@ Rotating material properties from local to global coordinates: # Material orientation: Euler angles (radians) psi, theta, phi = 0.5, 0.3, 0.7 - # Method 1: Using Rotation class + # Using Rotation class r = smc.Rotation.from_euler(psi, theta, phi, "zxz") L_global = r.apply_stiffness(L_local) - # Method 2: Using free function with rotation matrix - R = smc.Rotation.from_euler(psi, theta, phi, "zxz").as_matrix() - L_global = smc.rotate_stiffness_R(L_local, R) - Example 2: Stress Transformation -------------------------------- diff --git a/examples/continuum_mechanics/rotation.py b/examples/continuum_mechanics/rotation.py index b6b90c9b2..ab1f2aa0c 100644 --- a/examples/continuum_mechanics/rotation.py +++ b/examples/continuum_mechanics/rotation.py @@ -19,56 +19,56 @@ m = np.eye(3) angle = np.pi / 4 # 45 degrees axis = 2 # y-axis (1=x, 2=y, 3=z) -copy = True active = True # %% -# 1. Rotate a vector using a rotation matrix +# 1. Rotate a vector using the Rotation class # -# This example shows how to build a rotation matrix using the Rotation class -# and rotate a vector. -# It uses :class:`simcoon.Rotation` and :func:`simcoon.rotate_vec_R`. +# This example shows how to build a rotation from an angle and axis +# and rotate a vector using :meth:`simcoon.Rotation.apply`. rot = sim.Rotation.from_axis_angle(angle, axis) Rmat = rot.as_matrix() -v_rot1 = sim.rotate_vec_R(v, Rmat, copy) -print("Rotated vector (using R):", v_rot1) -print("Rotation matrix (using R):", Rmat) +v_rot1 = rot.apply(v) +print("Rotated vector:", v_rot1) +print("Rotation matrix:", Rmat) # %% -# 2. Rotate a vector using angle/axis +# 2. Rotate a vector using a different constructor # -# This example shows how to rotate a vector using an angle and an axis directly. -# It uses :func:`simcoon.rotate_vec_angle`. +# This example shows how to create a rotation from Euler angles +# and rotate a vector using :meth:`simcoon.Rotation.apply`. -v_rot2 = sim.rotate_vec_angle(v, angle, axis, copy) -print("Rotated vector (using angle/axis):", v_rot2) +rot2 = sim.Rotation.from_axis_angle(angle, axis) +v_rot2 = rot2.apply(v) +print("Rotated vector:", v_rot2) # %% -# 3. Rotate a matrix using a rotation matrix +# 3. Rotate a matrix (3x3 tensor) # -# This example shows how to rotate a matrix using a rotation matrix. -# It uses :func:`simcoon.rotate_mat_R`. +# This example shows how to rotate a 3x3 tensor using +# :meth:`simcoon.Rotation.apply_tensor`. -m_rot1 = sim.rotate_mat_R(m, Rmat, copy) -print("Rotated matrix (using R):\n", m_rot1) +m_rot1 = rot.apply_tensor(m) +print("Rotated matrix:\n", m_rot1) # %% -# 4. Rotate a matrix using angle/axis +# 4. Rotate a matrix using a rotation from Euler angles # -# This example shows how to rotate a matrix using an angle and an axis directly. -# It uses :func:`simcoon.rotate_mat_angle`. +# This example shows how to rotate a matrix using a rotation +# created from Euler angles. -m_rot2 = sim.rotate_mat_angle(m, angle, axis, copy) -print("Rotated matrix (using angle/axis):\n", m_rot2) +rot_euler = sim.Rotation.from_axis_angle(angle, axis) +m_rot2 = rot_euler.apply_tensor(m) +print("Rotated matrix:\n", m_rot2) # %% @@ -90,81 +90,63 @@ # %% -# 6. Rotate a stress vector (single and batch) +# 6. Rotate a stress vector # -# This example uses :func:`simcoon.rotate_stress_angle`. +# This example uses :meth:`simcoon.Rotation.apply_stress`. stress = np.array([1, 2, 3, 4, 5, 6], dtype=float) -stress_batch = np.stack([stress, stress * 2], axis=1) -rot_stress1 = sim.rotate_stress_angle(stress, angle, axis, active, copy) -rot_stress2 = sim.rotate_stress_angle(stress_batch, angle, axis, active, copy) -print("Rotated stress (single):", rot_stress1) -print("Rotated stress (batch):\n", rot_stress2) +rot_stress1 = rot.apply_stress(stress, active) +print("Rotated stress:", rot_stress1) # %% -# 7. Rotate a strain vector (single and batch) +# 7. Rotate a strain vector # -# This example uses :func:`simcoon.rotate_strain_angle`. +# This example uses :meth:`simcoon.Rotation.apply_strain`. strain = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6], dtype=float) -strain_batch = np.stack([strain, strain * 2], axis=1) -rot_strain1 = sim.rotate_strain_angle(strain, angle, axis, active, copy) -rot_strain2 = sim.rotate_strain_angle(strain_batch, angle, axis, active, copy) -print("Rotated strain (single):", rot_strain1) -print("Rotated strain (batch):\n", rot_strain2) +rot_strain1 = rot.apply_strain(strain, active) +print("Rotated strain:", rot_strain1) # %% # 8. Rotate a stiffness matrix (L) # -# This example uses both :func:`simcoon.rotate_stiffness_angle` and -# :func:`simcoon.rotate_stiffness_R`. +# This example uses :meth:`simcoon.Rotation.apply_stiffness`. L6 = np.eye(6) -rotL1 = sim.rotate_stiffness_angle(L6, angle, axis, active, copy) -rotL2 = sim.rotate_stiffness_R(L6, Rmat, active, copy) -print("Rotated L (angle):\n", rotL1) -print("Rotated L (R):\n", rotL2) +rotL1 = rot.apply_stiffness(L6, active) +print("Rotated L:\n", rotL1) # %% # 9. Rotate a compliance matrix (M) # -# This example uses both :func:`simcoon.rotate_compliance_angle` and -# :func:`simcoon.rotate_compliance_R`. +# This example uses :meth:`simcoon.Rotation.apply_compliance`. M6 = np.eye(6) -rotM1 = sim.rotate_compliance_angle(M6, angle, axis, active, copy) -rotM2 = sim.rotate_compliance_R(M6, Rmat, active, copy) -print("Rotated M (angle):\n", rotM1) -print("Rotated M (R):\n", rotM2) +rotM1 = rot.apply_compliance(M6, active) +print("Rotated M:\n", rotM1) # %% # 10. Rotate a strain concentration tensor (A) # -# This example uses both :func:`simcoon.rotate_strain_concentration_angle` and -# :func:`simcoon.rotate_strain_concentration_R`. +# This example uses :meth:`simcoon.Rotation.apply_strain_concentration`. A6 = np.eye(6) -rotA1 = sim.rotate_strain_concentration_angle(A6, angle, axis, active, copy) -rotA2 = sim.rotate_strain_concentration_R(A6, Rmat, active, copy) -print("Rotated A (angle):\n", rotA1) -print("Rotated A (R):\n", rotA2) +rotA1 = rot.apply_strain_concentration(A6, active) +print("Rotated A:\n", rotA1) # %% # 11. Rotate a stress concentration tensor (B) # -# This example uses both :func:`simcoon.rotate_stress_concentration_angle` and -# :func:`simcoon.rotate_stress_concentration_R`. +# This example uses :meth:`simcoon.Rotation.apply_stress_concentration`. B6 = np.eye(6) -rotB1 = sim.rotate_stress_concentration_angle(B6, angle, axis, active, copy) -rotB2 = sim.rotate_stress_concentration_R(B6, Rmat, active, copy) -print("Rotated B (angle):\n", rotB1) -print("Rotated B (R):\n", rotB2) +rotB1 = rot.apply_stress_concentration(B6, active) +print("Rotated B:\n", rotB1) # %% @@ -187,12 +169,8 @@ alpha = np.pi / 4.0 -rot_matrix = np.array( - [[np.cos(alpha), -np.sin(alpha), 0.0], [np.sin(alpha), np.cos(alpha), 0], [0, 0, 1]] -) +rot_z = sim.Rotation.from_axis_angle(alpha, 3) -L_rotate = sim.rotate_stiffness_R(L, rot_matrix) -L_rotate_angle = sim.rotate_stiffness_angle(L, alpha, axis=3) +L_rotate = rot_z.apply_stiffness(L) print(np.array_str(L_rotate, suppress_small=True)) -print(np.array_str(L_rotate_angle, suppress_small=True)) diff --git a/simcoon-python-builder/CMakeLists.txt b/simcoon-python-builder/CMakeLists.txt index 339abd060..08ab86b77 100755 --- a/simcoon-python-builder/CMakeLists.txt +++ b/simcoon-python-builder/CMakeLists.txt @@ -50,7 +50,6 @@ pybind11_add_module(_core # Maths src/python_wrappers/Libraries/Maths/lagrange.cpp - src/python_wrappers/Libraries/Maths/rotation.cpp src/python_wrappers/Libraries/Maths/rotation_class.cpp # Solver diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp deleted file mode 100644 index b88c2235d..000000000 --- a/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -namespace simcoon_docs { - -constexpr auto rotate_vec_R = R"pbdoc( - Rotate a 3D vector using a rotation matrix. - - Parameters - ---------- - input : ndarray - 3D vector to rotate. - R : ndarray - 3x3 rotation matrix. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - Rotated 3D vector. - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - v = np.array([1.0, 0.0, 0.0]) - R = sim.Rotation.from_axis_angle(np.pi/4, 3).as_matrix() - v_rot = sim.rotate_vec_R(v, R) - print(v_rot) -)pbdoc"; - -constexpr auto rotate_vec_angle = R"pbdoc( - Rotate a 3D vector by an angle around a given axis. - - Parameters - ---------- - input : ndarray - 3D vector to rotate. - angle : float - Rotation angle in radians. - axis : int - Axis of rotation: 1=x, 2=y, 3=z. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - Rotated 3D vector. - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - v = np.array([1.0, 0.0, 0.0]) - v_rot = sim.rotate_vec_angle(v, np.pi/4, 3) - print(v_rot) -)pbdoc"; - -constexpr auto rotate_mat_R = R"pbdoc( - Rotate a 3x3 matrix using a rotation matrix. - - Parameters - ---------- - input : ndarray - 3x3 matrix to rotate. - R : ndarray - 3x3 rotation matrix. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - Rotated 3x3 matrix: R * input * R^T. - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - m = np.eye(3) - R = sim.Rotation.from_axis_angle(np.pi/4, 3).as_matrix() - m_rot = sim.rotate_mat_R(m, R) - print(m_rot) -)pbdoc"; - -constexpr auto rotate_mat_angle = R"pbdoc( - Rotate a 3x3 matrix by an angle around a given axis. - - Parameters - ---------- - input : ndarray - 3x3 matrix to rotate. - angle : float - Rotation angle in radians. - axis : int - Axis of rotation: 1=x, 2=y, 3=z. - copy : bool, optional - If True (default), return a copy of the result. - - Returns - ------- - ndarray - Rotated 3x3 matrix. - - Examples - -------- - .. code-block:: python - - import numpy as np - import simcoon as sim - - m = np.eye(3) - m_rot = sim.rotate_mat_angle(m, np.pi/4, 3) - print(m_rot) -)pbdoc"; - -} // namespace simcoon_docs diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp deleted file mode 100755 index 83673e532..000000000 --- a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include -#include - -namespace simpy{ - -//This function returns a rotated vector (3) according to a rotation matrix -pybind11::array_t rotate_vec_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool ©=true); - -//This function returns a rotated vector (3) according to an angle and an axis -pybind11::array_t rotate_vec_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool ©=true); - -//This function returns a rotated matrix (3x3) according to a rotation matrix -pybind11::array_t rotate_mat_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool ©=true); - -//This function returns a rotated matrix (3x3) according to an angle and an axis -pybind11::array_t rotate_mat_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool ©=true); - -//These functions rotate a 6*6 stiffness matrix -pybind11::array_t rotate_stiffness_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_stiffness_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); - -//These functions rotate a 6*6 compliance matrix -pybind11::array_t rotate_compliance_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_compliance_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); - -//These functions rotate a 6*6 strain concentration tensor (A) -pybind11::array_t rotate_strain_concentration_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_strain_concentration_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); - -//These functions rotate a 6*6 stress concentration tensor (B) -pybind11::array_t rotate_stress_concentration_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_stress_concentration_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); - -//These functions rotate strain vectors - Can be used with stack of arrays (vectorized) -pybind11::array_t rotate_strain_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_strain_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); - -//These functions rotate stress vectors - Can be used with stack of arrays (vectorized) -pybind11::array_t rotate_stress_angle(const pybind11::array_t &input, const double &angle, const int &axis, const bool &active=true, const bool ©=true); -pybind11::array_t rotate_stress_R(const pybind11::array_t &input, const pybind11::array_t &R, const bool &active=true, const bool ©=true); - -} //namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp deleted file mode 100755 index 05ec65e8f..000000000 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp +++ /dev/null @@ -1,195 +0,0 @@ -#include -#include - -#include -#include -#include - -#include -#include - -using namespace std; -using namespace arma; -namespace py=pybind11; - -namespace simpy { - -py::array_t rotate_vec_R(const py::array_t &input, const py::array_t &R, const bool ©) { - vec v_cpp = carma::arr_to_col(input); - mat R_cpp = carma::arr_to_mat(R); - vec rotated_vec = simcoon::rotate_vec(v_cpp, R_cpp); - return carma::col_to_arr(rotated_vec, copy); -} - -py::array_t rotate_vec_angle(const py::array_t &input, const double &angle, const int &axis, const bool ©) { - vec v_cpp = carma::arr_to_col(input); - vec rotated_vec = simcoon::rotate_vec(v_cpp, angle, axis); - return carma::col_to_arr(rotated_vec, copy); -} - -py::array_t rotate_mat_R(const py::array_t &input, const py::array_t &R, const bool ©) { - mat m_cpp = carma::arr_to_mat(input); - mat R_cpp = carma::arr_to_mat(R); - mat rotated_mat = simcoon::rotate_mat(m_cpp, R_cpp); - return carma::mat_to_arr(rotated_mat, copy); -} - -py::array_t rotate_mat_angle(const py::array_t &input, const double &angle, const int &axis, const bool ©) { - mat m_cpp = carma::arr_to_mat(input); - mat rotated_mat = simcoon::rotate_mat(m_cpp, angle, axis); - return carma::mat_to_arr(rotated_mat, copy); -} - -//This function rotates a 6x6 stiffness matrix -py::array_t rotate_stiffness_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - mat stiffness = carma::arr_to_mat(input); - mat rotated_stiffness = simcoon::rotate_stiffness(stiffness, angle, axis, active); - return carma::mat_to_arr(rotated_stiffness, copy); -} - -//This function rotates a 6x6 stiffness matrix -py::array_t rotate_stiffness_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat stiffness = carma::arr_to_mat(input); - mat R_cpp = carma::arr_to_mat(R); - mat rotated_stiffness = simcoon::rotate_stiffness(stiffness, R_cpp, active); - return carma::mat_to_arr(rotated_stiffness, copy); -} - -//This function rotates a 6x6 compliance matrix -py::array_t rotate_compliance_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - mat compliance = carma::arr_to_mat(input); - mat rotated_compliance = simcoon::rotate_compliance(compliance, angle, axis, active); - return carma::mat_to_arr(rotated_compliance, copy); -} - -//This function rotates a 6x6 compliance matrix -py::array_t rotate_compliance_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat compliance = carma::arr_to_mat(input); - mat R_cpp = carma::arr_to_mat(R); - mat rotated_compliance = simcoon::rotate_compliance(compliance, R_cpp, active); - return carma::mat_to_arr(rotated_compliance, copy); -} - -//This function rotates a 6x6 strain concentration tensor -py::array_t rotate_strain_concentration_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - mat strain_concentration = carma::arr_to_mat(input); - mat rotated_strain_concentration = simcoon::rotate_strain_concentration(strain_concentration, angle, axis, active); - return carma::mat_to_arr(rotated_strain_concentration, copy); -} - -//This function rotates a 6x6 strain concentration tensor -py::array_t rotate_strain_concentration_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat strain_concentration = carma::arr_to_mat(input); - mat R_cpp = carma::arr_to_mat(R); - mat rotated_strain_concentration = simcoon::rotate_strain_concentration(strain_concentration, R_cpp, active); - return carma::mat_to_arr(rotated_strain_concentration, copy); -} - -//This function rotates a 6x6 stress concentration tensor -py::array_t rotate_stress_concentration_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - mat stress_concentration = carma::arr_to_mat(input); - mat rotated_stress_concentration = simcoon::rotate_stress_concentration(stress_concentration, angle, axis, active); - return carma::mat_to_arr(rotated_stress_concentration, copy); -} - -//This function rotates a 6x6 stress concentration tensor -py::array_t rotate_stress_concentration_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - mat stress_concentration = carma::arr_to_mat(input); - mat R_cpp = carma::arr_to_mat(R); - mat rotated_stress_concentration = simcoon::rotate_stress_concentration(stress_concentration, R_cpp, active); - return carma::mat_to_arr(rotated_stress_concentration, copy); -} - -//This function rotates stress vectors - Can be used with stack of arrays (vectorized) -py::array_t rotate_stress_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - if (input.ndim()==1){ - vec stress = carma::arr_to_col(input); - vec rotated_stress = simcoon::rotate_stress(stress, angle, axis, active); - return carma::col_to_arr(rotated_stress, copy); - } - else if (input.ndim() == 2){ - mat stress_batch = carma::arr_to_mat_view(input); - int nb_points = stress_batch.n_cols; - mat rotated_stress(6, nb_points); - for (int pt = 0; pt < nb_points; pt++) { - vec stress = stress_batch.unsafe_col(pt); - rotated_stress.col(pt) = simcoon::rotate_stress(stress, angle, axis, active); - } - return carma::mat_to_arr(rotated_stress, copy); - } - else{ - throw std::invalid_argument("input.ndim should be 1 or 2"); - } -} - -//This function rotates stress vectors - Can be used with stack of arrays (vectorized) -py::array_t rotate_stress_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - if (input.ndim()==1){ - vec stress = carma::arr_to_col(input); - mat R_cpp = carma::arr_to_mat(R); - vec rotated_stress = simcoon::rotate_stress(stress, R_cpp, active); - return carma::col_to_arr(rotated_stress, copy); - } - else if (input.ndim() == 2){ - mat stress_batch = carma::arr_to_mat_view(input); - cube R_cpp = carma::arr_to_cube_view(R); - int nb_points = stress_batch.n_cols; - mat rotated_stress(6, nb_points); - for (int pt = 0; pt < nb_points; pt++) { - vec stress = stress_batch.unsafe_col(pt); - rotated_stress.col(pt) = simcoon::rotate_stress(stress, R_cpp.slice(pt), active); - } - return carma::mat_to_arr(rotated_stress, copy); - } - else{ - throw std::invalid_argument("input.ndim should be 1 or 2"); - } -} - -//This function rotates strain vectors - Can be used with stack of arrays (vectorized) -py::array_t rotate_strain_angle(const py::array_t &input, const double &angle, const int &axis, const bool &active, const bool ©) { - if (input.ndim()==1){ - vec strain = carma::arr_to_col(input); - vec rotated_strain = simcoon::rotate_strain(strain, angle, axis, active); - return carma::col_to_arr(rotated_strain, copy); - } - else if (input.ndim() == 2){ - mat strain_batch = carma::arr_to_mat_view(input); - int nb_points = strain_batch.n_cols; - mat rotated_strain(6, nb_points); - for (int pt = 0; pt < nb_points; pt++) { - vec strain = strain_batch.unsafe_col(pt); - rotated_strain.col(pt) = simcoon::rotate_strain(strain, angle, axis, active); - } - return carma::mat_to_arr(rotated_strain, copy); - } - else{ - throw std::invalid_argument("input.ndim should be 1 or 2"); - } -} - -//This function rotates strain vectors - Can be used with stack of arrays (vectorized) -py::array_t rotate_strain_R(const py::array_t &input, const py::array_t &R, const bool &active, const bool ©) { - if (input.ndim()==1){ - vec strain = carma::arr_to_col(input); - mat R_cpp = carma::arr_to_mat(R); - vec rotated_strain = simcoon::rotate_strain(strain, R_cpp, active); - return carma::col_to_arr(rotated_strain, copy); - } - else if (input.ndim() == 2){ - mat strain_batch = carma::arr_to_mat_view(input); - cube R_cpp = carma::arr_to_cube_view(R); - int nb_points = strain_batch.n_cols; - mat rotated_strain(6, nb_points); - for (int pt = 0; pt < nb_points; pt++) { - vec strain = strain_batch.unsafe_col(pt); - rotated_strain.col(pt) = simcoon::rotate_strain(strain, R_cpp.slice(pt), active); - } - return carma::mat_to_arr(rotated_strain, copy); - } - else{ - throw std::invalid_argument("input.ndim should be 1 or 2"); - } -} - -} //namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/python_module.cpp b/simcoon-python-builder/src/python_wrappers/python_module.cpp index 7aaec49e6..b1e732a01 100755 --- a/simcoon-python-builder/src/python_wrappers/python_module.cpp +++ b/simcoon-python-builder/src/python_wrappers/python_module.cpp @@ -19,7 +19,6 @@ #include // #include -#include #include #include #include @@ -46,7 +45,6 @@ #include #include -#include using namespace std; using namespace arma; @@ -210,24 +208,6 @@ PYBIND11_MODULE(_core, m) m.def("Eshelby", &Eshelby, "L"_a, "a1"_a = 1., "a2"_a = 1., "a3"_a = 1., "mp"_a = 50, "np"_a = 50, "copy"_a = true, simcoon_docs::Eshelby); m.def("T_II", &T_II, "L"_a, "a1"_a = 1., "a2"_a = 1., "a3"_a = 1., "mp"_a = 50, "np"_a = 50, "copy"_a = true, simcoon_docs::T_II); - // Register the rotation library - m.def("rotate_vec_R", &rotate_vec_R, "input"_a, "R"_a, "copy"_a = true, simcoon_docs::rotate_vec_R); - m.def("rotate_vec_angle", &rotate_vec_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, simcoon_docs::rotate_vec_angle); - m.def("rotate_mat_R", &rotate_mat_R, "input"_a, "R"_a, "copy"_a = true, simcoon_docs::rotate_mat_R); - m.def("rotate_mat_angle", &rotate_mat_angle, "input"_a, "angle"_a, "axis"_a, "copy"_a = true, simcoon_docs::rotate_mat_angle); - m.def("rotate_stiffness_angle", &rotate_stiffness_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stiffness matrix according to an angle and an axis"); - m.def("rotate_stiffness_R", &rotate_stiffness_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stiffness matrix according to a rotation matrix"); - m.def("rotate_compliance_angle", &rotate_compliance_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 compliance matrix according to an angle and an axis"); - m.def("rotate_compliance_R", &rotate_compliance_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 compliance matrix according to a rotation matrix"); - m.def("rotate_strain_concentration_angle", &rotate_strain_concentration_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 strain concentration tensor according to an angle and an axis"); - m.def("rotate_strain_concentration_R", &rotate_strain_concentration_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 strain concentration tensor according to a rotation matrix"); - m.def("rotate_stress_concentration_angle", &rotate_stress_concentration_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stress concentration tensor according to an angle and an axis"); - m.def("rotate_stress_concentration_R", &rotate_stress_concentration_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = true, "Return the rotated 6x6 stress concentration tensor according to a rotation matrix"); - m.def("rotate_strain_angle", &rotate_strain_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = false, "Return the rotated strain vector using Voigt notation according to an angle and an axis"); - m.def("rotate_strain_R", &rotate_strain_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = false, "Return the rotated strain vector using Voigt notation according to a rotation matrix"); - m.def("rotate_stress_angle", &rotate_stress_angle, "input"_a, "angle"_a, "axis"_a, "active"_a = true, "copy"_a = false, "Return the rotated stress vector using Voigt notation according to an angle and an axis"); - m.def("rotate_stress_R", &rotate_stress_R, "input"_a, "R"_a, "active"_a = true, "copy"_a = false, "Return the rotated stress vector using Voigt notation according to a rotation matrix"); - // Register the Rotation class register_rotation_class(m); From 4548de3150e63d525cdbbf990a2eb2214c436ebd Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Feb 2026 16:56:33 +0100 Subject: [PATCH 13/31] Rename rotation_class to rotation Rename the Rotation pybind11 wrapper files and update references: rotation_class.hpp -> rotation.hpp and rotation_class.cpp -> rotation.cpp. Rename the registration function register_rotation_class -> register_rotation and update its declaration, definition, includes, and the call in python_module.cpp. Update CMakeLists.txt to build src/python_wrappers/Libraries/Maths/rotation.cpp. This unifies the wrapper naming and keeps module registration consistent. --- simcoon-python-builder/CMakeLists.txt | 2 +- .../Libraries/Maths/{rotation_class.hpp => rotation.hpp} | 2 +- .../Libraries/Maths/{rotation_class.cpp => rotation.cpp} | 4 ++-- simcoon-python-builder/src/python_wrappers/python_module.cpp | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/{rotation_class.hpp => rotation.hpp} (76%) rename simcoon-python-builder/src/python_wrappers/Libraries/Maths/{rotation_class.cpp => rotation.cpp} (99%) diff --git a/simcoon-python-builder/CMakeLists.txt b/simcoon-python-builder/CMakeLists.txt index 08ab86b77..168ffa22b 100755 --- a/simcoon-python-builder/CMakeLists.txt +++ b/simcoon-python-builder/CMakeLists.txt @@ -50,7 +50,7 @@ pybind11_add_module(_core # Maths src/python_wrappers/Libraries/Maths/lagrange.cpp - src/python_wrappers/Libraries/Maths/rotation_class.cpp + src/python_wrappers/Libraries/Maths/rotation.cpp # Solver src/python_wrappers/Libraries/Solver/read.cpp diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation_class.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp similarity index 76% rename from simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation_class.hpp rename to simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp index 3a0a8e9a5..100308d62 100644 --- a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation_class.hpp +++ b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Maths/rotation.hpp @@ -5,6 +5,6 @@ namespace simpy { // Function to register the Rotation class with pybind11 -void register_rotation_class(pybind11::module_& m); +void register_rotation(pybind11::module_& m); } // namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp similarity index 99% rename from simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp rename to simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp index 30bcdfef1..f50b898d3 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation_class.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include using namespace std; using namespace arma; @@ -40,7 +40,7 @@ namespace { } } // anonymous namespace -void register_rotation_class(py::module_& m) { +void register_rotation(py::module_& m) { py::class_(m, "Rotation", R"doc( A class representing 3D rotations using unit quaternions. diff --git a/simcoon-python-builder/src/python_wrappers/python_module.cpp b/simcoon-python-builder/src/python_wrappers/python_module.cpp index b1e732a01..670ebae83 100755 --- a/simcoon-python-builder/src/python_wrappers/python_module.cpp +++ b/simcoon-python-builder/src/python_wrappers/python_module.cpp @@ -19,7 +19,7 @@ #include // #include -#include +#include #include #include #include @@ -209,7 +209,7 @@ PYBIND11_MODULE(_core, m) m.def("T_II", &T_II, "L"_a, "a1"_a = 1., "a2"_a = 1., "a3"_a = 1., "mp"_a = 50, "np"_a = 50, "copy"_a = true, simcoon_docs::T_II); // Register the Rotation class - register_rotation_class(m); + register_rotation(m); // Register the from-python converters for lagrange m.def("lagrange_exp", &lagrange_exp, "This function is used to determine an exponential Lagrange Multiplier (like contact in Abaqus)"); From 66fb22333e658962ba85002914f10287e993c938 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 11:36:47 +0100 Subject: [PATCH 14/31] Integrate scipy Rotation; add Python wrapper/tests Make simcoon.Rotation a subclass of scipy.spatial.transform.Rotation and add full scipy interoperability. Adds a pure-Python simcoon.rotation.Rotation wrapper that inherits scipy Rotation and delegates mechanics operations (apply_stress/strain/stiffness/etc.) to the C++ backend via a new _CppRotation interface. The C++ pybind wrapper was adjusted to expose simcoon::Rotation as _CppRotation and to accept plain scipy-like Rotation objects (duck-typed via as_quat) for composition, slerp and equality. Updated package metadata to require scipy, refreshed docs and examples to use scipy Euler conventions and show interoperability, and added comprehensive tests for type identity, mechanics methods, scipy upgrades, and C++ handling of scipy objects. Also import Rotation in __init__ to override the star-imported C++ class. --- .../functions/doc_rotation.rst | 170 ++++----- examples/continuum_mechanics/rotation.py | 143 ++++++-- pyproject.toml | 1 + python-setup/simcoon/__init__.py | 1 + python-setup/simcoon/rotation.py | 310 ++++++++++++++++ .../Libraries/Maths/rotation.cpp | 120 +++--- .../test/test_core/test_rotation.py | 342 ++++++++++++++++++ 7 files changed, 910 insertions(+), 177 deletions(-) create mode 100644 python-setup/simcoon/rotation.py create mode 100644 simcoon-python-builder/test/test_core/test_rotation.py diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index 8066824db..3ebe019b9 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -2,7 +2,9 @@ The Rotation Library ==================== The rotation library provides comprehensive tools for 3D rotations in continuum mechanics. -The ``Rotation`` class provides a powerful object-oriented interface for all rotation operations. +The ``Rotation`` class inherits from ``scipy.spatial.transform.Rotation``, so all scipy +features (batch operations, ``mean()``, ``Slerp``, ``RotationSpline``, etc.) are available +directly, while simcoon adds continuum-mechanics methods for Voigt-notation tensors. .. contents:: Contents :local: @@ -11,12 +13,15 @@ The ``Rotation`` class provides a powerful object-oriented interface for all rot The Rotation Class ------------------ -The ``Rotation`` class provides an object-oriented interface for working with 3D rotations. -It uses unit quaternions internally for numerical stability and efficient composition. +The ``Rotation`` class extends ``scipy.spatial.transform.Rotation`` with methods for +rotating stress, strain, stiffness, and compliance tensors. It uses unit quaternions +internally for numerical stability and efficient composition. Creating Rotations ~~~~~~~~~~~~~~~~~~ +All scipy factory methods are available and return a ``simcoon.Rotation`` instance: + .. code-block:: python import simcoon as smc @@ -25,11 +30,11 @@ Creating Rotations # Identity rotation (no rotation) r = smc.Rotation.identity() - # From Euler angles (convention must be specified) - r = smc.Rotation.from_euler(psi, theta, phi, "zxz") + # From Euler angles (scipy convention: uppercase = intrinsic) + r = smc.Rotation.from_euler('ZXZ', [psi, theta, phi]) - # From Euler angles with options - r = smc.Rotation.from_euler(psi, theta, phi, conv="xyz", intrinsic=False, degrees=True) + # From Euler angles in degrees, extrinsic + r = smc.Rotation.from_euler('xyz', [roll, pitch, yaw], degrees=True) # From rotation matrix R = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]]) # 90° around x @@ -43,23 +48,18 @@ Creating Rotations rotvec = np.array([0, 0, np.pi/2]) # 90° around z r = smc.Rotation.from_rotvec(rotvec) - # From axis and angle + # From axis and angle (simcoon-specific) r = smc.Rotation.from_axis_angle(np.pi/2, 3) # 90° around z (axis 3) # Random rotation (uniform distribution) r = smc.Rotation.random() - # From a scipy.spatial.transform.Rotation (requires scipy) - from scipy.spatial.transform import Rotation as R - scipy_rot = R.from_euler('z', 45, degrees=True) - r = smc.Rotation.from_scipy(scipy_rot) - Converting Between Representations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python - r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + r = smc.Rotation.from_euler('ZXZ', [0.5, 0.3, 0.7]) # To rotation matrix R = r.as_matrix() # 3×3 numpy array @@ -67,21 +67,18 @@ Converting Between Representations # To quaternion q = r.as_quat() # [qx, qy, qz, qw] - # To Euler angles - angles = r.as_euler("zxz") # [psi, theta, phi] - angles_deg = r.as_euler("zxz", degrees=True) + # To Euler angles (scipy convention) + angles = r.as_euler('ZXZ') # [psi, theta, phi] + angles_deg = r.as_euler('ZXZ', degrees=True) # To rotation vector rotvec = r.as_rotvec() rotvec_deg = r.as_rotvec(degrees=True) - # To Voigt rotation matrices + # To Voigt rotation matrices (simcoon-specific) QS = r.as_voigt_stress_rotation() # 6×6 for stress QE = r.as_voigt_strain_rotation() # 6×6 for strain - # To scipy.spatial.transform.Rotation (requires scipy) - scipy_rot = r.to_scipy() - Applying Rotations ~~~~~~~~~~~~~~~~~~ @@ -155,63 +152,92 @@ Composing Rotations Interpolating Rotations ~~~~~~~~~~~~~~~~~~~~~~~ -SLERP (Spherical Linear Interpolation) provides smooth interpolation: +SLERP (Spherical Linear Interpolation) is available via scipy's ``Slerp`` class: .. code-block:: python + from scipy.spatial.transform import Slerp + r_start = smc.Rotation.identity() - r_end = smc.Rotation.from_euler(np.pi/2, np.pi/4, 0, "zxz") + r_end = smc.Rotation.from_euler('ZXZ', [np.pi/2, np.pi/4, 0]) - # Interpolate at t=0.5 (halfway) - r_mid = r_start.slerp(r_end, 0.5) + key_rots = smc.Rotation.concatenate([r_start, r_end]) + slerp = Slerp([0, 1], key_rots) # Interpolate along path for t in np.linspace(0, 1, 11): - r_t = r_start.slerp(r_end, t) - print(f"t={t:.1f}: angle={r_t.magnitude(degrees=True):.1f}°") + r_t = slerp(t) + print(f"t={t:.1f}: angle={np.degrees(r_t.magnitude()):.1f}°") + + # Or use the convenience method + r_mid = r_start.slerp_to(r_end, 0.5) Utility Methods ~~~~~~~~~~~~~~~ .. code-block:: python - r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + r = smc.Rotation.from_euler('ZXZ', [0.5, 0.3, 0.7]) # Get rotation magnitude (angle) angle = r.magnitude() # radians - angle_deg = r.magnitude(degrees=True) + angle_deg = np.degrees(r.magnitude()) # Check if identity is_id = r.is_identity() - # Check equality - r2 = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + # Check equality (accounts for quaternion sign ambiguity) + r2 = smc.Rotation.from_euler('ZXZ', [0.5, 0.3, 0.7]) are_equal = r.equals(r2, tol=1e-12) -Scipy Interoperability -~~~~~~~~~~~~~~~~~~~~~ +Scipy Integration +~~~~~~~~~~~~~~~~~ -Both simcoon and scipy use the scalar-last quaternion convention ``[qx, qy, qz, qw]``, -so conversion between the two is done via quaternion transfer with no trigonometric -or matrix computation overhead. Scipy is an **optional** dependency — it is only -imported when ``to_scipy()`` is called. +Since ``simcoon.Rotation`` inherits from ``scipy.spatial.transform.Rotation``, +all scipy features work directly: .. code-block:: python from scipy.spatial.transform import Rotation as R - # scipy → simcoon + # simcoon.Rotation IS a scipy Rotation + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + isinstance(r, R) # True + + # All scipy methods work directly + r.as_mrp() # Modified Rodrigues parameters + r.as_davenport(...) # Davenport angles + + # Batch operations + rots = smc.Rotation.random(100) + rots.mean() + + # RotationSpline + from scipy.spatial.transform import RotationSpline + times = [0, 1, 2, 3] + key_rots = smc.Rotation.random(4) + spline = RotationSpline(times, key_rots) + r_interp = spline(1.5) + +**Passing plain scipy Rotations to simcoon:** + +If you already have a ``scipy.spatial.transform.Rotation`` (e.g. from another +library), you can upgrade it to a ``simcoon.Rotation`` with ``from_scipy()``: + +.. code-block:: python + + from scipy.spatial.transform import Rotation as R + + # Created somewhere else, e.g. by a third-party library scipy_rot = R.from_euler('z', 45, degrees=True) - r = smc.Rotation.from_scipy(scipy_rot) - # simcoon → scipy - r = smc.Rotation.from_axis_angle(np.pi/4, 3) - scipy_rot = r.to_scipy() + # Upgrade to simcoon.Rotation to unlock mechanics methods + r = smc.Rotation.from_scipy(scipy_rot) + L_rot = r.apply_stiffness(L) # now available - # Round-trip is lossless - q_original = scipy_rot.as_quat() - q_roundtrip = smc.Rotation.from_scipy(scipy_rot).to_scipy().as_quat() - # q_original == q_roundtrip +The C++ layer also accepts plain scipy ``Rotation`` objects directly wherever +a rotation parameter is expected (composition, ``equals``, ``slerp``), so +mixing scipy and simcoon rotations in the same expression works seamlessly. Active vs Passive Rotations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -238,31 +264,22 @@ rotation convention: Input Validation ~~~~~~~~~~~~~~~~ -All methods that accept NumPy arrays validate the input shape and raise +All mechanics methods that accept NumPy arrays validate the input shape and raise ``ValueError`` with a descriptive message if the dimensions are wrong: .. code-block:: python r = smc.Rotation.from_axis_angle(np.pi/4, 3) - r.apply(np.array([1.0, 2.0])) - # ValueError: v must have 3 elements, got 2 - r.apply_stress(np.array([1.0, 2.0, 3.0])) # ValueError: sigma must have 6 elements, got 3 - smc.Rotation.from_matrix(np.eye(4)) - # ValueError: R must have shape (3, 3), got (4, 4) - r.apply_stiffness(np.eye(3)) # ValueError: L must have shape (6, 6), got (3, 3) Expected input shapes: -- ``from_quat``: 1D array, 4 elements -- ``from_matrix``: 2D array, shape (3, 3) -- ``from_rotvec``: 1D array, 3 elements -- ``apply``: 1D array, 3 elements +- ``apply``: 1D array, 3 elements (or Nx3 array for batch) - ``apply_tensor``: 2D array, shape (3, 3) - ``apply_stress``, ``apply_strain``: 1D array, 6 elements - ``apply_stiffness``, ``apply_compliance``: 2D array, shape (6, 6) @@ -275,32 +292,26 @@ Expected input shapes: Euler Angle Conventions ----------------------- -Simcoon supports multiple Euler angle conventions: +Simcoon uses scipy's Euler angle conventions: + +- **Uppercase** letters (``'ZXZ'``, ``'XYZ'``, etc.) → intrinsic rotations (rotating frame) +- **Lowercase** letters (``'zxz'``, ``'xyz'``, etc.) → extrinsic rotations (fixed frame) **Proper Euler angles** (axis sequence where first = last): -- ``zxz`` -- ``zyz`` -- ``xyx``, ``xzx`` -- ``yxy``, ``yzy`` +- ``ZXZ``, ``ZYZ``, ``XYX``, ``XZX``, ``YXY``, ``YZY`` **Tait-Bryan angles** (all axes different): -- ``xyz`` (roll-pitch-yaw) -- ``xzy``, ``yxz``, ``yzx``, ``zxy``, ``zyx`` - -**Intrinsic vs Extrinsic:** - -- **Intrinsic** (default): rotations about axes of the rotating frame -- **Extrinsic**: rotations about fixed world axes +- ``XYZ`` (roll-pitch-yaw), ``XZY``, ``YXZ``, ``YZX``, ``ZXY``, ``ZYX`` .. code-block:: python # Intrinsic ZXZ - r1 = smc.Rotation.from_euler(psi, theta, phi, "zxz", intrinsic=True) + r1 = smc.Rotation.from_euler('ZXZ', [psi, theta, phi]) - # Extrinsic XYZ (aerospace convention) - r2 = smc.Rotation.from_euler(roll, pitch, yaw, "xyz", intrinsic=False) + # Extrinsic xyz (same physical rotation as intrinsic ZYX) + r2 = smc.Rotation.from_euler('xyz', [roll, pitch, yaw]) Practical Examples ------------------ @@ -341,8 +352,8 @@ Example 2: Stress Transformation in a Rotated Element # Element orientation (Euler angles) psi, theta, phi = np.radians([30, 45, 60]) - # Create rotation - r = smc.Rotation.from_euler(psi, theta, phi, "zxz") + # Create rotation (scipy convention: uppercase = intrinsic) + r = smc.Rotation.from_euler('ZXZ', [psi, theta, phi]) # Transform stress to local element coordinates sigma_local = r.inv().apply_stress(sigma_global) @@ -359,15 +370,12 @@ Example 3: Averaging Orientations import numpy as np # Generate random orientations - rotations = [smc.Rotation.random() for _ in range(10)] + rotations = smc.Rotation.random(10) - # Approximate mean using iterative SLERP - r_mean = rotations[0] - for i, r in enumerate(rotations[1:], 2): - t = 1.0 / i # Weight for new rotation - r_mean = r_mean.slerp(r, t) + # Use scipy's built-in mean + r_mean = rotations.mean() - print("Mean rotation angle:", r_mean.magnitude(degrees=True), "degrees") + print("Mean rotation angle:", np.degrees(r_mean.magnitude()), "degrees") See Also -------- diff --git a/examples/continuum_mechanics/rotation.py b/examples/continuum_mechanics/rotation.py index ab1f2aa0c..4c0150d80 100644 --- a/examples/continuum_mechanics/rotation.py +++ b/examples/continuum_mechanics/rotation.py @@ -1,13 +1,16 @@ """ Rotation library Examples ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The simcoon ``Rotation`` class inherits from ``scipy.spatial.transform.Rotation``, +so all scipy factory methods and operations are available directly. Plain scipy +``Rotation`` objects can be upgraded with ``from_scipy()`` or passed directly to +C++ methods that accept rotations. """ import numpy as np -import matplotlib.pyplot as plt -from scipy.spatial.transform import Rotation as R +from scipy.spatial.transform import Rotation as ScipyRotation import simcoon as sim -import os # %% @@ -28,22 +31,20 @@ # This example shows how to build a rotation from an angle and axis # and rotate a vector using :meth:`simcoon.Rotation.apply`. - rot = sim.Rotation.from_axis_angle(angle, axis) Rmat = rot.as_matrix() v_rot1 = rot.apply(v) print("Rotated vector:", v_rot1) -print("Rotation matrix:", Rmat) +print("Rotation matrix:\n", Rmat) # %% -# 2. Rotate a vector using a different constructor +# 2. Rotate a vector using Euler angles (scipy syntax) # -# This example shows how to create a rotation from Euler angles -# and rotate a vector using :meth:`simcoon.Rotation.apply`. - +# Uppercase letters indicate intrinsic rotations (rotating frame). +# Lowercase letters indicate extrinsic rotations (fixed frame). -rot2 = sim.Rotation.from_axis_angle(angle, axis) +rot2 = sim.Rotation.from_euler("ZXZ", [np.pi / 6, np.pi / 4, np.pi / 3]) v_rot2 = rot2.apply(v) print("Rotated vector:", v_rot2) @@ -54,32 +55,25 @@ # This example shows how to rotate a 3x3 tensor using # :meth:`simcoon.Rotation.apply_tensor`. - m_rot1 = rot.apply_tensor(m) print("Rotated matrix:\n", m_rot1) # %% # 4. Rotate a matrix using a rotation from Euler angles -# -# This example shows how to rotate a matrix using a rotation -# created from Euler angles. - -rot_euler = sim.Rotation.from_axis_angle(angle, axis) +rot_euler = sim.Rotation.from_euler("ZXZ", [np.pi / 6, np.pi / 4, np.pi / 3]) m_rot2 = rot_euler.apply_tensor(m) print("Rotated matrix:\n", m_rot2) # %% # 5. Build a rotation matrix from Euler angles -# -# This example shows how to create a rotation from Euler angles using the Rotation class. psi, theta, phi = np.pi / 6, np.pi / 4, np.pi / 3 -r_euler = sim.Rotation.from_euler(psi, theta, phi, "zxz") +r_euler = sim.Rotation.from_euler("ZXZ", [psi, theta, phi]) R_euler = r_euler.as_matrix() -print("Rotation matrix from Euler angles (zxz):\n", R_euler) +print("Rotation matrix from Euler angles (ZXZ):\n", R_euler) # %% @@ -168,9 +162,112 @@ print(x) alpha = np.pi / 4.0 - rot_z = sim.Rotation.from_axis_angle(alpha, 3) - L_rotate = rot_z.apply_stiffness(L) - print(np.array_str(L_rotate, suppress_small=True)) + + +# %% +# Scipy interoperability +# +# ``simcoon.Rotation`` inherits from ``scipy.spatial.transform.Rotation``, +# so every scipy feature is available. Plain scipy ``Rotation`` objects can +# be upgraded to ``simcoon.Rotation`` via ``from_scipy()`` to unlock the +# mechanics methods. + + +# %% +# 13. simcoon.Rotation IS a scipy Rotation +# +# Type checks and scipy methods work directly. + +r = sim.Rotation.from_axis_angle(np.pi / 4, 3) +print("isinstance(r, ScipyRotation):", isinstance(r, ScipyRotation)) +print("Quaternion:", r.as_quat()) +print("Rotation vector:", r.as_rotvec()) +print("Euler ZXZ:", r.as_euler("ZXZ", degrees=True)) + + +# %% +# 14. Upgrade a plain scipy Rotation to simcoon +# +# Use ``from_scipy()`` to add mechanics capabilities to an existing scipy +# rotation (e.g. one returned by a third-party library). + +scipy_rot = ScipyRotation.from_euler("z", 45, degrees=True) +print("Type before:", type(scipy_rot)) + +smc_rot = sim.Rotation.from_scipy(scipy_rot) +print("Type after:", type(smc_rot)) + +# The quaternion is preserved exactly +print("Quaternions match:", np.allclose(scipy_rot.as_quat(), smc_rot.as_quat())) + +# Now mechanics methods are available +L_iso = sim.L_iso([70000, 0.3], "Enu") +L_rotated = smc_rot.apply_stiffness(L_iso) +print("Rotated stiffness L[0,0]:", L_rotated[0, 0]) + + +# %% +# 15. Verify scipy and simcoon produce the same rotation +# +# A simcoon rotation and a scipy rotation built from the same parameters +# give identical stiffness results. + +angle_test = np.pi / 6 +r_sim = sim.Rotation.from_axis_angle(angle_test, 3) +r_scipy = sim.Rotation.from_scipy(ScipyRotation.from_rotvec([0, 0, angle_test])) + +L_sim = r_sim.apply_stiffness(L_iso) +L_scipy = r_scipy.apply_stiffness(L_iso) +print("Stiffness matrices match:", np.allclose(L_sim, L_scipy)) + + +# %% +# 16. Compose simcoon and scipy rotations +# +# Because simcoon.Rotation inherits ``__mul__`` from scipy, composition +# with either type works directly. + +r1 = sim.Rotation.from_axis_angle(np.pi / 4, 3) # simcoon +r2 = ScipyRotation.from_euler("x", 30, degrees=True) # plain scipy + +r_composed = r1 * r2 # returns simcoon.Rotation +print("Composed type:", type(r_composed)) +print("Composed is simcoon Rotation:", isinstance(r_composed, sim.Rotation)) + + +# %% +# 17. Batch rotations and mean (scipy features) +# +# Generate random orientations and compute their mean — all scipy batch +# operations work out of the box. + +rots = sim.Rotation.random(100) +r_mean = rots.mean() +print("Mean rotation type:", type(r_mean)) +print("Mean rotation angle:", np.degrees(r_mean.magnitude()), "degrees") + + +# %% +# 18. Slerp interpolation +# +# Use scipy's ``Slerp`` class for multi-keyframe interpolation, or the +# convenience ``slerp_to()`` method for simple two-rotation interpolation. + +from scipy.spatial.transform import Slerp + +r_start = sim.Rotation.identity() +r_end = sim.Rotation.from_axis_angle(np.pi / 2, 3) + +# Via scipy Slerp class +key_rots = sim.Rotation.concatenate([r_start, r_end]) +slerp = Slerp([0, 1], key_rots) +for t in [0.0, 0.25, 0.5, 0.75, 1.0]: + r_t = slerp(t) + print(f" t={t:.2f}: angle = {np.degrees(r_t.magnitude()):.1f} deg") + +# Via convenience method +r_mid = r_start.slerp_to(r_end, 0.5) +print("Midpoint angle:", np.degrees(r_mid.magnitude()), "degrees") diff --git a/pyproject.toml b/pyproject.toml index 20defb5a3..c65bfec0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ maintainers = [ requires-python = ">=3.10" dependencies = [ "numpy>=2.0", + "scipy>=1.10", ] classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/python-setup/simcoon/__init__.py b/python-setup/simcoon/__init__.py index 266fd2118..09d94462c 100755 --- a/python-setup/simcoon/__init__.py +++ b/python-setup/simcoon/__init__.py @@ -1,5 +1,6 @@ from simcoon._core import * from simcoon.__version__ import __version__ +from simcoon.rotation import Rotation # override _CppRotation from star-import # Backward compatibility alias - simmit was the legacy module name from simcoon import _core as simmit diff --git a/python-setup/simcoon/rotation.py b/python-setup/simcoon/rotation.py new file mode 100644 index 000000000..dea3d04f1 --- /dev/null +++ b/python-setup/simcoon/rotation.py @@ -0,0 +1,310 @@ +""" +Rotation class extending scipy.spatial.transform.Rotation with mechanics operations. + +This module provides a ``Rotation`` class that inherits from scipy's ``Rotation``, +giving users access to all scipy rotation features (batch operations, ``mean()``, +``Slerp``, ``RotationSpline``, etc.) while adding simcoon's continuum-mechanics +methods (``apply_stress``, ``apply_stiffness``, etc.). +""" + +import numpy as np +from scipy.spatial.transform import Rotation as ScipyRotation + +from simcoon._core import _CppRotation + + +class Rotation(ScipyRotation): + """Rotation class extending scipy with continuum-mechanics operations. + + Inherits all scipy.spatial.transform.Rotation functionality (batch rotations, + ``mean()``, ``Slerp``, ``RotationSpline``, ``from_euler``, ``from_quat``, etc.) + and adds methods for rotating Voigt-notation stress, strain, stiffness, and + compliance tensors via the C++ backend. + + Examples + -------- + >>> import simcoon as smc + >>> import numpy as np + + >>> # All scipy factory methods return a simcoon Rotation + >>> r = smc.Rotation.from_rotvec([0, 0, np.pi/4]) + >>> isinstance(r, smc.Rotation) + True + + >>> # Rotate a stiffness matrix + >>> L = smc.L_iso([70000, 0.3], 'Enu') + >>> L_rot = r.apply_stiffness(L) + + >>> # Simcoon-specific: create rotation around a principal axis + >>> r = smc.Rotation.from_axis_angle(np.pi/4, 3) # 45 deg around z + """ + + # ------------------------------------------------------------------ + # Simcoon-specific factory methods + # ------------------------------------------------------------------ + + @classmethod + def from_axis_angle(cls, angle, axis, degrees=False): + """Create a rotation around a principal axis. + + Parameters + ---------- + angle : float + Rotation angle. + axis : int + Axis of rotation: 1 = x, 2 = y, 3 = z. + degrees : bool, optional + If True, *angle* is in degrees. Default is False (radians). + + Returns + ------- + Rotation + """ + if axis not in (1, 2, 3): + raise ValueError(f"axis must be 1, 2, or 3, got {axis}") + direction = np.zeros(3) + direction[axis - 1] = 1.0 + return cls.from_rotvec(direction * (angle if not degrees else np.radians(angle))) + + @classmethod + def from_scipy(cls, scipy_rot): + """Create a simcoon Rotation from a plain scipy Rotation. + + This is a convenience method for upgrading a + ``scipy.spatial.transform.Rotation`` to a ``simcoon.Rotation`` so + that the mechanics methods (``apply_stress``, ``apply_stiffness``, + etc.) become available. + + Parameters + ---------- + scipy_rot : scipy.spatial.transform.Rotation + A scipy Rotation object (single or batch). + + Returns + ------- + Rotation + """ + return cls.from_quat(scipy_rot.as_quat()) + + # ------------------------------------------------------------------ + # Internal helper + # ------------------------------------------------------------------ + + def _to_cpp(self): + """Convert to a _CppRotation for C++ method dispatch.""" + return _CppRotation.from_quat(self.as_quat()) + + # ------------------------------------------------------------------ + # Mechanics methods (delegate to _CppRotation) + # ------------------------------------------------------------------ + + def apply_stress(self, sigma, active=True): + """Apply rotation to a stress vector in Voigt notation. + + Parameters + ---------- + sigma : array_like + 6-component stress vector [s11, s22, s33, s12, s13, s23]. + active : bool, optional + If True (default), active rotation. + + Returns + ------- + numpy.ndarray + Rotated stress vector. + """ + return self._to_cpp().apply_stress(np.asarray(sigma, dtype=float), active).ravel() + + def apply_strain(self, epsilon, active=True): + """Apply rotation to a strain vector in Voigt notation. + + Parameters + ---------- + epsilon : array_like + 6-component strain vector [e11, e22, e33, 2*e12, 2*e13, 2*e23]. + active : bool, optional + If True (default), active rotation. + + Returns + ------- + numpy.ndarray + Rotated strain vector. + """ + return self._to_cpp().apply_strain(np.asarray(epsilon, dtype=float), active).ravel() + + def apply_stiffness(self, L, active=True): + """Apply rotation to a 6x6 stiffness matrix. + + Parameters + ---------- + L : array_like + 6x6 stiffness matrix in Voigt notation. + active : bool, optional + If True (default), active rotation. + + Returns + ------- + numpy.ndarray + Rotated 6x6 stiffness matrix. + """ + return self._to_cpp().apply_stiffness(np.asarray(L, dtype=float), active) + + def apply_compliance(self, M, active=True): + """Apply rotation to a 6x6 compliance matrix. + + Parameters + ---------- + M : array_like + 6x6 compliance matrix in Voigt notation. + active : bool, optional + If True (default), active rotation. + + Returns + ------- + numpy.ndarray + Rotated 6x6 compliance matrix. + """ + return self._to_cpp().apply_compliance(np.asarray(M, dtype=float), active) + + def apply_strain_concentration(self, A, active=True): + """Apply rotation to a 6x6 strain concentration tensor. + + Parameters + ---------- + A : array_like + 6x6 strain concentration tensor in Voigt notation. + active : bool, optional + If True (default), active rotation. + + Returns + ------- + numpy.ndarray + Rotated strain concentration tensor: QE * A * QS^T. + """ + return self._to_cpp().apply_strain_concentration(np.asarray(A, dtype=float), active) + + def apply_stress_concentration(self, B, active=True): + """Apply rotation to a 6x6 stress concentration tensor. + + Parameters + ---------- + B : array_like + 6x6 stress concentration tensor in Voigt notation. + active : bool, optional + If True (default), active rotation. + + Returns + ------- + numpy.ndarray + Rotated stress concentration tensor: QS * B * QE^T. + """ + return self._to_cpp().apply_stress_concentration(np.asarray(B, dtype=float), active) + + def apply_tensor(self, m, inverse=False): + """Apply rotation to a 3x3 tensor (matrix). + + Parameters + ---------- + m : array_like + 3x3 tensor to rotate. + inverse : bool, optional + If True, apply inverse rotation. Default is False. + + Returns + ------- + numpy.ndarray + Rotated tensor: R * m * R^T (or R^T * m * R for inverse). + """ + return self._to_cpp().apply_tensor(np.asarray(m, dtype=float), inverse) + + def as_voigt_stress_rotation(self, active=True): + """Get 6x6 rotation matrix for stress tensors in Voigt notation. + + Parameters + ---------- + active : bool, optional + If True (default), active rotation; if False, passive. + + Returns + ------- + numpy.ndarray + 6x6 stress rotation matrix (QS). + """ + return self._to_cpp().as_voigt_stress_rotation(active) + + def as_voigt_strain_rotation(self, active=True): + """Get 6x6 rotation matrix for strain tensors in Voigt notation. + + Parameters + ---------- + active : bool, optional + If True (default), active rotation; if False, passive. + + Returns + ------- + numpy.ndarray + 6x6 strain rotation matrix (QE). + """ + return self._to_cpp().as_voigt_strain_rotation(active) + + # ------------------------------------------------------------------ + # Compatibility helpers + # ------------------------------------------------------------------ + + def equals(self, other, tol=1e-12): + """Check if this rotation equals another within tolerance. + + Accounts for quaternion antipodal equivalence (q and -q represent + the same rotation). + + Parameters + ---------- + other : Rotation + Rotation to compare with. + tol : float, optional + Tolerance for comparison. Default is 1e-12. + + Returns + ------- + bool + """ + q1 = self.as_quat() + q2 = other.as_quat() + return np.allclose(q1, q2, atol=tol) or np.allclose(q1, -q2, atol=tol) + + def is_identity(self, tol=1e-12): + """Check if this rotation is identity (no rotation). + + Parameters + ---------- + tol : float, optional + Tolerance for comparison. Default is 1e-12. + + Returns + ------- + bool + """ + return self.magnitude() < tol + + def slerp_to(self, other, t): + """Spherical linear interpolation between this rotation and *other*. + + For full-featured interpolation over multiple keyframes, use + ``scipy.spatial.transform.Slerp`` instead. + + Parameters + ---------- + other : Rotation + Target rotation. + t : float + Interpolation parameter in [0, 1] (0 = self, 1 = other). + + Returns + ------- + Rotation + Interpolated rotation. + """ + from scipy.spatial.transform import Slerp + key_rots = type(self).concatenate([self, other]) + interp = Slerp([0.0, 1.0], key_rots) + return interp(t) diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp index f50b898d3..1347b106c 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp @@ -41,7 +41,7 @@ namespace { } // anonymous namespace void register_rotation(py::module_& m) { - py::class_(m, "Rotation", + py::class_(m, "_CppRotation", R"doc( A class representing 3D rotations using unit quaternions. @@ -200,67 +200,6 @@ void register_rotation(py::module_& m) { .def_static("random", &simcoon::Rotation::random, "Create a uniformly distributed random rotation") - .def_static("from_scipy", - [](py::object scipy_rot) { - py::array_t q = scipy_rot.attr("as_quat")().cast>(); - validate_vector_size(q, 4, "scipy_rot.as_quat()"); - vec qv = carma::arr_to_col(q); - return simcoon::Rotation::from_quat(qv); - }, - py::arg("scipy_rot"), - R"doc( - Create rotation from a scipy.spatial.transform.Rotation object. - - Converts via unit quaternion (scalar-last convention shared by both - libraries), so there is no trigonometric or matrix conversion overhead. - - Parameters - ---------- - scipy_rot : scipy.spatial.transform.Rotation - A scipy Rotation object - - Returns - ------- - Rotation - Rotation object - - Example - ------- - >>> from scipy.spatial.transform import Rotation as R - >>> scipy_rot = R.from_euler('z', 45, degrees=True) - >>> r = smc.Rotation.from_scipy(scipy_rot) - )doc") - - .def("to_scipy", - [](const simcoon::Rotation& self) { - py::module_ sp_rot = py::module_::import("scipy.spatial.transform"); - py::object R_class = sp_rot.attr("Rotation"); - vec q(self.as_quat()); - py::array_t q_arr = carma::col_to_arr(q); - // scipy expects shape (4,), carma returns (4,1) - q_arr = q_arr.attr("flatten")().cast>(); - return R_class.attr("from_quat")(q_arr); - }, - R"doc( - Convert to a scipy.spatial.transform.Rotation object. - - Converts via unit quaternion (scalar-last convention shared by both - libraries), so there is no trigonometric or matrix conversion overhead. - - Requires scipy to be installed. - - Returns - ------- - scipy.spatial.transform.Rotation - Equivalent scipy Rotation object - - Example - ------- - >>> r = smc.Rotation.from_axis_angle(np.pi/4, 3) - >>> scipy_rot = r.to_scipy() - >>> scipy_rot.as_euler('zxz', degrees=True) - )doc") - // Conversion methods .def("as_quat", [](const simcoon::Rotation& self) { @@ -582,15 +521,45 @@ void register_rotation(py::module_& m) { Rotation angle )doc") - .def("__mul__", &simcoon::Rotation::operator*, + .def("__mul__", + [](const simcoon::Rotation& self, py::object other) { + if (py::isinstance(other)) { + return self * other.cast(); + } + // Accept any object with as_quat() (e.g. simcoon.Rotation scipy subclass) + py::array_t q = other.attr("as_quat")().cast>(); + validate_vector_size(q, 4, "other.as_quat()"); + return self * simcoon::Rotation::from_quat(carma::arr_to_col(q)); + }, py::arg("other"), "Compose this rotation with another (self * other)") - .def("__imul__", &simcoon::Rotation::operator*=, + .def("__imul__", + [](simcoon::Rotation& self, py::object other) -> simcoon::Rotation& { + if (py::isinstance(other)) { + self *= other.cast(); + } else { + py::array_t q = other.attr("as_quat")().cast>(); + validate_vector_size(q, 4, "other.as_quat()"); + self *= simcoon::Rotation::from_quat(carma::arr_to_col(q)); + } + return self; + }, py::arg("other"), "Compose this rotation with another in-place") - .def("slerp", &simcoon::Rotation::slerp, + .def("slerp", + [](const simcoon::Rotation& self, py::object other, double t) { + simcoon::Rotation other_rot; + if (py::isinstance(other)) { + other_rot = other.cast(); + } else { + py::array_t q = other.attr("as_quat")().cast>(); + validate_vector_size(q, 4, "other.as_quat()"); + other_rot = simcoon::Rotation::from_quat(carma::arr_to_col(q)); + } + return self.slerp(other_rot, t); + }, py::arg("other"), py::arg("t"), R"doc( Spherical linear interpolation between this rotation and another. @@ -608,7 +577,18 @@ void register_rotation(py::module_& m) { Interpolated rotation )doc") - .def("equals", &simcoon::Rotation::equals, + .def("equals", + [](const simcoon::Rotation& self, py::object other, double tol) { + simcoon::Rotation other_rot; + if (py::isinstance(other)) { + other_rot = other.cast(); + } else { + py::array_t q = other.attr("as_quat")().cast>(); + validate_vector_size(q, 4, "other.as_quat()"); + other_rot = simcoon::Rotation::from_quat(carma::arr_to_col(q)); + } + return self.equals(other_rot, tol); + }, py::arg("other"), py::arg("tol") = 1e-12, R"doc( Check if this rotation equals another within tolerance. @@ -642,13 +622,7 @@ void register_rotation(py::module_& m) { True if this is the identity rotation )doc") - .def("__repr__", - [](const simcoon::Rotation& self) { - vec::fixed<4> q = self.as_quat(); - return ""; - }); + ; } } // namespace simpy diff --git a/simcoon-python-builder/test/test_core/test_rotation.py b/simcoon-python-builder/test/test_core/test_rotation.py new file mode 100644 index 000000000..b0c819cbb --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_rotation.py @@ -0,0 +1,342 @@ +"""Tests for the Rotation class and scipy interoperability.""" + +import numpy as np +import pytest +from scipy.spatial.transform import Rotation as ScipyRotation + +import simcoon as sim +from simcoon._core import _CppRotation + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def angle(): + return np.pi / 4 + + +@pytest.fixture +def sim_rot(angle): + """A simcoon.Rotation (scipy subclass).""" + return sim.Rotation.from_axis_angle(angle, 3) + + +@pytest.fixture +def scipy_rot(angle): + """A plain scipy Rotation representing the same rotation.""" + return ScipyRotation.from_rotvec([0, 0, angle]) + + +@pytest.fixture +def cpp_rot(angle): + """A _CppRotation representing the same rotation.""" + return _CppRotation.from_axis_angle(angle, 3) + + +@pytest.fixture +def L(): + """An isotropic stiffness matrix.""" + return sim.L_iso([70000, 0.3], "Enu") + + +@pytest.fixture +def M(): + """An isotropic compliance matrix.""" + return sim.M_iso([70000, 0.3], "Enu") + + +@pytest.fixture +def sigma(): + return np.array([100.0, 50.0, 0.0, 25.0, 0.0, 0.0]) + + +@pytest.fixture +def epsilon(): + return np.array([0.01, -0.005, -0.005, 0.002, 0.001, 0.0]) + + +# =================================================================== +# 1. Type identity +# =================================================================== + +class TestTypeIdentity: + """simcoon.Rotation IS a scipy Rotation.""" + + def test_isinstance_scipy(self, sim_rot): + assert isinstance(sim_rot, ScipyRotation) + + def test_isinstance_simcoon(self, sim_rot): + assert isinstance(sim_rot, sim.Rotation) + + def test_from_euler_returns_simcoon(self): + r = sim.Rotation.from_euler("ZXZ", [0.5, 0.3, 0.7]) + assert isinstance(r, sim.Rotation) + + def test_from_quat_returns_simcoon(self): + r = sim.Rotation.from_quat([0, 0, 0.38268343, 0.92387953]) + assert isinstance(r, sim.Rotation) + + def test_from_matrix_returns_simcoon(self): + R = np.eye(3) + r = sim.Rotation.from_matrix(R) + assert isinstance(r, sim.Rotation) + + def test_from_rotvec_returns_simcoon(self): + r = sim.Rotation.from_rotvec([0, 0, 0.5]) + assert isinstance(r, sim.Rotation) + + def test_identity_returns_simcoon(self): + r = sim.Rotation.identity() + assert isinstance(r, sim.Rotation) + + def test_random_returns_simcoon(self): + r = sim.Rotation.random() + assert isinstance(r, sim.Rotation) + + def test_composition_preserves_type(self, sim_rot): + r2 = sim.Rotation.from_axis_angle(np.pi / 6, 1) + assert isinstance(sim_rot * r2, sim.Rotation) + + def test_inv_preserves_type(self, sim_rot): + assert isinstance(sim_rot.inv(), sim.Rotation) + + +# =================================================================== +# 2. from_scipy conversion +# =================================================================== + +class TestFromScipy: + """Upgrade a plain scipy Rotation to simcoon.Rotation.""" + + def test_from_scipy_type(self, scipy_rot): + r = sim.Rotation.from_scipy(scipy_rot) + assert isinstance(r, sim.Rotation) + + def test_from_scipy_preserves_quaternion(self, scipy_rot): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose(r.as_quat(), scipy_rot.as_quat(), atol=1e-15) + + def test_from_scipy_preserves_matrix(self, scipy_rot): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose(r.as_matrix(), scipy_rot.as_matrix(), atol=1e-14) + + def test_from_scipy_mechanics_work(self, scipy_rot, L): + r = sim.Rotation.from_scipy(scipy_rot) + L_rot = r.apply_stiffness(L) + assert L_rot.shape == (6, 6) + + +# =================================================================== +# 3. Mechanics methods +# =================================================================== + +class TestMechanics: + """Mechanics methods on simcoon.Rotation delegate to C++ correctly.""" + + def test_apply_stress_shape(self, sim_rot, sigma): + result = sim_rot.apply_stress(sigma) + assert result.shape == (6,) + + def test_apply_strain_shape(self, sim_rot, epsilon): + result = sim_rot.apply_strain(epsilon) + assert result.shape == (6,) + + def test_apply_stiffness_shape(self, sim_rot, L): + result = sim_rot.apply_stiffness(L) + assert result.shape == (6, 6) + + def test_apply_compliance_shape(self, sim_rot, M): + result = sim_rot.apply_compliance(M) + assert result.shape == (6, 6) + + def test_apply_tensor_shape(self, sim_rot): + T = np.eye(3) + result = sim_rot.apply_tensor(T) + assert result.shape == (3, 3) + + def test_apply_strain_concentration_shape(self, sim_rot): + A = np.eye(6) + result = sim_rot.apply_strain_concentration(A) + assert result.shape == (6, 6) + + def test_apply_stress_concentration_shape(self, sim_rot): + B = np.eye(6) + result = sim_rot.apply_stress_concentration(B) + assert result.shape == (6, 6) + + def test_voigt_stress_rotation_shape(self, sim_rot): + QS = sim_rot.as_voigt_stress_rotation() + assert QS.shape == (6, 6) + + def test_voigt_strain_rotation_shape(self, sim_rot): + QE = sim_rot.as_voigt_strain_rotation() + assert QE.shape == (6, 6) + + def test_identity_stiffness_unchanged(self, L): + r = sim.Rotation.identity() + np.testing.assert_allclose(r.apply_stiffness(L), L, atol=1e-10) + + def test_roundtrip_stress(self, sim_rot, sigma): + sigma_rot = sim_rot.apply_stress(sigma) + sigma_back = sim_rot.apply_stress(sigma_rot, active=False) + np.testing.assert_allclose(sigma_back, sigma, atol=1e-10) + + def test_roundtrip_stiffness(self, sim_rot, L): + L_rot = sim_rot.apply_stiffness(L) + L_back = sim_rot.apply_stiffness(L_rot, active=False) + np.testing.assert_allclose(L_back, L, atol=1e-10) + + def test_stiffness_via_voigt(self, sim_rot, L): + """apply_stiffness(L) == QS @ L @ QS.T""" + QS = sim_rot.as_voigt_stress_rotation() + L_rot_direct = sim_rot.apply_stiffness(L) + L_rot_manual = QS @ L @ QS.T + np.testing.assert_allclose(L_rot_direct, L_rot_manual, atol=1e-10) + + +# =================================================================== +# 4. Mechanics from scipy-created rotation (via from_scipy) +# =================================================================== + +class TestMechanicsFromScipy: + """Verify that a scipy Rotation upgraded via from_scipy produces + identical results to a natively-created simcoon Rotation.""" + + def test_stiffness_matches(self, sim_rot, scipy_rot, L): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose( + r.apply_stiffness(L), sim_rot.apply_stiffness(L), atol=1e-10 + ) + + def test_compliance_matches(self, sim_rot, scipy_rot, M): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose( + r.apply_compliance(M), sim_rot.apply_compliance(M), atol=1e-10 + ) + + def test_stress_matches(self, sim_rot, scipy_rot, sigma): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose( + r.apply_stress(sigma), sim_rot.apply_stress(sigma), atol=1e-10 + ) + + def test_strain_matches(self, sim_rot, scipy_rot, epsilon): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose( + r.apply_strain(epsilon), sim_rot.apply_strain(epsilon), atol=1e-10 + ) + + def test_tensor_matches(self, sim_rot, scipy_rot): + T = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose( + r.apply_tensor(T), sim_rot.apply_tensor(T), atol=1e-10 + ) + + def test_voigt_stress_rotation_matches(self, sim_rot, scipy_rot): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose( + r.as_voigt_stress_rotation(), + sim_rot.as_voigt_stress_rotation(), + atol=1e-10, + ) + + def test_voigt_strain_rotation_matches(self, sim_rot, scipy_rot): + r = sim.Rotation.from_scipy(scipy_rot) + np.testing.assert_allclose( + r.as_voigt_strain_rotation(), + sim_rot.as_voigt_strain_rotation(), + atol=1e-10, + ) + + +# =================================================================== +# 5. C++ _CppRotation accepts plain scipy objects +# =================================================================== + +class TestCppAcceptsScipy: + """_CppRotation methods that take a Rotation parameter accept plain + scipy.spatial.transform.Rotation objects via as_quat() duck-typing.""" + + def test_cpp_mul_with_scipy(self, cpp_rot, scipy_rot): + result = cpp_rot * scipy_rot + assert isinstance(result, _CppRotation) + + def test_cpp_equals_with_scipy(self, cpp_rot, scipy_rot): + assert cpp_rot.equals(scipy_rot) + + def test_cpp_slerp_with_scipy(self, cpp_rot, scipy_rot): + mid = cpp_rot.slerp(scipy_rot, 0.5) + assert isinstance(mid, _CppRotation) + + def test_cpp_mul_with_simcoon(self, cpp_rot, sim_rot): + result = cpp_rot * sim_rot + assert isinstance(result, _CppRotation) + + def test_cpp_equals_with_simcoon(self, cpp_rot, sim_rot): + assert cpp_rot.equals(sim_rot) + + def test_cpp_slerp_with_simcoon(self, cpp_rot, sim_rot): + mid = cpp_rot.slerp(sim_rot, 0.5) + assert isinstance(mid, _CppRotation) + + def test_cpp_mul_produces_correct_result(self, cpp_rot, scipy_rot): + """C++ composition with scipy gives same quaternion as two _CppRotation.""" + cpp_rot2 = _CppRotation.from_quat(scipy_rot.as_quat()) + result_scipy = cpp_rot * scipy_rot + result_cpp = cpp_rot * cpp_rot2 + np.testing.assert_allclose( + result_scipy.as_quat().ravel(), + result_cpp.as_quat().ravel(), + atol=1e-14, + ) + + +# =================================================================== +# 6. Utility methods +# =================================================================== + +class TestUtilities: + + def test_equals_same(self, sim_rot): + r2 = sim.Rotation.from_axis_angle(np.pi / 4, 3) + assert sim_rot.equals(r2) + + def test_equals_different(self, sim_rot): + r2 = sim.Rotation.from_axis_angle(np.pi / 3, 3) + assert not sim_rot.equals(r2) + + def test_equals_with_scipy(self, sim_rot, scipy_rot): + assert sim_rot.equals(scipy_rot) + + def test_is_identity_true(self): + r = sim.Rotation.identity() + assert r.is_identity() + + def test_is_identity_false(self, sim_rot): + assert not sim_rot.is_identity() + + def test_slerp_to_endpoints(self, sim_rot): + r_id = sim.Rotation.identity() + r0 = r_id.slerp_to(sim_rot, 0.0) + r1 = r_id.slerp_to(sim_rot, 1.0) + np.testing.assert_allclose(r0.as_quat(), r_id.as_quat(), atol=1e-12) + np.testing.assert_allclose( + np.abs(r1.as_quat() @ sim_rot.as_quat()), 1.0, atol=1e-12 + ) + + def test_slerp_to_with_scipy_target(self, scipy_rot): + r_id = sim.Rotation.identity() + r_mid = r_id.slerp_to(scipy_rot, 0.5) + assert isinstance(r_mid, sim.Rotation) + + def test_from_axis_angle_degrees(self): + r_rad = sim.Rotation.from_axis_angle(np.pi / 4, 3) + r_deg = sim.Rotation.from_axis_angle(45, 3, degrees=True) + np.testing.assert_allclose(r_rad.as_quat(), r_deg.as_quat(), atol=1e-12) + + def test_from_axis_angle_invalid_axis(self): + with pytest.raises(ValueError, match="axis must be 1, 2, or 3"): + sim.Rotation.from_axis_angle(0.5, 4) From 475b5bd5d0c250e9c4b92057b49d2c0016aba56d Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 11:52:53 +0100 Subject: [PATCH 15/31] Update rotation.rst --- docs/cpp_api/simulation/rotation.rst | 31 +++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst index c4d508204..324f7b10d 100644 --- a/docs/cpp_api/simulation/rotation.rst +++ b/docs/cpp_api/simulation/rotation.rst @@ -71,9 +71,9 @@ Python API import simcoon as smc import numpy as np - # Create rotations + # Create rotations (scipy factory methods + simcoon's from_axis_angle) r1 = smc.Rotation.identity() - r2 = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + r2 = smc.Rotation.from_euler('ZXZ', [0.5, 0.3, 0.7]) r3 = smc.Rotation.from_axis_angle(np.pi/4, 3) r4 = smc.Rotation.from_matrix(R) r5 = smc.Rotation.random() @@ -81,7 +81,7 @@ Python API # Convert to different representations R = r2.as_matrix() q = r2.as_quat() - euler = r2.as_euler("zxz") + euler = r2.as_euler('ZXZ') rotvec = r2.as_rotvec() # Apply to vectors and tensors @@ -94,7 +94,7 @@ Python API r_inv = r2.inv() # Interpolation - r_mid = r2.slerp(r3, 0.5) + r_mid = r2.slerp_to(r3, 0.5) Factory Methods --------------- @@ -111,8 +111,8 @@ Factory Methods - Create from quaternion [qx, qy, qz, qw] (scalar-last) * - ``from_matrix(R)`` - Create from 3x3 rotation matrix - * - ``from_euler(psi, theta, phi, conv, intrinsic, degrees)`` - - Create from Euler angles with specified convention + * - ``from_euler(seq, angles, degrees)`` + - Create from Euler angles (scipy convention, e.g. ``'ZXZ'``) * - ``from_rotvec(rotvec, degrees)`` - Create from rotation vector (axis × angle) * - ``from_axis_angle(angle, axis, degrees)`` @@ -220,13 +220,13 @@ Operations - Description * - ``inv()`` - Return inverse rotation - * - ``magnitude(degrees)`` - - Return rotation angle + * - ``magnitude()`` + - Return rotation angle in radians * - ``r1 * r2`` - Compose rotations (apply r2 first, then r1) * - ``r1 *= r2`` - Compose in-place - * - ``slerp(other, t)`` + * - ``slerp_to(other, t)`` - Spherical linear interpolation (t ∈ [0, 1]) * - ``equals(other, tol)`` - Check equality within tolerance @@ -319,7 +319,7 @@ Rotating material properties from local to global coordinates: psi, theta, phi = 0.5, 0.3, 0.7 # Using Rotation class - r = smc.Rotation.from_euler(psi, theta, phi, "zxz") + r = smc.Rotation.from_euler('ZXZ', [psi, theta, phi]) L_global = r.apply_stiffness(L_local) Example 2: Stress Transformation @@ -336,7 +336,7 @@ Transforming stress from global to local coordinates: sigma_global = np.array([100.0, 50.0, 25.0, 10.0, 5.0, 2.0]) # Create rotation from orientation - r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") + r = smc.Rotation.from_euler('ZXZ', [0.5, 0.3, 0.7]) # Transform to local coordinates (inverse rotation) sigma_local = r.inv().apply_stress(sigma_global) @@ -377,16 +377,19 @@ Smooth interpolation between two orientations using SLERP: import simcoon as smc import numpy as np + from scipy.spatial.transform import Slerp # Start and end orientations r_start = smc.Rotation.identity() - r_end = smc.Rotation.from_euler(np.pi/2, np.pi/4, 0, "zxz") + r_end = smc.Rotation.from_euler('ZXZ', [np.pi/2, np.pi/4, 0]) # Interpolate at 10 steps + key_rots = smc.Rotation.concatenate([r_start, r_end]) + slerp = Slerp([0, 1], key_rots) for t in np.linspace(0, 1, 10): - r_t = r_start.slerp(r_end, t) + r_t = slerp(t) R = r_t.as_matrix() - print(f"t={t:.1f}: angle={r_t.magnitude(degrees=True):.1f}°") + print(f"t={t:.1f}: angle={np.degrees(r_t.magnitude()):.1f}°") Theory ====== From 62742fd66f0f12b9ee6bd1900ea1a8ee0e1dc476 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 11:53:26 +0100 Subject: [PATCH 16/31] Update rotation.cpp --- .../Libraries/Maths/rotation.cpp | 512 +----------------- 1 file changed, 17 insertions(+), 495 deletions(-) diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp index 1347b106c..f3b0f3eab 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp @@ -16,8 +16,6 @@ namespace py = pybind11; namespace simpy { namespace { - // Validation helper functions for professional solver integration - void validate_vector_size(const py::array_t& arr, size_t expected, const string& name) { if (arr.ndim() != 1) { throw invalid_argument(name + " must be a 1D array, got " + to_string(arr.ndim()) + "D"); @@ -43,43 +41,14 @@ namespace { void register_rotation(py::module_& m) { py::class_(m, "_CppRotation", R"doc( - A class representing 3D rotations using unit quaternions. - - Inspired by scipy.spatial.transform.Rotation, this class provides a unified - interface for working with rotations in various representations (quaternion, - rotation matrix, Euler angles, rotation vector) while maintaining high - numerical precision and performance. - - The internal representation uses unit quaternions in scalar-last convention [qx, qy, qz, qw]. + Internal C++ rotation backend using unit quaternions (scalar-last). - Example - ------- - >>> import simcoon as smc - >>> import numpy as np - >>> - >>> # Create rotation from Euler angles - >>> r = smc.Rotation.from_euler(0.5, 0.3, 0.7, "zxz") - >>> - >>> # Apply to a vector - >>> v = np.array([1.0, 0.0, 0.0]) - >>> v_rot = r.apply(v) - >>> - >>> # Compose rotations - >>> r2 = smc.Rotation.from_axis_angle(np.pi/4, 3) - >>> r_combined = r * r2 - >>> - >>> # Apply to Voigt stress tensor - >>> sigma = np.array([100.0, 50.0, 0.0, 25.0, 0.0, 0.0]) - >>> sigma_rot = r.apply_stress(sigma) + End users should use ``simcoon.Rotation`` instead, which inherits from + ``scipy.spatial.transform.Rotation`` and delegates mechanics operations + to this class. )doc") - // Default constructor - .def(py::init<>(), "Create an identity rotation") - - // Static factory methods - .def_static("identity", &simcoon::Rotation::identity, - "Create an identity rotation (no rotation)") - + // The only factory method needed — Python Rotation._to_cpp() uses this .def_static("from_quat", [](py::array_t quat) { validate_vector_size(quat, 4, "quat"); @@ -87,252 +56,24 @@ void register_rotation(py::module_& m) { return simcoon::Rotation::from_quat(q); }, py::arg("quat"), - R"doc( - Create rotation from a unit quaternion. - - Parameters - ---------- - quat : array_like - Quaternion in [qx, qy, qz, qw] format (scalar-last). - Will be normalized if not already unit length. - - Returns - ------- - Rotation - Rotation object - )doc") - - .def_static("from_matrix", - [](py::array_t R) { - validate_matrix_size(R, 3, 3, "R"); - mat R_mat = carma::arr_to_mat(R); - return simcoon::Rotation::from_matrix(R_mat); - }, - py::arg("R"), - R"doc( - Create rotation from a 3x3 rotation matrix. - - Parameters - ---------- - R : array_like - 3x3 rotation matrix (must be orthogonal with det=1) - - Returns - ------- - Rotation - Rotation object - )doc") - - .def_static("from_euler", - py::overload_cast( - &simcoon::Rotation::from_euler), - py::arg("psi"), py::arg("theta"), py::arg("phi"), - py::arg("conv"), py::arg("intrinsic") = true, py::arg("degrees") = false, - R"doc( - Create rotation from Euler angles. - - Parameters - ---------- - psi : float - First Euler angle - theta : float - Second Euler angle - phi : float - Third Euler angle - conv : str - Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", - "xyx", "xzx", "yxy", "yzy", or "user". - intrinsic : bool, optional - If True (default), intrinsic rotations (rotating frame); - if False, extrinsic rotations (fixed frame) - degrees : bool, optional - If True, angles are in degrees; if False (default), radians - - Returns - ------- - Rotation - Rotation object - )doc") - - .def_static("from_rotvec", - [](py::array_t rotvec, bool degrees) { - validate_vector_size(rotvec, 3, "rotvec"); - vec rv = carma::arr_to_col(rotvec); - return simcoon::Rotation::from_rotvec(rv, degrees); - }, - py::arg("rotvec"), py::arg("degrees") = false, - R"doc( - Create rotation from a rotation vector. - - Parameters - ---------- - rotvec : array_like - Rotation vector where direction is axis and magnitude is angle - degrees : bool, optional - If True, magnitude is in degrees; if False (default), radians - - Returns - ------- - Rotation - Rotation object - )doc") - - .def_static("from_axis_angle", &simcoon::Rotation::from_axis_angle, - py::arg("angle"), py::arg("axis"), py::arg("degrees") = false, - R"doc( - Create rotation around a single axis. - - Parameters - ---------- - angle : float - Rotation angle - axis : int - Axis of rotation: 1=x, 2=y, 3=z - degrees : bool, optional - If True, angle is in degrees; if False (default), radians - - Returns - ------- - Rotation - Rotation object - )doc") - - .def_static("random", &simcoon::Rotation::random, - "Create a uniformly distributed random rotation") - - // Conversion methods - .def("as_quat", - [](const simcoon::Rotation& self) { - return carma::col_to_arr(vec(self.as_quat())); - }, - R"doc( - Get rotation as a unit quaternion. - - Returns - ------- - ndarray - Quaternion in [qx, qy, qz, qw] format (scalar-last) - )doc") - - .def("as_matrix", - [](const simcoon::Rotation& self) { - return carma::mat_to_arr(mat(self.as_matrix())); - }, - R"doc( - Get rotation as a 3x3 rotation matrix. - - Returns - ------- - ndarray - 3x3 orthogonal rotation matrix - )doc") - - .def("as_euler", - [](const simcoon::Rotation& self, const string& conv, bool intrinsic, bool degrees) { - return carma::col_to_arr(vec(self.as_euler(conv, intrinsic, degrees))); - }, - py::arg("conv"), py::arg("intrinsic") = true, py::arg("degrees") = false, - R"doc( - Get rotation as Euler angles. - - Parameters - ---------- - conv : str - Euler angle convention: "zxz", "zyz", "xyz", "xzy", "yxz", "yzx", "zxy", "zyx", - "xyx", "xzx", "yxy", "yzy", or "user". - intrinsic : bool, optional - If True (default), return intrinsic angles; if False, extrinsic - degrees : bool, optional - If True, return angles in degrees; if False (default), radians - - Returns - ------- - ndarray - 3-element array of Euler angles [psi, theta, phi] - )doc") - - .def("as_rotvec", - [](const simcoon::Rotation& self, bool degrees) { - return carma::col_to_arr(vec(self.as_rotvec(degrees))); - }, - py::arg("degrees") = false, - R"doc( - Get rotation as a rotation vector. - - Parameters - ---------- - degrees : bool, optional - If True, magnitude is in degrees; if False (default), radians - - Returns - ------- - ndarray - Rotation vector (axis * angle) - )doc") + "Create rotation from quaternion [qx, qy, qz, qw] (scalar-last)") + // Voigt rotation matrices .def("as_voigt_stress_rotation", [](const simcoon::Rotation& self, bool active) { return carma::mat_to_arr(mat(self.as_voigt_stress_rotation(active))); }, py::arg("active") = true, - R"doc( - Get 6x6 rotation matrix for stress tensors in Voigt notation. - - Parameters - ---------- - active : bool, optional - If True (default), active rotation; if False, passive - - Returns - ------- - ndarray - 6x6 stress rotation matrix (QS) - )doc") + "Get 6x6 rotation matrix for stress tensors in Voigt notation") .def("as_voigt_strain_rotation", [](const simcoon::Rotation& self, bool active) { return carma::mat_to_arr(mat(self.as_voigt_strain_rotation(active))); }, py::arg("active") = true, - R"doc( - Get 6x6 rotation matrix for strain tensors in Voigt notation. - - Parameters - ---------- - active : bool, optional - If True (default), active rotation; if False, passive - - Returns - ------- - ndarray - 6x6 strain rotation matrix (QE) - )doc") - - // Apply methods - .def("apply", - [](const simcoon::Rotation& self, py::array_t v, bool inverse) { - validate_vector_size(v, 3, "v"); - vec v_cpp = carma::arr_to_col(v); - vec result = self.apply(v_cpp, inverse); - return carma::col_to_arr(result); - }, - py::arg("v"), py::arg("inverse") = false, - R"doc( - Apply rotation to a 3D vector. - - Parameters - ---------- - v : array_like - 3D vector to rotate - inverse : bool, optional - If True, apply inverse rotation. Default is False. - - Returns - ------- - ndarray - Rotated vector - )doc") + "Get 6x6 rotation matrix for strain tensors in Voigt notation") + // Apply methods — the core mechanics operations .def("apply_tensor", [](const simcoon::Rotation& self, py::array_t m, bool inverse) { validate_matrix_size(m, 3, 3, "m"); @@ -341,21 +82,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("m"), py::arg("inverse") = false, - R"doc( - Apply rotation to a 3x3 tensor (matrix). - - Parameters - ---------- - m : array_like - 3x3 tensor to rotate - inverse : bool, optional - If True, apply inverse rotation. Default is False. - - Returns - ------- - ndarray - Rotated tensor: R * m * R^T (or R^T * m * R for inverse) - )doc") + "Apply rotation to a 3x3 tensor: R * m * R^T") .def("apply_stress", [](const simcoon::Rotation& self, py::array_t sigma, bool active) { @@ -365,21 +92,7 @@ void register_rotation(py::module_& m) { return carma::col_to_arr(result); }, py::arg("sigma"), py::arg("active") = true, - R"doc( - Apply rotation to a stress vector in Voigt notation. - - Parameters - ---------- - sigma : array_like - 6-component stress vector [s11, s22, s33, s12, s13, s23] - active : bool, optional - If True (default), active rotation - - Returns - ------- - ndarray - Rotated stress vector - )doc") + "Apply rotation to a 6-component stress vector in Voigt notation") .def("apply_strain", [](const simcoon::Rotation& self, py::array_t epsilon, bool active) { @@ -389,21 +102,7 @@ void register_rotation(py::module_& m) { return carma::col_to_arr(result); }, py::arg("epsilon"), py::arg("active") = true, - R"doc( - Apply rotation to a strain vector in Voigt notation. - - Parameters - ---------- - epsilon : array_like - 6-component strain vector [e11, e22, e33, 2*e12, 2*e13, 2*e23] - active : bool, optional - If True (default), active rotation - - Returns - ------- - ndarray - Rotated strain vector - )doc") + "Apply rotation to a 6-component strain vector in Voigt notation") .def("apply_stiffness", [](const simcoon::Rotation& self, py::array_t L, bool active) { @@ -413,21 +112,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("L"), py::arg("active") = true, - R"doc( - Apply rotation to a 6x6 stiffness matrix. - - Parameters - ---------- - L : array_like - 6x6 stiffness matrix in Voigt notation - active : bool, optional - If True (default), active rotation - - Returns - ------- - ndarray - Rotated stiffness matrix: QS * L * QS^T - )doc") + "Apply rotation to a 6x6 stiffness matrix") .def("apply_compliance", [](const simcoon::Rotation& self, py::array_t M, bool active) { @@ -437,21 +122,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("M"), py::arg("active") = true, - R"doc( - Apply rotation to a 6x6 compliance matrix. - - Parameters - ---------- - M : array_like - 6x6 compliance matrix in Voigt notation - active : bool, optional - If True (default), active rotation - - Returns - ------- - ndarray - Rotated compliance matrix: QE * M * QE^T - )doc") + "Apply rotation to a 6x6 compliance matrix") .def("apply_strain_concentration", [](const simcoon::Rotation& self, py::array_t A, bool active) { @@ -461,21 +132,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("A"), py::arg("active") = true, - R"doc( - Apply rotation to a 6x6 strain concentration tensor. - - Parameters - ---------- - A : array_like - 6x6 strain concentration tensor in Voigt notation - active : bool, optional - If True (default), active rotation - - Returns - ------- - ndarray - Rotated strain concentration tensor: QE * A * QS^T - )doc") + "Apply rotation to a 6x6 strain concentration tensor") .def("apply_stress_concentration", [](const simcoon::Rotation& self, py::array_t B, bool active) { @@ -485,142 +142,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("B"), py::arg("active") = true, - R"doc( - Apply rotation to a 6x6 stress concentration tensor. - - Parameters - ---------- - B : array_like - 6x6 stress concentration tensor in Voigt notation - active : bool, optional - If True (default), active rotation - - Returns - ------- - ndarray - Rotated stress concentration tensor: QS * B * QE^T - )doc") - - // Operations - .def("inv", &simcoon::Rotation::inv, - "Get the inverse (conjugate) rotation") - - .def("magnitude", &simcoon::Rotation::magnitude, - py::arg("degrees") = false, - R"doc( - Get the magnitude (rotation angle) of this rotation. - - Parameters - ---------- - degrees : bool, optional - If True, return in degrees; if False (default), radians - - Returns - ------- - float - Rotation angle - )doc") - - .def("__mul__", - [](const simcoon::Rotation& self, py::object other) { - if (py::isinstance(other)) { - return self * other.cast(); - } - // Accept any object with as_quat() (e.g. simcoon.Rotation scipy subclass) - py::array_t q = other.attr("as_quat")().cast>(); - validate_vector_size(q, 4, "other.as_quat()"); - return self * simcoon::Rotation::from_quat(carma::arr_to_col(q)); - }, - py::arg("other"), - "Compose this rotation with another (self * other)") - - .def("__imul__", - [](simcoon::Rotation& self, py::object other) -> simcoon::Rotation& { - if (py::isinstance(other)) { - self *= other.cast(); - } else { - py::array_t q = other.attr("as_quat")().cast>(); - validate_vector_size(q, 4, "other.as_quat()"); - self *= simcoon::Rotation::from_quat(carma::arr_to_col(q)); - } - return self; - }, - py::arg("other"), - "Compose this rotation with another in-place") - - .def("slerp", - [](const simcoon::Rotation& self, py::object other, double t) { - simcoon::Rotation other_rot; - if (py::isinstance(other)) { - other_rot = other.cast(); - } else { - py::array_t q = other.attr("as_quat")().cast>(); - validate_vector_size(q, 4, "other.as_quat()"); - other_rot = simcoon::Rotation::from_quat(carma::arr_to_col(q)); - } - return self.slerp(other_rot, t); - }, - py::arg("other"), py::arg("t"), - R"doc( - Spherical linear interpolation between this rotation and another. - - Parameters - ---------- - other : Rotation - Target rotation - t : float - Interpolation parameter in [0, 1] (0 = self, 1 = other) - - Returns - ------- - Rotation - Interpolated rotation - )doc") - - .def("equals", - [](const simcoon::Rotation& self, py::object other, double tol) { - simcoon::Rotation other_rot; - if (py::isinstance(other)) { - other_rot = other.cast(); - } else { - py::array_t q = other.attr("as_quat")().cast>(); - validate_vector_size(q, 4, "other.as_quat()"); - other_rot = simcoon::Rotation::from_quat(carma::arr_to_col(q)); - } - return self.equals(other_rot, tol); - }, - py::arg("other"), py::arg("tol") = 1e-12, - R"doc( - Check if this rotation equals another within tolerance. - - Parameters - ---------- - other : Rotation - Rotation to compare with - tol : float, optional - Tolerance for comparison. Default is 1e-12. - - Returns - ------- - bool - True if rotations are equal (or antipodal quaternions) - )doc") - - .def("is_identity", &simcoon::Rotation::is_identity, - py::arg("tol") = 1e-12, - R"doc( - Check if this rotation is identity (no rotation). - - Parameters - ---------- - tol : float, optional - Tolerance for comparison. Default is 1e-12. - - Returns - ------- - bool - True if this is the identity rotation - )doc") + "Apply rotation to a 6x6 stress concentration tensor") ; } From e6600880c7d510c366a8e4cf53cc8db1ec5a99c8 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 11:53:29 +0100 Subject: [PATCH 17/31] Update test_rotation.py --- .../test/test_core/test_rotation.py | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/simcoon-python-builder/test/test_core/test_rotation.py b/simcoon-python-builder/test/test_core/test_rotation.py index b0c819cbb..a9f43a50b 100644 --- a/simcoon-python-builder/test/test_core/test_rotation.py +++ b/simcoon-python-builder/test/test_core/test_rotation.py @@ -5,7 +5,6 @@ from scipy.spatial.transform import Rotation as ScipyRotation import simcoon as sim -from simcoon._core import _CppRotation # --------------------------------------------------------------------------- @@ -29,12 +28,6 @@ def scipy_rot(angle): return ScipyRotation.from_rotvec([0, 0, angle]) -@pytest.fixture -def cpp_rot(angle): - """A _CppRotation representing the same rotation.""" - return _CppRotation.from_axis_angle(angle, 3) - - @pytest.fixture def L(): """An isotropic stiffness matrix.""" @@ -253,49 +246,7 @@ def test_voigt_strain_rotation_matches(self, sim_rot, scipy_rot): # =================================================================== -# 5. C++ _CppRotation accepts plain scipy objects -# =================================================================== - -class TestCppAcceptsScipy: - """_CppRotation methods that take a Rotation parameter accept plain - scipy.spatial.transform.Rotation objects via as_quat() duck-typing.""" - - def test_cpp_mul_with_scipy(self, cpp_rot, scipy_rot): - result = cpp_rot * scipy_rot - assert isinstance(result, _CppRotation) - - def test_cpp_equals_with_scipy(self, cpp_rot, scipy_rot): - assert cpp_rot.equals(scipy_rot) - - def test_cpp_slerp_with_scipy(self, cpp_rot, scipy_rot): - mid = cpp_rot.slerp(scipy_rot, 0.5) - assert isinstance(mid, _CppRotation) - - def test_cpp_mul_with_simcoon(self, cpp_rot, sim_rot): - result = cpp_rot * sim_rot - assert isinstance(result, _CppRotation) - - def test_cpp_equals_with_simcoon(self, cpp_rot, sim_rot): - assert cpp_rot.equals(sim_rot) - - def test_cpp_slerp_with_simcoon(self, cpp_rot, sim_rot): - mid = cpp_rot.slerp(sim_rot, 0.5) - assert isinstance(mid, _CppRotation) - - def test_cpp_mul_produces_correct_result(self, cpp_rot, scipy_rot): - """C++ composition with scipy gives same quaternion as two _CppRotation.""" - cpp_rot2 = _CppRotation.from_quat(scipy_rot.as_quat()) - result_scipy = cpp_rot * scipy_rot - result_cpp = cpp_rot * cpp_rot2 - np.testing.assert_allclose( - result_scipy.as_quat().ravel(), - result_cpp.as_quat().ravel(), - atol=1e-14, - ) - - -# =================================================================== -# 6. Utility methods +# 5. Utility methods # =================================================================== class TestUtilities: From 092bfbe0726de2f8afdc612aba9445f98a0531a1 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 12:00:58 +0100 Subject: [PATCH 18/31] Update doc_rotation.rst --- .../functions/doc_rotation.rst | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index 3ebe019b9..fc699f4fe 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -10,6 +10,45 @@ directly, while simcoon adds continuum-mechanics methods for Voigt-notation tens :local: :depth: 2 +Architecture +------------ + +The rotation system is organized in three layers: + +**C++ core** (``simcoon::Rotation`` in ``include/simcoon/Simulation/Maths/rotation.hpp``): + The full rotation class with quaternion internals, all factory methods, conversions, + algebra (composition, inverse, SLERP), and the Voigt mechanics operations. This is + what C++ code uses directly. + +**pybind11 bridge** (``_CppRotation``): + A minimal binding that exposes only what the Python layer needs from C++: a single + factory method (``from_quat``) and the nine mechanics operations (``apply_stress``, + ``apply_stiffness``, etc.). Everything else — creation, conversion, composition, + interpolation — is handled by scipy on the Python side. + +**Python class** (``simcoon.Rotation``): + Inherits from ``scipy.spatial.transform.Rotation``. Users get the full scipy API + for free (factory methods, conversions, batch operations, ``mean()``, ``Slerp``, + ``RotationSpline``). The mechanics methods delegate to C++ by converting the + quaternion on each call: + +.. code-block:: text + + r.apply_stiffness(L) + | + v + Python: self._to_cpp() --> _CppRotation.from_quat(self.as_quat()) + | ^ scipy provides the quaternion + v + C++: simcoon::Rotation::from_quat(q) --> .apply_stiffness(L) + | + v + Python: returns 6x6 numpy array + +The quaternion transfer (4 doubles) is negligible. This design avoids duplicating +scipy's well-tested rotation algebra while keeping the performance-critical Voigt +operations in C++. + The Rotation Class ------------------ @@ -235,10 +274,6 @@ library), you can upgrade it to a ``simcoon.Rotation`` with ``from_scipy()``: r = smc.Rotation.from_scipy(scipy_rot) L_rot = r.apply_stiffness(L) # now available -The C++ layer also accepts plain scipy ``Rotation`` objects directly wherever -a rotation parameter is expected (composition, ``equals``, ``slerp``), so -mixing scipy and simcoon rotations in the same expression works seamlessly. - Active vs Passive Rotations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From d8a7f0bbc3f028898980c5f5f0be3d25c608ecbe Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:30:38 +0100 Subject: [PATCH 19/31] Add Fastor header-only library via FetchContent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare and populate the Fastor dependency using FetchContent (pointing to the master branch). As Fastor is header-only, no build step is performed — FetchContent_Populate is used and the source dir is printed for visibility. The Fastor include path is added to the simcoon target as SYSTEM PUBLIC (via $) to consume the headers and suppress warnings. --- CMakeLists.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf271c1aa..888ef118e 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,11 +166,21 @@ FetchContent_Declare( GIT_TAG "v3.0.1" ) +FetchContent_Declare( + Fastor + GIT_REPOSITORY "https://github.com/3MAH/Fastor.git" + GIT_TAG "master" +) + #set(BUILD_SHARED_LIBS ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) FetchContent_MakeAvailable(dylib) set_target_properties(dylib PROPERTIES UNITY_BUILD OFF) +# Fastor is header-only — just populate, no build step needed +FetchContent_Populate(Fastor) +message(STATUS "Fastor include dir: ${fastor_SOURCE_DIR}") + # Python dependencies (only if building Python bindings) if(BUILD_PYTHON_BINDINGS) find_package(Python3 COMPONENTS Interpreter Development NumPy REQUIRED) @@ -299,6 +309,9 @@ target_include_directories(simcoon # Add Armadillo as system headers to suppress warnings target_include_directories(simcoon SYSTEM PUBLIC ${ARMADILLO_INCLUDE_DIRS}) +# Add Fastor as system headers (header-only tensor library, build-time only) +target_include_directories(simcoon SYSTEM PUBLIC $) + # Define restrict macro (MSVC and GCC/Clang handle restrict differently) target_compile_definitions(simcoon PUBLIC restrict=) From f01c9332e88fa9dca9ff1243489597d8d366b5a1 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:31:18 +0100 Subject: [PATCH 20/31] Add Fastor-Armadillo bridge helpers Introduce include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp providing conversion and utility helpers between Armadillo and Fastor tensor types. Adds a voigt_map constant and functions: zero-copy arma_to_fastor2 (const and non-const), fastor2_to_arma, voigt_to_fastor4, fastor4_to_voigt (stiffness/Voigt convention), and push_forward_4 / pull_back_4 for 4th-order tensor transforms. Header includes license boilerplate and documents column-major zero-copy for 3x3 matrices and the minor-symmetry handling used in Voigt conversions. --- .../Functions/fastor_bridge.hpp | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp diff --git a/include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp b/include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp new file mode 100644 index 000000000..53b56a464 --- /dev/null +++ b/include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp @@ -0,0 +1,196 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +#pragma once +#include +#include + +namespace simcoon { + +/** + * @file fastor_bridge.hpp + * @brief Conversion helpers between Armadillo and Fastor tensor types. + * + * These functions replace the FTensor conversion functions in transfer.hpp + * for use with the tensor2/tensor4 classes and Fastor-based operations. + */ + +// Voigt index mapping: (i,j) -> Voigt index I +// (0,0)->0, (1,1)->1, (2,2)->2, (0,1)->3, (0,2)->4, (1,2)->5 +inline constexpr int voigt_map[3][3] = { + {0, 3, 4}, + {3, 1, 5}, + {4, 5, 2} +}; + +/** + * @brief Zero-copy TensorMap wrapping an arma::mat::fixed<3,3> memory. + * + * Both Armadillo and Fastor use column-major storage, so this is safe. + * The returned TensorMap is valid only as long as the source matrix is alive. + */ +inline Fastor::TensorMap arma_to_fastor2(arma::mat::fixed<3,3> &m) { + return Fastor::TensorMap(m.memptr()); +} + +inline Fastor::TensorMap arma_to_fastor2(const arma::mat::fixed<3,3> &m) { + return Fastor::TensorMap(m.memptr()); +} + +/** + * @brief Copy a Fastor Tensor to an arma::mat::fixed<3,3>. + */ +inline arma::mat::fixed<3,3> fastor2_to_arma(const Fastor::Tensor &T) { + arma::mat::fixed<3,3> m; + // Both column-major, direct memcpy + std::memcpy(m.memptr(), T.data(), 9 * sizeof(double)); + return m; +} + +/** + * @brief Voigt 6x6 matrix -> Fastor Tensor + * + * The conversion factors depend on the tensor type. For stiffness convention + * (the default, matching mat_FTensor4), no factors are needed: + * C_ijkl = L(voigt_map[i][j], voigt_map[k][l]) with minor symmetry enforcement. + * + * This matches the existing mat_FTensor4 behavior: the Voigt matrix stores + * stiffness components directly without factor corrections. + */ +inline Fastor::Tensor voigt_to_fastor4(const arma::mat::fixed<6,6> &L) { + Fastor::Tensor C; + C.zeros(); + + for (int i = 0; i < 3; ++i) { + for (int j = i; j < 3; ++j) { + int ij = voigt_map[i][j]; + for (int k = 0; k < 3; ++k) { + for (int l = k; l < 3; ++l) { + int kl = voigt_map[k][l]; + double val = L(ij, kl); + C(i,j,k,l) = val; + C(i,j,l,k) = val; + C(j,i,k,l) = val; + C(j,i,l,k) = val; + } + } + } + } + return C; +} + +/** + * @brief Fastor Tensor -> Voigt 6x6 matrix + * + * Matches the existing FTensor4_mat behavior: averages minor-symmetric pairs. + * This is the stiffness convention (no factor corrections). + */ +inline arma::mat::fixed<6,6> fastor4_to_voigt(const Fastor::Tensor &C) { + arma::mat::fixed<6,6> L; + L.zeros(); + + // Row 0: (0,0,*,*) + L(0,0) = C(0,0,0,0); + L(0,1) = C(0,0,1,1); + L(0,2) = C(0,0,2,2); + L(0,3) = 0.5*(C(0,0,0,1) + C(0,0,1,0)); + L(0,4) = 0.5*(C(0,0,0,2) + C(0,0,2,0)); + L(0,5) = 0.5*(C(0,0,1,2) + C(0,0,2,1)); + + // Row 1: (1,1,*,*) + L(1,0) = C(1,1,0,0); + L(1,1) = C(1,1,1,1); + L(1,2) = C(1,1,2,2); + L(1,3) = 0.5*(C(1,1,0,1) + C(1,1,1,0)); + L(1,4) = 0.5*(C(1,1,0,2) + C(1,1,2,0)); + L(1,5) = 0.5*(C(1,1,1,2) + C(1,1,2,1)); + + // Row 2: (2,2,*,*) + L(2,0) = C(2,2,0,0); + L(2,1) = C(2,2,1,1); + L(2,2) = C(2,2,2,2); + L(2,3) = 0.5*(C(2,2,0,1) + C(2,2,1,0)); + L(2,4) = 0.5*(C(2,2,0,2) + C(2,2,2,0)); + L(2,5) = 0.5*(C(2,2,1,2) + C(2,2,2,1)); + + // Row 3: (0,1,*,*) + L(3,0) = C(0,1,0,0); + L(3,1) = C(0,1,1,1); + L(3,2) = C(0,1,2,2); + L(3,3) = 0.5*(C(0,1,0,1) + C(0,1,1,0)); + L(3,4) = 0.5*(C(0,1,0,2) + C(0,1,2,0)); + L(3,5) = 0.5*(C(0,1,1,2) + C(0,1,2,1)); + + // Row 4: (0,2,*,*) + L(4,0) = C(0,2,0,0); + L(4,1) = C(0,2,1,1); + L(4,2) = C(0,2,2,2); + L(4,3) = 0.5*(C(0,2,0,1) + C(0,2,1,0)); + L(4,4) = 0.5*(C(0,2,0,2) + C(0,2,2,0)); + L(4,5) = 0.5*(C(0,2,1,2) + C(0,2,2,1)); + + // Row 5: (1,2,*,*) + L(5,0) = C(1,2,0,0); + L(5,1) = C(1,2,1,1); + L(5,2) = C(1,2,2,2); + L(5,3) = 0.5*(C(1,2,0,1) + C(1,2,1,0)); + L(5,4) = 0.5*(C(1,2,0,2) + C(1,2,2,0)); + L(5,5) = 0.5*(C(1,2,1,2) + C(1,2,2,1)); + + return L; +} + +/** + * @brief Push-forward a 4th-order tensor via Fastor: C'_isrp = F_iL F_sJ F_rM F_pN C_LJMN + * + * This replaces the FTensor boilerplate in objective_rates.cpp. + */ +inline Fastor::Tensor push_forward_4( + const Fastor::Tensor &C, + const Fastor::Tensor &F) +{ + Fastor::Tensor result; + result.zeros(); + + for (int i = 0; i < 3; ++i) + for (int s = 0; s < 3; ++s) + for (int r = 0; r < 3; ++r) + for (int p = 0; p < 3; ++p) { + double sum = 0.0; + for (int L = 0; L < 3; ++L) + for (int J = 0; J < 3; ++J) + for (int M = 0; M < 3; ++M) + for (int N = 0; N < 3; ++N) { + sum += F(i,L) * F(s,J) * F(r,M) * F(p,N) * C(L,J,M,N); + } + result(i,s,r,p) = sum; + } + return result; +} + +/** + * @brief Pull-back a 4th-order tensor via Fastor: C'_LJMN = F^-1_lN F^-1_kM F^-1_jJ F^-1_iL C_ijkl + */ +inline Fastor::Tensor pull_back_4( + const Fastor::Tensor &C, + const Fastor::Tensor &invF) +{ + // Pull-back is just push-forward with F^{-1} + return push_forward_4(C, invF); +} + +} // namespace simcoon From 1e3340009b7b045fcee1c5a12c7eec24df4f436e Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:31:31 +0100 Subject: [PATCH 21/31] Create tensor.hpp --- .../Continuum_mechanics/Functions/tensor.hpp | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 include/simcoon/Continuum_mechanics/Functions/tensor.hpp diff --git a/include/simcoon/Continuum_mechanics/Functions/tensor.hpp b/include/simcoon/Continuum_mechanics/Functions/tensor.hpp new file mode 100644 index 000000000..861387b5a --- /dev/null +++ b/include/simcoon/Continuum_mechanics/Functions/tensor.hpp @@ -0,0 +1,252 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +#pragma once +#include +#include +#include + +namespace simcoon { + +// Forward declarations +class Rotation; +class tensor4; + +/** + * @brief Type tag for 2nd-order tensors, determines Voigt conversion factors and rotation rules. + * + * - stress: Voigt = [s11, s22, s33, s12, s13, s23] — shear as-is + * - strain: Voigt = [e11, e22, e33, 2*e12, 2*e13, 2*e23] — shear doubled + * - generic: same storage as stress (symmetric tensor, no physical convention) + * - none: non-symmetric tensor (e.g. F), .voigt() throws + */ +enum class VoigtType { + stress, + strain, + generic, + none +}; + +/** + * @brief Type tag for 4th-order tensors, determines rotation rules and Voigt factor conventions. + * + * Rotation dispatch: + * - stiffness: QS * L * QS^T + * - compliance: QE * M * QE^T + * - strain_concentration: QE * A * QS^T + * - stress_concentration: QS * B * QE^T + * - generic: QS * C * QS^T (default) + */ +enum class Tensor4Type { + stiffness, + compliance, + strain_concentration, + stress_concentration, + generic +}; + +/** + * @brief A 2nd-order tensor with type tag for Voigt convention and rotation dispatch. + * + * Storage: arma::mat::fixed<3,3> (column-major, 72 bytes). + * The Voigt vector is computed on the fly (no cache). + */ +class tensor2 { +private: + arma::mat::fixed<3,3> _mat; + VoigtType _vtype; + mutable bool _symmetric; + mutable bool _symmetry_checked; + +public: + // Constructors + tensor2(); + explicit tensor2(VoigtType vtype); + tensor2(const arma::mat::fixed<3,3> &m, VoigtType vtype); + tensor2(const arma::mat &m, VoigtType vtype); + + // Construct from Voigt vector + static tensor2 from_voigt(const arma::vec::fixed<6> &v, VoigtType vtype); + static tensor2 from_voigt(const arma::vec &v, VoigtType vtype); + + // Static factories + static tensor2 zeros(VoigtType vtype = VoigtType::stress); + static tensor2 identity(VoigtType vtype = VoigtType::stress); + + // Copy/move + tensor2(const tensor2 &other) = default; + tensor2(tensor2 &&other) noexcept = default; + tensor2& operator=(const tensor2 &other) = default; + tensor2& operator=(tensor2 &&other) noexcept = default; + ~tensor2() = default; + + // Accessors + const arma::mat::fixed<3,3>& mat() const { return _mat; } + arma::mat::fixed<3,3>& mat_mut(); + void set_mat(const arma::mat::fixed<3,3> &m); + void set_mat(const arma::mat &m); + void set_voigt(const arma::vec::fixed<6> &v); + void set_voigt(const arma::vec &v); + + VoigtType vtype() const { return _vtype; } + + /** + * @brief Compute Voigt vector on the fly (6 lookups + factor-2 on shear for strain). + * @return vec::fixed<6> by value + * @throws std::runtime_error if VoigtType::none + */ + arma::vec::fixed<6> voigt() const; + + /** + * @brief Zero-copy Fastor TensorMap wrapping the 3x3 matrix memory. + */ + Fastor::TensorMap fastor(); + Fastor::TensorMap fastor() const; + + // Symmetry + bool is_symmetric(double tol = 1e-12) const; + + // Conversion helpers + arma::mat to_arma_mat() const { return arma::mat(_mat); } + arma::vec to_arma_voigt() const { return arma::vec(voigt()); } + + // Rotation + tensor2 rotate(const Rotation &R, bool active = true) const; + + // Push-forward / pull-back (dispatch on VoigtType) + tensor2 push_forward(const arma::mat::fixed<3,3> &F) const; + tensor2 pull_back(const arma::mat::fixed<3,3> &F) const; + + // Arithmetic + tensor2 operator+(const tensor2 &other) const; + tensor2 operator-(const tensor2 &other) const; + tensor2 operator*(double scalar) const; + tensor2& operator+=(const tensor2 &other); + tensor2& operator-=(const tensor2 &other); + tensor2& operator*=(double scalar); + friend tensor2 operator*(double scalar, const tensor2 &t); + + // Comparison + bool operator==(const tensor2 &other) const; +}; + +// Free functions for tensor2 +tensor2 stress(const arma::mat::fixed<3,3> &m); +tensor2 stress(const arma::vec::fixed<6> &v); +tensor2 strain(const arma::mat::fixed<3,3> &m); +tensor2 strain(const arma::vec::fixed<6> &v); +arma::vec::fixed<6> dev(const tensor2 &t); +double Mises(const tensor2 &t); +double trace(const tensor2 &t); + +/** + * @brief A 4th-order tensor with type tag for rotation dispatch and lazy Fastor cache. + * + * Storage: arma::mat::fixed<6,6> Voigt matrix (primary). + * Fastor Tensor is lazily computed and cached. + */ +class tensor4 { +private: + arma::mat::fixed<6,6> _voigt; + Tensor4Type _type; + mutable Fastor::Tensor _fastor; + mutable bool _fastor_valid; + + void _invalidate_fastor() { _fastor_valid = false; } + void _ensure_fastor() const; + +public: + // Constructors + tensor4(); + explicit tensor4(Tensor4Type type); + tensor4(const arma::mat::fixed<6,6> &m, Tensor4Type type); + tensor4(const arma::mat &m, Tensor4Type type); + + // Static factories wrapping constitutive.hpp functions + static tensor4 identity(Tensor4Type type = Tensor4Type::stiffness); + static tensor4 volumetric(Tensor4Type type = Tensor4Type::stiffness); + static tensor4 deviatoric(Tensor4Type type = Tensor4Type::stiffness); + static tensor4 identity2(Tensor4Type type = Tensor4Type::stiffness); + static tensor4 deviatoric2(Tensor4Type type = Tensor4Type::stiffness); + static tensor4 zeros(Tensor4Type type = Tensor4Type::stiffness); + + // Copy/move + tensor4(const tensor4 &other); + tensor4(tensor4 &&other) noexcept; + tensor4& operator=(const tensor4 &other); + tensor4& operator=(tensor4 &&other) noexcept; + ~tensor4() = default; + + // Accessors + const arma::mat::fixed<6,6>& mat() const { return _voigt; } + arma::mat::fixed<6,6>& mat_mut(); + void set_mat(const arma::mat::fixed<6,6> &m); + void set_mat(const arma::mat &m); + + Tensor4Type type() const { return _type; } + + /** + * @brief Get the cached Fastor 3x3x3x3 tensor (lazy recomputation if dirty). + */ + const Fastor::Tensor& fastor() const; + + // Conversion + arma::mat to_arma_mat() const { return arma::mat(_voigt); } + + /** + * @brief Contract with a tensor2: result_voigt = mat * t.voigt() + * + * Output VoigtType is inferred from Tensor4Type: + * - stiffness (sigma = L : epsilon) -> stress + * - compliance (epsilon = M : sigma) -> strain + * - strain_concentration (epsilon = A : epsilon) -> strain + * - stress_concentration (sigma = B : sigma) -> stress + * - generic -> stress (default) + */ + tensor2 contract(const tensor2 &t) const; + + /** + * @brief Push-forward: C'_isrp = F_iL F_sJ F_rM F_pN C_LJMN + */ + tensor4 push_forward(const arma::mat::fixed<3,3> &F) const; + + /** + * @brief Pull-back: C'_LJMN = invF_lN invF_kM invF_jJ invF_iL C_ijkl + */ + tensor4 pull_back(const arma::mat::fixed<3,3> &F) const; + + // Rotation + tensor4 rotate(const Rotation &R, bool active = true) const; + + // Arithmetic + tensor4 operator+(const tensor4 &other) const; + tensor4 operator-(const tensor4 &other) const; + tensor4 operator*(double scalar) const; + tensor4& operator+=(const tensor4 &other); + tensor4& operator-=(const tensor4 &other); + tensor4& operator*=(double scalar); + friend tensor4 operator*(double scalar, const tensor4 &t); + + // Comparison + bool operator==(const tensor4 &other) const; +}; + +// Free functions for tensor4 +tensor4 dyadic(const tensor2 &a, const tensor2 &b); +tensor4 auto_dyadic(const tensor2 &a); + +} // namespace simcoon From d2ee7c5cb47326c0eb593bd4615b40e6c7c46a4a Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:31:36 +0100 Subject: [PATCH 22/31] Update __init__.py --- python-setup/simcoon/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python-setup/simcoon/__init__.py b/python-setup/simcoon/__init__.py index 09d94462c..3a5bbcbf8 100755 --- a/python-setup/simcoon/__init__.py +++ b/python-setup/simcoon/__init__.py @@ -1,6 +1,7 @@ from simcoon._core import * from simcoon.__version__ import __version__ from simcoon.rotation import Rotation # override _CppRotation from star-import +from simcoon.tensor import Tensor2, Tensor4 # high-level tensor wrappers # Backward compatibility alias - simmit was the legacy module name from simcoon import _core as simmit From 994cd8bb921f1b1ed96e4445ec76402f94c42a63 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:32:23 +0100 Subject: [PATCH 23/31] Create tensor.py --- python-setup/simcoon/tensor.py | 390 +++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 python-setup/simcoon/tensor.py diff --git a/python-setup/simcoon/tensor.py b/python-setup/simcoon/tensor.py new file mode 100644 index 000000000..efeed99f1 --- /dev/null +++ b/python-setup/simcoon/tensor.py @@ -0,0 +1,390 @@ +""" +Tensor2 and Tensor4 classes with type-tagged Voigt convention and rotation dispatch. + +This module provides ``Tensor2`` and ``Tensor4`` classes that wrap simcoon's C++ +tensor objects, providing: + +- Automatic Voigt convention handling (stress vs strain factors) +- Type-dispatched rotation (stiffness, compliance, etc.) +- Push-forward / pull-back operations +- Integration with ``simcoon.Rotation`` + +Examples +-------- +>>> import simcoon as smc +>>> import numpy as np + +>>> # Build isotropic stiffness +>>> L = smc.Tensor4.stiffness(smc.L_iso([70000, 0.3], 'Enu')) + +>>> # Build a strain state +>>> eps = smc.Tensor2.strain(np.array([0.01, -0.003, -0.003, 0.005, 0.002, 0.001])) + +>>> # Contract: sigma = L : epsilon +>>> sigma = L @ eps +>>> sigma.vtype # automatically inferred as stress +VoigtType.stress +""" + +import numpy as np + +from simcoon._core import ( + _CppTensor2, + _CppTensor4, + VoigtType, + Tensor4Type, +) + + +class Tensor2: + """A 2nd-order tensor with type tag for Voigt convention and rotation dispatch. + + Do not construct directly --- use the class methods ``stress()``, ``strain()``, + ``from_mat()``, ``from_voigt()``, ``zeros()``, or ``identity()``. + """ + + __slots__ = ("_cpp",) + + def __init__(self, cpp_obj): + if not isinstance(cpp_obj, _CppTensor2): + raise TypeError("Use Tensor2.stress(), Tensor2.strain(), etc.") + self._cpp = cpp_obj + + # ------------------------------------------------------------------ + # Factory methods + # ------------------------------------------------------------------ + + @classmethod + def stress(cls, data): + """Create a stress tensor from a 3x3 matrix or 6-element Voigt vector. + + Parameters + ---------- + data : array_like + A (3, 3) matrix or (6,) Voigt vector. + """ + return cls._from_data(data, VoigtType.stress) + + @classmethod + def strain(cls, data): + """Create a strain tensor from a 3x3 matrix or 6-element Voigt vector. + + Parameters + ---------- + data : array_like + A (3, 3) matrix or (6,) Voigt vector (with doubled shear). + """ + return cls._from_data(data, VoigtType.strain) + + @classmethod + def from_mat(cls, m, vtype): + """Create from a 3x3 numpy array with explicit VoigtType.""" + m = np.asarray(m, dtype=np.float64) + return cls(_CppTensor2.from_mat(m, vtype)) + + @classmethod + def from_voigt(cls, v, vtype): + """Create from a 6-element Voigt vector with explicit VoigtType.""" + v = np.asarray(v, dtype=np.float64).ravel() + return cls(_CppTensor2.from_voigt(v, vtype)) + + @classmethod + def zeros(cls, vtype=VoigtType.stress): + """Create a zero tensor.""" + return cls(_CppTensor2.zeros(vtype)) + + @classmethod + def identity(cls, vtype=VoigtType.stress): + """Create an identity tensor.""" + return cls(_CppTensor2.identity(vtype)) + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + + @property + def mat(self): + """3x3 matrix representation as numpy array.""" + return self._cpp.mat + + @property + def voigt(self): + """6-element Voigt vector as numpy array (shape (6,)).""" + return self._cpp.voigt.ravel() + + @property + def vtype(self): + """VoigtType tag.""" + return self._cpp.vtype + + # ------------------------------------------------------------------ + # Methods + # ------------------------------------------------------------------ + + def is_symmetric(self, tol=1e-12): + """Check if the tensor is symmetric.""" + return self._cpp.is_symmetric(tol) + + def rotate(self, R, active=True): + """Rotate the tensor using a Rotation object. + + Parameters + ---------- + R : simcoon.Rotation + The rotation to apply. + active : bool, optional + If True (default), apply active rotation. + """ + from simcoon.rotation import Rotation + if isinstance(R, Rotation): + cpp_rot = R._to_cpp() + else: + cpp_rot = R + return Tensor2(self._cpp.rotate(cpp_rot, active)) + + def push_forward(self, F): + """Push-forward via deformation gradient F.""" + F = np.asarray(F, dtype=np.float64) + return Tensor2(self._cpp.push_forward(F)) + + def pull_back(self, F): + """Pull-back via deformation gradient F.""" + F = np.asarray(F, dtype=np.float64) + return Tensor2(self._cpp.pull_back(F)) + + def mises(self): + """Compute von Mises equivalent.""" + from simcoon._core import _CppTensor2 + # Use the C++ free function via the voigt approach + from simcoon.tensor import _mises_impl + return _mises_impl(self) + + def trace(self): + """Compute trace.""" + m = self.mat + return m[0, 0] + m[1, 1] + m[2, 2] + + # ------------------------------------------------------------------ + # Arithmetic + # ------------------------------------------------------------------ + + def __add__(self, other): + if isinstance(other, Tensor2): + return Tensor2(self._cpp + other._cpp) + return NotImplemented + + def __sub__(self, other): + if isinstance(other, Tensor2): + return Tensor2(self._cpp - other._cpp) + return NotImplemented + + def __mul__(self, other): + if isinstance(other, (int, float)): + return Tensor2(self._cpp * float(other)) + return NotImplemented + + def __rmul__(self, other): + if isinstance(other, (int, float)): + return Tensor2(self._cpp * float(other)) + return NotImplemented + + def __eq__(self, other): + if isinstance(other, Tensor2): + return self._cpp == other._cpp + return NotImplemented + + def __repr__(self): + return f"Tensor2(vtype={self.vtype.name})" + + # ------------------------------------------------------------------ + # Internal helper + # ------------------------------------------------------------------ + + @classmethod + def _from_data(cls, data, vtype): + data = np.asarray(data, dtype=np.float64) + if data.shape == (3, 3): + return cls(_CppTensor2.from_mat(data, vtype)) + elif data.shape == (6,) or (data.ndim == 1 and data.size == 6): + return cls(_CppTensor2.from_voigt(data.ravel(), vtype)) + else: + raise ValueError( + f"Expected (3,3) matrix or (6,) vector, got shape {data.shape}" + ) + + +class Tensor4: + """A 4th-order tensor with type tag for rotation dispatch and Voigt storage. + + Do not construct directly --- use the class methods ``stiffness()``, + ``compliance()``, ``from_mat()``, etc. + """ + + __slots__ = ("_cpp",) + + def __init__(self, cpp_obj): + if not isinstance(cpp_obj, _CppTensor4): + raise TypeError("Use Tensor4.stiffness(), Tensor4.compliance(), etc.") + self._cpp = cpp_obj + + # ------------------------------------------------------------------ + # Factory methods + # ------------------------------------------------------------------ + + @classmethod + def stiffness(cls, data): + """Create a stiffness tensor from a 6x6 Voigt matrix. + + Parameters + ---------- + data : array_like + A (6, 6) Voigt stiffness matrix. + """ + data = np.asarray(data, dtype=np.float64) + return cls(_CppTensor4.from_mat(data, Tensor4Type.stiffness)) + + @classmethod + def compliance(cls, data): + """Create a compliance tensor from a 6x6 Voigt matrix. + + Parameters + ---------- + data : array_like + A (6, 6) Voigt compliance matrix. + """ + data = np.asarray(data, dtype=np.float64) + return cls(_CppTensor4.from_mat(data, Tensor4Type.compliance)) + + @classmethod + def from_mat(cls, m, tensor_type): + """Create from a 6x6 numpy array with explicit Tensor4Type.""" + m = np.asarray(m, dtype=np.float64) + return cls(_CppTensor4.from_mat(m, tensor_type)) + + @classmethod + def identity(cls, tensor_type=Tensor4Type.stiffness): + """Create symmetric identity (Ireal).""" + return cls(_CppTensor4.identity(tensor_type)) + + @classmethod + def volumetric(cls, tensor_type=Tensor4Type.stiffness): + """Create volumetric projector (Ivol).""" + return cls(_CppTensor4.volumetric(tensor_type)) + + @classmethod + def deviatoric(cls, tensor_type=Tensor4Type.stiffness): + """Create deviatoric projector (Idev).""" + return cls(_CppTensor4.deviatoric(tensor_type)) + + @classmethod + def zeros(cls, tensor_type=Tensor4Type.stiffness): + """Create zero tensor.""" + return cls(_CppTensor4.zeros(tensor_type)) + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + + @property + def mat(self): + """6x6 Voigt matrix as numpy array.""" + return self._cpp.mat + + @property + def type(self): + """Tensor4Type tag.""" + return self._cpp.type + + # ------------------------------------------------------------------ + # Methods + # ------------------------------------------------------------------ + + def contract(self, t): + """Contract with a Tensor2: result = mat @ t.voigt(). + + Parameters + ---------- + t : Tensor2 + The tensor to contract with. + + Returns + ------- + Tensor2 + Result with VoigtType inferred from Tensor4Type. + """ + return Tensor2(self._cpp.contract(t._cpp)) + + def rotate(self, R, active=True): + """Rotate the tensor using a Rotation object.""" + from simcoon.rotation import Rotation + if isinstance(R, Rotation): + cpp_rot = R._to_cpp() + else: + cpp_rot = R + return Tensor4(self._cpp.rotate(cpp_rot, active)) + + def push_forward(self, F): + """Push-forward via deformation gradient F.""" + F = np.asarray(F, dtype=np.float64) + return Tensor4(self._cpp.push_forward(F)) + + def pull_back(self, F): + """Pull-back via deformation gradient F.""" + F = np.asarray(F, dtype=np.float64) + return Tensor4(self._cpp.pull_back(F)) + + # ------------------------------------------------------------------ + # Arithmetic + # ------------------------------------------------------------------ + + def __add__(self, other): + if isinstance(other, Tensor4): + return Tensor4(self._cpp + other._cpp) + return NotImplemented + + def __sub__(self, other): + if isinstance(other, Tensor4): + return Tensor4(self._cpp - other._cpp) + return NotImplemented + + def __mul__(self, other): + if isinstance(other, (int, float)): + return Tensor4(self._cpp * float(other)) + return NotImplemented + + def __rmul__(self, other): + if isinstance(other, (int, float)): + return Tensor4(self._cpp * float(other)) + return NotImplemented + + def __matmul__(self, other): + """Contract with a Tensor2 using the @ operator.""" + if isinstance(other, Tensor2): + return self.contract(other) + return NotImplemented + + def __eq__(self, other): + if isinstance(other, Tensor4): + return self._cpp == other._cpp + return NotImplemented + + def __repr__(self): + return f"Tensor4(type={self.type.name})" + + +def _mises_impl(t): + """Compute von Mises equivalent for a Tensor2.""" + v = t.voigt + tr = v[0] + v[1] + v[2] + d = v.copy() + d[0] -= tr / 3.0 + d[1] -= tr / 3.0 + d[2] -= tr / 3.0 + + if t.vtype == VoigtType.stress or t.vtype == VoigtType.generic: + return np.sqrt(1.5 * (d[0]**2 + d[1]**2 + d[2]**2 + + 2.0 * (d[3]**2 + d[4]**2 + d[5]**2))) + elif t.vtype == VoigtType.strain: + return np.sqrt(2.0/3.0 * (d[0]**2 + d[1]**2 + d[2]**2 + + 0.5 * (d[3]**2 + d[4]**2 + d[5]**2))) + raise ValueError("Mises not defined for VoigtType.none") From ff602951c5284c4d1eb8646e55bed02481cc7c73 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:32:26 +0100 Subject: [PATCH 24/31] Update CMakeLists.txt --- simcoon-python-builder/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/simcoon-python-builder/CMakeLists.txt b/simcoon-python-builder/CMakeLists.txt index 168ffa22b..95b406d47 100755 --- a/simcoon-python-builder/CMakeLists.txt +++ b/simcoon-python-builder/CMakeLists.txt @@ -33,6 +33,7 @@ pybind11_add_module(_core src/python_wrappers/Libraries/Continuum_mechanics/objective_rates.cpp src/python_wrappers/Libraries/Continuum_mechanics/recovery_props.cpp src/python_wrappers/Libraries/Continuum_mechanics/stress.cpp + src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp src/python_wrappers/Libraries/Continuum_mechanics/transfer.cpp src/python_wrappers/Libraries/Continuum_mechanics/umat.cpp From ebc064465984869ab44040efede51f7856e7dc67 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:32:39 +0100 Subject: [PATCH 25/31] Add pybind11 bindings for Continuum tensors Introduce Python bindings for simcoon continuum-mechanics tensors. Adds header declare register_tensor and implementation that registers VoigtType and Tensor4Type enums and two internal classes (_CppTensor2, _CppTensor4) via pybind11. The bindings provide constructors, static factories (from_mat, from_voigt, zeros, identity, etc.), properties (mat, voigt, vtype/type), methods (rotate, push_forward, pull_back, contract), arithmetic operators, and __repr__ implementations. Includes numpy input validation helpers and conversions using carma/armadillo and depends on existing simcoon C++ tensor and Rotation types to expose C++ tensor functionality to Python. --- .../Libraries/Continuum_mechanics/tensor.hpp | 10 + .../Libraries/Continuum_mechanics/tensor.cpp | 267 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/tensor.hpp create mode 100644 simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/tensor.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/tensor.hpp new file mode 100644 index 000000000..92d92ab4c --- /dev/null +++ b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/tensor.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +#include + +namespace simpy { + +// Function to register tensor2 and tensor4 classes with pybind11 +void register_tensor(pybind11::module_& m); + +} // namespace simpy diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp new file mode 100644 index 000000000..1bb2b0532 --- /dev/null +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp @@ -0,0 +1,267 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace std; +using namespace arma; +namespace py = pybind11; + +namespace simpy { + +namespace { + void validate_vector_size(const py::array_t& arr, size_t expected, const string& name) { + if (arr.ndim() != 1) { + throw invalid_argument(name + " must be a 1D array, got " + to_string(arr.ndim()) + "D"); + } + if (static_cast(arr.size()) != expected) { + throw invalid_argument(name + " must have " + to_string(expected) + + " elements, got " + to_string(arr.size())); + } + } + + void validate_matrix_size(const py::array_t& arr, size_t rows, size_t cols, const string& name) { + if (arr.ndim() != 2) { + throw invalid_argument(name + " must be a 2D array, got " + to_string(arr.ndim()) + "D"); + } + auto shape = arr.shape(); + if (static_cast(shape[0]) != rows || static_cast(shape[1]) != cols) { + throw invalid_argument(name + " must have shape (" + to_string(rows) + ", " + + to_string(cols) + "), got (" + to_string(shape[0]) + ", " + to_string(shape[1]) + ")"); + } + } +} // anonymous namespace + +void register_tensor(py::module_& m) { + + // VoigtType enum + py::enum_(m, "VoigtType", + "Type tag for 2nd-order tensors, determines Voigt conversion factors and rotation rules.") + .value("stress", simcoon::VoigtType::stress, "Voigt = [s11, s22, s33, s12, s13, s23]") + .value("strain", simcoon::VoigtType::strain, "Voigt = [e11, e22, e33, 2*e12, 2*e13, 2*e23]") + .value("generic", simcoon::VoigtType::generic, "Same storage as stress (symmetric tensor)") + .value("none", simcoon::VoigtType::none, "Non-symmetric tensor, voigt() throws") + .export_values(); + + // Tensor4Type enum + py::enum_(m, "Tensor4Type", + "Type tag for 4th-order tensors, determines rotation rules.") + .value("stiffness", simcoon::Tensor4Type::stiffness, "QS * L * QS^T") + .value("compliance", simcoon::Tensor4Type::compliance, "QE * M * QE^T") + .value("strain_concentration", simcoon::Tensor4Type::strain_concentration, "QE * A * QS^T") + .value("stress_concentration", simcoon::Tensor4Type::stress_concentration, "QS * B * QE^T") + .value("generic", simcoon::Tensor4Type::generic, "QS * C * QS^T (default)") + .export_values(); + + // _CppTensor2 class + py::class_(m, "_CppTensor2", + R"doc( + Internal C++ tensor2 backend with type-tagged Voigt convention. + + End users should use ``simcoon.Tensor2`` instead. + )doc") + + // Constructors + .def(py::init<>()) + .def(py::init(), py::arg("vtype")) + + .def_static("from_mat", + [](py::array_t m, simcoon::VoigtType vtype) { + validate_matrix_size(m, 3, 3, "m"); + mat m_cpp = carma::arr_to_mat(m); + return simcoon::tensor2(mat::fixed<3,3>(m_cpp), vtype); + }, + py::arg("m"), py::arg("vtype"), + "Create tensor2 from a 3x3 numpy array") + + .def_static("from_voigt", + [](py::array_t v, simcoon::VoigtType vtype) { + validate_vector_size(v, 6, "v"); + vec v_cpp = carma::arr_to_col(v); + return simcoon::tensor2::from_voigt(vec::fixed<6>(v_cpp.memptr()), vtype); + }, + py::arg("v"), py::arg("vtype"), + "Create tensor2 from a 6-element Voigt vector") + + .def_static("zeros", + &simcoon::tensor2::zeros, + py::arg("vtype") = simcoon::VoigtType::stress) + + .def_static("identity", + &simcoon::tensor2::identity, + py::arg("vtype") = simcoon::VoigtType::stress) + + // Properties + .def_property_readonly("mat", + [](const simcoon::tensor2& self) { + return carma::mat_to_arr(mat(self.mat())); + }, + "3x3 matrix representation as numpy array") + + .def_property_readonly("voigt", + [](const simcoon::tensor2& self) { + return carma::col_to_arr(vec(self.voigt())); + }, + "6-element Voigt vector as numpy array") + + .def_property_readonly("vtype", &simcoon::tensor2::vtype) + + // Methods + .def("is_symmetric", &simcoon::tensor2::is_symmetric, + py::arg("tol") = 1e-12) + + .def("rotate", + [](const simcoon::tensor2& self, const simcoon::Rotation& R, bool active) { + return self.rotate(R, active); + }, + py::arg("R"), py::arg("active") = true, + "Rotate the tensor using the Rotation object") + + .def("push_forward", + [](const simcoon::tensor2& self, py::array_t F) { + validate_matrix_size(F, 3, 3, "F"); + mat F_cpp = carma::arr_to_mat(F); + return self.push_forward(mat::fixed<3,3>(F_cpp)); + }, + py::arg("F"), + "Push-forward the tensor via deformation gradient F") + + .def("pull_back", + [](const simcoon::tensor2& self, py::array_t F) { + validate_matrix_size(F, 3, 3, "F"); + mat F_cpp = carma::arr_to_mat(F); + return self.pull_back(mat::fixed<3,3>(F_cpp)); + }, + py::arg("F"), + "Pull-back the tensor via deformation gradient F") + + // Arithmetic + .def("__add__", &simcoon::tensor2::operator+) + .def("__sub__", &simcoon::tensor2::operator-) + .def("__mul__", &simcoon::tensor2::operator*) + .def("__rmul__", [](const simcoon::tensor2& self, double scalar) { + return scalar * self; + }) + .def("__eq__", &simcoon::tensor2::operator==) + + // String representation + .def("__repr__", [](const simcoon::tensor2& self) { + string vtype_str; + switch (self.vtype()) { + case simcoon::VoigtType::stress: vtype_str = "stress"; break; + case simcoon::VoigtType::strain: vtype_str = "strain"; break; + case simcoon::VoigtType::generic: vtype_str = "generic"; break; + case simcoon::VoigtType::none: vtype_str = "none"; break; + } + return "Tensor2(vtype=" + vtype_str + ")"; + }); + + // _CppTensor4 class + py::class_(m, "_CppTensor4", + R"doc( + Internal C++ tensor4 backend with type-tagged rotation dispatch. + + End users should use ``simcoon.Tensor4`` instead. + )doc") + + // Constructors + .def(py::init<>()) + .def(py::init(), py::arg("type")) + + .def_static("from_mat", + [](py::array_t m, simcoon::Tensor4Type type) { + validate_matrix_size(m, 6, 6, "m"); + mat m_cpp = carma::arr_to_mat(m); + return simcoon::tensor4(mat::fixed<6,6>(m_cpp), type); + }, + py::arg("m"), py::arg("type"), + "Create tensor4 from a 6x6 Voigt matrix") + + // Static factories + .def_static("identity", &simcoon::tensor4::identity, + py::arg("type") = simcoon::Tensor4Type::stiffness) + .def_static("volumetric", &simcoon::tensor4::volumetric, + py::arg("type") = simcoon::Tensor4Type::stiffness) + .def_static("deviatoric", &simcoon::tensor4::deviatoric, + py::arg("type") = simcoon::Tensor4Type::stiffness) + .def_static("identity2", &simcoon::tensor4::identity2, + py::arg("type") = simcoon::Tensor4Type::stiffness) + .def_static("deviatoric2", &simcoon::tensor4::deviatoric2, + py::arg("type") = simcoon::Tensor4Type::stiffness) + .def_static("zeros", &simcoon::tensor4::zeros, + py::arg("type") = simcoon::Tensor4Type::stiffness) + + // Properties + .def_property_readonly("mat", + [](const simcoon::tensor4& self) { + return carma::mat_to_arr(mat(self.mat())); + }, + "6x6 Voigt matrix as numpy array") + + .def_property_readonly("type", &simcoon::tensor4::type) + + // Methods + .def("contract", + &simcoon::tensor4::contract, + py::arg("t"), + "Contract with a tensor2: result = mat * t.voigt()") + + .def("rotate", + [](const simcoon::tensor4& self, const simcoon::Rotation& R, bool active) { + return self.rotate(R, active); + }, + py::arg("R"), py::arg("active") = true, + "Rotate the tensor using the Rotation object") + + .def("push_forward", + [](const simcoon::tensor4& self, py::array_t F) { + validate_matrix_size(F, 3, 3, "F"); + mat F_cpp = carma::arr_to_mat(F); + return self.push_forward(mat::fixed<3,3>(F_cpp)); + }, + py::arg("F"), + "Push-forward via deformation gradient F") + + .def("pull_back", + [](const simcoon::tensor4& self, py::array_t F) { + validate_matrix_size(F, 3, 3, "F"); + mat F_cpp = carma::arr_to_mat(F); + return self.pull_back(mat::fixed<3,3>(F_cpp)); + }, + py::arg("F"), + "Pull-back via deformation gradient F") + + // Arithmetic + .def("__add__", &simcoon::tensor4::operator+) + .def("__sub__", &simcoon::tensor4::operator-) + .def("__mul__", &simcoon::tensor4::operator*) + .def("__rmul__", [](const simcoon::tensor4& self, double scalar) { + return scalar * self; + }) + .def("__matmul__", [](const simcoon::tensor4& self, const simcoon::tensor2& t) { + return self.contract(t); + }, "Contract with tensor2 using @ operator") + .def("__eq__", &simcoon::tensor4::operator==) + + // String representation + .def("__repr__", [](const simcoon::tensor4& self) { + string type_str; + switch (self.type()) { + case simcoon::Tensor4Type::stiffness: type_str = "stiffness"; break; + case simcoon::Tensor4Type::compliance: type_str = "compliance"; break; + case simcoon::Tensor4Type::strain_concentration: type_str = "strain_concentration"; break; + case simcoon::Tensor4Type::stress_concentration: type_str = "stress_concentration"; break; + case simcoon::Tensor4Type::generic: type_str = "generic"; break; + } + return "Tensor4(type=" + type_str + ")"; + }); +} + +} // namespace simpy From 8cfa3bc09b23a86239bbe82034751bdf64516bf6 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:32:53 +0100 Subject: [PATCH 26/31] Update python_module.cpp --- simcoon-python-builder/src/python_wrappers/python_module.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simcoon-python-builder/src/python_wrappers/python_module.cpp b/simcoon-python-builder/src/python_wrappers/python_module.cpp index 670ebae83..4ba27b17f 100755 --- a/simcoon-python-builder/src/python_wrappers/python_module.cpp +++ b/simcoon-python-builder/src/python_wrappers/python_module.cpp @@ -19,6 +19,7 @@ #include // #include +#include #include #include #include @@ -208,6 +209,9 @@ PYBIND11_MODULE(_core, m) m.def("Eshelby", &Eshelby, "L"_a, "a1"_a = 1., "a2"_a = 1., "a3"_a = 1., "mp"_a = 50, "np"_a = 50, "copy"_a = true, simcoon_docs::Eshelby); m.def("T_II", &T_II, "L"_a, "a1"_a = 1., "a2"_a = 1., "a3"_a = 1., "mp"_a = 50, "np"_a = 50, "copy"_a = true, simcoon_docs::T_II); + // Register tensor2 and tensor4 classes + register_tensor(m); + // Register the Rotation class register_rotation(m); From 29324a0add5dd8355b8084cf55bdd1d85f45d374 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:32:56 +0100 Subject: [PATCH 27/31] Create test_tensor.py --- .../test/test_core/test_tensor.py | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 simcoon-python-builder/test/test_core/test_tensor.py diff --git a/simcoon-python-builder/test/test_core/test_tensor.py b/simcoon-python-builder/test/test_core/test_tensor.py new file mode 100644 index 000000000..0c46238c5 --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_tensor.py @@ -0,0 +1,315 @@ +"""Tests for the Tensor2 and Tensor4 classes.""" + +import numpy as np +import pytest + +import simcoon as sim + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def sigma_mat(): + return np.array([[100, 30, 20], [30, 200, 40], [20, 40, 300]], dtype=float) + + +@pytest.fixture +def eps_mat(): + return np.array([[0.01, 0.005, 0.003], [0.005, 0.02, 0.004], [0.003, 0.004, 0.03]], dtype=float) + + +@pytest.fixture +def sigma_voigt(): + return np.array([100, 200, 300, 30, 20, 40], dtype=float) + + +@pytest.fixture +def eps_voigt(): + return np.array([0.01, 0.02, 0.03, 0.01, 0.006, 0.008], dtype=float) + + +@pytest.fixture +def L(): + return sim.L_iso([70000, 0.3], "Enu") + + +@pytest.fixture +def M(): + return sim.M_iso([70000, 0.3], "Enu") + + +@pytest.fixture +def F(): + return np.array([[1.1, 0.1, 0.05], [0.02, 0.95, 0.03], [0.01, 0.04, 1.05]]) + + +@pytest.fixture +def rot(): + return sim.Rotation.from_euler("ZXZ", [0.3, 0.5, 0.7]) + + +# =================================================================== +# Tensor2 tests +# =================================================================== + +class TestTensor2Construction: + + def test_stress_from_mat(self, sigma_mat): + t = sim.Tensor2.stress(sigma_mat) + assert t.vtype == sim.VoigtType.stress + np.testing.assert_allclose(t.mat, sigma_mat, atol=1e-14) + + def test_strain_from_mat(self, eps_mat): + t = sim.Tensor2.strain(eps_mat) + assert t.vtype == sim.VoigtType.strain + np.testing.assert_allclose(t.mat, eps_mat, atol=1e-14) + + def test_stress_from_voigt(self, sigma_voigt): + t = sim.Tensor2.stress(sigma_voigt) + assert t.vtype == sim.VoigtType.stress + np.testing.assert_allclose(t.voigt, sigma_voigt, atol=1e-12) + + def test_strain_from_voigt(self, eps_voigt): + t = sim.Tensor2.strain(eps_voigt) + assert t.vtype == sim.VoigtType.strain + np.testing.assert_allclose(t.voigt, eps_voigt, atol=1e-12) + + def test_zeros(self): + t = sim.Tensor2.zeros(sim.VoigtType.strain) + np.testing.assert_allclose(t.mat, np.zeros((3, 3)), atol=1e-15) + + def test_identity(self): + t = sim.Tensor2.identity() + np.testing.assert_allclose(t.mat, np.eye(3), atol=1e-15) + + def test_bad_shape_raises(self): + with pytest.raises(ValueError): + sim.Tensor2.stress(np.zeros(5)) + + +class TestTensor2VoigtConventions: + + def test_stress_voigt_matches_t2v_stress(self, sigma_mat): + t = sim.Tensor2.stress(sigma_mat) + ref = sim.t2v_stress(sigma_mat).ravel() + np.testing.assert_allclose(t.voigt, ref, atol=1e-12) + + def test_strain_voigt_matches_t2v_strain(self, eps_mat): + t = sim.Tensor2.strain(eps_mat) + ref = sim.t2v_strain(eps_mat).ravel() + np.testing.assert_allclose(t.voigt, ref, atol=1e-12) + + def test_stress_voigt_roundtrip(self, sigma_voigt): + t = sim.Tensor2.stress(sigma_voigt) + np.testing.assert_allclose(t.voigt, sigma_voigt, atol=1e-12) + + def test_strain_voigt_roundtrip(self, eps_voigt): + t = sim.Tensor2.strain(eps_voigt) + np.testing.assert_allclose(t.voigt, eps_voigt, atol=1e-12) + + +class TestTensor2Arithmetic: + + def test_addition(self, sigma_mat): + t1 = sim.Tensor2.stress(sigma_mat) + t2 = sim.Tensor2.stress(sigma_mat) + t3 = t1 + t2 + np.testing.assert_allclose(t3.mat, 2.0 * sigma_mat, atol=1e-12) + + def test_scalar_multiply(self, sigma_mat): + t = sim.Tensor2.stress(sigma_mat) + t2 = t * 3.0 + t3 = 3.0 * t + np.testing.assert_allclose(t2.mat, 3.0 * sigma_mat, atol=1e-12) + np.testing.assert_allclose(t3.mat, 3.0 * sigma_mat, atol=1e-12) + + def test_equality(self, sigma_mat): + t1 = sim.Tensor2.stress(sigma_mat) + t2 = sim.Tensor2.stress(sigma_mat) + t3 = sim.Tensor2.strain(sigma_mat) + assert t1 == t2 + assert not (t1 == t3) + + +class TestTensor2FreeFunctions: + + def test_trace(self, sigma_mat): + t = sim.Tensor2.stress(sigma_mat) + assert abs(t.trace() - 600.0) < 1e-10 + + def test_mises_uniaxial(self): + Y = 250.0 + sigma = np.array([Y, 0, 0, 0, 0, 0], dtype=float) + t = sim.Tensor2.stress(sigma) + assert abs(t.mises() - Y) < 1e-10 + + def test_mises_hydrostatic_zero(self): + sigma = np.array([100, 100, 100, 0, 0, 0], dtype=float) + t = sim.Tensor2.stress(sigma) + assert t.mises() < 1e-10 + + +class TestTensor2Rotation: + + def test_stress_rotation_roundtrip(self, sigma_mat, rot): + t = sim.Tensor2.stress(sigma_mat) + t_rot = t.rotate(rot, active=True) + inv_rot = rot.inv() + t_back = t_rot.rotate(inv_rot, active=True) + np.testing.assert_allclose(t.mat, t_back.mat, atol=1e-9) + + def test_strain_rotation_roundtrip(self, eps_mat, rot): + t = sim.Tensor2.strain(eps_mat) + t_rot = t.rotate(rot, active=True) + inv_rot = rot.inv() + t_back = t_rot.rotate(inv_rot, active=True) + np.testing.assert_allclose(t.mat, t_back.mat, atol=1e-9) + + +class TestTensor2PushPull: + + def test_stress_push_pull_roundtrip(self, sigma_mat, F): + t = sim.Tensor2.stress(sigma_mat) + t_push = t.push_forward(F) + t_back = t_push.pull_back(F) + np.testing.assert_allclose(t.mat, t_back.mat, atol=1e-9) + + def test_strain_push_pull_roundtrip(self, eps_mat, F): + t = sim.Tensor2.strain(eps_mat) + t_push = t.push_forward(F) + t_back = t_push.pull_back(F) + np.testing.assert_allclose(t.mat, t_back.mat, atol=1e-9) + + +# =================================================================== +# Tensor4 tests +# =================================================================== + +class TestTensor4Construction: + + def test_stiffness(self, L): + t = sim.Tensor4.stiffness(L) + assert t.type == sim.Tensor4Type.stiffness + np.testing.assert_allclose(t.mat, L, atol=1e-12) + + def test_compliance(self, M): + t = sim.Tensor4.compliance(M) + assert t.type == sim.Tensor4Type.compliance + np.testing.assert_allclose(t.mat, M, atol=1e-12) + + def test_identity_matches_Ireal(self): + t = sim.Tensor4.identity() + ref = sim.Ireal() + np.testing.assert_allclose(t.mat, ref, atol=1e-12) + + def test_volumetric_matches_Ivol(self): + t = sim.Tensor4.volumetric() + np.testing.assert_allclose(t.mat, sim.Ivol(), atol=1e-12) + + def test_deviatoric_matches_Idev(self): + t = sim.Tensor4.deviatoric() + np.testing.assert_allclose(t.mat, sim.Idev(), atol=1e-12) + + +class TestTensor4Contraction: + + def test_stiffness_contract_gives_stress(self, L): + stiff = sim.Tensor4.stiffness(L) + eps_v = np.array([0.01, 0.0, 0.0, 0.0, 0.0, 0.0]) + eps = sim.Tensor2.strain(eps_v) + sigma = stiff.contract(eps) + assert sigma.vtype == sim.VoigtType.stress + np.testing.assert_allclose(sigma.voigt, L @ eps_v, atol=1e-10) + + def test_compliance_contract_gives_strain(self, M): + comp = sim.Tensor4.compliance(M) + sig_v = np.array([100, 0, 0, 0, 0, 0], dtype=float) + sig = sim.Tensor2.stress(sig_v) + eps = comp.contract(sig) + assert eps.vtype == sim.VoigtType.strain + np.testing.assert_allclose(eps.voigt, M @ sig_v, atol=1e-10) + + def test_matmul_operator(self, L): + stiff = sim.Tensor4.stiffness(L) + eps_v = np.array([0.01, -0.003, -0.003, 0.005, 0.002, 0.001]) + eps = sim.Tensor2.strain(eps_v) + sigma = stiff @ eps + np.testing.assert_allclose(sigma.voigt, L @ eps_v, atol=1e-10) + + +class TestTensor4Arithmetic: + + def test_isotropic_from_projectors(self, L): + E = 70000.0 + nu = 0.3 + K = E / (3.0 * (1.0 - 2.0 * nu)) + mu = E / (2.0 * (1.0 + nu)) + L_proj = 3.0 * K * sim.Tensor4.volumetric() + 2.0 * mu * sim.Tensor4.deviatoric() + np.testing.assert_allclose(L_proj.mat, L, atol=1e-8) + + def test_scalar_multiply(self, L): + t = sim.Tensor4.stiffness(L) + t2 = t * 2.0 + t3 = 2.0 * t + np.testing.assert_allclose(t2.mat, 2.0 * L, atol=1e-12) + np.testing.assert_allclose(t3.mat, 2.0 * L, atol=1e-12) + + +class TestTensor4Rotation: + + def test_isotropic_stiffness_invariant(self, L, rot): + """Isotropic stiffness is invariant under any rotation.""" + t = sim.Tensor4.stiffness(L) + t_rot = t.rotate(rot, active=True) + np.testing.assert_allclose(t.mat, t_rot.mat, atol=1e-8) + + def test_isotropic_compliance_invariant(self, M, rot): + t = sim.Tensor4.compliance(M) + t_rot = t.rotate(rot, active=True) + np.testing.assert_allclose(t.mat, t_rot.mat, atol=1e-8) + + +class TestTensor4PushPull: + + def test_push_pull_roundtrip(self, L, F): + t = sim.Tensor4.stiffness(L) + t_push = t.push_forward(F) + t_back = t_push.pull_back(F) + np.testing.assert_allclose(t.mat, t_back.mat, atol=1e-8) + + +# =================================================================== +# Integration tests +# =================================================================== + +class TestIntegration: + + def test_full_cycle_stress_rotate_contract(self, L, rot): + """L:eps in material frame == (rotate L) : (rotate eps) in global frame.""" + stiff = sim.Tensor4.stiffness(L) + eps_v = np.array([0.01, -0.003, -0.003, 0.005, 0.002, 0.001]) + eps = sim.Tensor2.strain(eps_v) + + # Material frame + sigma = stiff @ eps + + # Global frame + stiff_rot = stiff.rotate(rot, active=True) + eps_rot = eps.rotate(rot, active=True) + sigma_rot_direct = stiff_rot @ eps_rot + + # Rotate material-frame stress to global frame + sigma_rot_indirect = sigma.rotate(rot, active=True) + + np.testing.assert_allclose( + sigma_rot_direct.mat, sigma_rot_indirect.mat, atol=1e-8 + ) + + def test_repr(self): + t2 = sim.Tensor2.stress(np.eye(3)) + assert "stress" in repr(t2) + + t4 = sim.Tensor4.stiffness(np.eye(6)) + assert "stiffness" in repr(t4) From 4737c54ae8bd3e5567a55353bb9efe6f63dfa945 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:32:59 +0100 Subject: [PATCH 28/31] Update CMakeLists.txt --- src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 439b5125f..5a07e1e96 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ target_sources(simcoon PRIVATE Continuum_mechanics/Functions/objective_rates.cpp Continuum_mechanics/Functions/recovery_props.cpp Continuum_mechanics/Functions/stress.cpp + Continuum_mechanics/Functions/tensor.cpp Continuum_mechanics/Functions/transfer.cpp # Continuum Mechanics - Homogenization From bde6fb86278f280cc469823e767cdb38df78c066 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:33:10 +0100 Subject: [PATCH 29/31] Add tensor2/tensor4 implementations with Fastor Add new src/Continuum_mechanics/Functions/tensor.cpp implementing tensor2 and tensor4 types. Provides constructors, Voigt conversions, Fastor/Armadillo bridges, rotation, push-forward/pull-back, contraction, dyadic operations, Mises/dev/trace helpers, and arithmetic operators. Includes Fastor caching and conversion utilities to accelerate 4th-order tensor ops and integrates with existing Rotation/constitutive helpers for continuum mechanics use. --- src/Continuum_mechanics/Functions/tensor.cpp | 585 +++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 src/Continuum_mechanics/Functions/tensor.cpp diff --git a/src/Continuum_mechanics/Functions/tensor.cpp b/src/Continuum_mechanics/Functions/tensor.cpp new file mode 100644 index 000000000..86591cd10 --- /dev/null +++ b/src/Continuum_mechanics/Functions/tensor.cpp @@ -0,0 +1,585 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file tensor.cpp +///@brief Implementation of tensor2 and tensor4 classes with type tags and Fastor integration. +///@version 1.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace simcoon { + +// Helper: compute inv of a 3x3 fixed matrix, returning fixed +static arma::mat::fixed<3,3> inv33(const arma::mat::fixed<3,3> &F) { + arma::mat tmp = arma::inv(arma::mat(F)); + arma::mat::fixed<3,3> result; + result = tmp; + return result; +} + +// ============================================================================ +// tensor2 implementation +// ============================================================================ + +tensor2::tensor2() : _mat(arma::fill::zeros), _vtype(VoigtType::stress), + _symmetric(true), _symmetry_checked(true) {} + +tensor2::tensor2(VoigtType vtype) : _mat(arma::fill::zeros), _vtype(vtype), + _symmetric(true), _symmetry_checked(true) {} + +tensor2::tensor2(const arma::mat::fixed<3,3> &m, VoigtType vtype) + : _mat(m), _vtype(vtype), _symmetric(false), _symmetry_checked(false) {} + +tensor2::tensor2(const arma::mat &m, VoigtType vtype) + : _vtype(vtype), _symmetric(false), _symmetry_checked(false) { + assert(m.n_rows == 3 && m.n_cols == 3); + _mat = m; +} + +tensor2 tensor2::from_voigt(const arma::vec::fixed<6> &v, VoigtType vtype) { + arma::mat::fixed<3,3> m; + if (vtype == VoigtType::strain) { + m(0,0) = v(0); m(1,1) = v(1); m(2,2) = v(2); + m(0,1) = 0.5 * v(3); m(1,0) = 0.5 * v(3); + m(0,2) = 0.5 * v(4); m(2,0) = 0.5 * v(4); + m(1,2) = 0.5 * v(5); m(2,1) = 0.5 * v(5); + } else if (vtype == VoigtType::stress || vtype == VoigtType::generic) { + m(0,0) = v(0); m(1,1) = v(1); m(2,2) = v(2); + m(0,1) = v(3); m(1,0) = v(3); + m(0,2) = v(4); m(2,0) = v(4); + m(1,2) = v(5); m(2,1) = v(5); + } else { + throw std::runtime_error("Cannot construct tensor2 from Voigt with VoigtType::none"); + } + tensor2 result(m, vtype); + result._symmetric = true; + result._symmetry_checked = true; + return result; +} + +tensor2 tensor2::from_voigt(const arma::vec &v, VoigtType vtype) { + assert(v.n_elem == 6); + arma::vec::fixed<6> vf(v.memptr()); + return from_voigt(vf, vtype); +} + +tensor2 tensor2::zeros(VoigtType vtype) { + return tensor2(vtype); +} + +tensor2 tensor2::identity(VoigtType vtype) { + return tensor2(arma::mat::fixed<3,3>(arma::fill::eye), vtype); +} + +arma::mat::fixed<3,3>& tensor2::mat_mut() { + _symmetry_checked = false; + return _mat; +} + +void tensor2::set_mat(const arma::mat::fixed<3,3> &m) { + _mat = m; + _symmetry_checked = false; +} + +void tensor2::set_mat(const arma::mat &m) { + assert(m.n_rows == 3 && m.n_cols == 3); + _mat = m; + _symmetry_checked = false; +} + +void tensor2::set_voigt(const arma::vec::fixed<6> &v) { + if (_vtype == VoigtType::strain) { + _mat(0,0) = v(0); _mat(1,1) = v(1); _mat(2,2) = v(2); + _mat(0,1) = 0.5 * v(3); _mat(1,0) = 0.5 * v(3); + _mat(0,2) = 0.5 * v(4); _mat(2,0) = 0.5 * v(4); + _mat(1,2) = 0.5 * v(5); _mat(2,1) = 0.5 * v(5); + } else if (_vtype == VoigtType::stress || _vtype == VoigtType::generic) { + _mat(0,0) = v(0); _mat(1,1) = v(1); _mat(2,2) = v(2); + _mat(0,1) = v(3); _mat(1,0) = v(3); + _mat(0,2) = v(4); _mat(2,0) = v(4); + _mat(1,2) = v(5); _mat(2,1) = v(5); + } else { + throw std::runtime_error("Cannot set_voigt on VoigtType::none tensor"); + } + _symmetric = true; + _symmetry_checked = true; +} + +void tensor2::set_voigt(const arma::vec &v) { + assert(v.n_elem == 6); + arma::vec::fixed<6> vf(v.memptr()); + set_voigt(vf); +} + +arma::vec::fixed<6> tensor2::voigt() const { + if (_vtype == VoigtType::none) { + throw std::runtime_error("Cannot compute Voigt vector for VoigtType::none (non-symmetric tensor)"); + } + + arma::vec::fixed<6> v; + v(0) = _mat(0,0); v(1) = _mat(1,1); v(2) = _mat(2,2); + + if (_vtype == VoigtType::strain) { + v(3) = _mat(0,1) + _mat(1,0); + v(4) = _mat(0,2) + _mat(2,0); + v(5) = _mat(1,2) + _mat(2,1); + } else { + v(3) = 0.5 * (_mat(0,1) + _mat(1,0)); + v(4) = 0.5 * (_mat(0,2) + _mat(2,0)); + v(5) = 0.5 * (_mat(1,2) + _mat(2,1)); + } + return v; +} + +Fastor::TensorMap tensor2::fastor() { + return arma_to_fastor2(_mat); +} + +Fastor::TensorMap tensor2::fastor() const { + return arma_to_fastor2(_mat); +} + +bool tensor2::is_symmetric(double tol) const { + if (_symmetry_checked) return _symmetric; + + _symmetric = (std::abs(_mat(0,1) - _mat(1,0)) < tol) && + (std::abs(_mat(0,2) - _mat(2,0)) < tol) && + (std::abs(_mat(1,2) - _mat(2,1)) < tol); + _symmetry_checked = true; + return _symmetric; +} + +tensor2 tensor2::rotate(const Rotation &R, bool active) const { + switch (_vtype) { + case VoigtType::stress: { + arma::vec::fixed<6> v_rot = R.apply_stress(voigt(), active); + return tensor2::from_voigt(v_rot, VoigtType::stress); + } + case VoigtType::strain: { + arma::vec::fixed<6> v_rot = R.apply_strain(voigt(), active); + return tensor2::from_voigt(v_rot, VoigtType::strain); + } + case VoigtType::generic: + case VoigtType::none: { + arma::mat::fixed<3,3> R_mat = R.as_matrix(); + arma::mat::fixed<3,3> result; + if (active) { + result = R_mat * _mat * R_mat.t(); + } else { + result = R_mat.t() * _mat * R_mat; + } + return tensor2(result, _vtype); + } + } + return *this; +} + +tensor2 tensor2::push_forward(const arma::mat::fixed<3,3> &F) const { + switch (_vtype) { + case VoigtType::stress: { + arma::mat::fixed<3,3> result; + result = F * _mat * F.t(); + return tensor2(result, VoigtType::stress); + } + case VoigtType::strain: { + arma::mat::fixed<3,3> invF = inv33(F); + arma::mat::fixed<3,3> result; + result = invF.t() * _mat * invF; + return tensor2(result, VoigtType::strain); + } + case VoigtType::generic: + case VoigtType::none: + throw std::runtime_error("push_forward requires VoigtType::stress or VoigtType::strain"); + } + return *this; +} + +tensor2 tensor2::pull_back(const arma::mat::fixed<3,3> &F) const { + switch (_vtype) { + case VoigtType::stress: { + arma::mat::fixed<3,3> invF = inv33(F); + arma::mat::fixed<3,3> result; + result = invF * _mat * invF.t(); + return tensor2(result, VoigtType::stress); + } + case VoigtType::strain: { + arma::mat::fixed<3,3> result; + result = F.t() * _mat * F; + return tensor2(result, VoigtType::strain); + } + case VoigtType::generic: + case VoigtType::none: + throw std::runtime_error("pull_back requires VoigtType::stress or VoigtType::strain"); + } + return *this; +} + +tensor2 tensor2::operator+(const tensor2 &other) const { + arma::mat::fixed<3,3> result; + result = _mat + other._mat; + return tensor2(result, _vtype); +} + +tensor2 tensor2::operator-(const tensor2 &other) const { + arma::mat::fixed<3,3> result; + result = _mat - other._mat; + return tensor2(result, _vtype); +} + +tensor2 tensor2::operator*(double scalar) const { + arma::mat::fixed<3,3> result; + result = _mat * scalar; + return tensor2(result, _vtype); +} + +tensor2& tensor2::operator+=(const tensor2 &other) { + _mat += other._mat; + _symmetry_checked = false; + return *this; +} + +tensor2& tensor2::operator-=(const tensor2 &other) { + _mat -= other._mat; + _symmetry_checked = false; + return *this; +} + +tensor2& tensor2::operator*=(double scalar) { + _mat *= scalar; + return *this; +} + +tensor2 operator*(double scalar, const tensor2 &t) { + arma::mat::fixed<3,3> result; + result = t._mat * scalar; + return tensor2(result, t._vtype); +} + +bool tensor2::operator==(const tensor2 &other) const { + return _vtype == other._vtype && arma::approx_equal(_mat, other._mat, "absdiff", 1e-14); +} + +// Free functions +tensor2 stress(const arma::mat::fixed<3,3> &m) { + return tensor2(m, VoigtType::stress); +} + +tensor2 stress(const arma::vec::fixed<6> &v) { + return tensor2::from_voigt(v, VoigtType::stress); +} + +tensor2 strain(const arma::mat::fixed<3,3> &m) { + return tensor2(m, VoigtType::strain); +} + +tensor2 strain(const arma::vec::fixed<6> &v) { + return tensor2::from_voigt(v, VoigtType::strain); +} + +arma::vec::fixed<6> dev(const tensor2 &t) { + arma::vec::fixed<6> v = t.voigt(); + double tr_val = v(0) + v(1) + v(2); + arma::vec::fixed<6> result = v; + result(0) -= tr_val / 3.0; + result(1) -= tr_val / 3.0; + result(2) -= tr_val / 3.0; + return result; +} + +double Mises(const tensor2 &t) { + arma::vec::fixed<6> d = dev(t); + if (t.vtype() == VoigtType::stress || t.vtype() == VoigtType::generic) { + return std::sqrt(1.5 * (d(0)*d(0) + d(1)*d(1) + d(2)*d(2) + + 2.0*(d(3)*d(3) + d(4)*d(4) + d(5)*d(5)))); + } else if (t.vtype() == VoigtType::strain) { + return std::sqrt(2.0/3.0 * (d(0)*d(0) + d(1)*d(1) + d(2)*d(2) + + 0.5*(d(3)*d(3) + d(4)*d(4) + d(5)*d(5)))); + } + throw std::runtime_error("Mises not defined for VoigtType::none"); +} + +double trace(const tensor2 &t) { + return t.mat()(0,0) + t.mat()(1,1) + t.mat()(2,2); +} + +// ============================================================================ +// tensor4 implementation +// ============================================================================ + +tensor4::tensor4() : _voigt(arma::fill::zeros), _type(Tensor4Type::stiffness), + _fastor_valid(false) {} + +tensor4::tensor4(Tensor4Type type) : _voigt(arma::fill::zeros), _type(type), + _fastor_valid(false) {} + +tensor4::tensor4(const arma::mat::fixed<6,6> &m, Tensor4Type type) + : _voigt(m), _type(type), _fastor_valid(false) {} + +tensor4::tensor4(const arma::mat &m, Tensor4Type type) + : _type(type), _fastor_valid(false) { + assert(m.n_rows == 6 && m.n_cols == 6); + _voigt = m; +} + +tensor4::tensor4(const tensor4 &other) + : _voigt(other._voigt), _type(other._type), + _fastor(other._fastor), _fastor_valid(other._fastor_valid) {} + +tensor4::tensor4(tensor4 &&other) noexcept + : _voigt(std::move(other._voigt)), _type(other._type), + _fastor(std::move(other._fastor)), _fastor_valid(other._fastor_valid) {} + +tensor4& tensor4::operator=(const tensor4 &other) { + if (this != &other) { + _voigt = other._voigt; + _type = other._type; + _fastor = other._fastor; + _fastor_valid = other._fastor_valid; + } + return *this; +} + +tensor4& tensor4::operator=(tensor4 &&other) noexcept { + if (this != &other) { + _voigt = std::move(other._voigt); + _type = other._type; + _fastor = std::move(other._fastor); + _fastor_valid = other._fastor_valid; + } + return *this; +} + +tensor4 tensor4::identity(Tensor4Type type) { + arma::mat::fixed<6,6> m = Ireal(); + return tensor4(m, type); +} + +tensor4 tensor4::volumetric(Tensor4Type type) { + arma::mat::fixed<6,6> m = Ivol(); + return tensor4(m, type); +} + +tensor4 tensor4::deviatoric(Tensor4Type type) { + arma::mat::fixed<6,6> m = Idev(); + return tensor4(m, type); +} + +tensor4 tensor4::identity2(Tensor4Type type) { + arma::mat::fixed<6,6> m = Ireal2(); + return tensor4(m, type); +} + +tensor4 tensor4::deviatoric2(Tensor4Type type) { + arma::mat::fixed<6,6> m = Idev2(); + return tensor4(m, type); +} + +tensor4 tensor4::zeros(Tensor4Type type) { + return tensor4(type); +} + +arma::mat::fixed<6,6>& tensor4::mat_mut() { + _invalidate_fastor(); + return _voigt; +} + +void tensor4::set_mat(const arma::mat::fixed<6,6> &m) { + _voigt = m; + _invalidate_fastor(); +} + +void tensor4::set_mat(const arma::mat &m) { + assert(m.n_rows == 6 && m.n_cols == 6); + _voigt = m; + _invalidate_fastor(); +} + +void tensor4::_ensure_fastor() const { + if (!_fastor_valid) { + _fastor = voigt_to_fastor4(_voigt); + _fastor_valid = true; + } +} + +const Fastor::Tensor& tensor4::fastor() const { + _ensure_fastor(); + return _fastor; +} + +tensor2 tensor4::contract(const tensor2 &t) const { + arma::vec::fixed<6> v_in = t.voigt(); + arma::vec::fixed<6> v_out; + + for (int i = 0; i < 6; ++i) { + double sum = 0.0; + for (int j = 0; j < 6; ++j) { + sum += _voigt(i,j) * v_in(j); + } + v_out(i) = sum; + } + + VoigtType out_vtype; + switch (_type) { + case Tensor4Type::stiffness: + case Tensor4Type::stress_concentration: + out_vtype = VoigtType::stress; + break; + case Tensor4Type::compliance: + case Tensor4Type::strain_concentration: + out_vtype = VoigtType::strain; + break; + case Tensor4Type::generic: + default: + out_vtype = VoigtType::stress; + break; + } + + return tensor2::from_voigt(v_out, out_vtype); +} + +tensor4 tensor4::push_forward(const arma::mat::fixed<3,3> &F) const { + _ensure_fastor(); + + Fastor::Tensor F_fastor; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + F_fastor(i,j) = F(i,j); + + Fastor::Tensor result = push_forward_4(_fastor, F_fastor); + arma::mat::fixed<6,6> voigt_result = fastor4_to_voigt(result); + return tensor4(voigt_result, _type); +} + +tensor4 tensor4::pull_back(const arma::mat::fixed<3,3> &F) const { + _ensure_fastor(); + + arma::mat::fixed<3,3> invF = inv33(F); + + Fastor::Tensor invF_fastor; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + invF_fastor(i,j) = invF(i,j); + + Fastor::Tensor result = pull_back_4(_fastor, invF_fastor); + arma::mat::fixed<6,6> voigt_result = fastor4_to_voigt(result); + return tensor4(voigt_result, _type); +} + +tensor4 tensor4::rotate(const Rotation &R, bool active) const { + switch (_type) { + case Tensor4Type::stiffness: + case Tensor4Type::generic: { + arma::mat::fixed<6,6> result = R.apply_stiffness(_voigt, active); + return tensor4(result, _type); + } + case Tensor4Type::compliance: { + arma::mat::fixed<6,6> result = R.apply_compliance(_voigt, active); + return tensor4(result, _type); + } + case Tensor4Type::strain_concentration: { + arma::mat::fixed<6,6> result = R.apply_strain_concentration(_voigt, active); + return tensor4(result, _type); + } + case Tensor4Type::stress_concentration: { + arma::mat::fixed<6,6> result = R.apply_stress_concentration(_voigt, active); + return tensor4(result, _type); + } + } + return *this; +} + +tensor4 tensor4::operator+(const tensor4 &other) const { + arma::mat::fixed<6,6> result; + result = _voigt + other._voigt; + return tensor4(result, _type); +} + +tensor4 tensor4::operator-(const tensor4 &other) const { + arma::mat::fixed<6,6> result; + result = _voigt - other._voigt; + return tensor4(result, _type); +} + +tensor4 tensor4::operator*(double scalar) const { + arma::mat::fixed<6,6> result; + result = _voigt * scalar; + return tensor4(result, _type); +} + +tensor4& tensor4::operator+=(const tensor4 &other) { + _voigt += other._voigt; + _invalidate_fastor(); + return *this; +} + +tensor4& tensor4::operator-=(const tensor4 &other) { + _voigt -= other._voigt; + _invalidate_fastor(); + return *this; +} + +tensor4& tensor4::operator*=(double scalar) { + _voigt *= scalar; + _invalidate_fastor(); + return *this; +} + +tensor4 operator*(double scalar, const tensor4 &t) { + arma::mat::fixed<6,6> result; + result = t._voigt * scalar; + return tensor4(result, t._type); +} + +bool tensor4::operator==(const tensor4 &other) const { + return _type == other._type && arma::approx_equal(_voigt, other._voigt, "absdiff", 1e-14); +} + +// Free functions +tensor4 dyadic(const tensor2 &a, const tensor2 &b) { + arma::vec::fixed<6> a_v, b_v; + + const arma::mat::fixed<3,3> &am = a.mat(); + const arma::mat::fixed<3,3> &bm = b.mat(); + + a_v(0) = am(0,0); a_v(1) = am(1,1); a_v(2) = am(2,2); + a_v(3) = 0.5*(am(0,1) + am(1,0)); + a_v(4) = 0.5*(am(0,2) + am(2,0)); + a_v(5) = 0.5*(am(1,2) + am(2,1)); + + b_v(0) = bm(0,0); b_v(1) = bm(1,1); b_v(2) = bm(2,2); + b_v(3) = 0.5*(bm(0,1) + bm(1,0)); + b_v(4) = 0.5*(bm(0,2) + bm(2,0)); + b_v(5) = 0.5*(bm(1,2) + bm(2,1)); + + arma::mat::fixed<6,6> C; + C = a_v * b_v.t(); + return tensor4(C, Tensor4Type::stiffness); +} + +tensor4 auto_dyadic(const tensor2 &a) { + return dyadic(a, a); +} + +} // namespace simcoon From df442dd9c71e4126617a08b4f603902968d763e4 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:33:12 +0100 Subject: [PATCH 30/31] Update CMakeLists.txt --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 42fb4fb1a..3a1968037 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -18,6 +18,7 @@ set(TEST_SRCS test/Libraries/Continuum_mechanics/Tobjective_rates.cpp test/Libraries/Continuum_mechanics/Trecovery_props.cpp test/Libraries/Continuum_mechanics/Tstress.cpp + test/Libraries/Continuum_mechanics/Ttensor.cpp test/Libraries/Continuum_mechanics/Ttransfer.cpp # Libraries - Homogenization From f54bb5005c3dc14b9f479c79f9d9ba1d8f7c5d49 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 13 Feb 2026 23:33:17 +0100 Subject: [PATCH 31/31] Create Ttensor.cpp --- .../Libraries/Continuum_mechanics/Ttensor.cpp | 654 ++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 test/Libraries/Continuum_mechanics/Ttensor.cpp diff --git a/test/Libraries/Continuum_mechanics/Ttensor.cpp b/test/Libraries/Continuum_mechanics/Ttensor.cpp new file mode 100644 index 000000000..619acd3c9 --- /dev/null +++ b/test/Libraries/Continuum_mechanics/Ttensor.cpp @@ -0,0 +1,654 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file Ttensor.cpp +///@brief Tests for tensor2 and tensor4 classes +///@version 1.0 + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std; +using namespace arma; +using namespace simcoon; + +// ============================================================================ +// tensor2 constructors +// ============================================================================ + +TEST(Ttensor2, DefaultConstructor) +{ + tensor2 t; + EXPECT_TRUE(arma::approx_equal(t.mat(), mat::fixed<3,3>(fill::zeros), "absdiff", 1e-15)); + EXPECT_EQ(t.vtype(), VoigtType::stress); +} + +TEST(Ttensor2, VoigtTypeConstructor) +{ + tensor2 t(VoigtType::strain); + EXPECT_TRUE(arma::approx_equal(t.mat(), mat::fixed<3,3>(fill::zeros), "absdiff", 1e-15)); + EXPECT_EQ(t.vtype(), VoigtType::strain); +} + +TEST(Ttensor2, MatrixConstructor) +{ + mat::fixed<3,3> m = {{1, 2, 3}, {2, 5, 6}, {3, 6, 9}}; + tensor2 t(m, VoigtType::stress); + EXPECT_TRUE(arma::approx_equal(t.mat(), m, "absdiff", 1e-15)); +} + +TEST(Ttensor2, DynamicMatConstructor) +{ + mat m = {{1, 2, 3}, {2, 5, 6}, {3, 6, 9}}; + tensor2 t(m, VoigtType::stress); + mat::fixed<3,3> expected = {{1, 2, 3}, {2, 5, 6}, {3, 6, 9}}; + EXPECT_TRUE(arma::approx_equal(t.mat(), expected, "absdiff", 1e-15)); +} + +TEST(Ttensor2, Factories) +{ + tensor2 z = tensor2::zeros(VoigtType::strain); + EXPECT_TRUE(arma::approx_equal(z.mat(), mat::fixed<3,3>(fill::zeros), "absdiff", 1e-15)); + EXPECT_EQ(z.vtype(), VoigtType::strain); + + tensor2 id = tensor2::identity(VoigtType::stress); + EXPECT_TRUE(arma::approx_equal(id.mat(), mat::fixed<3,3>(fill::eye), "absdiff", 1e-15)); +} + +// ============================================================================ +// tensor2 Voigt conventions — cross-validate with transfer.hpp +// ============================================================================ + +TEST(Ttensor2, StressVoigtMatchesTransfer) +{ + // Create a symmetric stress tensor + mat::fixed<3,3> sigma = {{100, 30, 20}, {30, 200, 40}, {20, 40, 300}}; + tensor2 t(sigma, VoigtType::stress); + + vec::fixed<6> v = t.voigt(); + vec v_ref = t2v_stress(mat(sigma)); + + EXPECT_LT(norm(vec(v) - v_ref, 2), 1e-12); +} + +TEST(Ttensor2, StrainVoigtMatchesTransfer) +{ + // Create a symmetric strain tensor + mat::fixed<3,3> eps = {{0.01, 0.005, 0.003}, {0.005, 0.02, 0.004}, {0.003, 0.004, 0.03}}; + tensor2 t(eps, VoigtType::strain); + + vec::fixed<6> v = t.voigt(); + vec v_ref = t2v_strain(mat(eps)); + + EXPECT_LT(norm(vec(v) - v_ref, 2), 1e-12); +} + +TEST(Ttensor2, FromVoigtStress) +{ + vec::fixed<6> v = {100, 200, 300, 30, 20, 40}; + tensor2 t = tensor2::from_voigt(v, VoigtType::stress); + + // Should reconstruct the matrix + mat m_ref = v2t_stress(vec(v)); + EXPECT_LT(norm(mat(t.mat()) - m_ref, "fro"), 1e-12); + + // And the voigt should roundtrip + EXPECT_LT(norm(vec(t.voigt()) - vec(v), 2), 1e-12); +} + +TEST(Ttensor2, FromVoigtStrain) +{ + vec::fixed<6> v = {0.01, 0.02, 0.03, 0.01, 0.006, 0.008}; + tensor2 t = tensor2::from_voigt(v, VoigtType::strain); + + // Should reconstruct the matrix (shear halved in matrix form) + mat m_ref = v2t_strain(vec(v)); + EXPECT_LT(norm(mat(t.mat()) - m_ref, "fro"), 1e-12); + + // And the voigt should roundtrip + EXPECT_LT(norm(vec(t.voigt()) - vec(v), 2), 1e-12); +} + +TEST(Ttensor2, VoigtThrowsForNone) +{ + mat::fixed<3,3> F = {{1.1, 0.1, 0}, {0, 1, 0.05}, {0, 0, 0.95}}; + tensor2 t(F, VoigtType::none); + EXPECT_THROW(t.voigt(), std::runtime_error); +} + +// ============================================================================ +// tensor2 set_voigt / set_mat +// ============================================================================ + +TEST(Ttensor2, SetVoigt) +{ + tensor2 t(VoigtType::stress); + vec::fixed<6> v = {100, 200, 300, 30, 20, 40}; + t.set_voigt(v); + EXPECT_LT(norm(vec(t.voigt()) - vec(v), 2), 1e-12); +} + +TEST(Ttensor2, SetMat) +{ + tensor2 t(VoigtType::stress); + mat::fixed<3,3> m = {{10, 3, 2}, {3, 20, 4}, {2, 4, 30}}; + t.set_mat(m); + EXPECT_TRUE(arma::approx_equal(t.mat(), m, "absdiff", 1e-15)); +} + +// ============================================================================ +// tensor2 symmetry check +// ============================================================================ + +TEST(Ttensor2, SymmetryCheck) +{ + mat::fixed<3,3> sym = {{1, 2, 3}, {2, 5, 6}, {3, 6, 9}}; + tensor2 ts(sym, VoigtType::stress); + EXPECT_TRUE(ts.is_symmetric()); + + mat::fixed<3,3> nonsym = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + tensor2 tn(nonsym, VoigtType::none); + EXPECT_FALSE(tn.is_symmetric()); +} + +// ============================================================================ +// tensor2 Fastor zero-copy map +// ============================================================================ + +TEST(Ttensor2, FastorMap) +{ + mat::fixed<3,3> m = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + tensor2 t(m, VoigtType::none); + + auto fmap = t.fastor(); + // Check a few elements (Fastor and armadillo are both column-major) + EXPECT_DOUBLE_EQ(fmap(0,0), 1.0); + EXPECT_DOUBLE_EQ(fmap(1,1), 5.0); + EXPECT_DOUBLE_EQ(fmap(2,2), 9.0); +} + +// ============================================================================ +// tensor2 free functions +// ============================================================================ + +TEST(Ttensor2, StressStrainFreeFunctions) +{ + mat::fixed<3,3> m = {{100, 30, 20}, {30, 200, 40}, {20, 40, 300}}; + tensor2 s = stress(m); + EXPECT_EQ(s.vtype(), VoigtType::stress); + + tensor2 e = strain(m); + EXPECT_EQ(e.vtype(), VoigtType::strain); +} + +TEST(Ttensor2, Trace) +{ + mat::fixed<3,3> m = {{100, 30, 20}, {30, 200, 40}, {20, 40, 300}}; + tensor2 t(m, VoigtType::stress); + EXPECT_DOUBLE_EQ(trace(t), 600.0); +} + +TEST(Ttensor2, DevStress) +{ + // Hydrostatic stress => deviatoric part is zero + mat::fixed<3,3> hydro = {{100, 0, 0}, {0, 100, 0}, {0, 0, 100}}; + tensor2 t(hydro, VoigtType::stress); + vec::fixed<6> d = dev(t); + EXPECT_LT(norm(d, 2), 1e-12); +} + +TEST(Ttensor2, Mises) +{ + // Pure tension sigma_11 = Y => von Mises = Y + double Y = 250.0; + mat::fixed<3,3> tension = fill::zeros; + tension(0,0) = Y; + tensor2 t(tension, VoigtType::stress); + EXPECT_NEAR(Mises(t), Y, 1e-10); +} + +// ============================================================================ +// tensor2 arithmetic +// ============================================================================ + +TEST(Ttensor2, Addition) +{ + mat::fixed<3,3> a = {{1, 2, 3}, {2, 5, 6}, {3, 6, 9}}; + mat::fixed<3,3> b = {{10, 20, 30}, {20, 50, 60}, {30, 60, 90}}; + tensor2 ta(a, VoigtType::stress); + tensor2 tb(b, VoigtType::stress); + tensor2 tc = ta + tb; + mat::fixed<3,3> expected; + expected = a + b; + EXPECT_TRUE(arma::approx_equal(tc.mat(), expected, "absdiff", 1e-14)); +} + +TEST(Ttensor2, ScalarMultiply) +{ + mat::fixed<3,3> a = {{1, 2, 3}, {2, 5, 6}, {3, 6, 9}}; + tensor2 ta(a, VoigtType::stress); + tensor2 tb = ta * 3.0; + tensor2 tc = 3.0 * ta; + mat::fixed<3,3> expected; + expected = a * 3.0; + EXPECT_TRUE(arma::approx_equal(tb.mat(), expected, "absdiff", 1e-14)); + EXPECT_TRUE(arma::approx_equal(tc.mat(), expected, "absdiff", 1e-14)); +} + +TEST(Ttensor2, Equality) +{ + mat::fixed<3,3> a = {{1, 2, 3}, {2, 5, 6}, {3, 6, 9}}; + tensor2 ta(a, VoigtType::stress); + tensor2 tb(a, VoigtType::stress); + tensor2 tc(a, VoigtType::strain); + EXPECT_TRUE(ta == tb); + EXPECT_FALSE(ta == tc); +} + +// ============================================================================ +// tensor2 rotation +// ============================================================================ + +TEST(Ttensor2, RotationStressRoundtrip) +{ + // Rotate a stress tensor by some angle and back: should recover original + mat::fixed<3,3> sigma = {{100, 30, 20}, {30, 200, 40}, {20, 40, 300}}; + tensor2 t(sigma, VoigtType::stress); + + Rotation R = Rotation::from_euler(0.3, 0.5, 0.7, "zxz"); + tensor2 t_rot = t.rotate(R, true); + Rotation R_inv = Rotation::from_euler(-0.7, -0.5, -0.3, "zxz"); + tensor2 t_back = t_rot.rotate(R_inv, true); + + EXPECT_LT(norm(mat(t.mat()) - mat(t_back.mat()), "fro"), 1e-9); +} + +TEST(Ttensor2, RotationStrainRoundtrip) +{ + mat::fixed<3,3> eps = {{0.01, 0.005, 0.003}, {0.005, 0.02, 0.004}, {0.003, 0.004, 0.03}}; + tensor2 t(eps, VoigtType::strain); + + Rotation R = Rotation::from_euler(0.3, 0.5, 0.7, "zxz"); + tensor2 t_rot = t.rotate(R, true); + Rotation R_inv = Rotation::from_euler(-0.7, -0.5, -0.3, "zxz"); + tensor2 t_back = t_rot.rotate(R_inv, true); + + EXPECT_LT(norm(mat(t.mat()) - mat(t_back.mat()), "fro"), 1e-9); +} + +// ============================================================================ +// tensor2 push-forward / pull-back +// ============================================================================ + +TEST(Ttensor2, PushPullBackStress) +{ + // Push-forward then pull-back should be identity + mat::fixed<3,3> sigma = {{100, 30, 20}, {30, 200, 40}, {20, 40, 300}}; + tensor2 t(sigma, VoigtType::stress); + + mat::fixed<3,3> F = {{1.1, 0.1, 0.05}, {0.02, 0.95, 0.03}, {0.01, 0.04, 1.05}}; + + tensor2 t_push = t.push_forward(F); + tensor2 t_back = t_push.pull_back(F); + + EXPECT_LT(norm(mat(t.mat()) - mat(t_back.mat()), "fro"), 1e-9); +} + +TEST(Ttensor2, PushPullBackStrain) +{ + mat::fixed<3,3> eps = {{0.01, 0.005, 0.003}, {0.005, 0.02, 0.004}, {0.003, 0.004, 0.03}}; + tensor2 t(eps, VoigtType::strain); + + mat::fixed<3,3> F = {{1.1, 0.1, 0.05}, {0.02, 0.95, 0.03}, {0.01, 0.04, 1.05}}; + + tensor2 t_push = t.push_forward(F); + tensor2 t_back = t_push.pull_back(F); + + EXPECT_LT(norm(mat(t.mat()) - mat(t_back.mat()), "fro"), 1e-9); +} + +// ============================================================================ +// tensor4 constructors +// ============================================================================ + +TEST(Ttensor4, DefaultConstructor) +{ + tensor4 t; + EXPECT_TRUE(arma::approx_equal(t.mat(), mat::fixed<6,6>(fill::zeros), "absdiff", 1e-15)); + EXPECT_EQ(t.type(), Tensor4Type::stiffness); +} + +TEST(Ttensor4, MatrixConstructor) +{ + mat::fixed<6,6> L = L_iso(70000., 0.3, "Enu"); + tensor4 t(L, Tensor4Type::stiffness); + EXPECT_TRUE(arma::approx_equal(t.mat(), L, "absdiff", 1e-15)); +} + +// ============================================================================ +// tensor4 static factories cross-validate with constitutive.hpp +// ============================================================================ + +TEST(Ttensor4, IdentityMatchesIreal) +{ + tensor4 t = tensor4::identity(); + mat Iref = Ireal(); + EXPECT_LT(norm(mat(t.mat()) - Iref, "fro"), 1e-12); +} + +TEST(Ttensor4, VolumetricMatchesIvol) +{ + tensor4 t = tensor4::volumetric(); + mat Iref = Ivol(); + EXPECT_LT(norm(mat(t.mat()) - Iref, "fro"), 1e-12); +} + +TEST(Ttensor4, DeviatoricMatchesIdev) +{ + tensor4 t = tensor4::deviatoric(); + mat Iref = Idev(); + EXPECT_LT(norm(mat(t.mat()) - Iref, "fro"), 1e-12); +} + +TEST(Ttensor4, Identity2MatchesIreal2) +{ + tensor4 t = tensor4::identity2(); + mat Iref = Ireal2(); + EXPECT_LT(norm(mat(t.mat()) - Iref, "fro"), 1e-12); +} + +TEST(Ttensor4, Deviatoric2MatchesIdev2) +{ + tensor4 t = tensor4::deviatoric2(); + mat Iref = Idev2(); + EXPECT_LT(norm(mat(t.mat()) - Iref, "fro"), 1e-12); +} + +// ============================================================================ +// tensor4 contraction with type inference +// ============================================================================ + +TEST(Ttensor4, ContractStiffnessGivesStress) +{ + double E = 70000.0; + double nu = 0.3; + mat::fixed<6,6> L = L_iso(E, nu, "Enu"); + tensor4 stiffness(L, Tensor4Type::stiffness); + + // Uniaxial strain eps_11 = 0.01 + vec::fixed<6> eps_v = {0.01, 0.0, 0.0, 0.0, 0.0, 0.0}; + tensor2 eps = tensor2::from_voigt(eps_v, VoigtType::strain); + + tensor2 sigma = stiffness.contract(eps); + EXPECT_EQ(sigma.vtype(), VoigtType::stress); + + // Cross-validate with raw matrix-vector product + vec sig_ref = L * eps_v; + EXPECT_LT(norm(vec(sigma.voigt()) - sig_ref, 2), 1e-10); +} + +TEST(Ttensor4, ContractComplianceGivesStrain) +{ + double E = 70000.0; + double nu = 0.3; + mat::fixed<6,6> M = M_iso(E, nu, "Enu"); + tensor4 compliance(M, Tensor4Type::compliance); + + vec::fixed<6> sig_v = {100, 0, 0, 0, 0, 0}; + tensor2 sig = tensor2::from_voigt(sig_v, VoigtType::stress); + + tensor2 eps = compliance.contract(sig); + EXPECT_EQ(eps.vtype(), VoigtType::strain); + + vec eps_ref = M * sig_v; + EXPECT_LT(norm(vec(eps.voigt()) - eps_ref, 2), 1e-10); +} + +TEST(Ttensor4, StiffnessComplianceInverse) +{ + double E = 70000.0; + double nu = 0.3; + mat::fixed<6,6> L = L_iso(E, nu, "Enu"); + mat::fixed<6,6> M = M_iso(E, nu, "Enu"); + + tensor4 stiff(L, Tensor4Type::stiffness); + tensor4 comp(M, Tensor4Type::compliance); + + // L * M should be the 6x6 identity + mat LM = L * M; + EXPECT_LT(norm(LM - eye(6,6), "fro"), 1e-9); +} + +// ============================================================================ +// tensor4 Fastor cache +// ============================================================================ + +TEST(Ttensor4, FastorCacheLazyCompute) +{ + mat::fixed<6,6> L = L_iso(70000., 0.3, "Enu"); + tensor4 t(L, Tensor4Type::stiffness); + + // First access computes the Fastor tensor + const auto& f = t.fastor(); + // Check a diagonal element: C_0000 = L(0,0) + EXPECT_NEAR(f(0,0,0,0), L(0,0), 1e-12); + // Check a shear element: C_0101 = L(3,3) for stiffness convention + EXPECT_NEAR(f(0,1,0,1), L(3,3), 1e-12); +} + +// ============================================================================ +// tensor4 arithmetic +// ============================================================================ + +TEST(Ttensor4, IsotropicFromProjectors) +{ + // L_iso = 3K * Ivol + 2mu * Idev + double E = 70000.0; + double nu = 0.3; + double K = E / (3.0 * (1.0 - 2.0 * nu)); + double mu = E / (2.0 * (1.0 + nu)); + + tensor4 L_from_proj = 3.0 * K * tensor4::volumetric() + 2.0 * mu * tensor4::deviatoric(); + + mat::fixed<6,6> L_ref = L_iso(E, nu, "Enu"); + EXPECT_LT(norm(mat(L_from_proj.mat()) - mat(L_ref), "fro"), 1e-8); +} + +TEST(Ttensor4, ScalarMultiply) +{ + mat::fixed<6,6> L = L_iso(70000., 0.3, "Enu"); + tensor4 t(L, Tensor4Type::stiffness); + tensor4 t2 = t * 2.0; + tensor4 t3 = 2.0 * t; + mat::fixed<6,6> expected; + expected = L * 2.0; + EXPECT_TRUE(arma::approx_equal(t2.mat(), expected, "absdiff", 1e-12)); + EXPECT_TRUE(arma::approx_equal(t3.mat(), expected, "absdiff", 1e-12)); +} + +// ============================================================================ +// tensor4 rotation +// ============================================================================ + +TEST(Ttensor4, RotationStiffnessRoundtrip) +{ + mat::fixed<6,6> L = L_iso(70000., 0.3, "Enu"); + tensor4 t(L, Tensor4Type::stiffness); + + // For isotropic material, rotation should give the same stiffness + Rotation R = Rotation::from_euler(0.3, 0.5, 0.7, "zxz"); + tensor4 t_rot = t.rotate(R, true); + + EXPECT_LT(norm(mat(t.mat()) - mat(t_rot.mat()), "fro"), 1e-8); +} + +TEST(Ttensor4, RotationComplianceRoundtrip) +{ + mat::fixed<6,6> M = M_iso(70000., 0.3, "Enu"); + tensor4 t(M, Tensor4Type::compliance); + + // Isotropic compliance should be invariant under rotation + Rotation R = Rotation::from_euler(0.4, 0.6, 0.8, "zxz"); + tensor4 t_rot = t.rotate(R, true); + + EXPECT_LT(norm(mat(t.mat()) - mat(t_rot.mat()), "fro"), 1e-8); +} + +// ============================================================================ +// tensor4 push-forward / pull-back +// ============================================================================ + +TEST(Ttensor4, PushPullBackRoundtrip) +{ + mat::fixed<6,6> L = L_iso(70000., 0.3, "Enu"); + tensor4 t(L, Tensor4Type::stiffness); + + mat::fixed<3,3> F = {{1.1, 0.1, 0.05}, {0.02, 0.95, 0.03}, {0.01, 0.04, 1.05}}; + + tensor4 t_push = t.push_forward(F); + tensor4 t_back = t_push.pull_back(F); + + EXPECT_LT(norm(mat(t.mat()) - mat(t_back.mat()), "fro"), 1e-8); +} + +// ============================================================================ +// tensor4 dyadic product +// ============================================================================ + +TEST(Ttensor4, Dyadic) +{ + mat::fixed<3,3> a = {{1, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + mat::fixed<3,3> b = {{0, 0, 0}, {0, 1, 0}, {0, 0, 0}}; + tensor2 ta(a, VoigtType::stress); + tensor2 tb(b, VoigtType::stress); + + tensor4 d = dyadic(ta, tb); + // a has Voigt [1,0,0,0,0,0], b has Voigt [0,1,0,0,0,0] + // dyadic -> 6x6 matrix with d(0,1) = 1, rest zero + EXPECT_NEAR(d.mat()(0,1), 1.0, 1e-14); + EXPECT_NEAR(d.mat()(0,0), 0.0, 1e-14); + EXPECT_NEAR(d.mat()(1,1), 0.0, 1e-14); +} + +TEST(Ttensor4, AutoDyadic) +{ + mat::fixed<3,3> hydro = fill::eye; + tensor2 t(hydro, VoigtType::stress); + + tensor4 ad = auto_dyadic(t); + // Identity tensor has Voigt [1,1,1,0,0,0] + // auto_dyadic -> outer product with itself + EXPECT_NEAR(ad.mat()(0,0), 1.0, 1e-14); + EXPECT_NEAR(ad.mat()(0,1), 1.0, 1e-14); + EXPECT_NEAR(ad.mat()(1,0), 1.0, 1e-14); + EXPECT_NEAR(ad.mat()(3,3), 0.0, 1e-14); +} + +// ============================================================================ +// tensor4 equality +// ============================================================================ + +TEST(Ttensor4, Equality) +{ + mat::fixed<6,6> L = L_iso(70000., 0.3, "Enu"); + tensor4 a(L, Tensor4Type::stiffness); + tensor4 b(L, Tensor4Type::stiffness); + tensor4 c(L, Tensor4Type::compliance); + EXPECT_TRUE(a == b); + EXPECT_FALSE(a == c); +} + +// ============================================================================ +// Integration: L : epsilon = sigma workflow +// ============================================================================ + +TEST(Ttensor_integration, StiffnessContractStrain) +{ + double E = 70000.0; + double nu = 0.3; + + // Build stiffness tensor + tensor4 L(L_iso(E, nu, "Enu"), Tensor4Type::stiffness); + + // A general strain state + vec::fixed<6> eps_v = {0.01, -0.003, -0.003, 0.005, 0.002, 0.001}; + tensor2 eps = tensor2::from_voigt(eps_v, VoigtType::strain); + + // Contract: sigma = L : epsilon + tensor2 sigma = L.contract(eps); + + // Verify against raw arma computation + vec sig_ref = L_iso(E, nu, "Enu") * eps_v; + EXPECT_LT(norm(vec(sigma.voigt()) - sig_ref, 2), 1e-10); + EXPECT_EQ(sigma.vtype(), VoigtType::stress); +} + +TEST(Ttensor_integration, ComplianceContractStress) +{ + double E = 70000.0; + double nu = 0.3; + + // Build compliance tensor + tensor4 M(M_iso(E, nu, "Enu"), Tensor4Type::compliance); + + // A general stress state + vec::fixed<6> sig_v = {100, 50, 75, 30, 20, 10}; + tensor2 sig = tensor2::from_voigt(sig_v, VoigtType::stress); + + // Contract: epsilon = M : sigma + tensor2 eps = M.contract(sig); + + // Verify against raw arma computation + vec eps_ref = M_iso(E, nu, "Enu") * sig_v; + EXPECT_LT(norm(vec(eps.voigt()) - eps_ref, 2), 1e-10); + EXPECT_EQ(eps.vtype(), VoigtType::strain); +} + +TEST(Ttensor_integration, FullCycle_Stress_Rotate_Contract) +{ + double E = 70000.0; + double nu = 0.3; + + // Build stiffness and strain + tensor4 L(L_iso(E, nu, "Enu"), Tensor4Type::stiffness); + vec::fixed<6> eps_v = {0.01, -0.003, -0.003, 0.005, 0.002, 0.001}; + tensor2 eps = tensor2::from_voigt(eps_v, VoigtType::strain); + + // Compute stress in material frame + tensor2 sigma = L.contract(eps); + + // Rotate stiffness and strain to global frame + Rotation R = Rotation::from_euler(0.3, 0.5, 0.7, "zxz"); + tensor4 L_rot = L.rotate(R, true); + tensor2 eps_rot = eps.rotate(R, true); + + // Compute stress in global frame + tensor2 sigma_rot_direct = L_rot.contract(eps_rot); + + // Rotate material-frame stress to global frame + tensor2 sigma_rot_indirect = sigma.rotate(R, true); + + // Both should give the same result + EXPECT_LT(norm(mat(sigma_rot_direct.mat()) - mat(sigma_rot_indirect.mat()), "fro"), 1e-8); +}