Files
MeanField_SERiF/tests/solver/linear_overflow.cpp
Emily Boudreaux cb20faeb3d 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.
2026-11-09 13:53:17 -05:00

100 lines
5.2 KiB
C++

#include <limits>
#include <memory>
#include <vector>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "mfem/fem/fe_coll.hpp"
#include "serif/backend/mfem/discretization/dof_map.hpp"
#include "serif/backend/mfem/solver/linear/solve.hpp"
#include "serif/backend/mfem/solver/normalization/operators.hpp"
#include "serif/backend/mfem/operators/actions.hpp"
#include "serif/backend/mfem/geometry/surface/boundary_rows.hpp"
namespace {
/** @brief Select every serial DOF through the same map used by production term bindings. */
class FullBinding : public serif::discretization::backend::mfem::DofMap {
/** @brief Use all elements of the test domain. */
[[nodiscard]] static ::mfem::Array<int> support(const ::mfem::FiniteElementSpace& space) {
::mfem::Array<int> supported(space.GetNE());
supported = 1;
return supported;
}
public:
/** @brief Borrow the immutable finite-element space. */
explicit FullBinding(::mfem::FiniteElementSpace& space) : DofMap(space, support(space)) {}
};
struct Fixture {
::mfem::Mesh mesh{::mfem::Mesh::MakeCartesian3D(2, 2, 1, ::mfem::Element::HEXAHEDRON)};
::mfem::H1_FECollection collection{3, 3};
::mfem::FiniteElementSpace space{&mesh, &collection};
FullBinding binding{space};
};
/** @brief Exercise the real physical DOF input check before the outer action adapter can inspect its output. */
class CheckedPhysicalIdentity final : public ::mfem::Operator {
public:
/** @brief Borrow the same supported finite-element binding used by field actions. */
explicit CheckedPhysicalIdentity(const FullBinding& binding) : ::mfem::Operator(binding.size()), m_binding(binding) {}
/** @brief Validate dimensional coefficients at the physical action boundary. */
void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override {
m_binding.validate_values(input);
output = input;
}
private:
const FullBinding& m_binding;
};
/** @brief Keep this failure-classification test independent of inverse conditioning. */
class IdentityInverse final : public ::mfem::Solver {
public:
/** @brief Match the physical coefficient count. */
explicit IdentityInverse(int size) : ::mfem::Solver(size) {}
/** @brief The identity inverse does not store a borrowed operator. */
void SetOperator(const ::mfem::Operator&) override {}
/** @brief Apply the unchanged vector. */
void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override { output = input; }
};
}
TEST_CASE("Checked Krylov classifies physical DOF and pressure-boundary overflow without hiding shape errors", "[solver][linear][overflow]") {
namespace solver = serif::solver;
Fixture fixture;
IdentityInverse inverse(fixture.binding.size());
::mfem::Vector rhs(fixture.binding.size()), correction(rhs.Size());
rhs = 0.0;
rhs[0] = 1.0;
SECTION("Overflowing a finite recycled guess inside physical normalization resets it to zero") {
const CheckedPhysicalIdentity physical(fixture.binding);
const double tiny = std::numeric_limits<double>::min();
const auto coordinates = solver::normalization::make_diagonal_normalization(std::vector(rhs.Size(), tiny), std::vector(rhs.Size(), tiny));
REQUIRE(coordinates);
solver::normalization::backend::mfem::ScaledJacobianOperator normalized(physical, *coordinates);
correction = 1.0e100;
const auto report = solver::linear::backend::mfem::solve({}, normalized, inverse,
solver::preconditioner::ApplicationContract::fixed_linear, rhs, correction);
REQUIRE(report.converged());
CHECK(report.initial_residual_norm == 1.0);
CHECK(correction[0] == Catch::Approx(1.0));
}
SECTION("A finite boundary derivative producing infinity becomes a numerical failure report") {
struct Boundary { std::shared_ptr<const serif::discretization::backend::mfem::DofSelection> selected_rows; };
::mfem::Array<int> rows{0};
const Boundary boundary{std::make_shared<const serif::discretization::backend::mfem::DofSelection>(fixture.binding, rows)};
const auto action = [boundary](const auto& input, auto& output) {
output = input;
::mfem::Vector values(1);
values[0] = input[0] * std::numeric_limits<double>::max();
values[0] *= std::numeric_limits<double>::max();
serif::geometry::surface::backend::mfem::detail::replace_boundary_rows(boundary, values, output);
};
const auto operation = serif::operators::backend::mfem::linearized_action(rhs.Size(), rhs.Size(), action, action);
correction = 0.0;
const auto report = solver::linear::backend::mfem::solve({}, *operation.action(), inverse,
solver::preconditioner::ApplicationContract::fixed_linear, rhs, correction);
CHECK(report.status == solver::linear::Status::non_finite);
}
::mfem::Vector wrong_size(1);
wrong_size = 0.0;
CHECK_THROWS_AS(fixture.binding.validate_values(wrong_size), std::invalid_argument);
}