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.
226 lines
10 KiB
C++
226 lines
10 KiB
C++
#include <array>
|
|
#include <cmath>
|
|
#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/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<double> entries)
|
|
{
|
|
REQUIRE(entries.size() == static_cast<std::size_t>(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<double> values)
|
|
{
|
|
mfem::Vector result(static_cast<int>(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<double>::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<CountedOperator>(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<mfem::DenseMatrix>(3, count);
|
|
auto reverse = std::make_shared<mfem::DenseMatrix>(count, 3);
|
|
auto diagonal = std::make_shared<mfem::DenseMatrix>(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<VariableInverse>();
|
|
const auto forward = std::make_shared<mfem::DenseMatrix>(matrix(1, {0.75}));
|
|
const auto reverse = std::make_shared<mfem::DenseMatrix>(matrix(1, {1.25}));
|
|
const auto diagonal = std::make_shared<mfem::DenseMatrix>(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<mfem::DenseMatrix>(matrix(1, {0.5}))};
|
|
auto prepared = backend::try_bordered_inverse(fields, {.first_from_second = std::make_shared<mfem::DenseMatrix>(1, 0), .second_from_first = std::make_shared<mfem::DenseMatrix>(0, 1)}, std::make_shared<mfem::DenseMatrix>(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<mfem::DenseMatrix>(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);
|
|
}
|