Sync public subset from Flux

This commit is contained in:
Gitea CI
2025-10-06 20:21:40 +00:00
parent b2d00af0e1
commit 8892d58e66
15 changed files with 1825 additions and 0 deletions

0
test/.gitkeep Normal file
View File

2
test/test_all.cpp Normal file
View File

@@ -0,0 +1,2 @@
#define TEST_MAIN
#include "test_common.h"

52
test/test_common.h Normal file
View File

@@ -0,0 +1,52 @@
#ifndef _test_common_n_
#define _test_common_n_
#pragma once
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
struct TestFailure : public std::runtime_error {
using std::runtime_error::runtime_error;
};
#define CHECK(cond, msg) do { if (!(cond)) throw TestFailure(msg); } while (0)
#define CHECK_EQ(a,b,msg) do { if (!((a)==(b))) { throw TestFailure(std::string(msg) + " (" #a " != " #b ")"); } } while (0)
#define TEST_CASE(name) \
static void name(); \
struct name##_registrar { name##_registrar(){ TestRegistry::add(#name, &name);} } name##_registrar_instance; \
static void name()
struct TestRegistry {
using Fn = void(*)();
static std::vector<std::pair<std::string, Fn>>& list() {
static std::vector<std::pair<std::string, Fn>> v; return v;
}
static void add(const std::string& name, Fn fn) { list().push_back({name, fn}); }
};
// Default test runner main()
#ifdef TEST_MAIN
int main() {
int fails = 0;
for (auto& t : TestRegistry::list()) {
try {
t.second();
//std::cout << "[PASS] " << t.first << "\n";
} catch (const TestFailure& e) {
std::cerr << "[FAIL] " << t.first << " -> " << e.what() << "\n";
++fails;
} catch (const std::exception& e) {
std::cerr << "[ERROR] " << t.first << " -> " << e.what() << "\n";
++fails;
}
}
std::cout << (fails ? "Some tests failed ❌\n" : "All tests passed ✅\n");
return fails ? 1 : 0;
}
#endif
#endif // _test_common_n_

155
test/test_eye.cpp Normal file
View File

@@ -0,0 +1,155 @@
#include "test_common.h"
#include "./utils/matrix.h"
#include "./numerics/initializers/eye.h"
#include <chrono>
// Tiny helper
template <typename T>
static bool is_identity(const utils::Matrix<T>& M) {
if (M.rows() != M.cols()) return false;
for (std::uint64_t i = 0; i < M.rows(); ++i)
for (std::uint64_t j = 0; j < M.cols(); ++j)
if ((i == j && M(i,j) != T{1}) ||
(i != j && M(i,j) != T{0})) return false;
return true;
}
// ============ Basic correctness ============
TEST_CASE(Generate_EYE_double) {
utils::Matrix<double> I = numerics::eye<double>(5);
CHECK_EQ(I.rows(), 5, "rows should be 5");
CHECK_EQ(I.cols(), 5, "cols should be 5");
CHECK(is_identity(I), "eye<double>(5) is not identity");
}
TEST_CASE(Generate_EYE_int) {
utils::Matrix<uint64_t> I = numerics::eye<uint64_t>(3);
CHECK_EQ(I.rows(), 3, "rows should be 3");
CHECK_EQ(I.cols(), 3, "cols should be 3");
CHECK(is_identity(I), "eye<int>(3) is not identity");
}
TEST_CASE(Inplace_EYE_resize_creates_identity) {
utils::Md A; // empty
numerics::inplace_eye(A, 7); // resize to 7x7 and set identity
CHECK_EQ(A.rows(), 7, "rows should be 7");
CHECK_EQ(A.cols(), 7, "cols should be 7");
CHECK(is_identity(A), "inplace_eye resize did not create identity");
}
TEST_CASE(Inplace_EYE_inplace_on_square) {
utils::Md A(4,4, 42.0); // junk values
numerics::inplace_eye(A); // N==0 → must be square, zero + diag
CHECK(is_identity(A), "inplace_eye on square did not produce identity");
}
TEST_CASE(Inplace_EYE_throws_on_non_square) {
utils::Md A(2,3, 5.0);
bool threw = false;
try {
numerics::inplace_eye(A); // N==0 and non-square → throws
} catch (const std::runtime_error&) {
threw = true;
}
CHECK(threw, "inplace_eye should throw on non-square when N==0");
}
// ============ OpenMP variants (compiled only when -fopenmp is used) ============
#ifdef _OPENMP
TEST_CASE(EYE_OMP_matches_serial) {
utils::Md I1 = numerics::eye<double>(64);
utils::Md I2 = numerics::eye_omp<double>(64);
for (uint64_t i = 0; i < I1.rows(); ++i){
for (uint64_t j = 0; j < I1.cols(); ++j){
CHECK(I1(i,j) == I2(i,j), "eye_omp != eye");
}
}
utils::Md A(64,64, 3.14);
numerics::inplace_eye_omp(A); // N==0 → must be square
CHECK(is_identity(A), "inplace_eye_omp did not produce identity");
}
TEST_CASE(EYE_OMP_speed) {
uint64_t prev = omp_get_max_threads();
if (prev <= 1) return;
omp_set_num_threads(16);
uint64_t N = 16384;
utils::Matrix<uint8_t> I1(N,N,1), I2(N,N,1);
auto t0 = std::chrono::high_resolution_clock::now();
numerics::inplace_eye<uint8_t>(I1);
double t1 = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
t0 = std::chrono::high_resolution_clock::now();
numerics::inplace_eye_omp<uint8_t>(I2);
double t2 = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
omp_set_num_threads(prev);
CHECK(t2 <= t1, "collapse_omp: multi-thread slower than single-thread");
}
TEST_CASE(EYE_OMP_nested_ok) {
// Ensure nested regions are allowed for this test (won't error if not)
uint64_t prev_levels = omp_get_max_active_levels();
uint64_t prev = omp_get_max_threads();
omp_set_max_active_levels(2);
// Outer team size (small); inner will be large when nesting is allowed
const int outer_threads = 1;
// Inner team size to request during nested-enabled run
const int inner_threads = 12;
uint64_t N = 4096*8;
utils::Matrix<uint8_t> I1(N, N, 7);
utils::Matrix<uint8_t> I2(N, N, 7);
// ---------- baseline: inner runs with a small team (no outer region) ----------
omp_set_max_active_levels(1); // disable nesting for baseline
omp_set_num_threads(outer_threads);
for (uint64_t i = 0; i < 2; ++i){
numerics::inplace_eye_omp<uint8_t>(I1);
}
// ---------- nested: outer×inner (only one outer thread launches inner) ----------
omp_set_max_active_levels(2); // allow one nested level
#pragma omp parallel num_threads(outer_threads)
{
#pragma omp single // avoid racing on I1
{
omp_set_num_threads(inner_threads);
for (uint64_t i = 0; i < 2; ++i){
numerics::inplace_eye_omp<uint8_t>(I2);
}
}
}
omp_set_max_active_levels(prev_levels);
omp_set_num_threads(prev);
CHECK(is_identity(I1), "EYE_OMP_nested did not produce identity");
CHECK(is_identity(I2), "EYE_OMP_nested did not produce identity");
}
TEST_CASE(EYE_OMP_auto_is_identity) {
auto I = numerics::eye_omp_auto<double>(32);
CHECK(is_identity(I), "eye_omp_auto did not produce identity");
}
#endif

View File

