LU and LU inverse is done
This commit is contained in:
+95
-106
@@ -4,123 +4,112 @@
|
||||
#include "./numerics/matmul.h"
|
||||
|
||||
|
||||
TEST_CASE(Inverse_2x2_WellConditioned) {
|
||||
using T = double;
|
||||
// A = [[4,7],[2,6]] inverse = (1/10) * [[6,-7],[-2,4]]
|
||||
utils::Matrix<T> A(2,2, T{0});
|
||||
A(0,0)=4; A(0,1)=7;
|
||||
A(1,0)=2; A(1,1)=6;
|
||||
|
||||
auto Ainv = numerics::inverse<T>(A); // out-of-place
|
||||
|
||||
// Check A * Ainv ≈ I and Ainv * A ≈ I
|
||||
auto Ileft = numerics::matmul(A, Ainv);
|
||||
auto Iright = numerics::matmul(Ainv, A);
|
||||
|
||||
utils::Md Iref(2,2, T{0});
|
||||
for (uint64_t i=0;i<Iref.rows();++i) Iref(i,i)=T{1};
|
||||
|
||||
//auto Iref = eye<T>(2);
|
||||
|
||||
CHECK((Ileft.nearly_equal(Iref, 1e-12)), "A * inverse(A) ≠ I");
|
||||
CHECK((Iright.nearly_equal(Iref, 1e-12)), "inverse(A) * A ≠ I");
|
||||
}
|
||||
|
||||
TEST_CASE(Inverse_InPlace_Equals_OutOfPlace) {
|
||||
TEST_CASE(Inverse_GJ_Basic_3x3) {
|
||||
using T = double;
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
// A = [[3, 0, 2],
|
||||
// [2, 0, -2],
|
||||
// [0, 1, 1]]
|
||||
A(0,0)=3; A(0,1)=0; A(0,2)= 2;
|
||||
A(1,0)=2; A(1,1)=0; A(1,2)=-2;
|
||||
A(2,0)=0; A(2,1)=1; A(2,2)= 1;
|
||||
// Well-conditioned 3x3
|
||||
A(0,0)=3; A(0,1)=0.2; A(0,2)=-0.1;
|
||||
A(1,0)=0.1; A(1,1)=2.5; A(1,2)=0.3;
|
||||
A(2,0)=-0.2;A(2,1)=0.4; A(2,2)=2.0;
|
||||
|
||||
auto Ainv_ref = numerics::inverse<T>(A); // copy path
|
||||
auto Ainv = numerics::inverse<T>(A, "Gauss-Jordan");
|
||||
utils::Matrix<T> I;
|
||||
I.eye(3);
|
||||
auto prod = numerics::matmul<T>(A, Ainv);
|
||||
|
||||
auto A_inp = A;
|
||||
numerics::inplace_inverse<T>(A_inp); // in-place path
|
||||
CHECK(prod.nearly_equal(I, (T)1e-10), "inverse(GJ): A*A^{-1} ≈ I");
|
||||
}
|
||||
|
||||
CHECK((A_inp.nearly_equal(Ainv_ref, 1e-12)), "in-place inverse differs from out-of-place");
|
||||
TEST_CASE(Inverse_LU_Basic_3x3) {
|
||||
using T = double;
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
A(0,0)=3; A(0,1)=0.2; A(0,2)=-0.1;
|
||||
A(1,0)=0.1; A(1,1)=2.5; A(1,2)=0.3;
|
||||
A(2,0)=-0.2;A(2,1)=0.4; A(2,2)=2.0;
|
||||
|
||||
auto Ainv = numerics::inverse<T>(A, "LU");
|
||||
utils::Matrix<T> I;
|
||||
I.eye(3);
|
||||
auto prod = numerics::matmul<T>(A, Ainv);
|
||||
|
||||
CHECK(prod.nearly_equal(I, (T)1e-10), "inverse(LU): A*A^{-1} ≈ I");
|
||||
}
|
||||
|
||||
TEST_CASE(Inverse_GJ_vs_LU_Consistency) {
|
||||
using T = double;
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
A(0,0)=4; A(0,1)=1; A(0,2)=2;
|
||||
A(1,0)=0; A(1,1)=3; A(1,2)=-1;
|
||||
A(2,0)=0; A(2,1)=0; A(2,2)=2;
|
||||
|
||||
auto GJ = numerics::inverse<T>(A, "Gauss-Jordan");
|
||||
auto LU = numerics::inverse<T>(A, "LU");
|
||||
|
||||
CHECK(GJ.nearly_equal(LU, (T)1e-12), "inverse: GJ and LU produce nearly the same result");
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE(Inplace_Inverse_LU) {
|
||||
using T = double;
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
A(0,0)=3; A(0,1)=0.2; A(0,2)=-0.1;
|
||||
A(1,0)=0.1; A(1,1)=2.5; A(1,2)=0.3;
|
||||
A(2,0)=-0.2;A(2,1)=0.4; A(2,2)=2.0;
|
||||
|
||||
auto Ainv_ref = numerics::inverse<T>(A, "LU"); // out-of-place
|
||||
auto A_copy = A;
|
||||
numerics::inplace_inverse<T>(A_copy, "LU"); // in-place
|
||||
|
||||
CHECK(A_copy.nearly_equal(Ainv_ref, (T)1e-12), "inplace_inverse(LU) equals out-of-place");
|
||||
}
|
||||
|
||||
TEST_CASE(Inplace_Inverse_GJ) {
|
||||
using T = double;
|
||||
utils::Matrix<T> A(2,2, T{0});
|
||||
A(0,0)=2; A(0,1)=1;
|
||||
A(1,0)=1; A(1,1)=3;
|
||||
|
||||
auto Ainv_ref = numerics::inverse<T>(A, "Gauss-Jordan");
|
||||
auto A_copy = A;
|
||||
numerics::inplace_inverse<T>(A_copy, "Gauss-Jordan");
|
||||
|
||||
CHECK(A_copy.nearly_equal(Ainv_ref, (T)1e-12), "inplace_inverse(GJ) equals out-of-place");
|
||||
}
|
||||
|
||||
TEST_CASE(Inverse_Identity) {
|
||||
using T = double;
|
||||
utils::Matrix<T> I;
|
||||
I.eye(3);
|
||||
auto invI = numerics::inverse<T>(I, "LU");
|
||||
CHECK(invI.nearly_equal(I, (T)0), "inverse(I) == I");
|
||||
}
|
||||
TEST_CASE(Inverse_NonSquare_Throws) {
|
||||
using T = double;
|
||||
utils::Matrix<T> A(2,3, T{0}); // non-square
|
||||
bool threw1=false, threw2=false;
|
||||
try { auto X = numerics::inverse<T>(A, "LU"); (void)X; } catch(...) { threw1=true; }
|
||||
try { numerics::inplace_inverse<T>(A, "Gauss-Jordan"); } catch(...) { threw2=true; }
|
||||
CHECK(threw1 && threw2, "inverse throws on non-square for both methods");
|
||||
}
|
||||
|
||||
TEST_CASE(Inverse_Singular_Throws) {
|
||||
using T = double;
|
||||
utils::Matrix<T> S(2,2, T{0});
|
||||
// Singular: rows are multiples → det = 0
|
||||
S(0,0)=1; S(0,1)=2;
|
||||
S(1,0)=2; S(1,1)=4;
|
||||
utils::Matrix<T> S(3,3, T{0});
|
||||
S(0,0)=1; S(0,1)=2; S(0,2)=3;
|
||||
S(1,0)=1; S(1,1)=2; S(1,2)=3; // duplicate row -> singular
|
||||
S(2,0)=0; S(2,1)=1; S(2,2)=0;
|
||||
|
||||
bool threw=false;
|
||||
try {
|
||||
auto _ = numerics::inverse<T>(S);
|
||||
(void)_;
|
||||
} catch (const std::runtime_error&) { threw = true; }
|
||||
CHECK(threw, "inverse should throw on singular matrix");
|
||||
|
||||
threw=false;
|
||||
try {
|
||||
numerics::inplace_inverse<T>(S);
|
||||
} catch (const std::runtime_error&) { threw = true; }
|
||||
CHECK(threw, "inplace_inverse should throw on singular matrix");
|
||||
bool threw_gj=false, threw_lu=false;
|
||||
try { auto X = numerics::inverse<T>(S, "Gauss-Jordan"); (void)X; } catch(...) { threw_gj=true; }
|
||||
try { auto X = numerics::inverse<T>(S, "LU"); (void)X; } catch(...) { threw_lu=true; }
|
||||
CHECK(threw_gj && threw_lu, "inverse throws on singular for both methods");
|
||||
}
|
||||
|
||||
TEST_CASE(Inverse_RoundTrip_DiagonallyDominant_5x5) {
|
||||
// Build a well-conditioned 5x5: diagonally dominant
|
||||
utils::Md A(5,5,0.0);
|
||||
for (uint64_t i=0;i<5;++i) {
|
||||
double rowsum = 0.0;
|
||||
for (uint64_t j=0;j<5;++j) {
|
||||
if (i==j) continue;
|
||||
A(i,j) = 0.01 * double(1 + ((i+1)*(j+3)) % 7);
|
||||
rowsum += std::fabs(A(i,j));
|
||||
}
|
||||
A(i,i) = rowsum + 1.0; // strictly diagonally dominant
|
||||
}
|
||||
|
||||
utils::Md A_copy = A; // ensure wrapper doesn't mutate input
|
||||
utils::Md Ainv = numerics::inverse<double>(A);
|
||||
|
||||
// Input must be unchanged by the non-inplace wrapper
|
||||
CHECK(A.nearly_equal(A_copy, 0.0), "inverse wrapper modified input");
|
||||
|
||||
|
||||
utils::Md I(5,5, 0);
|
||||
for (uint64_t i=0;i<I.rows();++i) I(i,i)=1;
|
||||
|
||||
|
||||
auto L = numerics::matmul<double>(A, Ainv);
|
||||
auto R = numerics::matmul<double>(Ainv, A);
|
||||
|
||||
CHECK(L.nearly_equal(I, 1e-10), "A * Ainv not close to I");
|
||||
CHECK(R.nearly_equal(I, 1e-10), "Ainv * A not close to I");
|
||||
}
|
||||
|
||||
TEST_CASE(Inverse_NonSquare_Throws) {
|
||||
// Non-square: 2x3 — algorithm expects square; should throw
|
||||
utils::Md A(2,3,0.0);
|
||||
bool threw = false;
|
||||
try {
|
||||
numerics::inplace_inverse<double>(A);
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
} catch (...) {
|
||||
threw = true; // any failure is fine; must not silently succeed
|
||||
}
|
||||
CHECK(threw, "inplace_inverse should throw on non-square matrix");
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE(Inverse_Unknown_Method_Throws) {
|
||||
|
||||
utils::Md A(3,3, 0);
|
||||
for (uint64_t i=0;i<A.rows();++i) A(i,i)=1;
|
||||
|
||||
bool threw = false;
|
||||
try {
|
||||
numerics::inplace_inverse<double>(A, "NotARealMethod");
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
CHECK(threw, "should throw for unknown inverse method");
|
||||
using T = double;
|
||||
utils::Matrix<T> A(2,2, T{0});
|
||||
A(0,0)=1; A(1,1)=1;
|
||||
bool threw=false;
|
||||
try { auto X = numerics::inverse<T>(A, "Foobar"); (void)X; } catch(...) { threw=true; }
|
||||
CHECK(threw, "inverse unknown method throws");
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
#include "test_common.h"
|
||||
|
||||
#include "./utils/utils.h" // brings in vector.h, matrix.h, etc.
|
||||
#include "./numerics/matmul.h" // numerics::matmul
|
||||
|
||||
#include "./decomp/lu.h"
|
||||
|
||||
//#include <chrono>
|
||||
|
||||
|
||||
TEST_CASE(LU_Solve_Vector_Basic) {
|
||||
using T = double;
|
||||
|
||||
// A * x = b with exact solution x = [1, 1, 2]^T
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
A(0,0)=2; A(0,1)=1; A(0,2)=1;
|
||||
A(1,0)=4; A(1,1)=-6; A(1,2)=0;
|
||||
A(2,0)=-2; A(2,1)=7; A(2,2)=2;
|
||||
|
||||
utils::Vector<T> b(3, T{0});
|
||||
b[0]=5; b[1]=-2; b[2]=9;
|
||||
|
||||
decomp::LUdcmpd lu(A);
|
||||
auto x = lu.solve(b);
|
||||
|
||||
utils::Vector<T> x_true(3, T{0});
|
||||
x_true[0]=1; x_true[1]=1; x_true[2]=2;
|
||||
|
||||
CHECK( (x.nearly_equal(x_true,1e-12)), "LU solve (vector RHS) failed" );
|
||||
}
|
||||
|
||||
TEST_CASE(LU_Solve_MatrixRHS_TwoColumns) {
|
||||
using T = double;
|
||||
|
||||
// Same A, solve two RHS at once
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
A(0,0)=2; A(0,1)=1; A(0,2)=1;
|
||||
A(1,0)=4; A(1,1)=-6; A(1,2)=0;
|
||||
A(2,0)=-2; A(2,1)=7; A(2,2)=2;
|
||||
|
||||
utils::Matrix<T> B(3,2, T{0});
|
||||
// First column b1 (same as previous test)
|
||||
B(0,0)=5; B(1,0)=-2; B(2,0)=9;
|
||||
// Second column b2 → choose solution x2 = [0, 2, 1]^T
|
||||
// Compute b2 = A * x2 by hand:
|
||||
// Row0: 2*0 + 1*2 + 1*1 = 3
|
||||
// Row1: 4*0 -6*2 + 0*1 = -12
|
||||
// Row2: -2*0 +7*2 + 2*1 = 16
|
||||
B(0,1)=3; B(1,1)=-12; B(2,1)=16;
|
||||
|
||||
decomp::LUdcmpd lu(A);
|
||||
auto X = lu.solve(B);
|
||||
|
||||
// Check A*X ≈ B
|
||||
auto AX = numerics::matmul(A, X);
|
||||
CHECK( AX.nearly_equal(B, 1e-12), "A * X does not match B for matrix RHS" );
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE(LU_Determinant_Known) {
|
||||
using T = double;
|
||||
|
||||
// Determinant of:
|
||||
// [[1,2,3],[0,1,4],[5,6,0]] is 1
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
A(0,0)=1; A(0,1)=2; A(0,2)=3;
|
||||
A(1,0)=0; A(1,1)=1; A(1,2)=4;
|
||||
A(2,0)=5; A(2,1)=6; A(2,2)=0;
|
||||
|
||||
decomp::LUdcmpd lu(A);
|
||||
T det = lu.det();
|
||||
CHECK( std::fabs(det - T{1}) < 1e-12, "det(A) should be 1" );
|
||||
}
|
||||
|
||||
TEST_CASE(LU_Pivoting_Handles_Zero_Leading) {
|
||||
using T = double;
|
||||
|
||||
// Requires pivoting (A(0,0)=0); system has solution x=[1,2]^T, b = A*x = [2,3]^T
|
||||
utils::Matrix<T> A(2,2, T{0});
|
||||
A(0,0)=0; A(0,1)=1;
|
||||
A(1,0)=1; A(1,1)=1;
|
||||
|
||||
utils::Vector<T> b(2, T{0});
|
||||
b[0]=2; b[1]=3;
|
||||
|
||||
decomp::LUdcmpd lu(A);
|
||||
auto x = lu.solve(b);
|
||||
|
||||
utils::Vector<T> x_true(2, T{0}); x_true[0]=1; x_true[1]=2;
|
||||
CHECK( (x.nearly_equal(x_true,1e-12)), "Pivoting failed on zero-leading matrix" );
|
||||
}
|
||||
|
||||
TEST_CASE(LU_Input_Unchanged_By_NonInplace_Path) {
|
||||
using T = double;
|
||||
|
||||
utils::Matrix<T> A(4,4, T{0});
|
||||
for (uint64_t i=0;i<4;++i) {
|
||||
for (uint64_t j=0;j<4;++j) {
|
||||
A(i,j) = (i==j) ? 3.0 : 0.1 * ((i+1)*(j+2) % 5 + 1);
|
||||
}
|
||||
}
|
||||
utils::Matrix<T> A_copy = A;
|
||||
|
||||
decomp::LUdcmpd lu(A); // constructor should not mutate input A
|
||||
CHECK( A.nearly_equal(A_copy, 0.0), "LU constructor modified input matrix" );
|
||||
|
||||
// Also check solve doesn't mutate RHS copy when using out-of-place convenience
|
||||
utils::Vector<T> b(4, 0.0);
|
||||
for (uint64_t i=0;i<4;++i) b[i] = double(i+1);
|
||||
auto b_copy = b;
|
||||
|
||||
auto x = lu.solve(b);
|
||||
(void)x;
|
||||
CHECK( (b.nearly_equal(b_copy, 1e-300)), "solve(b) modified its input vector" );
|
||||
}
|
||||
|
||||
TEST_CASE(LU_Inplace_Equals_OutOfPlace_Solve_Vector) {
|
||||
using T = double;
|
||||
|
||||
utils::Matrix<T> A(3,3, T{0});
|
||||
A(0,0)=4; A(0,1)=1; A(0,2)=2;
|
||||
A(1,0)=0; A(1,1)=3; A(1,2)=-1;
|
||||
A(2,0)=0; A(2,1)=0; A(2,2)=2;
|
||||
|
||||
utils::Vector<T> b(3, T{0}); b[0]=7; b[1]=5; b[2]=4;
|
||||
|
||||
decomp::LUdcmpd lu(A);
|
||||
|
||||
auto x1 = lu.solve(b);
|
||||
utils::Vector<T> x2(3, T{0});
|
||||
lu.inplace_solve(b, x2);
|
||||
|
||||
CHECK( (x1.nearly_equal(x2,1e-12)), "inplace_solve(b,x) differs from solve(b)" );
|
||||
}
|
||||
|
||||
TEST_CASE(LU_Singular_Throws) {
|
||||
using T = double;
|
||||
|
||||
// Singular (row2 = 2 * row1)
|
||||
utils::Matrix<T> S(2,2, T{0});
|
||||
S(0,0)=1; S(0,1)=2;
|
||||
S(1,0)=2; S(1,1)=4;
|
||||
|
||||
bool threw=false;
|
||||
try {
|
||||
decomp::LUdcmpd lu(S);
|
||||
(void)lu;
|
||||
} catch (const std::runtime_error&) { threw = true; }
|
||||
CHECK(threw, "LU should throw on singular matrix");
|
||||
}
|
||||
|
||||
TEST_CASE(LU_NonSquare_Throws) {
|
||||
using T = double;
|
||||
|
||||
utils::Matrix<T> A(3,2, T{0});
|
||||
bool threw = false;
|
||||
try {
|
||||
decomp::LUdcmpd lu(A);
|
||||
(void)lu;
|
||||
} catch (const std::runtime_error&) { threw = true; }
|
||||
CHECK(threw, "LU should throw on non-square input");
|
||||
}
|
||||
|
||||
TEST_CASE(LU_Inverse_RoundTrip) {
|
||||
using T = double;
|
||||
|
||||
// Build a strictly diagonally dominant 5x5
|
||||
utils::Matrix<T> A(5,5, T{0});
|
||||
for (uint64_t i=0;i<5;++i) {
|
||||
T rowsum = 0;
|
||||
for (uint64_t j=0;j<5;++j) {
|
||||
if (i==j) continue;
|
||||
A(i,j) = T(0.01 * double(1 + ((i+2)*(j+3)) % 7));
|
||||
rowsum += std::fabs(A(i,j));
|
||||
}
|
||||
A(i,i) = rowsum + T{1};
|
||||
}
|
||||
|
||||
decomp::LUdcmpd lu(A);
|
||||
auto Ainv = lu.inverse();
|
||||
|
||||
utils::Md I(5,5, 0.0);
|
||||
for (uint64_t i=0;i<I.rows();++i) I(i,i)=1.0;
|
||||
|
||||
auto L = numerics::matmul(A, Ainv);
|
||||
auto R = numerics::matmul(Ainv, A);
|
||||
|
||||
CHECK(L.nearly_equal(I, 1e-10), "A * inverse(A) not close to I");
|
||||
CHECK(R.nearly_equal(I, 1e-10), "inverse(A) * A not close to I");
|
||||
}
|
||||
Reference in New Issue
Block a user