From 2c567ddeebe7471a2718bcb872988d21b445e051 Mon Sep 17 00:00:00 2001 From: Brendan Barnes Date: Fri, 1 Nov 2024 03:34:49 +0000 Subject: [PATCH] save --- include/squint/tensor/tensor_math.hpp | 78 +++++++++++++++++++++++++ tests/tensor_math_tests.cpp | 83 +++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/include/squint/tensor/tensor_math.hpp b/include/squint/tensor/tensor_math.hpp index e624e57..e75b43c 100644 --- a/include/squint/tensor/tensor_math.hpp +++ b/include/squint/tensor/tensor_math.hpp @@ -134,6 +134,83 @@ template auto solve_general(T1 &A, T2 &B) { return info; } +#ifdef SQUINT_BLAS_BACKEND_NONE +/** + * @brief Computes the inverse of a square matrix. + * @param A The matrix to invert. + * @return The inverted matrix. + * @throws std::runtime_error if the matrix is singular or an error occurs during inversion. + */ +template auto inv(const T &A) { + inversion_compatible(A); + static_assert(dimensionless_scalar); + using blas_type = blas_type_t>; + using result_type = + tensor, typename T::shape_type, typename T::strides_type, + T::error_checking(), ownership_type::owner, memory_space::host>; + + // Create copy of input matrix that will become our result + result_type result = A; + const int n = A.shape()[0]; + + // Create identity matrix of same shape and type + auto identity = result_type::eye(); + // Fill with identity pattern + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + identity(i, j) = (i == j) ? typename T::value_type{1} : typename T::value_type{0}; + } + } + + // Perform Gauss-Jordan elimination with partial pivoting + for (int i = 0; i < n; i++) { + // Find pivot (largest element in current column) + auto max_element = std::abs(result(i, i)); + int max_row = i; + for (int k = i + 1; k < n; k++) { + if (std::abs(result(k, i)) > max_element) { + max_element = std::abs(result(k, i)); + max_row = k; + } + } + + // Check if matrix is singular + if (approx_equal(max_element, typename T::value_type{0})) { + throw std::runtime_error("Matrix is singular"); + } + + // Swap maximum row with current row if needed + if (max_row != i) { + for (int k = 0; k < n; k++) { + std::swap(result(i, k), result(max_row, k)); + std::swap(identity(i, k), identity(max_row, k)); + } + } + + // Scale current row to make the diagonal element 1 + auto pivot = result(i, i); + for (int k = 0; k < n; k++) { + result(i, k) /= pivot; + identity(i, k) /= pivot; + } + + // Eliminate current column elements in other rows + for (int j = 0; j < n; j++) { + if (j != i) { + auto factor = result(j, i); + for (int k = 0; k < n; k++) { + result(j, k) -= factor * result(i, k); + identity(j, k) -= factor * identity(i, k); + } + } + } + } + + // At this point, result should be the identity matrix and + // identity should contain the inverse + return identity; +} +#else /** * @brief Computes the inverse of a square matrix. * @param A The matrix to invert. @@ -201,6 +278,7 @@ template auto inv(const T &A) { return result; } +#endif /** * @brief Computes the Moore-Penrose pseudoinverse of a matrix. diff --git a/tests/tensor_math_tests.cpp b/tests/tensor_math_tests.cpp index cbb54fb..68632b8 100644 --- a/tests/tensor_math_tests.cpp +++ b/tests/tensor_math_tests.cpp @@ -248,6 +248,89 @@ TEST_CASE("inv()") { CHECK(A_T_inv(1, 0) == doctest::Approx(1.0)); CHECK(A_T_inv(1, 1) == doctest::Approx(-0.5)); } + + SUBCASE("1000 random 4x4 matrices") { + // Use a fixed seed for deterministic random number generation + std::mt19937 gen(42); + std::uniform_real_distribution dis(-10.0, 10.0); + + // Create test matrices + using mat4d = tensor>; + + for (int test = 0; test < 1000; ++test) { + // Generate a random matrix with deterministic values + mat4d A; + bool singular; + do { + singular = false; + // Fill matrix with random values + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + A(i, j) = dis(gen); + } + } + + // Check if matrix is singular by computing determinant + // For 4x4, we can use the direct formula instead of LU decomposition + double det = + A(0,0) * ( + A(1,1) * (A(2,2) * A(3,3) - A(2,3) * A(3,2)) - + A(1,2) * (A(2,1) * A(3,3) - A(2,3) * A(3,1)) + + A(1,3) * (A(2,1) * A(3,2) - A(2,2) * A(3,1)) + ) - + A(0,1) * ( + A(1,0) * (A(2,2) * A(3,3) - A(2,3) * A(3,2)) - + A(1,2) * (A(2,0) * A(3,3) - A(2,3) * A(3,0)) + + A(1,3) * (A(2,0) * A(3,2) - A(2,2) * A(3,0)) + ) + + A(0,2) * ( + A(1,0) * (A(2,1) * A(3,3) - A(2,3) * A(3,1)) - + A(1,1) * (A(2,0) * A(3,3) - A(2,3) * A(3,0)) + + A(1,3) * (A(2,0) * A(3,1) - A(2,1) * A(3,0)) + ) - + A(0,3) * ( + A(1,0) * (A(2,1) * A(3,2) - A(2,2) * A(3,1)) - + A(1,1) * (A(2,0) * A(3,2) - A(2,2) * A(3,0)) + + A(1,2) * (A(2,0) * A(3,1) - A(2,1) * A(3,0)) + ); + + singular = std::abs(det) < 1e-10; + } while (singular); + + // Compute inverse + auto A_inv = inv(A); + + // Multiply A * A_inv, should get identity matrix + auto result = A * A_inv; + auto I = mat4::eye(); + + // Check that result is identity matrix + const double tolerance = 1e-10; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + if (i == j) { + // Diagonal elements should be 1 + CHECK(result(i, j) == doctest::Approx(1.0).epsilon(tolerance)); + } else { + // Off-diagonal elements should be 0 + CHECK(result(i, j) == doctest::Approx(0.0).epsilon(tolerance)); + } + } + } + + // Also check A_inv * A = I + auto result2 = A_inv * A; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + if (i == j) { + CHECK(result2(i, j) == doctest::Approx(1.0).epsilon(tolerance)); + } else { + CHECK(result2(i, j) == doctest::Approx(0.0).epsilon(tolerance)); + } + } + } + } + } } TEST_CASE("pinv()") {