@@ -0,0 +1,155 @@
#include "test_common.h"
#include "./utils/matrix.h"
#include "./utils/vector.h"
#include "./numerics/interpolation1d.h"
// ------------ helpers ------------
template <typename T>
static void make_uniform_xy(utils::Vector<T>& x, utils::Vector<T>& y,
std::uint64_t N, T x0, T dx,
T a, T b) {
x.resize(N, T(0));
y.resize(N, T(0));
for (std::uint64_t i=0; i<N; ++i) {
T xi = x0 + dx * T(i);
x[i] = xi;
y[i] = a*xi + b; // y = a x + b
}
}
template <typename T>
static void make_uniform_xy_fun(utils::Vector<T>& x, utils::Vector<T>& y,
std::uint64_t N, T x0, T dx,
T (*f)(T)) {
x.resize(N, T(0));
y.resize(N, T(0));
for (std::uint64_t i=0; i<N; ++i) {
T xi = x0 + dx * T(i);
x[i] = xi;
y[i] = f(xi);
}
}
template <typename T>
static inline bool almost_eq(T a, T b, double tol=1e-12) {
return std::fabs(double(a)-double(b)) <= tol;
}
// ------------ tests ------------
// 1) All interpolants reproduce the data points exactly (or to tiny FP error)
TEST_CASE(Interp_Reproduces_Data_Nodes) {
const std::uint64_t N = 25;
utils::Vd x, y;
// linear data: y = 0.1 x + 0.9 on x = 1..25
make_uniform_xy<double>(x, y, N, 1.0, 1.0, 0.1, 0.9);
numerics::interp_linear<double> lin(x,y);
numerics::interp_polynomial<double> pol(x,y, 3);
numerics::interp_cubic_spline<double> spl(x,y);
numerics::interp_rational<double> rat(x,y, 2);
numerics::interp_barycentric<double> bar(x,y, 2);
for (std::uint64_t i=0; i<N; ++i) {
double xi = x[i];
double yi = y[i];
CHECK(almost_eq(lin.interp(xi), yi), "linear fails at node");
CHECK(almost_eq(pol.interp(xi), yi), "polynomial fails at node");
CHECK(almost_eq(spl.interp(xi), yi), "spline fails at node");
CHECK(almost_eq(rat.interp(xi), yi), "rational fails at node");
CHECK(almost_eq(bar.interp(xi), yi), "barycentric fails at node");
}
}
// 2) Linear dataset: all interpolants match the analytic line at off-grid points
TEST_CASE(Interp_Linear_Dataset_OffGrid) {
const std::uint64_t N = 100;
utils::Vd x, y;
// your quick test dataset: x = 1..100, y = 0.1 x + 0.9
make_uniform_xy<double>(x, y, N, 1.0, 1.0, 0.1, 0.9);
numerics::interp_linear<double> lin(x,y);
numerics::interp_polynomial<double> pol(x,y, 3);
numerics::interp_cubic_spline<double> spl(x,y);
numerics::interp_rational<double> rat(x,y, 3);
numerics::interp_barycentric<double> bar(x,y, 3);
std::vector<double> qs = { 1.5, 5.5, 5.51, 50.01, 99.9 };
for (double q : qs) {
const double ytrue = 0.1*q + 0.9;
CHECK(almost_eq(lin.interp(q), ytrue, 1e-9), "linear off-grid mismatch");
CHECK(almost_eq(pol.interp(q), ytrue, 1e-9), "polynomial off-grid mismatch");
CHECK(almost_eq(spl.interp(q), ytrue, 1e-9), "spline off-grid mismatch");
CHECK(almost_eq(rat.interp(q), ytrue, 1e-9), "rational off-grid mismatch");
CHECK(almost_eq(bar.interp(q), ytrue, 1e-9), "barycentric off-grid mismatch");
}
// endpoints should match exactly (no extrapolation)
CHECK(almost_eq(lin.interp(x[0]), y[0]), "linear endpoint");
CHECK(almost_eq(lin.interp(x[N-1]), y[N-1]), "linear endpoint");
CHECK(almost_eq(spl.interp(x[0]), y[0]), "spline endpoint");
CHECK(almost_eq(spl.interp(x[N-1]), y[N-1]), "spline endpoint");
}
// 3) Quadratic dataset: degree-2 polynomial interpolation should be exact
static double f_quad(double t) { return t*t - 3.0*t + 2.0; } // (t-1)(t-2)
TEST_CASE(Interp_Polynomial_Deg2_Exact_On_Quadratic) {
const std::uint64_t N = 21;
utils::Vd x, y;
make_uniform_xy_fun<double>(x, y, N, -2.0, 0.5, &f_quad);
numerics::interp_polynomial<double> pol(x,y, 3);
std::vector<double> qs = { -1.75, -0.1, 0.3, 1.4, 3.9, 7.25 };
for (double q : qs) {
// only test inside the data domain to avoid extrap behavior
if (q >= x[0] && q <= x[N-1]) {
const double ytrue = f_quad(q);
//std::cout << pol.interp(q) << ", " << ytrue << ", "<< q <<std::endl;
CHECK(almost_eq(pol.interp(q), ytrue, 1e-10), "degree-2 polynomial should be exact on quadratic");
}
}
}
// 4) Cross-method consistency: methods agree closely on smooth data (cosine wave)
static double f_cos(double t) { return std::cos(0.1*t); }
TEST_CASE(Interp_Cross_Method_Consistency) {
const std::uint64_t N = 60;
utils::Vd x, y;
make_uniform_xy_fun<double>(x, y, N, 0.0, 0.5, &f_cos);
numerics::interp_linear<double> lin(x,y);
numerics::interp_polynomial<double> pol(x,y, 3); // small local degree
numerics::interp_cubic_spline<double> spl(x,y);
numerics::interp_rational<double> rat(x,y, 3);
numerics::interp_barycentric<double> bar(x,y, 3);
// sample a handful of interior points and require all methods to be mutually close
std::vector<double> qs = { 1.25, 7.75, 12.1, 18.6, 22.75 };
for (double q : qs) {
// skip if q is outside just in case
if (q < x[0] || q > x[N-1]) continue;
double yl = lin.interp(q);
double yp = pol.interp(q);
double ys = spl.interp(q);
double yr = rat.interp(q);
double yb = bar.interp(q);
//std::cout << "lin: " << yl << std::endl;
//std::cout << "pol: " << yp << std::endl;
//std::cout << "spl: " << ys << std::endl;
//std::cout << "rat: " << yr << std::endl;
//std::cout << "bar: " << yb << std::endl;
// spline is usually the smoothest; use it as the anchor
CHECK(almost_eq(yl, ys, 5e-4), "linear vs spline");
CHECK(almost_eq(yp, ys, 5e-4), "polynomial vs spline");
CHECK(almost_eq(yr, ys, 5e-4), "rational vs spline");
CHECK(almost_eq(yb, ys, 5e-4), "barycentric vs spline");
}
}

141
test/test_inverse.cpp Normal file
View File

