From 684eab7b8a2fd6cf3d6dc55a6904aeff678a7d3a Mon Sep 17 00:00:00 2001
From: Dmitri Naumov <github@naumov.de>
Date: Thu, 28 Dec 2023 14:06:58 +0100
Subject: [PATCH] [ML] Vectorized tensor utilities

The deformation gradient and alike quantities are second order tensors
stored as a vector. Convenience operations on these tensors are
collected in the namespace.

The conversion to a Eigen matrix and back are used so far in tests only
for comparisons with Eigen operations.
---
 MathLib/VectorizedTensor.cpp       |  28 ++++
 MathLib/VectorizedTensor.h         | 229 +++++++++++++++++++++++++++++
 Tests/MathLib/VectorizedTensor.cpp | 225 ++++++++++++++++++++++++++++
 3 files changed, 482 insertions(+)
 create mode 100644 MathLib/VectorizedTensor.cpp
 create mode 100644 MathLib/VectorizedTensor.h
 create mode 100644 Tests/MathLib/VectorizedTensor.cpp

diff --git a/MathLib/VectorizedTensor.cpp b/MathLib/VectorizedTensor.cpp
new file mode 100644
index 00000000000..c760468bee7
--- /dev/null
+++ b/MathLib/VectorizedTensor.cpp
@@ -0,0 +1,28 @@
+/**
+ * \file
+ * \copyright
+ * Copyright (c) 2012-2024, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ */
+
+#include "VectorizedTensor.h"
+
+namespace MathLib ::VectorizedTensor
+{
+
+bool isTensorConvertibleTo1d(Eigen::Matrix3d const& tensor)
+{
+    return (tensor(0, 1) == 0. && tensor(1, 0) == 0. &&  //
+            tensor(0, 2) == 0. && tensor(2, 0) == 0. &&  //
+            tensor(1, 2) == 0. && tensor(2, 1) == 0.);
+}
+
+bool isTensorConvertibleTo2d(Eigen::Matrix3d const& tensor)
+{
+    return (tensor(0, 2) == 0. && tensor(1, 2) == 0. &&  //
+            tensor(2, 0) == 0. && tensor(2, 1) == 0.);
+}
+
+}  // namespace MathLib::VectorizedTensor
diff --git a/MathLib/VectorizedTensor.h b/MathLib/VectorizedTensor.h
new file mode 100644
index 00000000000..3a70bee3b0d
--- /dev/null
+++ b/MathLib/VectorizedTensor.h
@@ -0,0 +1,229 @@
+/**
+ * \file
+ * \copyright
+ * Copyright (c) 2012-2024, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ */
+
+#pragma once
+
+#include <Eigen/Core>
+#include <Eigen/LU>
+#include <type_traits>
+
+#include "BaseLib/Error.h"
+
+namespace MathLib
+{
+/// Namespace for vectorized second-order tensor types and operations on those.
+/// The tensors are for example, deformation gradients.
+namespace VectorizedTensor
+{
+/// Vectorized tensor size for given displacement dimension.
+constexpr int size(int const displacement_dim)
+{
+    if (displacement_dim == 1)
+    {
+        return 3;
+    }
+    if (displacement_dim == 2)
+    {
+        return 5;
+    }
+    if (displacement_dim == 3)
+    {
+        return 9;
+    }
+    OGS_FATAL(
+        "Cannot convert displacement dimension {} to vectorized tensor size.",
+        displacement_dim);
+}
+
+/// Displacement dimension of a vectorized tensor.
+template <typename VectorizedTensor>
+constexpr int dimension()
+{
+    static_assert(VectorizedTensor::ColsAtCompileTime == 1);
+    constexpr int rows = VectorizedTensor::RowsAtCompileTime;
+    if (rows == 3)
+    {
+        return 1;
+    }
+    if (rows == 5)
+    {
+        return 2;
+    }
+    if (rows == 9)
+    {
+        return 3;
+    }
+    OGS_FATAL(
+        "Cannot convert vectorized tensor of size {} to displacement "
+        "dimension.",
+        rows);
+}
+
+/// Vectorized tensor type for given displacement dimension.
+/// \note The Eigen vector is always a fixed size vector in contrast to a shape
+/// matrix policy types like GMatrixPolicyType::GradientVectorType.
+template <int DisplacementDim>
+using Type = Eigen::Matrix<double, size(DisplacementDim), 1, Eigen::ColMajor>;
+
+/// Vectorized identity tensor expression. Corresponds to 3x3 identity matrix in
+/// all dimensions.
+template <int DisplacementDim>
+constexpr auto identity()
+{
+    static_assert(
+        0 < DisplacementDim && DisplacementDim <= 3,
+        "Identity is implemented only for displacement dimension 1, 2, or 3.");
+    return Type<DisplacementDim>::NullaryExpr(
+        size(DisplacementDim), 1,
+        [](Eigen::Index const row, [[maybe_unused]] Eigen::Index const col)
+        {
+            assert(col == 0);
+            if constexpr (DisplacementDim == 1)
+            {  // All entries are diagonal entries.
+                return 1;
+            }
+            if constexpr (DisplacementDim == 2)
+            {
+                if (row == 0 || row == 3 || row == 4)
+                {
+                    return 1;
+                }
+            }
+            if constexpr (DisplacementDim == 3)
+            {
+                if (row == 0 || row == 4 || row == 8)
+                {
+                    return 1;
+                }
+            }
+            return 0;
+        });
+}
+
+/// Computes determinant of a vectorized tensor.
+template <typename Derived>
+double determinant(Eigen::MatrixBase<Derived> const& tensor)
+{
+    constexpr int displacement_dim = dimension<Derived>();
+    static_assert(0 < displacement_dim && displacement_dim <= 3,
+                  "Vectorized tensor determinant is implemented only for "
+                  "displacement dimension 1, 2, or 3.");
+
+    if constexpr (displacement_dim == 1)
+    {
+        return tensor[0] * tensor[1] * tensor[2];
+    }
+    if constexpr (displacement_dim == 2)
+    {
+        Eigen::Map<Eigen::Matrix2d const> const top_left{
+            tensor.derived().data()};
+
+        return top_left.determinant() * tensor[4];
+    }
+    if constexpr (displacement_dim == 3)
+    {
+        return Eigen::Map<Eigen::Matrix3d const>(tensor.derived().data())
+            .determinant();
+    }
+}
+
+/// Only a diagonal tensor can be converted to a 1d vectorized tensor.
+bool isTensorConvertibleTo1d(Eigen::Matrix3d const& tensor);
+
+/// Only a tensor of form
+/// \f$ \left( \begin{array}{ccc}
+/// a & b & 0 \\%
+/// c & d & 0 \\%
+/// 0 & 0 & e \\%
+/// \end{array} \right)\f$
+/// can be converted to a 2d vectorized tensor.
+bool isTensorConvertibleTo2d(Eigen::Matrix3d const& tensor);
+
+/// Converts a 3x3 matrix expression to a vectorized tensor if the conversion
+/// for that dimension is possible; see isTensorConvertibleTo1d() and
+/// isTensorConvertibleTo2d() functions.
+template <int DisplacementDim, typename Derived>
+Type<DisplacementDim> toVector(Eigen::MatrixBase<Derived> const& tensor)
+{
+    static_assert(0 < DisplacementDim && DisplacementDim <= 3,
+                  "Conversion to displacement dimension other than 1, 2, or 3 "
+                  "is not valid.");
+
+    constexpr int rows = Derived::RowsAtCompileTime;
+    constexpr int cols = Derived::ColsAtCompileTime;
+    static_assert(rows == 3 || rows == Eigen::Dynamic);
+    static_assert(cols == 3 || cols == Eigen::Dynamic);
+    if (tensor.rows() != 3 || tensor.cols() != 3)
+    {
+        OGS_FATAL(
+            "Incorrect tensor size, must be 3x3, but tensor is {:d}x{:d}.",
+            tensor.rows(), tensor.cols());
+    }
+
+    if constexpr (DisplacementDim == 1)
+    {
+        if (!isTensorConvertibleTo1d(tensor))
+        {
+            OGS_FATAL(
+                "Cannot convert a tensor with non-zero off-diagonal elements "
+                "to a 1d vectorized tensor representation.");
+        }
+        return tensor.diagonal();
+    }
+    if constexpr (DisplacementDim == 2)
+    {
+        if (!isTensorConvertibleTo2d(tensor))
+        {
+            OGS_FATAL(
+                "Cannot convert a tensor with non-zero elements at (0, 2), (1, "
+                "2), (2, 0), and (2, 1) positions to a 2d vectorized tensor "
+                "representation.");
+        }
+        Type<2> result;
+        result.template head<4>() =
+            tensor.template block<2, 2>(0, 0).reshaped();
+        result(4) = tensor(2, 2);
+        return result;
+    }
+    if constexpr (DisplacementDim == 3)
+    {
+        return tensor.reshaped();
+    }
+}
+
+/// Converts a vectorized tensor to a 3x3 matrix.
+template <int DisplacementDim>
+Eigen::Matrix3d toTensor(Type<DisplacementDim> const& tensor)
+{
+    static_assert(
+        DisplacementDim == 1 || DisplacementDim == 2 || DisplacementDim == 3,
+        "Conversion from displacement dimension other than 1, 2, or 3 "
+        "is not valid.");
+
+    using Matrix = Eigen::Matrix3d;
+    if constexpr (DisplacementDim == 1)
+    {
+        return tensor.asDiagonal();
+    }
+    if constexpr (DisplacementDim == 2)
+    {
+        Matrix m = Matrix::Zero();
+        m.template block<2, 2>(0, 0) =
+            Eigen::Map<Eigen::Matrix<double, 2, 2> const>(tensor.data());
+        m(2, 2) = tensor(4);
+        return m;
+    }
+    if constexpr (DisplacementDim == 3)
+    {
+        return Eigen::Map<Matrix const>(tensor.data());
+    }
+}
+
+}  // namespace VectorizedTensor
+}  // namespace MathLib
diff --git a/Tests/MathLib/VectorizedTensor.cpp b/Tests/MathLib/VectorizedTensor.cpp
new file mode 100644
index 00000000000..76822209a38
--- /dev/null
+++ b/Tests/MathLib/VectorizedTensor.cpp
@@ -0,0 +1,225 @@
+/**
+ * \file
+ * \copyright
+ * Copyright (c) 2012-2024, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ */
+
+#include "MathLib/VectorizedTensor.h"
+
+#include <gtest/gtest.h>
+
+#include <Eigen/LU>
+#include <range/v3/algorithm/for_each.hpp>
+#include <range/v3/view/cartesian_product.hpp>
+#include <range/v3/view/iota.hpp>
+
+#include "Tests/AutoCheckTools.h"
+
+using namespace MathLib;
+namespace ac = autocheck;
+
+TEST(VectorizedTensorTest, DimensionsAndSizes)
+{
+    namespace VT = MathLib::VectorizedTensor;
+
+    // Type implicitly uses size(), which is being tested.
+    static_assert(VT::dimension<VT::Type<1>>() == 1);
+    static_assert(VT::dimension<VT::Type<2>>() == 2);
+    static_assert(VT::dimension<VT::Type<3>>() == 3);
+}
+
+//
+// Self-tests of a tensor in matrix form to a vector form and back conversion.
+//
+template <int DisplacementDim>
+auto vectorizedTensorToMatrixMapping()
+{
+    static constexpr int size = VectorizedTensor::size(DisplacementDim);
+    ac::randomEigenMatrixGenerator<double, size, 1> generator;
+
+    auto to_tensor_and_back =
+        [](VectorizedTensor::Type<DisplacementDim> const& v)
+    {
+        return (v == VectorizedTensor::toVector<DisplacementDim>(
+                         VectorizedTensor::toTensor<DisplacementDim>(v)));
+    };
+
+    return ac::check<VectorizedTensor::Type<DisplacementDim>>(
+        to_tensor_and_back, 1000, ac::make_arbitrary(generator),
+        ac::gtest_reporter());
+}
+
+template <int DisplacementDim, typename Generator>
+auto matrixToVectorizedTensorMapping(Generator const& generator)
+{
+    auto to_vector_and_back = [](Eigen::Matrix3d const& t)
+    {
+        return (t == VectorizedTensor::toTensor<DisplacementDim>(
+                         VectorizedTensor::toVector<DisplacementDim>(t)));
+    };
+
+    return ac::check<Eigen::Matrix3d>(to_vector_and_back, 1000,
+                                      ac::make_arbitrary(generator),
+                                      ac::gtest_reporter());
+}
+
+struct random1dConvertibleEigenMatrixGenerator
+{
+    ac::generator<double> source;
+    using result_type = Eigen::Matrix<double, 3, 3>;
+
+    result_type operator()(std::size_t size = 0)
+    {
+        result_type rv = result_type::Zero();
+        rv.diagonal()(0) = fix(size, source)();
+        rv.diagonal()(1) = fix(size, source)();
+        rv.diagonal()(2) = fix(size, source)();
+        if (!VectorizedTensor::isTensorConvertibleTo1d(rv))
+        {
+            OGS_FATAL(
+                "VectorizedTensorTest; 1d test matrix generation failed.");
+        }
+        return rv;
+    }
+};
+
+struct random2dConvertibleEigenMatrixGenerator
+{
+    ac::generator<double> source;
+    using result_type = Eigen::Matrix<double, 3, 3>;
+
+    result_type operator()(std::size_t size = 0)
+    {
+        result_type rv = random1dConvertibleEigenMatrixGenerator{}(size);
+        rv(0, 1) = fix(size, source)();
+        rv(1, 0) = fix(size, source)();
+        if (!VectorizedTensor::isTensorConvertibleTo2d(rv))
+        {
+            OGS_FATAL(
+                "VectorizedTensorTest; 2d test matrix generation failed.");
+        }
+        return rv;
+    }
+};
+
+TEST(VectorizedTensorTest, SelfTestMapping1)
+{
+    vectorizedTensorToMatrixMapping<1>();
+    matrixToVectorizedTensorMapping<1>(
+        random1dConvertibleEigenMatrixGenerator{});
+}
+TEST(VectorizedTensorTest, SelfTestMapping2)
+{
+    vectorizedTensorToMatrixMapping<2>();
+    matrixToVectorizedTensorMapping<2>(
+        random2dConvertibleEigenMatrixGenerator{});
+}
+TEST(VectorizedTensorTest, SelfTestMapping3)
+{
+    vectorizedTensorToMatrixMapping<3>();
+    matrixToVectorizedTensorMapping<3>(
+        ac::randomEigenMatrixGenerator<double, 3, 3>{});
+}
+
+TEST(VectorizedTensorTest, DynamicSizeTensorToVector)
+{
+    // Testing only for dimension 1, because that code part is dimension
+    // independent.
+    EXPECT_NO_THROW(VectorizedTensor::toVector<1>(Eigen::MatrixXd::Zero(3, 3)));
+    EXPECT_THROW(VectorizedTensor::toVector<1>(Eigen::MatrixXd::Zero(3, 4)),
+                 std::runtime_error);
+    EXPECT_THROW(VectorizedTensor::toVector<1>(Eigen::MatrixXd::Zero(4, 3)),
+                 std::runtime_error);
+    EXPECT_THROW(VectorizedTensor::toVector<1>(Eigen::MatrixXd::Zero(3, 2)),
+                 std::runtime_error);
+    EXPECT_THROW(VectorizedTensor::toVector<1>(Eigen::MatrixXd::Zero(2, 3)),
+                 std::runtime_error);
+}
+
+TEST(VectorizedTensorTest, ConvertibleAndNonconvertibleTensors)
+{
+    auto one_ij = [](int i, int j)
+    {
+        Eigen::Matrix3d m = Eigen::Matrix3d::Zero();
+        m(i, j) = 1.;
+        return m;
+    };
+
+    // 1d
+    EXPECT_TRUE(
+        VectorizedTensor::isTensorConvertibleTo1d(Eigen::MatrixXd::Zero(3, 3)));
+    EXPECT_TRUE(VectorizedTensor::isTensorConvertibleTo1d(
+        Eigen::MatrixXd::Identity(3, 3)));
+    using namespace ranges::views;
+    ranges::for_each(
+        cartesian_product(iota(0, 3), iota(0, 3)),
+        [&](auto const ij)
+        {
+            auto const [i, j] = ij;
+            EXPECT_EQ(i == j,
+                      VectorizedTensor::isTensorConvertibleTo1d(one_ij(i, j)));
+        });
+
+    // 2d
+    EXPECT_TRUE(
+        VectorizedTensor::isTensorConvertibleTo2d(Eigen::MatrixXd::Zero(3, 3)));
+    EXPECT_TRUE(VectorizedTensor::isTensorConvertibleTo2d(
+        Eigen::MatrixXd::Identity(3, 3)));
+    {
+        EXPECT_FALSE(VectorizedTensor::isTensorConvertibleTo2d(one_ij(0, 2)));
+        EXPECT_FALSE(VectorizedTensor::isTensorConvertibleTo2d(one_ij(1, 2)));
+        EXPECT_FALSE(VectorizedTensor::isTensorConvertibleTo2d(one_ij(2, 0)));
+        EXPECT_FALSE(VectorizedTensor::isTensorConvertibleTo2d(one_ij(2, 1)));
+    }
+}
+//
+// Identities of a vectorized tensors correspond to Eigen matrix identities.
+//
+TEST(VectorizedTensorTest, Identity)
+{
+    EXPECT_EQ(VectorizedTensor::toTensor<1>(VectorizedTensor::identity<1>()),
+              Eigen::Matrix3d::Identity());
+    EXPECT_EQ(VectorizedTensor::toTensor<2>(VectorizedTensor::identity<2>()),
+              Eigen::Matrix3d::Identity());
+    EXPECT_EQ(VectorizedTensor::toTensor<3>(VectorizedTensor::identity<3>()),
+              Eigen::Matrix3d::Identity());
+}
+
+//
+// Determinants of a vectorized tensor and corresponding Eigen::Matrix are
+// equal.
+//
+template <int DisplacementDim>
+auto vectorizedTensorDeterminant()
+{
+    static constexpr int size = VectorizedTensor::size(DisplacementDim);
+    ac::randomEigenMatrixGenerator<double, size, 1> generator;
+
+    auto f = [](VectorizedTensor::Type<DisplacementDim> const& v)
+    {
+        return std::abs(VectorizedTensor::determinant(v) -
+                        VectorizedTensor::toTensor<DisplacementDim>(v)
+                            .determinant()) <=
+               std::numeric_limits<double>::epsilon() *
+                   std::pow(v.norm(), 3.07);
+    };
+
+    ac::check<VectorizedTensor::Type<DisplacementDim>>(
+        f, 1000, ac::make_arbitrary(generator), ac::gtest_reporter());
+}
+
+TEST(VectorizedTensorTest, Determinant1)
+{
+    vectorizedTensorDeterminant<1>();
+}
+TEST(VectorizedTensorTest, Determinant2)
+{
+    vectorizedTensorDeterminant<2>();
+}
+TEST(VectorizedTensorTest, Determinant3)
+{
+    vectorizedTensorDeterminant<3>();
+}
-- 
GitLab