test(tests): added test suite

The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

View File

@@ -0,0 +1,150 @@
#include <array>
#include <limits>
#include <memory>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "mfem/linalg/densemat.hpp"
#include "serif/backend/mfem/solver/preconditioner/border.hpp"
#include "serif/backend/mfem/solver/preconditioner/inverse/dense.hpp"
namespace
{
namespace backend = serif::solver::preconditioner::backend::mfem;
using serif::solver::preconditioner::ApplicationContract;
using serif::solver::preconditioner::strategies::coupling::CouplingOrder;
/** @brief Construct a small row-major matrix independently of the implementation's block algebra. */
[[nodiscard]] std::shared_ptr<mfem::DenseMatrix> matrix(int height, int width, std::initializer_list<double> entries)
{
REQUIRE(entries.size() == static_cast<std::size_t>(height * width));
auto result = std::make_shared<mfem::DenseMatrix>(height, width);
auto entry = entries.begin();
for (int row = 0; row < height; ++row)
{
for (int column = 0; column < width; ++column)
{
(*result)(row, column) = *entry++;
}
}
return result;
}
/** @brief Compare complete numerical actions without ignoring small absolute discrepancies. */
[[nodiscard]] double difference(const mfem::Vector& left, const mfem::Vector& right)
{
mfem::Vector result(left);
result -= right;
return result.Norml2();
}
} // namespace
TEST_CASE("Two-group coupling policies reproduce their complete nonsymmetric factorizations", "[preconditioner][coupling][unit]")
{
const backend::Inverse first{.action = matrix(2, 2, {0.5, 0.0, 0.0, 0.25})};
const backend::Inverse second{.action = matrix(2, 2, {0.2, 0.0, 0.0, 0.1})};
const backend::TwoBlockCouplings couplings{.first_from_second = matrix(2, 2, {1, 2, -1, 3}), .second_from_first = matrix(2, 2, {3, -2, 4, 1})};
const std::array orders{CouplingOrder::independent, CouplingOrder::second_then_first, CouplingOrder::lower_diagonal_upper};
const std::array systems{
matrix(4, 4, {2, 0, 0, 0, 0, 4, 0, 0, 0, 0, 5, 0, 0, 0, 0, 10}),
matrix(4, 4, {2, 0, 1, 2, 0, 4, -1, 3, 0, 0, 5, 0, 0, 0, 0, 10}),
matrix(4, 4, {2, 0, 1, 2, 0, 4, -1, 3, 3, -2, 7, 1.5, 4, 1, 1.75, 14.75})};
mfem::Vector rhs(4), actual(4), expected(4);
rhs[0] = 1.0;
rhs[1] = -2.0;
rhs[2] = 3.0;
rhs[3] = 4.0;
for (std::size_t policy = 0; policy < orders.size(); ++policy)
{
CAPTURE(policy);
auto inverse = backend::try_coupled_inverse(first, second, couplings, orders[policy]);
REQUIRE(inverse);
CHECK(inverse->contract == ApplicationContract::fixed_linear);
mfem::DenseMatrixInverse exact(*systems[policy]);
exact.Mult(rhs, expected);
double* const storage = actual.GetData();
inverse->action->Mult(rhs, actual);
CHECK(actual.GetData() == storage);
CHECK(difference(actual, expected) < 2.0e-13);
mfem::Vector aliased(rhs);
inverse->action->Mult(aliased, aliased);
CHECK(difference(aliased, expected) < 2.0e-13);
inverse->action->Mult(rhs, actual);
CHECK(difference(actual, expected) < 2.0e-13);
}
}
TEST_CASE("A matrix-free Schur action shares the eliminated-field orientation", "[preconditioner][coupling][unit]")
{
const backend::Inverse eliminated{.action = matrix(2, 2, {0.5, 0.0, 0.0, 0.25})};
const backend::TwoBlockCouplings couplings{.first_from_second = matrix(2, 2, {1, 2, -1, 3}), .second_from_first = matrix(2, 2, {3, -2, 4, 1})};
auto schur = backend::try_schur_operator(matrix(2, 2, {7, 1.5, 1.75, 14.75}), eliminated, couplings);
REQUIRE(schur);
mfem::Vector values(2);
values[0] = 3.0;
values[1] = -2.0;
(*schur)->Mult(values, values);
CHECK(values[0] == Catch::Approx(15.0).margin(2.0e-14));
CHECK(values[1] == Catch::Approx(-20.0).margin(2.0e-14));
backend::Inverse variable = eliminated;
variable.contract = ApplicationContract::variable;
CHECK_FALSE(backend::try_schur_operator(matrix(2, 2, {7, 1.5, 1.75, 14.75}), variable, couplings));
}
TEST_CASE("Coupled inverse validation rejects unsupported policies and preserves outputs", "[preconditioner][coupling][validation]")
{
using serif::utils::errors::SERiFErrorCode;
const backend::Inverse identity{.action = matrix(1, 1, {1.0})};
const backend::TwoBlockCouplings full{.first_from_second = matrix(1, 1, {2.0}), .second_from_first = matrix(1, 1, {3.0})};
CHECK(backend::try_coupled_inverse(identity, identity, {}, CouplingOrder::independent));
CHECK(backend::try_coupled_inverse(identity, identity, {.first_from_second = full.first_from_second}, CouplingOrder::second_then_first));
auto missing = backend::try_coupled_inverse({}, identity, full, CouplingOrder::independent);
REQUIRE_FALSE(missing);
CHECK(missing.error().code == SERiFErrorCode::PRECONDITIONER_INVALID_OPERATOR);
auto incompatible = backend::try_coupled_inverse(identity, identity, {}, CouplingOrder::lower_diagonal_upper);
REQUIRE_FALSE(incompatible);
CHECK(incompatible.error().code == SERiFErrorCode::PRECONDITIONER_SIZE_MISMATCH);
auto unknown = backend::try_coupled_inverse(identity, identity, full, static_cast<CouplingOrder>(99));
REQUIRE_FALSE(unknown);
CHECK(unknown.error().code == SERiFErrorCode::PRECONDITIONER_INVALID_OPTIONS);
backend::Inverse variable = identity;
variable.contract = ApplicationContract::variable;
auto composed = backend::try_coupled_inverse(identity, variable, {}, CouplingOrder::independent);
REQUIRE(composed);
CHECK(composed->contract == ApplicationContract::variable);
mfem::Vector rhs(2), output(2), short_rhs(1);
rhs = 1.0;
output = 7.0;
CHECK_THROWS(composed->action->Mult(short_rhs, output));
CHECK(output[0] == 7.0);
CHECK(output[1] == 7.0);
rhs[1] = std::numeric_limits<double>::quiet_NaN();
CHECK_THROWS(composed->action->Mult(rhs, output));
CHECK(output[0] == 7.0);
CHECK(output[1] == 7.0);
}
TEST_CASE("Selected operators preserve distinct equation and unknown orders and their transpose", "[preconditioner][coupling][coordinates]")
{
const auto source = matrix(3, 4, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12});
const std::array rows{2, 0};
const std::array columns{3, 1};
auto selected = backend::try_restrict_operator(source, rows, columns);
REQUIRE(selected);
mfem::Vector values(2);
values[0] = 2.0;
values[1] = -1.0;
(*selected)->Mult(values, values);
CHECK(values[0] == 14.0);
CHECK(values[1] == 6.0);
values[0] = 2.0;
values[1] = -1.0;
(*selected)->MultTranspose(values, values);
CHECK(values[0] == 20.0);
CHECK(values[1] == 18.0);
CHECK_FALSE(backend::try_restrict_operator(source, std::array{2, 2}, columns));
CHECK_FALSE(backend::try_restrict_operator(source, std::array{3}, columns));
CHECK_FALSE(backend::try_restrict_operator(source, rows, std::array{-1}));
}