@@ -0,0 +1,141 @@
#include "test_common.h"
#include "./utils/matrix.h"
#include "./utils/vector.h"
#include "./numerics/inverse.h"
#include "./numerics/matmul.h"
// ---------- helpers ----------
template <typename T>
static utils::Matrix<T> identity(std::uint64_t n) {
utils::Matrix<T> I(n,n,T(0));
for (std::uint64_t i=0;i<n;++i) I(i,i) = T(1);
return I;
}
template <typename T>
static bool mats_equal_tol(const utils::Matrix<T>& X,
const utils::Matrix<T>& Y,
double tol = 1e-12) {
if (X.rows()!=Y.rows() || X.cols()!=Y.cols()) return false;
for (std::uint64_t i=0;i<X.rows();++i)
for (std::uint64_t j=0;j<X.cols();++j)
if (std::fabs(double(X(i,j) - Y(i,j))) > tol) return false;
return true;
}
// A small well-conditioned SPD 3x3
static utils::Matrix<double> make_A3() {
utils::Matrix<double> A(3,3,0.0);
// [ 4 3 0
// 3 4 -1
// 0 -1 4 ]
A(0,0)=4; A(0,1)=3; A(0,2)=0;
A(1,0)=3; A(1,1)=4; A(1,2)=-1;
A(2,0)=0; A(2,1)=-1; A(2,2)=4;
return A;
}
// A random-ish SPD 5x5 (constructed deterministically)
static utils::Matrix<double> make_A5_spd() {
utils::Matrix<double> R(5,5,0.0);
// Fill R with a simple pattern
double v=1.0;
for (std::uint64_t i=0;i<5;++i)
for (std::uint64_t j=0;j<5;++j, v+=0.37)
R(i,j) = std::fmod(v, 3.0) - 1.0; // values in [-1,2)
// A = R^T R + 5 I → SPD and well-conditioned
utils::Matrix<double> Rt(5,5,0.0);
for (std::uint64_t i=0;i<5;++i)
for (std::uint64_t j=0;j<5;++j)
Rt(i,j) = R(j,i);
auto RtR = numerics::matmul(Rt, R);
for (std::uint64_t i=0;i<5;++i) RtR(i,i) += 5.0;
return RtR;
}
// ---------- tests ----------
TEST_CASE(Inverse_GJ_3x3) {
auto A = make_A3();
auto A_copy = A;
auto Inv = numerics::inverse(A, "Gauss-Jordan"); // non-inplace
auto I = identity<double>(3);
auto AInv = numerics::matmul(A, Inv);
auto InvA = numerics::matmul(Inv, A);
CHECK(mats_equal_tol(AInv, I, 1e-11), "A*Inv != I (Gauss-Jordan)");
CHECK(mats_equal_tol(InvA, I, 1e-11), "Inv*A != I (Gauss-Jordan)");
// A should be unchanged by numerics::inverse (it copies internally)
CHECK(mats_equal_tol(A, A_copy, 1e-15), "inverse() modified input A");
}
TEST_CASE(Inverse_LU_3x3) {
auto A = make_A3();
auto Inv = numerics::inverse(A, "LU");
auto I = identity<double>(3);
auto AInv = numerics::matmul(A, Inv);
auto InvA = numerics::matmul(Inv, A);
CHECK(mats_equal_tol(AInv, I, 1e-11), "A*Inv != I (LU)");
CHECK(mats_equal_tol(InvA, I, 1e-11), "Inv*A != I (LU)");
}
TEST_CASE(Inplace_Inverse_Both_Methods_Agree_5x5) {
auto A = make_A5_spd();
auto GJ = A; numerics::inplace_inverse(GJ, "Gauss-Jordan");
auto LU = A; numerics::inplace_inverse(LU, "LU");
// Both should be valid inverses
auto I = identity<double>(5);
CHECK(mats_equal_tol(numerics::matmul(A, GJ), I, 1e-10), "A*GJ != I");
CHECK(mats_equal_tol(numerics::matmul(GJ, A), I, 1e-10), "GJ*A != I");
CHECK(mats_equal_tol(numerics::matmul(A, LU), I, 1e-10), "A*LU != I");
CHECK(mats_equal_tol(numerics::matmul(LU, A), I, 1e-10), "LU*A != I");
// And they should be very close to each other
CHECK(mats_equal_tol(GJ, LU, 1e-10), "Gauss-Jordan inverse != LU inverse");
}
TEST_CASE(Inverse_NonSquare_Throws) {
utils::Matrix<double> A(2,3,1.0);
bool threw=false;
try { auto B = numerics::inverse(A, "Gauss-Jordan"); (void)B; } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "inverse should throw on non-square (Gauss-Jordan)");
threw=false;
try { numerics::inplace_inverse(A, "LU"); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "inplace_inverse should throw on non-square (LU)");
}
TEST_CASE(Inverse_Singular_Throws) {
utils::Matrix<double> A(3,3,0.0);
// Two identical rows → singular
A(0,0)=1; A(0,1)=2; A(0,2)=3;
A(1,0)=1; A(1,1)=2; A(1,2)=3;
A(2,0)=0; A(2,1)=1; A(2,2)=4;
bool threw=false;
try { auto B = numerics::inverse(A, "Gauss-Jordan"); (void)B; } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "inverse(GJ) should throw on singular");
threw=false;
try { auto B = numerics::inverse(A, "LU"); (void)B; } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "inverse(LU) should throw on singular");
}
TEST_CASE(Inverse_Invalid_Method_Throws) {
auto A = make_A3();
bool threw=false;
try { auto B = numerics::inverse(A, "NotAThing"); (void)B; } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "inverse should throw on unknown method");
}

169
test/test_lu.cpp Normal file
View File

@@ -0,0 +1,169 @@
#include "test_common.h"
#include "./utils/matrix.h"
#include "./utils/vector.h"
#include "./numerics/matmul.h"
#include "./numerics/matvec.h"
#include "./decomp/lu.h"
//#include <chrono>
// ---------- helpers ----------
template <typename T>
static bool mats_equal_tol(const utils::Matrix<T>& X,
const utils::Matrix<T>& Y,
double tol = 1e-12) {
if (X.rows()!=Y.rows() || X.cols()!=Y.cols()) return false;
for (std::uint64_t i=0;i<X.rows();++i)
for (std::uint64_t j=0;j<X.cols();++j)
if (std::fabs(double(X(i,j) - Y(i,j))) > tol) return false;
return true;
}
template <typename T>
static utils::Matrix<T> identity(std::uint64_t n) {
utils::Matrix<T> I(n,n,T(0));
for (std::uint64_t i=0;i<n;++i) I(i,i) = T(1);
return I;
}
template <typename T>
static void split_LU(const utils::Matrix<T>& lu,
utils::Matrix<T>& L,
utils::Matrix<T>& U) {
const std::uint64_t n = lu.rows();
L.resize(n,n,T(0));
U.resize(n,n,T(0));
for (std::uint64_t i=0;i<n;++i) {
for (std::uint64_t j=0;j<n;++j) {
if (i>j) L(i,j) = lu(i,j);
else if (i==j){ L(i,i) = T(1); U(i,i) = lu(i,i); }
else U(i,j) = lu(i,j);
}
}
}
template <typename T>
static utils::Matrix<T> permutation_from_indx(const std::vector<std::uint64_t>& indx) {
const std::uint64_t n = indx.size();
auto P = identity<T>(n);
// Apply the same sequence of row swaps that was applied during factorization
for (std::uint64_t k=0;k<n;++k) {
const std::uint64_t imax = indx[k];
if (imax != k) P.swap_rows(k, imax);
}
return P;
}
// A well-conditioned 3x3 (symmetric positive definite)
static utils::Matrix<double> make_A_spd() {
utils::Matrix<double> A(3,3,0.0);
// [ 4 3 0
// 3 4 -1
// 0 -1 4 ]
A(0,0)=4; A(0,1)=3; A(0,2)=0;
A(1,0)=3; A(1,1)=4; A(1,2)=-1;
A(2,0)=0; A(2,1)=-1; A(2,2)=4;
return A;
}
TEST_CASE(LU_PA_equals_LU) {
auto A = make_A_spd();
decomp::LUdcmpd lu(A);
utils::Matrix<double> L,U;
split_LU(lu.lu, L, U);
auto P = permutation_from_indx<double>(lu.indx);
auto PA = numerics::matmul(P, A);
auto LU = numerics::matmul(L, U);
CHECK(mats_equal_tol(PA, LU, 1e-12), "PA should equal LU");
}
TEST_CASE(LU_Solve_Vector) {
auto A = make_A_spd();
decomp::LUdcmpd lu(A);
utils::Vd b(3,0.0);
b[0]=1.0; b[1]=2.0; b[2]=3.0;
auto x = lu.solve(b);
auto Ax = numerics::matvec(A, x);
CHECK(b.nearly_equal_vec(Ax, 1e-12), "A*x should equal b");
}
TEST_CASE(LU_Solve_Matrix_MultiRHS) {
auto A = make_A_spd();
decomp::LUdcmpd lu(A);
utils::Matrix<double> B(3,2,0.0);
// two RHS columns
B(0,0)=1; B(1,0)=2; B(2,0)=3;
B(0,1)=4; B(1,1)=5; B(2,1)=6;
auto X = lu.solve(B); // 3x2
// Check A*X == B
auto AX = numerics::matmul(A, X);
CHECK(mats_equal_tol(AX, B, 1e-12), "A*X should equal B");
// And that column-wise solve agrees
utils::Vd b0(3,0.0), b1(3,0.0);
for (int i=0;i<3;++i){ b0[i]=B(i,0); b1[i]=B(i,1); }
auto x0 = lu.solve(b0);
auto x1 = lu.solve(b1);
CHECK(std::fabs(double(X(0,0)-x0[0]))<1e-12 &&
std::fabs(double(X(1,0)-x0[1]))<1e-12 &&
std::fabs(double(X(2,0)-x0[2]))<1e-12, "column 0 mismatch");
CHECK(std::fabs(double(X(0,1)-x1[0]))<1e-12 &&
std::fabs(double(X(1,1)-x1[1]))<1e-12 &&
std::fabs(double(X(2,1)-x1[2]))<1e-12, "column 1 mismatch");
}
TEST_CASE(LU_Determinant) {
auto A = make_A_spd();
decomp::LUdcmpd lu(A);
// For this A, det = 24
double d = lu.det();
CHECK(std::fabs(d - 24.0) < 1e-12, "determinant incorrect");
}
TEST_CASE(LU_Inverse_via_SolveI) {
auto A = make_A_spd();
decomp::LUdcmpd lu(A);
// Build identity and solve A * X = I
auto I = identity<double>(3);
auto Inv = lu.solve(I);
// Check A*Inv == I (and Inv*A == I for good measure)
auto AInv = numerics::matmul(A, Inv);
auto InvA = numerics::matmul(Inv, A);
CHECK(mats_equal_tol(AInv, I, 1e-11), "A*Inv should be I");
CHECK(mats_equal_tol(InvA, I, 1e-11), "Inv*A should be I");
}
TEST_CASE(LU_NonSquare_Throws) {
utils::Matrix<double> A(2,3,1.0);
bool threw=false;
try { decomp::LUdcmpd lu(A); } catch (const std::runtime_error&) { threw = true; }
CHECK(threw, "LU should throw on non-square");
}
TEST_CASE(LU_Singular_Throws) {
utils::Matrix<double> A(3,3,0.0);
// Make two identical rows
A(0,0)=1; A(0,1)=2; A(0,2)=3;
A(1,0)=1; A(1,1)=2; A(1,2)=3;
A(2,0)=0; A(2,1)=1; A(2,2)=4;
bool threw=false;
try { decomp::LUdcmpd lu(A); } catch (const std::runtime_error&) { threw = true; }
CHECK(threw, "LU should throw on singular matrix");
}

