diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index fc699f4f..0c35531c 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -314,10 +314,31 @@ All mechanics methods that accept NumPy arrays validate the input shape and rais Expected input shapes: -- ``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) +.. list-table:: + :widths: 35 30 30 + :header-rows: 1 + + * - Method + - Single rotation + - Batch (N rotations) + * - ``apply`` + - ``(3,)`` + - ``(N, 3)`` + * - ``apply_tensor`` + - ``(3, 3)`` + - ``(3, 3, N)`` + * - ``apply_stress``, ``apply_strain`` + - ``(6,)`` + - ``(6, N)`` + * - ``apply_stiffness``, ``apply_compliance`` + - ``(6, 6)`` + - ``(6, 6, N)`` + * - ``apply_strain_concentration``, ``apply_stress_concentration`` + - ``(6, 6)`` + - ``(6, 6, N)`` + * - ``as_voigt_stress_rotation``, ``as_voigt_strain_rotation`` + - returns ``(6, 6)`` + - returns ``(N, 6, 6)`` .. note:: @@ -396,7 +417,41 @@ Example 2: Stress Transformation in a Rotated Element print("Global stress:", sigma_global) print("Local stress:", sigma_local) -Example 3: Averaging Orientations +Example 3: Batch Rotation of Gauss-Point Quantities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When working with finite element data, you often need to rotate stress or strain +at many Gauss points simultaneously. The ``Rotation`` class supports batch operations +natively: + +.. code-block:: python + + import simcoon as smc + import numpy as np + + N = 100 # number of Gauss points + + # DR contains incremental rotation matrices at each Gauss point: shape (3, 3, N) + DR = np.random.randn(3, 3, N) + # (in practice DR comes from polar decomposition of F) + + # Build batch rotations from (N, 3, 3) matrices + rotations = smc.Rotation.from_matrix(DR.transpose(2, 0, 1)) + + # Rotate all Gauss-point stresses at once: shape (6, N) + stress = np.random.randn(6, N) + stress_rotated = rotations.apply_stress(stress) # (6, N) + + # Rotate stiffness tensors: shape (6, 6, N) + L = np.zeros((6, 6, N)) + for i in range(N): + L[:, :, i] = smc.L_iso([210e3, 0.3], "Enu") + L_rotated = rotations.apply_stiffness(L) # (6, 6, N) + + # Get all 6x6 Voigt rotation matrices at once + QS = rotations.as_voigt_stress_rotation() # (N, 6, 6) + +Example 4: Averaging Orientations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python diff --git a/docs/cpp_api/simulation/rotation.rst b/docs/cpp_api/simulation/rotation.rst index 324f7b10..4c7887b0 100644 --- a/docs/cpp_api/simulation/rotation.rst +++ b/docs/cpp_api/simulation/rotation.rst @@ -209,6 +209,31 @@ Apply Methods The ``active`` parameter controls whether the rotation is **active** (alibi, rotating the object) or **passive** (alias, rotating the coordinate system). +.. code-block:: cpp + + // Get the 6x6 Voigt rotation matrices directly + Rotation r = Rotation::from_axis_angle(M_PI/4, 3); + + arma::mat QS = r.as_voigt_stress_rotation(); // 6x6 stress rotation + arma::mat QE = r.as_voigt_strain_rotation(); // 6x6 strain rotation + + // Manual rotation via matrices (equivalent to apply_stiffness): + arma::mat L_rot = QS * L * QS.t(); + + // Passive rotation (alias convention): + arma::mat QS_passive = r.as_voigt_stress_rotation(false); + +.. code-block:: python + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + + QS = r.as_voigt_stress_rotation() # (6, 6) stress rotation matrix + QE = r.as_voigt_strain_rotation() # (6, 6) strain rotation matrix + + # Batch: N rotations produce (N, 6, 6) arrays + rots = smc.Rotation.random(100) + QS_batch = rots.as_voigt_stress_rotation() # (100, 6, 6) + Operations ---------- 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 00000000..a9cdc0df --- /dev/null +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp @@ -0,0 +1,278 @@ +#pragma once + +namespace simcoon_docs { + +constexpr auto CppRotation_class = R"pbdoc( + Internal C++ rotation backend using unit quaternions (scalar-last). + + End users should use ``simcoon.Rotation`` instead, which inherits from + ``scipy.spatial.transform.Rotation`` and delegates mechanics operations + to this class. +)pbdoc"; + +constexpr auto CppRotation_from_quat = R"pbdoc( + Create a rotation from a quaternion in scalar-last convention. + + Parameters + ---------- + quat : numpy.ndarray + A 1D array of 4 elements [qx, qy, qz, qw] (scalar-last). + + Returns + ------- + _CppRotation + A rotation object. + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as smc + + q = np.array([0, 0, np.sin(np.pi/4), np.cos(np.pi/4)]) # 90deg around z + r = smc.Rotation.from_quat(q) +)pbdoc"; + +constexpr auto as_voigt_stress_rotation = R"pbdoc( + Get the 6x6 rotation matrix for stress tensors in Voigt notation. + + The stress rotation matrix :math:`Q_\sigma` satisfies + :math:`\boldsymbol{\sigma}' = Q_\sigma \boldsymbol{\sigma}`. + + Parameters + ---------- + active : bool, optional + If True (default), returns the active (alibi) rotation matrix. + If False, returns the passive (alias) rotation matrix. + + Returns + ------- + numpy.ndarray + A (6, 6) rotation matrix for stress tensors. + For batch rotations, returns (N, 6, 6). + + Examples + -------- + .. code-block:: python + + import simcoon as smc + import numpy as np + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + QS = r.as_voigt_stress_rotation() # (6, 6) + + # Batch: N rotations + rots = smc.Rotation.random(100) + QS_batch = rots.as_voigt_stress_rotation() # (100, 6, 6) +)pbdoc"; + +constexpr auto as_voigt_strain_rotation = R"pbdoc( + Get the 6x6 rotation matrix for strain tensors in Voigt notation. + + The strain rotation matrix :math:`Q_\varepsilon` satisfies + :math:`\boldsymbol{\varepsilon}' = Q_\varepsilon \boldsymbol{\varepsilon}`. + + Parameters + ---------- + active : bool, optional + If True (default), returns the active (alibi) rotation matrix. + If False, returns the passive (alias) rotation matrix. + + Returns + ------- + numpy.ndarray + A (6, 6) rotation matrix for strain tensors. + For batch rotations, returns (N, 6, 6). + + Examples + -------- + .. code-block:: python + + import simcoon as smc + import numpy as np + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + QE = r.as_voigt_strain_rotation() # (6, 6) +)pbdoc"; + +constexpr auto apply_tensor = R"pbdoc( + Apply the rotation to a 3x3 tensor: :math:`R \cdot T \cdot R^T`. + + Parameters + ---------- + m : numpy.ndarray + A (3, 3) tensor. For batch rotations, shape (3, 3, N). + inverse : bool, optional + If True, applies the inverse rotation. Default is False. + + Returns + ------- + numpy.ndarray + The rotated tensor, same shape as input. + + Examples + -------- + .. code-block:: python + + import simcoon as smc + import numpy as np + + r = smc.Rotation.from_axis_angle(np.pi/2, 3) + T = np.diag([1.0, 2.0, 3.0]) + T_rot = r.apply_tensor(T) +)pbdoc"; + +constexpr auto apply_stress = R"pbdoc( + Apply the rotation to a stress vector in Voigt notation. + + Voigt convention: :math:`[\sigma_{11}, \sigma_{22}, \sigma_{33}, \sigma_{12}, \sigma_{13}, \sigma_{23}]`. + + Parameters + ---------- + sigma : numpy.ndarray + A 1D array of 6 elements (single rotation) or shape (6, N) for batch. + active : bool, optional + If True (default), active rotation. If False, passive rotation. + + Returns + ------- + numpy.ndarray + The rotated stress vector, same shape as input. + + Examples + -------- + .. code-block:: python + + import simcoon as smc + import numpy as np + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + sigma = np.array([100.0, 50.0, 25.0, 10.0, 5.0, 2.0]) + sigma_rot = r.apply_stress(sigma) +)pbdoc"; + +constexpr auto apply_strain = R"pbdoc( + Apply the rotation to a strain vector in Voigt notation. + + Voigt convention: :math:`[\varepsilon_{11}, \varepsilon_{22}, \varepsilon_{33}, 2\varepsilon_{12}, 2\varepsilon_{13}, 2\varepsilon_{23}]`. + + Parameters + ---------- + epsilon : numpy.ndarray + A 1D array of 6 elements (single rotation) or shape (6, N) for batch. + active : bool, optional + If True (default), active rotation. If False, passive rotation. + + Returns + ------- + numpy.ndarray + The rotated strain vector, same shape as input. + + Examples + -------- + .. code-block:: python + + import simcoon as smc + import numpy as np + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + epsilon = np.array([0.01, -0.005, -0.005, 0.002, 0.001, 0.0]) + epsilon_rot = r.apply_strain(epsilon) +)pbdoc"; + +constexpr auto apply_stiffness = R"pbdoc( + Apply the rotation to a 6x6 stiffness matrix. + + Computes :math:`L' = Q_\sigma \cdot L \cdot Q_\sigma^T`. + + Parameters + ---------- + L : numpy.ndarray + A (6, 6) stiffness matrix (single) or (6, 6, N) for batch. + active : bool, optional + If True (default), active rotation. If False, passive rotation. + + Returns + ------- + numpy.ndarray + The rotated stiffness matrix, same shape as input. + + Examples + -------- + .. code-block:: python + + import simcoon as smc + import numpy as np + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + L = smc.L_iso(np.array([210000, 0.3]), "Enu") + L_rot = r.apply_stiffness(L) +)pbdoc"; + +constexpr auto apply_compliance = R"pbdoc( + Apply the rotation to a 6x6 compliance matrix. + + Computes :math:`M' = Q_\varepsilon \cdot M \cdot Q_\varepsilon^T`. + + Parameters + ---------- + M : numpy.ndarray + A (6, 6) compliance matrix (single) or (6, 6, N) for batch. + active : bool, optional + If True (default), active rotation. If False, passive rotation. + + Returns + ------- + numpy.ndarray + The rotated compliance matrix, same shape as input. + + Examples + -------- + .. code-block:: python + + import simcoon as smc + import numpy as np + + r = smc.Rotation.from_axis_angle(np.pi/4, 3) + M = smc.M_iso(np.array([210000, 0.3]), "Enu") + M_rot = r.apply_compliance(M) +)pbdoc"; + +constexpr auto apply_strain_concentration = R"pbdoc( + Apply the rotation to a 6x6 strain concentration tensor. + + Computes :math:`A' = Q_\varepsilon \cdot A \cdot Q_\sigma^T`. + + Parameters + ---------- + A : numpy.ndarray + A (6, 6) strain concentration tensor (single) or (6, 6, N) for batch. + active : bool, optional + If True (default), active rotation. If False, passive rotation. + + Returns + ------- + numpy.ndarray + The rotated strain concentration tensor, same shape as input. +)pbdoc"; + +constexpr auto apply_stress_concentration = R"pbdoc( + Apply the rotation to a 6x6 stress concentration tensor. + + Computes :math:`B' = Q_\sigma \cdot B \cdot Q_\varepsilon^T`. + + Parameters + ---------- + B : numpy.ndarray + A (6, 6) stress concentration tensor (single) or (6, 6, N) for batch. + active : bool, optional + If True (default), active rotation. If False, passive rotation. + + Returns + ------- + numpy.ndarray + The rotated stress concentration tensor, same shape as input. +)pbdoc"; + +} // namespace simcoon_docs 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 f3b0f3ea..33d3bdae 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp @@ -8,6 +8,7 @@ #include #include +#include using namespace std; using namespace arma; @@ -40,13 +41,7 @@ namespace { void register_rotation(py::module_& m) { py::class_(m, "_CppRotation", - R"doc( - Internal C++ rotation backend using unit quaternions (scalar-last). - - End users should use ``simcoon.Rotation`` instead, which inherits from - ``scipy.spatial.transform.Rotation`` and delegates mechanics operations - to this class. - )doc") + simcoon_docs::CppRotation_class) // The only factory method needed — Python Rotation._to_cpp() uses this .def_static("from_quat", @@ -56,7 +51,7 @@ void register_rotation(py::module_& m) { return simcoon::Rotation::from_quat(q); }, py::arg("quat"), - "Create rotation from quaternion [qx, qy, qz, qw] (scalar-last)") + simcoon_docs::CppRotation_from_quat) // Voigt rotation matrices .def("as_voigt_stress_rotation", @@ -64,14 +59,14 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(mat(self.as_voigt_stress_rotation(active))); }, py::arg("active") = true, - "Get 6x6 rotation matrix for stress tensors in Voigt notation") + simcoon_docs::as_voigt_stress_rotation) .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, - "Get 6x6 rotation matrix for strain tensors in Voigt notation") + simcoon_docs::as_voigt_strain_rotation) // Apply methods — the core mechanics operations .def("apply_tensor", @@ -82,7 +77,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("m"), py::arg("inverse") = false, - "Apply rotation to a 3x3 tensor: R * m * R^T") + simcoon_docs::apply_tensor) .def("apply_stress", [](const simcoon::Rotation& self, py::array_t sigma, bool active) { @@ -92,7 +87,7 @@ void register_rotation(py::module_& m) { return carma::col_to_arr(result); }, py::arg("sigma"), py::arg("active") = true, - "Apply rotation to a 6-component stress vector in Voigt notation") + simcoon_docs::apply_stress) .def("apply_strain", [](const simcoon::Rotation& self, py::array_t epsilon, bool active) { @@ -102,7 +97,7 @@ void register_rotation(py::module_& m) { return carma::col_to_arr(result); }, py::arg("epsilon"), py::arg("active") = true, - "Apply rotation to a 6-component strain vector in Voigt notation") + simcoon_docs::apply_strain) .def("apply_stiffness", [](const simcoon::Rotation& self, py::array_t L, bool active) { @@ -112,7 +107,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("L"), py::arg("active") = true, - "Apply rotation to a 6x6 stiffness matrix") + simcoon_docs::apply_stiffness) .def("apply_compliance", [](const simcoon::Rotation& self, py::array_t M, bool active) { @@ -122,7 +117,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("M"), py::arg("active") = true, - "Apply rotation to a 6x6 compliance matrix") + simcoon_docs::apply_compliance) .def("apply_strain_concentration", [](const simcoon::Rotation& self, py::array_t A, bool active) { @@ -132,7 +127,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("A"), py::arg("active") = true, - "Apply rotation to a 6x6 strain concentration tensor") + simcoon_docs::apply_strain_concentration) .def("apply_stress_concentration", [](const simcoon::Rotation& self, py::array_t B, bool active) { @@ -142,7 +137,7 @@ void register_rotation(py::module_& m) { return carma::mat_to_arr(result); }, py::arg("B"), py::arg("active") = true, - "Apply rotation to a 6x6 stress concentration tensor") + simcoon_docs::apply_stress_concentration) ; }