Files
MeanField_SERiF/tests/preconditioner/material.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

90 lines
4.0 KiB
C++

#include <array>
#include <memory>
#include <span>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "mfem/linalg/densemat.hpp"
#include "serif/backend/mfem/solver/preconditioner/coupling.hpp"
#include "serif/backend/mfem/solver/preconditioner/inverse/diagonal.hpp"
namespace
{
namespace backend = serif::solver::preconditioner::backend::mfem;
using CouplingOrder = serif::solver::preconditioner::strategies::coupling::CouplingOrder;
/** @brief Build a small independent physical block from row-major entries. */
[[nodiscard]] backend::OperatorHandle material_block(int rows, int columns, std::span<const double> entries)
{
auto matrix = std::make_shared<::mfem::DenseMatrix>(rows, columns);
for (int row = 0; row < rows; ++row)
{
for (int column = 0; column < columns; ++column)
{
(*matrix)(row, column) = entries[row * columns + column];
}
}
return matrix;
}
/** @brief Use the production diagonal inverse for each independently scaled material component. */
[[nodiscard]] backend::Inverse scalar_inverse(double diagonal)
{
const auto inverse = backend::try_diagonal_inverse(std::span<const double>{&diagonal, 1});
REQUIRE(inverse);
return *inverse;
}
} // namespace
TEST_CASE("Material surface policies compose the same two-block inverse with signed triangular algebra", "[unit][preconditioner][material]")
{
const backend::TwoBlockCouplings material_couplings{.first_from_second = material_block(1, 1, std::array{4.0})};
const backend::TwoBlockCouplings surface_couplings{.first_from_second = material_block(2, 1, std::array{3.0, 8.0}), .second_from_first = material_block(1, 2, std::array{5.0, 7.0})};
const auto material = backend::try_coupled_inverse(scalar_inverse(2.0), scalar_inverse(9.0), material_couplings, CouplingOrder::second_then_first);
REQUIRE(material);
const auto diagonal_material = backend::try_coupled_inverse(scalar_inverse(2.0), scalar_inverse(9.0), {}, CouplingOrder::independent);
REQUIRE(diagonal_material);
// Each subsystem keeps its own native order: material=(density, enthalpy), followed by surface.
::mfem::Vector right_hand_side(3), actual(3);
right_hand_side[0] = 29.0;
right_hand_side[1] = 43.0;
right_hand_side[2] = 44.0;
const double enthalpy = 43.0 / 9.0;
const double density = (29.0 - 4.0 * enthalpy) / 2.0;
const double surface = 44.0 / 6.0;
const double corrected_enthalpy = (43.0 - 8.0 * surface) / 9.0;
const double ldu_surface = (44.0 - 5.0 * density - 7.0 * enthalpy) / 6.0;
const double ldu_enthalpy = (43.0 - 8.0 * ldu_surface) / 9.0;
struct PolicyCase
{
backend::Inverse material;
CouplingOrder order;
std::array<double, 3> expected;
};
const std::array cases{
PolicyCase{*diagonal_material, CouplingOrder::independent, {14.5, enthalpy, surface}},
PolicyCase{*material, CouplingOrder::independent, {density, enthalpy, surface}},
PolicyCase{*material, CouplingOrder::second_then_first, {(29.0 - 3.0 * surface - 4.0 * corrected_enthalpy) / 2.0, corrected_enthalpy, surface}},
PolicyCase{*material, CouplingOrder::lower_diagonal_upper, {(29.0 - 3.0 * ldu_surface - 4.0 * ldu_enthalpy) / 2.0, ldu_enthalpy, ldu_surface}}};
for (const PolicyCase& policy : cases)
{
const auto inverse = backend::try_coupled_inverse(policy.material, scalar_inverse(6.0), surface_couplings, policy.order);
REQUIRE(inverse);
actual = 7.0;
inverse->action->Mult(right_hand_side, actual);
for (int index = 0; index < 3; ++index)
{
CHECK(actual[index] == Catch::Approx(policy.expected[index]).margin(2e-13));
}
::mfem::Vector zero(3);
zero = 0.0;
inverse->action->Mult(zero, actual);
CHECK(actual.Norml2() == 0.0);
}
const auto schur = backend::try_schur_operator(material_block(1, 1, std::array{6.0}), *material, surface_couplings);
REQUIRE(schur);
}