108
test/test_matequal.cpp Normal file
View File

@@ -0,0 +1,108 @@
#include "test_common.h"
#include "./numerics/matequal.h"
using utils::Vf; using utils::Vd; using utils::Vi;
using utils::Mf; using utils::Md; using utils::Mi;
// ---------- helpers ----------
template <typename T>
static void fill_seq(utils::Matrix<T>& M, T start = T{0}, T step = T{1}) {
std::uint64_t k = 0;
for (std::uint64_t i = 0; i < M.rows(); ++i)
for (std::uint64_t j = 0; j < M.cols(); ++j, ++k)
M(i,j) = start + step * static_cast<T>(k);
}
// ---------- tests ----------
TEST_CASE(matequal_shape_mismatch) {
utils::Mi A(3,3,0), B(3,4,0);
CHECK(!numerics::matequal(A,B), "shape mismatch should be false (serial)");
#ifdef _OPENMP
CHECK(!numerics::matequal_omp(A,B), "shape mismatch should be false (omp)");
#endif
CHECK(!numerics::matequal_auto(A,B), "shape mismatch should be false (auto)");
}
TEST_CASE(matequal_int_true_false) {
utils::Mi A(4,5,0), B(4,5,0);
fill_seq(A, int64_t(0), int64_t(1));
fill_seq(B, int64_t(0), int64_t(1));
CHECK(numerics::matequal(A,B), "ints equal (serial)");
#ifdef _OPENMP
CHECK(numerics::matequal_omp(A,B), "ints equal (omp)");
#endif
// flip one element
B(2,3) += 1;
CHECK(!numerics::matequal(A,B), "ints differ (serial)");
#ifdef _OPENMP
CHECK(!numerics::matequal_omp(A,B), "ints differ (omp)"); // will FAIL if your omp branch uses '!='
#endif
}
TEST_CASE(matequal_double_tolerance) {
utils::Md A(3,3,0.0), B(3,3,0.0);
fill_seq(A, double(1.0), double(0.125));
fill_seq(B, double(1.0), double(0.125));
// tiny perturbation within default tol
B(1,1) += 1e-12;
CHECK(numerics::matequal(A,B), "double within tol (serial)");
#ifdef _OPENMP
CHECK(numerics::matequal_omp(A,B), "double within tol (omp)");
#endif
// larger perturbation exceeds tol
B(0,2) += 1e-6;
CHECK(!numerics::matequal(A,B, 1e-9), "double exceeds tol (serial)");
#ifdef _OPENMP
CHECK(!numerics::matequal_omp(A,B, 1e-9), "double exceeds tol (omp)");
#endif
}
TEST_CASE(matequal_auto_agrees) {
// Choose size so auto likely takes the OMP path when available,
// but this test only checks correctness, not which path was taken.
utils::Md A(256,256,0.0), B(256,256,0.0);
fill_seq(A, double(0.0), double(0.01));
fill_seq(B, double(0.0), double(0.01));
CHECK(numerics::matequal_auto(A,B), "auto equal");
B(5,7) += 1e-3;
CHECK(!numerics::matequal_auto(A,B, 1e-9), "auto detects mismatch");
}
#ifdef _OPENMP
TEST_CASE(mateequal_omp_nested_callsite) {
// Verify correctness when called inside an outer parallel region.
utils::Mi A(128,128,0), B(128,128,0);
fill_seq(A, int64_t(0), int64_t(1));
fill_seq(B, int64_t(0), int64_t(1));
// allow one nested level; inner region inside mateequal_omp may spawn a team
int prev_levels = omp_get_max_active_levels();
omp_set_max_active_levels(2);
bool ok_equal = false, ok_diff = false;
#pragma omp parallel num_threads(2) shared(ok_equal, ok_diff)
{
#pragma omp single
{
ok_equal = numerics::matequal_omp(A,B);
B(10,10) += 1; // introduce a mismatch
ok_diff = !numerics::matequal_omp(A,B);
}
}
omp_set_max_active_levels(prev_levels);
CHECK(ok_equal, "nested equal should be true");
CHECK(ok_diff, "nested mismatch should be false");
}
#endif

127
test/test_matmul.cpp Normal file
View File

