#include #include #include #include #include #include #include "mfem/linalg/densemat.hpp" #include "serif/backend/mfem/solver/preconditioner/border.hpp" #include "serif/backend/mfem/solver/normalization/operators.hpp" namespace { namespace backend = serif::solver::preconditioner::backend::mfem; using serif::solver::preconditioner::ApplicationContract; /** @brief Build independent dense examples in the legacy tests' row-major notation. */ [[nodiscard]] mfem::DenseMatrix matrix(int size, std::initializer_list entries) { REQUIRE(entries.size() == static_cast(size * size)); mfem::DenseMatrix result(size); auto entry = entries.begin(); for (int row = 0; row < size; ++row) { for (int column = 0; column < size; ++column) { result(row, column) = *entry++; } } return result; } /** @brief Keep test vectors explicit without relying on MFEM initializer-list support. */ [[nodiscard]] mfem::Vector vector(std::initializer_list values) { mfem::Vector result(static_cast(values.size())); int index = 0; for (double value : values) { result[index++] = value; } return result; } /** @brief Compare complete actions by their absolute error norm. */ [[nodiscard]] double difference(const mfem::Vector& left, const mfem::Vector& right) { mfem::Vector result(left); result -= right; return result.Norml2(); } /** @brief Count physical work while retaining independently specified dense actions. */ class CountedOperator final : public mfem::Operator { public: /** @brief Freeze the coefficients while allowing failure and work-count instrumentation. */ explicit CountedOperator(mfem::DenseMatrix coefficients) : Operator(coefficients.Height(), coefficients.Width()), coefficients(std::move(coefficients)) { } /** @brief Apply the exact dense action and optionally simulate a rejected numerical callback. */ void Mult(const mfem::Vector& input, mfem::Vector& output) const override { ++applications; coefficients.Mult(input, output); if (nonfinite) { output[0] = std::numeric_limits::infinity(); } } mfem::DenseMatrix coefficients; mutable int applications{0}; bool nonfinite{false}; }; /** @brief A nonlinear inner solve exposes the difference between cached linear responses and fresh solves. */ class VariableInverse final : public mfem::Operator { public: /** @brief Use one unknown so the independently derived correction is unambiguous. */ VariableInverse() : Operator(1) { } /** @brief Model a right-hand-side-dependent inverse with a known analytic response. */ void Mult(const mfem::Vector& input, mfem::Vector& output) const override { ++applications; output[0] = evaluate(input[0]); } /** @brief Evaluate the independent scalar nonlinear response. */ [[nodiscard]] static double evaluate(double value) { return value / (2.0 + std::abs(value)); } mutable int applications{0}; }; } // namespace TEST_CASE("Dense borders preserve exact factorizations and stationary response work counts", "[preconditioner][border][unit]") { // MeanField specification_border.cpp:845 retains one, two, and four generated scalars and both preparations. for (const int count : {1, 2, 4}) { CAPTURE(count); for (const int generation : {0, 1}) { CAPTURE(generation); auto fields = std::make_shared(matrix(3, {1.0 / (2 + 0.25 * generation), 0, 0, 0, 1.0 / (3 + 0.5 * generation), 0, 0, 0, 1.0 / (5 + 0.75 * generation)})); auto forward = std::make_shared(3, count); auto reverse = std::make_shared(count, 3); auto diagonal = std::make_shared(count); mfem::DenseMatrix complete(3 + count); complete = 0.0; complete(0, 0) = 2 + 0.25 * generation; complete(1, 1) = 3 + 0.5 * generation; complete(2, 2) = 5 + 0.75 * generation; for (int row = 0; row < count; ++row) { for (int column = 0; column < count; ++column) { (*diagonal)(row, column) = row == column ? 2.0 + row + 0.5 * generation : 0.01 * (row + column + 1); complete(3 + row, 3 + column) = (*diagonal)(row, column); } for (int column = 0; column < 3; ++column) { (*forward)(column, row) = -0.03 * ((column + 1) * (row + 2)); (*reverse)(row, column) = 0.04 * ((row + 1) * (column + 2)); complete(column, 3 + row) = (*forward)(column, row); complete(3 + row, column) = (*reverse)(row, column); } } auto prepared = backend::try_bordered_inverse({.action = fields}, {.first_from_second = forward, .second_from_first = reverse}, diagonal); REQUIRE(prepared); CHECK(fields->applications == count); mfem::Vector rhs(3 + count), actual(3 + count), expected(3 + count); for (int index = 0; index < rhs.Size(); ++index) { rhs[index] = 0.25 + 0.17 * (index + 1); } mfem::DenseMatrixInverse exact(complete); exact.Mult(rhs, expected); prepared->action->Mult(rhs, actual); CHECK(difference(actual, expected) / std::max(1.0, expected.Norml2()) <= 2.0e-13); CHECK(fields->applications == count + 1); prepared->action->Mult(rhs, rhs); CHECK(difference(rhs, expected) / std::max(1.0, expected.Norml2()) <= 2.0e-13); CHECK(fields->applications == count + 2); if (count == 2) { auto flexible = backend::try_bordered_inverse({.action = fields, .contract = ApplicationContract::variable}, {.first_from_second = forward, .second_from_first = reverse}, diagonal); REQUIRE(flexible); const int before = fields->applications; flexible->action->Mult(vector({0.42, 0.59, 0.76, 0.93, 1.10}), actual); CHECK(difference(actual, expected) / std::max(1.0, expected.Norml2()) <= 2.0e-13); CHECK(fields->applications == before + 2); } } } } TEST_CASE("Variable border inverses recompute the corrected field solve", "[preconditioner][border][unit]") { const auto fields = std::make_shared(); const auto forward = std::make_shared(matrix(1, {0.75})); const auto reverse = std::make_shared(matrix(1, {1.25})); const auto diagonal = std::make_shared(matrix(1, {4.0})); auto prepared = backend::try_bordered_inverse({.action = fields, .contract = ApplicationContract::variable}, {.first_from_second = forward, .second_from_first = reverse}, diagonal); REQUIRE(prepared); CHECK(prepared->contract == ApplicationContract::variable); CHECK(fields->applications == 1); mfem::Vector rhs = vector({2.5, -0.3}); mfem::Vector actual(2); const double response = VariableInverse::evaluate(0.75); const double first = VariableInverse::evaluate(rhs[0]); const double border = (rhs[1] - 1.25 * first) / (4.0 - 1.25 * response); const double corrected = VariableInverse::evaluate(rhs[0] - 0.75 * border); prepared->action->Mult(rhs, actual); CHECK(fields->applications == 3); CHECK(actual[0] == Catch::Approx(corrected).margin(2.0e-14)); CHECK(actual[1] == Catch::Approx(border).margin(2.0e-14)); CHECK(std::abs(actual[0] - (first - response * border)) > 1.0e-3); } TEST_CASE("An empty border preserves the original field inverse", "[preconditioner][border][unit]") { const backend::Inverse fields{.action = std::make_shared(matrix(1, {0.5}))}; auto prepared = backend::try_bordered_inverse(fields, {.first_from_second = std::make_shared(1, 0), .second_from_first = std::make_shared(0, 1)}, std::make_shared(0)); REQUIRE(prepared); CHECK(prepared->action == fields.action); } TEST_CASE("Unbordered field selection preserves nonunit normalization and typed packing", "[preconditioner][border][coordinates]") { namespace normalization = serif::solver::normalization; namespace scaled = normalization::backend::mfem; // Original eight-coordinate fixture: three uneven field groups and two excluded scalar borders. auto packed = std::make_shared(8); *packed = 0.0; for (int index = 0; index < 6; ++index) { (*packed)(index, index) = index + 2.0; } (*packed)(0, 5) = 0.4; (*packed)(4, 1) = -0.6; const std::array output_order{5, 2, 3, 4, 6, 0, 1, 7}; const std::array input_order{4, 5, 1, 2, 3, 6, 0, 7}; auto physical = backend::try_restrict_operator(packed, output_order, input_order); REQUIRE(physical); auto factors = normalization::make_diagonal_normalization({1.0 / 23, 1.0 / 29, 1.0 / 31, 1.0 / 37, 1.0 / 41, 1.0 / 43, 1.0 / 47, 1.0 / 53}, {0.5, 1.0 / 3, 0.2, 1.0 / 7, 1.0 / 11, 1.0 / 13, 1.0 / 17, 1.0 / 19}); REQUIRE(factors); scaled::ScaledInverseOperator inverse(**physical, *factors); mfem::Vector rhs = vector({1, 2, 3, 4, 5, 6, 7, 8}); const mfem::Vector expected = vector({42.0 / 23, 112.0 / 29, 275.0 / 31, 3.0 / 37, 0, 240.4 / 43, 45.0 / 47, 0}); mfem::Vector actual(8); inverse.Mult(rhs, actual); CHECK(difference(actual, expected) < 1.0e-13); inverse.Mult(rhs, rhs); CHECK(difference(rhs, expected) < 1.0e-13); }