Files
MeanField_SERiF/tests/model/scalar_row.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.1 KiB
C++

#include <limits>
#include <memory>
#include <optional>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "mfem/linalg/densemat.hpp"
#include "serif/backend/mfem/model/scalar_row.hpp"
#include "serif/backend/mfem/model/system.hpp"
TEST_CASE("A scalar residual row retains its exact functional without retaining or repeating the source action", "[model][scalar_row][unit]") {
namespace backend = serif::model::backend::mfem;
namespace execution = serif::operators::backend::mfem;
int forward_calls = 0, transpose_calls = 0;
std::optional<execution::LinearizedBlock> cached;
std::weak_ptr<int> lifetime;
{
auto owner = std::make_shared<int>(1);
lifetime = owner;
const auto source = execution::linearized_action(1, 3, [&, owner](const auto& input, auto& output) {
++forward_calls;
output[0] = 2.0 * input[0] - 3.0 * input[1] + 5.0 * input[2];
}, [&, owner](const auto& input, auto& output) {
++transpose_calls;
output[0] = 2.0 * input[0];
output[1] = -3.0 * input[0];
output[2] = 5.0 * input[0];
});
auto result = backend::cache_scalar_row(source);
REQUIRE(result);
cached.emplace(std::move(*result));
}
CHECK(lifetime.expired());
CHECK(forward_calls == 0);
CHECK(transpose_calls == 1);
const auto system = backend::make_physical_system(1, 3, {}, {{0, 0, *cached}});
mfem::Vector direction(3), action, transpose, dual(1);
direction[0] = 7.0;
direction[1] = 11.0;
direction[2] = 13.0;
dual[0] = 0.25;
system->jacobian().action()->Mult(direction, action);
CHECK(action[0] == Catch::Approx(46.0));
system->jacobian().action()->MultTranspose(dual, transpose);
CHECK(transpose[0] == Catch::Approx(0.5));
CHECK(transpose[1] == Catch::Approx(-0.75));
CHECK(transpose[2] == Catch::Approx(1.25));
const auto selected = system->block(0, 1, 1, 2);
mfem::Vector selected_direction(direction.GetData() + 1, 2);
selected.action()->Mult(selected_direction, action);
CHECK(action[0] == Catch::Approx(32.0));
const auto matrix = selected.assemble();
CHECK((*matrix)(0, 0) == Catch::Approx(-3.0));
CHECK((*matrix)(0, 1) == Catch::Approx(5.0));
CHECK(forward_calls == 0);
CHECK(transpose_calls == 1);
}
TEST_CASE("Scalar row caching rejects a spatial residual without invoking its transpose", "[model][scalar_row][unit]") {
namespace execution = serif::operators::backend::mfem;
int calls = 0;
const auto source = execution::linearized_action(2, 3, [](const auto&, auto&) {}, [&](const auto&, auto&) {
++calls;
});
CHECK_THROWS(serif::model::backend::mfem::cache_scalar_row(source));
CHECK(calls == 0);
}
TEST_CASE("Scalar row caching preserves numerical failure classification for nonfinite derivatives", "[model][scalar_row][unit]") {
namespace execution = serif::operators::backend::mfem;
for (const double value : {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()}) {
auto matrix = std::make_shared<mfem::DenseMatrix>(1, 1);
(*matrix)(0, 0) = value;
// A raw matrix deliberately bypasses the checked callback adapter to exercise the cache guard.
const auto source = execution::linearized_block(std::shared_ptr<const mfem::DenseMatrix>(matrix));
const auto raw_result = serif::model::backend::mfem::cache_scalar_row(source);
REQUIRE_FALSE(raw_result);
CHECK(raw_result.error().reason == execution::PreparationRejectionReason::NON_FINITE);
const auto checked = execution::linearized_action(1, 1, [source](const auto& input, auto& output) {
source.action()->Mult(input, output);
}, [source](const auto& input, auto& output) {
source.action()->MultTranspose(input, output);
});
const auto checked_result = serif::model::backend::mfem::cache_scalar_row(checked);
REQUIRE_FALSE(checked_result);
CHECK(checked_result.error().reason == execution::PreparationRejectionReason::NON_FINITE);
}
}