@@ -0,0 +1,127 @@
#include "test_common.h"
#include "./utils/utils.h"
#include "./numerics/matmul.h"
#include <chrono>
// ---------- helpers ----------
template <typename T>
static bool mats_equal(const utils::Matrix<T>& X, const utils::Matrix<T>& Y, double tol = 0.0) {
if (X.rows()!=Y.rows() || X.cols()!=Y.cols()) return false;
if (std::is_floating_point<T>::value) {
for (std::uint64_t i=0;i<X.rows();++i)
for (std::uint64_t j=0;j<X.cols();++j)
if (std::fabs(double(X(i,j)) - double(Y(i,j))) > tol) return false;
} else {
for (std::uint64_t i=0;i<X.rows();++i)
for (std::uint64_t j=0;j<X.cols();++j)
if (X(i,j) != Y(i,j)) return false;
}
return true;
}
template <typename T>
static void fill_seq(utils::Matrix<T>& M, T start = T(0), T step = T(1)) {
std::uint64_t k = 0;
for (std::uint64_t i=0;i<M.rows();++i)
for (std::uint64_t j=0;j<M.cols();++j,++k)
M(i,j) = start + step * static_cast<T>(k);
}
// ---------- tests ----------
// Small known example: (3x2) · (2x3)
TEST_CASE(Matmul_Small_Known) {
utils::Mi A(3,2,0), B(2,3,0);
// A = [1 2; 3 4; 5 6]
A(0,0)=1; A(0,1)=2;
A(1,0)=3; A(1,1)=4;
A(2,0)=5; A(2,1)=6;
// B = [7 8 9; 10 11 12]
B(0,0)=7; B(0,1)=8; B(0,2)=9;
B(1,0)=10; B(1,1)=11; B(1,2)=12;
auto C = numerics::matmul(A,B);
CHECK(C.rows()==3 && C.cols()==3, "shape 3x3 wrong");
// Expected C:
// [27 30 33]
// [61 68 75]
// [95 106 117]
CHECK(C(0,0)==27 && C(0,1)==30 && C(0,2)==33, "row 0 wrong");
CHECK(C(1,0)==61 && C(1,1)==68 && C(1,2)==75, "row 1 wrong");
CHECK(C(2,0)==95 && C(2,1)==106 && C(2,2)==117, "row 2 wrong");
}
TEST_CASE(Matmul_DimMismatch_Throws) {
utils::Md A(2,3,1.0), B(4,2,2.0); // A.cols()!=B.rows()
bool threw=false;
try { (void)numerics::matmul(A,B); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "matmul should throw on dim mismatch");
}
// Compare all variants vs serial on a moderate size
TEST_CASE(Matmul_Variants_Equal_Int) {
const std::uint64_t m=32, n=24, p=16;
utils::Mi A(m,n,0), B(n,p,0);
// deterministic fill (no randomness)
fill_seq(A, int64_t(1), int64_t(1));
fill_seq(B, int64_t(2), int64_t(3));
auto C_ref = numerics::matmul(A,B);
auto C_rows = numerics::matmul_rows_omp(A,B);
auto C_collapse = numerics::matmul_collapse_omp(A,B);
auto C_auto = numerics::matmul_auto(A,B);
CHECK(mats_equal(C_rows, C_ref), "rows_omp != serial");
CHECK(mats_equal(C_collapse, C_ref), "collapse_omp != serial");
CHECK(mats_equal(C_auto, C_ref), "auto != serial");
}
TEST_CASE(Matmul_Variants_Equal_Double) {
const std::uint64_t m=33, n=17, p=19;
utils::Md A(m,n,0.0), B(n,p,0.0);
fill_seq(A, 0.1, 0.01);
fill_seq(B, 1.0, 0.02);
auto C_ref = numerics::matmul(A,B);
auto C_rows = numerics::matmul_rows_omp(A,B);
auto C_collapse = numerics::matmul_collapse_omp(A,B);
auto C_auto = numerics::matmul_auto(A,B);
CHECK(mats_equal(C_rows, C_ref, 1e-9), "rows_omp != serial (double)");
CHECK(mats_equal(C_collapse, C_ref, 1e-9), "collapse_omp != serial (double)");
CHECK(mats_equal(C_auto, C_ref, 1e-9), "auto != serial (double)");
}
// Nested callsite sanity: call OMP variant from within an outer region
#ifdef _OPENMP
TEST_CASE(Matmul_OMP_Nested_Callsite) {
const std::uint64_t m=48, n=24, p=32;
utils::Mi A(m,n,0), B(n,p,0);
fill_seq(A, int64_t(1), int64_t(2));
fill_seq(B, int64_t(3), int64_t(1));
auto C_ref = numerics::matmul(A,B);
int prev_levels = omp_get_max_active_levels();
omp_set_max_active_levels(2);
utils::Mi C_nested;
#pragma omp parallel num_threads(2)
{
#pragma omp single
{
// either variant is fine; collapse(2) has more parallelism
C_nested = numerics::matmul_collapse_omp(A,B);
}
}
omp_set_max_active_levels(prev_levels);
CHECK(mats_equal(C_nested, C_ref), "nested collapse_omp result mismatch");
}
#endif

164
test/test_matrix.cpp Normal file
View File

@@ -0,0 +1,164 @@
#include "test_common.h"
#include "./utils/matrix.h"
using utils::Vf; using utils::Vd; using utils::Vi;
using utils::Mf; using utils::Md; using utils::Mi;
// tiny helper
template <typename T>
static bool mat_is_filled(const utils::Matrix<T>& M, T v) {
for (std::uint64_t i = 0; i < M.rows(); ++i)
for (std::uint64_t j = 0; j < M.cols(); ++j)
if (M(i,j) != v) return false;
return true;
}
// ------------------- basic construction -------------------
TEST_CASE(Matrix_Default_Construct) {
Md M;
CHECK_EQ(M.rows(), 0, "default rows should be 0");
CHECK_EQ(M.cols(), 0, "default cols should be 0");
}
TEST_CASE(Matrix_Filled_Construct) {
Md M(3, 4, 2.5);
CHECK_EQ(M.rows(), 3, "rows");
CHECK_EQ(M.cols(), 4, "cols");
CHECK(mat_is_filled(M, 2.5), "all elements should be 2.5");
}
// ------------------- element access / write -------------------
TEST_CASE(Matrix_Set_Get) {
Mi M(2, 3, 0);
M(0,0) = 42;
M(1,2) = -7;
CHECK_EQ(M(0,0), 42, "set/get (0,0)");
CHECK_EQ(M(1,2), -7, "set/get (1,2)");
}
// ------------------- resize semantics -------------------
TEST_CASE(Matrix_Resize_Grow) {
Mf M(2, 2, 1.0f);
M.resize(3, 4, 9.0f); // grow; newly appended elements get the fill value
CHECK_EQ(M.rows(), 3, "rows after resize");
CHECK_EQ(M.cols(), 4, "cols after resize");
CHECK(M(2,3) == 9.0f, "last element should be the fill value after grow");
}
TEST_CASE(Matrix_Resize_Shrink) {
Mi M(4, 4, 5);
M(0,0) = 11;
M.resize(2, 2, 999); // shrink; size reduces
CHECK_EQ(M.rows(), 2, "rows after shrink");
CHECK_EQ(M.cols(), 2, "cols after shrink");
// element mapping after shrink is implementation dependent; just check bounds usable
M(1,1) = 3;
CHECK_EQ(M(1,1), 3, "write after shrink works");
}
// ------------------- row helpers -------------------
TEST_CASE(Matrix_Get_Row) {
Mi M(3,4,0);
// set row 1 to [10,20,30,40]
for (std::uint64_t j=0;j<4;++j) M(1,j) = (j+1)*10;
auto r = M.get_row(1);
CHECK_EQ(r.size(), 4, "row size");
CHECK(r[0]==10 && r[1]==20 && r[2]==30 && r[3]==40, "row contents");
}
TEST_CASE(Matrix_Set_Row) {
Mi M(2,3,0);
utils::Vector<int64_t> v(3, 0);
v[0]=7; v[1]=8; v[2]=9;
M.set_row(0, v);
CHECK(M(0,0)==7 && M(0,1)==8 && M(0,2)==9, "set_row contents");
}
TEST_CASE(Matrix_Row_OutOfRange_Throws) {
Mi M(2,2,0);
bool threw=false;
try { (void)M.get_row(2); } catch(const std::out_of_range&) { threw=true; }
CHECK(threw, "get_row out-of-range should throw");
threw=false;
try {
utils::Vector<int64_t> v(3,1); // wrong size
M.set_row(1, v);
} catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "set_row size mismatch should throw");
}
// ------------------- col helpers -------------------
TEST_CASE(Matrix_Get_Col) {
Mi M(3,2,0);
M(0,1)=5; M(1,1)=6; M(2,1)=7;
auto c = M.get_col(1);
CHECK_EQ(c.size(), 3, "col size");
CHECK(c[0]==5 && c[1]==6 && c[2]==7, "col contents");
}
TEST_CASE(Matrix_Set_Col) {
Mi M(3,2,0);
utils::Vector<int64_t> v(3, 0);
v[0]=1; v[1]=4; v[2]=9;
M.set_col(0, v);
CHECK(M(0,0)==1 && M(1,0)==4 && M(2,0)==9, "set_col contents");
}
TEST_CASE(Matrix_Col_OutOfRange_Throws) {
Mi M(2,2,0);
bool threw=false;
try { (void)M.get_col(2); } catch(const std::out_of_range&) { threw=true; }
CHECK(threw, "get_col out-of-range should throw");
threw=false;
try {
utils::Vector<int64_t> v(1,1); // wrong size
M.set_col(1, v);
} catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "set_col size mismatch should throw");
}
// ------------------- swap rows/cols -------------------
TEST_CASE(Matrix_Swap_Rows) {
Mi M(2,3,0);
// row0: 1 2 3, row1: 4 5 6
for (std::uint64_t j=0;j<3;++j){ M(0,j)=j+1; M(1,j)=j+4; }
M.swap_rows(0,1);
CHECK(M(0,0)==4 && M(0,1)==5 && M(0,2)==6, "row0 after swap");
CHECK(M(1,0)==1 && M(1,1)==2 && M(1,2)==3, "row1 after swap");
}
TEST_CASE(Matrix_Swap_Cols) {
Mi M(3,3,0);
// col0: 9 8 7, col2: 1 2 3
M(0,0)=9; M(1,0)=8; M(2,0)=7;
M(0,2)=1; M(1,2)=2; M(2,2)=3;
M.swap_cols(0,2);
CHECK(M(0,0)==1 && M(1,0)==2 && M(2,0)==3, "col0 after swap");
CHECK(M(0,2)==9 && M(1,2)==8 && M(2,2)==7, "col2 after swap");
}
TEST_CASE(Matrix_Swap_OutOfRange_Throws) {
Mi M(2,2,0);
bool threw=false;
try { M.swap_rows(0,2); } catch(const std::out_of_range&) { threw=true; }
CHECK(threw, "swap_rows out-of-range should throw");
threw=false;
try { M.swap_cols(0,3); } catch(const std::out_of_range&) { threw=true; }
CHECK(threw, "swap_cols out-of-range should throw");
}
// ------------------- stream output (basic sanity) -------------------
TEST_CASE(Matrix_Stream_ToString) {
Mf M(2,2,0.0f);
M(0,0)=1.0f; M(0,1)=2.0f; M(1,0)=3.0f; M(1,1)=4.0f;
std::ostringstream os;
os << M;
auto s = os.str();
CHECK(s.find("[[") != std::string::npos, "starts with [[");
CHECK(s.find("1.000") != std::string::npos, "contains formatted 1.000");
CHECK(s.find("4.000") != std::string::npos, "contains formatted 4.000");
}

