Files
MeanField_SERiF/tests/model/normalized_system.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

116 lines
6.1 KiB
C++

#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "mfem/linalg/densemat.hpp"
#include "serif/backend/mfem/model/normalized_system.hpp"
#include "serif/utils/dependencies/computation_cache.hpp"
#include "serif/backend/mfem/model/normalization/computations.hpp"
namespace {
namespace model = serif::model;
namespace backend = model::backend::mfem;
namespace dependencies = serif::utils::dependencies;
namespace normalization = serif::solver::normalization;
namespace dimensions = serif::dimensions;
struct PhysicalResidual {};
struct ResidualFactor {};
/** @brief Give the frozen normalization adapter an independently owned physical action. */
[[nodiscard]] std::shared_ptr<const backend::PhysicalSystem> physical_system(double value) {
auto matrix = std::make_shared<mfem::DenseMatrix>(1);
(*matrix)(0, 0) = 3.0 + value;
const auto block = serif::operators::backend::mfem::linearized_block(std::shared_ptr<const mfem::DenseMatrix>(matrix));
return backend::make_physical_system(1, 1, {{.row_offset = 0, .values = {value}}}, {{.row_offset = 0, .column_offset = 0, .block = block}});
}
/** @brief Keep a valid reference fixed while failure is induced only in coordinate conversion. */
[[nodiscard]] normalization::StellarReferenceScales reference_scales() {
const auto scales = normalization::prepare_stellar_reference_scales(dimensions::MassValue{2.0}, dimensions::LengthValue{3.0});
REQUIRE(scales);
return *scales;
}
}
TEST_CASE("A normalization overflow rejects the new physical preparation without losing frozen solver coordinates", "[model][normalization][rejection]") {
using namespace dependencies;
double value = 1.0;
double factor = 2.0;
int physical_preparations = 0;
const auto scales = reference_scales();
const auto inputs = make_dependency_context(borrow<PhysicalResidual>(value), borrow<ResidualFactor>(factor), borrow<normalization::keys::Scales>(scales));
const auto physical = make_computation<model::keys::PhysicalSystem, InputList<PhysicalResidual>>([&](double residual) {
++physical_preparations;
return physical_system(residual);
});
const auto coordinates = make_computation<normalization::keys::Normalization, InputList<ResidualFactor>>([](double residual_factor) {
return normalization::make_diagonal_normalization({3.0}, {residual_factor});
});
const auto normalized = make_computation<model::normalization::PreparedSystem,
InputList<model::keys::PhysicalSystem, normalization::keys::Normalization, normalization::keys::Scales>>(backend::make_normalized_system);
auto graph = make_computation_graph(physical, coordinates, normalized);
auto cache = make_computation_cache(std::move(graph), inputs);
using Requested = KeyList<model::normalization::PreparedSystem>;
REQUIRE(cache.try_prepare(inputs, Requested{}));
const auto physical_snapshot = cache.get<model::keys::PhysicalSystem>();
const auto frozen = cache.template get<model::normalization::PreparedSystem>();
REQUIRE(frozen->physical() == physical_snapshot);
CHECK(frozen->residual() == std::vector<double>{2.0});
mfem::Vector direction(1), original_action, repeated_action;
direction[0] = 1.0;
frozen->jacobian().Mult(direction, original_action);
CHECK(original_action[0] == Catch::Approx(8.0 / 3.0));
const auto preparations = cache.preparation_count();
value = 2.0;
factor = std::numeric_limits<double>::max();
const auto rejected = cache.try_prepare(inputs, Requested{});
REQUIRE_FALSE(rejected);
CHECK_THAT(rejected.error().error, Catch::Matchers::ContainsSubstring("normalized stellar residual is not finite"));
CHECK(physical_preparations == 2);
CHECK(frozen->physical() == physical_snapshot);
CHECK_FALSE(cache.ready());
CHECK_FALSE(cache.template contains<model::keys::PhysicalSystem>());
CHECK_FALSE(cache.template contains<model::normalization::PreparedSystem>());
CHECK(cache.report().computed_count() == 0);
CHECK(cache.report().reused_count() == 0);
CHECK(cache.preparation_count() == preparations);
CHECK(frozen->residual() == std::vector<double>{2.0});
frozen->jacobian().Mult(direction, repeated_action);
CHECK(repeated_action[0] == original_action[0]);
value = 1.0;
factor = 2.0;
REQUIRE(cache.try_prepare(inputs, Requested{}));
CHECK(cache.get<model::keys::PhysicalSystem>() == physical_snapshot);
CHECK(cache.template get<model::normalization::PreparedSystem>() == frozen);
CHECK(cache.report().computed_count() == 0);
value = 2.0;
REQUIRE(cache.try_prepare(inputs, Requested{}));
CHECK(cache.get<model::keys::PhysicalSystem>() != physical_snapshot);
CHECK(cache.template get<model::normalization::PreparedSystem>()->residual() == std::vector<double>{4.0});
CHECK(frozen->residual() == std::vector<double>{2.0});
}
TEST_CASE("Normalized system construction diagnoses missing owners and incompatible coordinates", "[model][normalization][validation]") {
const auto factors = normalization::make_diagonal_normalization({1.0}, {2.0});
REQUIRE(factors);
CHECK_THROWS_WITH(backend::make_normalized_system({}, *factors, reference_scales()), Catch::Matchers::ContainsSubstring("physical system"));
const auto wrong_state = normalization::make_diagonal_normalization({1.0, 1.0}, {2.0});
const auto wrong_residual = normalization::make_diagonal_normalization({1.0}, {2.0, 2.0});
REQUIRE(wrong_state);
REQUIRE(wrong_residual);
CHECK_THROWS_AS(backend::make_normalized_system(physical_system(1.0), *wrong_state, reference_scales()), std::invalid_argument);
CHECK_THROWS_AS(backend::make_normalized_system(physical_system(1.0), *wrong_residual, reference_scales()), std::invalid_argument);
const auto excessive = normalization::make_diagonal_normalization({1.0}, {std::numeric_limits<double>::max()});
REQUIRE(excessive);
CHECK_FALSE(backend::make_normalized_system(physical_system(2.0), *excessive, reference_scales()));
}