236
test/test_matvec.cpp Normal file
View File

@@ -0,0 +1,236 @@
#include "test_common.h"
#include "./numerics/matvec.h" // numerics::matvec / inplace_transpose
#include <chrono>
using utils::Vi; using utils::Vf; using utils::Vd;
using utils::Mi; using utils::Mf; using utils::Md;
// ------------------------------------------------------------
// matvec: y = A * x
// ------------------------------------------------------------
TEST_CASE(Matvec_Serial_Simple) {
// A = [[1,2,3],
// [4,5,6]]
Md A(2,3,0.0);
A(0,0)=1; A(0,1)=2; A(0,2)=3;
A(1,0)=4; A(1,1)=5; A(1,2)=6;
Vd x(3,0.0); x[0]=7; x[1]=8; x[2]=9;
auto y = numerics::matvec<double>(A,x); // [ 1*7+2*8+3*9 , 4*7+5*8+6*9 ] = [50, 122]
CHECK(y.size()==2, "matvec size wrong");
CHECK(y[0]==50.0 && y[1]==122.0, "matvec values wrong");
}
TEST_CASE(Matvec_OMP_Equals_Serial) {
Md A(3,3,0.0);
// A = I * 2
for (uint64_t i=0;i<3;++i) A(i,i)=2.0;
Vd x(3,0.0); x[0]=1; x[1]=2; x[2]=3;
auto ys = numerics::matvec<double>(A,x);
auto yp = numerics::matvec_omp<double>(A,x);
CHECK((ys.nearly_equal_vec(yp)), "matvec_omp != matvec");
}
TEST_CASE(Matvec_Auto_Equals_Serial) {
Md A(2,2,0.0); A(0,0)=2; A(0,1)=1; A(1,0)=0.5; A(1,1)=3;
Vd x(2,0.0); x[0]=4; x[1]=5;
auto ys = numerics::matvec<double>(A,x);
auto ya = numerics::matvec_auto<double>(A,x);
CHECK((ys.nearly_equal_vec(ya)), "matvec_auto != serial");
}
TEST_CASE(Matvec_DimensionMismatch_Throws) {
Md A(2,3,0.0);
Vd x(4,0.0);
bool threw=false;
try { auto _ = numerics::matvec<double>(A,x); (void)_; }
catch (const std::runtime_error&) { threw=true; }
CHECK(threw, "matvec must throw on dimension mismatch");
}
TEST_CASE(Matvec_Zero_Edges) {
Md A(0,3,0.0); // 0x3
Vd x(3,1.0);
auto y = numerics::matvec<double>(A,x);
CHECK(y.size()==0, "0xN * x should return size 0 vector");
Md B(2,0,0.0); // 2x0
Vd z(0,0.0);
auto y2 = numerics::matvec<double>(B,z);
CHECK(y2.size()==2 && y2[0]==0.0 && y2[1]==0.0, "N×0 * 0 should return zeros of size N");
}
TEST_CASE(Matvec_Float_Tolerance) {
Mf A(2,2,0.0f); A(0,0)=1.0f; A(0,1)=2.0f; A(1,0)=3.0f; A(1,1)=4.0f;
Vf x(2,0.0f); x[0]=0.1f; x[1]=0.2f;
auto y1 = numerics::matvec<float>(A,x);
auto y2 = numerics::matvec_omp<float>(A,x);
CHECK((y1.nearly_equal_vec(y2,1e-6f)), "matvec float omp mismatch");
}
//
// ---------- Auto inside an outer parallel region (no accidental nested teams) ----------
// We just check correctness; performance is environment-dependent.
//
TEST_CASE(Matvec_Auto_Inside_Outer_Parallel_Correctness) {
const uint64_t m=64, n=64;
Md A(m,n,1.0); Vd x(n,2.0);
//fill_deterministic(A); fill_deterministic(x);
Vd ref = numerics::matvec<double>(A,x);
// Call auto inside an outer team
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
for (int rep=0; rep<32; ++rep) {
auto y = numerics::matvec_auto<double>(A,x);
// Each thread checks its own result equals reference
if (!(y.nearly_equal_vec(ref))) {
throw TestFailure("matvec_auto wrong under outer parallel region");
}
}
}
TEST_CASE(Matvec_Speed_Sanity) {
const uint64_t m=4096, n=4096; // ~16M MACs; adjust if needed
Md A(m,n,1.0); Vd x(n,2.0);
//fill_deterministic(A); fill_deterministic(x);
auto t0 = std::chrono::high_resolution_clock::now();
auto yS = numerics::matvec(A,x);
double tp = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
#ifdef _OPENMP
int threads = omp_get_max_threads();
#else
int threads = 1;
#endif
t0 = std::chrono::high_resolution_clock::now();
auto yP = numerics::matvec_omp(A,x);
double ts = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
CHECK((yS.nearly_equal_vec(yP)), "matvec_omp != matvec_serial (large)");
// Only enforce basic sanity if we *can* use >1 threads:
if (threads > 1) {
// Be generous: just require not significantly slower.
CHECK(tp >= ts, "matvec_omp unexpectedly much slower than serial");
}
}
// ------------------------------------------------------------
// vecmat: y = x * A
// ------------------------------------------------------------
TEST_CASE(Vecmat_Serial_Simple) {
// A = [[1,2],
// [3,4],
// [5,6]] (3x2)
Md A(3,2,0.0);
A(0,0)=1; A(0,1)=2;
A(1,0)=3; A(1,1)=4;
A(2,0)=5; A(2,1)=6;
Vd x(3,0.0); x[0]=7; x[1]=8; x[2]=9;
auto y = numerics::vecmat<double>(x,A); // 1*7+3*8+5*9= 76 ; 2*7+4*8+6*9=100
CHECK(y.size()==2, "vecmat size wrong");
CHECK(y[0]==76.0 && y[1]==100.0, "vecmat values wrong");
}
TEST_CASE(Vecmat_OMP_Equals_Serial) {
Md A(2,2,0.0); A(0,0)=2; A(0,1)=1; A(1,0)=5; A(1,1)=-1;
Vd x(2,0.0); x[0]=0.5; x[1]=1.5;
auto ys = numerics::vecmat<double>(x,A);
auto yp = numerics::vecmat_omp<double>(x,A);
CHECK((ys.nearly_equal_vec(yp)), "vecmat_omp != vecmat");
}
TEST_CASE(Vecmat_Auto_Equals_Serial) {
Md A(2,3,0.0);
A(0,0)=1; A(0,1)=2; A(0,2)=3;
A(1,0)=4; A(1,1)=5; A(1,2)=6;
Vd x(2,0.0); x[0]=1; x[1]=2;
auto ys = numerics::vecmat<double>(x,A);
auto ya = numerics::vecmat_auto<double>(x,A);
CHECK((ys.nearly_equal_vec(ya)), "vecmat_auto != serial");
}
TEST_CASE(Vecmat_DimensionMismatch_Throws) {
Md A(2,2,0.0);
Vd x(3,0.0);
bool threw=false;
try { auto _ = numerics::vecmat<double>(x,A); (void)_; }
catch (const std::runtime_error&) { threw=true; }
CHECK(threw, "vecmat must throw on dimension mismatch");
}
TEST_CASE(Vecmat_Zero_Edges) {
Md A(0,3,0.0);
Vd x(0,0.0);
auto y = numerics::vecmat<double>(x,A); // 0×N times N×M → 0×M
CHECK(y.size()==3 && y[0]==0.0 && y[1]==0.0 && y[2]==0.0, "0-length x times A wrong");
Md B(3,0,0.0);
Vd z(3,1.0);
auto y2 = numerics::vecmat<double>(z,B); // 1x3 * 3x0 → 1x0
CHECK(y2.size()==0, "vecmat with N×0 result size wrong");
}
//
// ---------- Auto inside an outer parallel region (no accidental nested teams) ----------
// We just check correctness; performance is environment-dependent.
//
TEST_CASE(Vecmat_Auto_Inside_Outer_Parallel_Correctness) {
const uint64_t m=64, n=64;
Md A(m,n,1.0); Vd x(m,2.0);
//fill_deterministic(A); fill_deterministic(x);
Vd ref = numerics::vecmat<double>(x,A);
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
for (int rep=0; rep<32; ++rep) {
auto y = numerics::vecmat_auto<double>(x,A);
if (!(y.nearly_equal_vec(ref))) {
throw TestFailure("vecmat_auto wrong under outer parallel region");
}
}
}
TEST_CASE(Vecmat_Speed_Sanity) {
const uint64_t m=4096, n=4096;
Md A(m,n,1.0); Vd x(m,2.0);
//fill_deterministic(A); fill_deterministic(x);
auto t0 = std::chrono::high_resolution_clock::now();
auto yS = numerics::vecmat<double>(x,A);
double ts = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
#ifdef _OPENMP
int threads = omp_get_max_threads();
#else
int threads = 1;
#endif
t0 = std::chrono::high_resolution_clock::now();
auto yP = numerics::vecmat_omp<double>(x,A);
double tp = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
CHECK((yS.nearly_equal_vec(yP)), "vecmat_omp != vecmat_serial (large)");
if (threads > 1) {
CHECK(tp <= ts, "vecmat_omp unexpectedly much slower than serial");
}
}

151
test/test_transpose.cpp Normal file
View File

@@ -0,0 +1,151 @@
#include "test_common.h"
//#include "./utils/matrix.h" // matrix.h, vector.h
#include "./numerics/transpose.h" // numerics::transpose / inplace_transpose
using utils::Mi; using utils::Mf; using utils::Md;
/// ---- helpers ----
template <typename T>
static void fill_seq(utils::Matrix<T>& M, T start = T(0), T step = T(1)) {
std::uint64_t k = 0;
for (std::uint64_t i=0; i<M.rows(); ++i)
for (std::uint64_t j=0; j<M.cols(); ++j, ++k)
M(i,j) = start + step * static_cast<T>(k);
}
template <typename T>
static bool mats_equal(const utils::Matrix<T>& X, const utils::Matrix<T>& Y) {
if (X.rows()!=Y.rows() || X.cols()!=Y.cols()) return false;
for (std::uint64_t i=0; i<X.rows(); ++i)
for (std::uint64_t j=0; j<X.cols(); ++j)
if (X(i,j) != Y(i,j)) return false;
return true;
}
// ---- tests ----
// Empty and 1x1 edge cases
TEST_CASE(Transpose_Edges) {
utils::Mi E; // 0x0
auto Et = numerics::transpose(E);
CHECK(Et.rows()==0 && Et.cols()==0, "transpose of empty should be empty");
utils::Mi S(1,1,42);
auto St = numerics::transpose(S);
CHECK(St.rows()==1 && St.cols()==1, "1x1 stays 1x1");
CHECK(St(0,0)==42, "1x1 value preserved");
}
// Rectangular out-of-place
TEST_CASE(Transpose_Rectangular) {
const std::uint64_t r=3, c=5;
utils::Mi A(r,c,0);
fill_seq(A, int64_t(1), int64_t(1));
auto B = numerics::transpose(A);
CHECK(B.rows()==c && B.cols()==r, "shape swapped");
for (std::uint64_t i=0;i<r;++i)
for (std::uint64_t j=0;j<c;++j)
CHECK(B(j,i)==A(i,j), "transpose content mismatch");
}
// Square: in-place equals out-of-place
TEST_CASE(Transpose_Inplace_Equals_OutOfPlace) {
const std::uint64_t n=7;
utils::Mi A(n,n,0);
fill_seq(A, int64_t(10), int64_t(3));
auto B = numerics::transpose(A);
auto C = A; // copy
numerics::inplace_transpose_square(C);
CHECK(mats_equal(B, C), "inplace transpose should match out-of-place");
}
// In-place should throw on non-square
TEST_CASE(Transpose_Inplace_Throws_On_Rect) {
utils::Mi A(2,3,0);
bool threw=false;
try { numerics::inplace_transpose_square(A); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "inplace_transpose_square must throw on non-square");
}
// --- OMP variants (compile only with -fopenmp) ---
#ifdef _OPENMP
TEST_CASE(Transpose_OMP_OutOfPlace_Equals_Serial) {
const std::uint64_t r=17, c=31;
utils::Mi A(r,c,0);
fill_seq(A, int64_t(5), int64_t(2));
auto B_serial = numerics::transpose(A);
auto B_omp = numerics::transpose_omp(A);
CHECK(mats_equal(B_serial, B_omp), "transpose_omp != transpose");
}
TEST_CASE(Transpose_OMP_Inplace_Equals_Serial) {
const std::uint64_t n=32;
utils::Mi A(n,n,0);
fill_seq(A, int64_t(0), int64_t(1));
auto B_ref = numerics::transpose(A);
auto C = A;
numerics::inplace_transpose_square_omp(C);
CHECK(mats_equal(B_ref, C), "inplace_transpose_square_omp != transpose");
}
// Auto selectors (if you added transpose_auto / inplace_transpose_square_auto_auto)
TEST_CASE(Transpose_Auto_Equals_Serial) {
// Rectangular: transpose_auto
utils::Mi A(23,11,0);
fill_seq(A, int64_t(1), int64_t(1));
auto B_ref = numerics::transpose(A);
auto B_auto = numerics::transpose_auto(A);
CHECK(mats_equal(B_ref, B_auto), "transpose_auto != transpose");
// Square: inplace_transpose_square_auto
utils::Mi S(29,29,0);
fill_seq(S, int64_t(4), int64_t(7));
auto Tref = numerics::transpose(S);
numerics::inplace_transpose_square_auto(S);
CHECK(mats_equal(Tref, S), "inplace_transpose_square_auto != transpose");
}
// Nested callsite sanity: call OMP versions inside an outer region
TEST_CASE(Transpose_OMP_Nested_Callsite) {
// Out-of-place on rectangular
utils::Mi A(19,37,0);
fill_seq(A, int64_t(2), int64_t(3));
auto Bref = numerics::transpose(A);
int prev_levels = omp_get_max_active_levels();
omp_set_max_active_levels(2);
utils::Mi Bnest;
#pragma omp parallel num_threads(2)
{
#pragma omp single
{
Bnest = numerics::transpose_omp(A);
}
}
CHECK(mats_equal(Bref, Bnest), "nested transpose_omp mismatch");
// In-place on square
utils::Mi S(41,41,0);
fill_seq(S, int64_t(1), int64_t(5));
auto Sref = numerics::transpose(S);
#pragma omp parallel num_threads(2)
{
#pragma omp single
{
numerics::inplace_transpose_square_omp(S);
}
}
omp_set_max_active_levels(prev_levels);
CHECK(mats_equal(Sref, S), "nested inplace_transpose_square_omp mismatch");
}
#endif

196
test/test_vector.cpp Normal file
View File

@@ -0,0 +1,196 @@
#include "test_common.h"
#include "./utils/utils.h"
using utils::Vf; using utils::Vd; using utils::Vi;
// ---------- helpers ----------
template <typename T>
static bool vec_equal_exact(const utils::Vector<T>& a, const utils::Vector<T>& b) {
if (a.size() != b.size()) return false;
for (std::uint64_t i=0;i<a.size();++i) if (a[i]!=b[i]) return false;
return true;
}
// ---------- tests ----------
TEST_CASE(Vector_Construct_Size_Fill) {
utils::Vd v0;
CHECK(v0.size()==0, "default size should be 0");
utils::Vd v1(5, 3.5);
CHECK(v1.size()==5, "size 5");
for (std::uint64_t i=0;i<5;++i) CHECK(v1[i]==3.5, "filled value 3.5");
}
TEST_CASE(Vector_PushBack_Resize) {
utils::Vi v;
v.push_back(1); v.push_back(2);
CHECK(v.size()==2, "push_back size");
CHECK(v[0]==1 && v[1]==2, "push_back contents");
v.resize(5, 7);
CHECK(v.size()==5, "resize size");
CHECK(v[2]==7 && v[3]==7 && v[4]==7, "resize fill value");
}
TEST_CASE(Vector_Data_ReadWrite) {
utils::Vd v(4, 0.0);
double* p = v.data();
for (std::uint64_t i=0;i<4;++i) p[i] = double(i+1);
CHECK(v[0]==1.0 && v[3]==4.0, "write via data()");
const utils::Vd& cv = v;
const double* cp = cv.data();
double s=0.0; for (std::uint64_t i=0;i<cv.size();++i) s += cp[i];
CHECK(std::fabs(s-10.0) < 1e-12, "read via const data()");
}
TEST_CASE(Vector_Equality_and_NearlyEqual) {
utils::Vd a(3, 1.0), b(3, 1.0);
CHECK(a==b, "operator== equal");
b[1] += 5e-7;
CHECK(a==b, "operator== within eps (1e-6)");
b[1] += 2e-6;
CHECK(!(a==b), "operator== beyond eps");
utils::Vd c = a;
c[2] += 1e-10;
CHECK(c.nearly_equal_vec(a, 1e-9), "nearly_equal_vec within tol");
c[2] += 1e-6;
CHECK(!c.nearly_equal_vec(a, 1e-9), "nearly_equal_vec beyond tol");
}
TEST_CASE(Vector_Scalar_Arithmetic) {
utils::Vi v(3, 10); // [10,10,10]
auto vadd = v + 2; // [12,12,12]
CHECK(vadd[0]==12 && vadd[2]==12, "scalar +");
v += 3; // [13,13,13]
CHECK(v[1]==13, "scalar +=");
auto vsub = v - 5; // [8,8,8]
CHECK(vsub[2]==8, "scalar -");
v -= 3; // [10,10,10]
CHECK(v[0]==10, "scalar -=");
auto vmul = v * 2; // [20,20,20]
CHECK(vmul[0]==20 && vmul[1]==20, "scalar *");
v *= 3; // [30,30,30]
CHECK(v[2]==30, "scalar *=");
auto vdiv = v / 3; // [10,10,10]
CHECK(vdiv[0]==10 && vdiv[1]==10, "scalar /");
v /= 2; // [15,15,15]
CHECK(v[0]==15 && v[2]==15, "scalar /=");
}
TEST_CASE(Vector_Vector_Arithmetic) {
utils::Vi a(4, 1), b(4, 2);
auto c = a + b; // [3,3,3,3]
CHECK(c[0]==3 && c[3]==3, "v+v");
a += b; // [3,3,3,3]
CHECK(a[1]==3, "v+=v");
auto d = a - b; // [1,1,1,1]
CHECK(d[2]==1, "v-v");
a -= b; // [1,1,1,1]
CHECK(a[0]==1, "v-=v");
auto e = a * b; // [2,2,2,2]
CHECK(e[1]==2, "v*v (elemwise)");
a *= b; // [2,2,2,2]
CHECK(a[3]==2, "v*=v");
auto f = e / b; // [1,1,1,1]
CHECK(f[0]==1 && f[3]==1, "v/v (elemwise)");
e /= b; // [1,1,1,1]
CHECK(e[2]==1, "v/=v");
}
TEST_CASE(Vector_Friend_Scalar_Left) {
utils::Vd v(3, 2.0); // [2,2,2]
auto s1 = 3.0 + v; // [5,5,5]
CHECK(s1[0]==5.0 && s1[2]==5.0, "left scalar +");
auto s2 = 4.0 * v; // [8,8,8]
CHECK(s2[1]==8.0, "left scalar *");
}
TEST_CASE(Vector_Power_and_Sqrt) {
utils::Vd v(3, 4.0); // [4,4,4]
auto p = v.power(2.0); // [16,16,16]
CHECK(p[0]==16.0 && p[2]==16.0, "power scalar");
v.inplace_sqrt(); // sqrt([4,4,4]) -> [2,2,2]
CHECK(v[0]==2.0 && v[1]==2.0, "inplace_sqrt");
}
TEST_CASE(Vector_Dot_Sum_Norm) {
utils::Vd a(3, 0.0), b(3, 0.0);
a[0]=1.0; a[1]=2.0; a[2]=3.0; // a = [1,2,3]
b[0]=4.0; b[1]=5.0; b[2]=6.0; // b = [4,5,6]
double dot = a.dot(b); // 1*4 + 2*5 + 3*6 = 32
CHECK(std::fabs(dot - 32.0) < 1e-12, "dot");
double s = a.sum(); // 6
CHECK(std::fabs(s - 6.0) < 1e-12, "sum");
double n = a.norm(); // sqrt(14)
CHECK(std::fabs(n - std::sqrt(14.0)) < 1e-12, "norm");
}
TEST_CASE(Vector_Normalize_and_Throws) {
utils::Vd v(3, 0.0);
v[0]=3.0; v[1]=4.0; v[2]=0.0; // norm = 5
auto u = v.normalize(); // returns new vector
CHECK(std::fabs(u.norm() - 1.0) < 1e-12, "normalize() unit length");
v.inplace_normalize();
CHECK(std::fabs(v.norm() - 1.0) < 1e-12, "inplace_normalize unit length");
utils::Vd z(3, 0.0);
bool threw=false;
try { z.inplace_normalize(); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "normalize should throw on zero vector");
}
// Size mismatch throws (elementwise ops)
TEST_CASE(Vector_Size_Mismatch_Throws) {
utils::Vi a(3,1), b(4,2);
bool threw=false;
try { (void)a.dot(b); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "dot size mismatch should throw");
threw=false;
try { a.inplace_add(b); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "add size mismatch should throw");
threw=false;
try { a.inplace_subtract(b); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "subtract size mismatch should throw");
threw=false;
try { a.inplace_multiply(b); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "multiply size mismatch should throw");
threw=false;
try { a.inplace_divide(b); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "divide size mismatch should throw");
threw=false;
try { a.inplace_power(b); } catch(const std::runtime_error&) { threw=true; }
CHECK(threw, "power size mismatch should throw");
}