Files
MeanField_SERiF/tests/surface/rejection.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

139 lines
7.9 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include "serif/tests/surface/contexts.hpp"
namespace {
using namespace serif;
using namespace serif::tests::surface;
using Linearization = physics::keys::Linearization;
struct NonFinitePartialEOS : GeneralEOS {
using GeneralEOS::partial_derivative;
/// An independently bad partial must reject the response even when the EOS value is finite.
[[nodiscard]] quantities::PartialDerivative<quantities::SpecificEnthalpy, ElectronFraction> partial_derivative(EnthalpyFromPressureAndComposition, quantities::WithRespectTo<ElectronFraction>, quantities::PressureValue, quantities::EntropyValue, quantities::QuantityValue<ElectronFraction>) const {
return quantities::PartialDerivative<quantities::SpecificEnthalpy, ElectronFraction>{std::numeric_limits<double>::infinity()};
}
};
}
TEST_CASE("Surface point validation rejects every required non-finite quantity before the EOS", "[surface][unit]") {
GeneralEOS model;
const physics::IsobaricSurface condition{quantities::PressureValue{0.4}};
for (const double invalid : {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()}) {
for (const auto& point : {general_point(invalid, 0.2, 0.1), general_point(1.7, invalid, 0.1), general_point(1.7, 0.2, invalid)}) {
const auto result = physics::evaluate_pressure_surface<GeneralFormulation>(model, condition, point);
REQUIRE_FALSE(result);
CHECK(result.error().reason == physics::SurfaceFailureReason::non_finite_state);
}
}
CHECK(model.evaluations == 0);
}
TEST_CASE("Surface rejects non-finite EOS results and partials without clipping them", "[surface][unit]") {
const physics::IsobaricSurface condition{quantities::PressureValue{0.4}};
SECTION("EOS value") {
const OffsetEOS model{.offset = std::numeric_limits<double>::infinity()};
using Formulation = physics::PressureSurfaceFormulation<OffsetEOS, BarotropicState>;
physics::SurfacePoint<Formulation> point{};
point.fields.get<Enthalpy>() = 0.4;
const auto result = physics::evaluate_pressure_surface<Formulation>(model, condition, point);
REQUIRE_FALSE(result);
CHECK(result.error().reason == physics::SurfaceFailureReason::non_finite_eos_response);
}
SECTION("EOS partial") {
using Formulation = physics::PressureSurfaceFormulation<NonFinitePartialEOS, GeneralState>;
const auto result = physics::evaluate_pressure_surface<Formulation>(NonFinitePartialEOS{}, condition, general_point(1.7, 0.2, 0.1));
REQUIRE_FALSE(result);
CHECK(result.error().reason == physics::SurfaceFailureReason::non_finite_eos_response);
}
SECTION("Residual subtraction overflows after a finite EOS result") {
const OffsetEOS model{.offset = -std::numeric_limits<double>::max()};
using Formulation = physics::PressureSurfaceFormulation<OffsetEOS, BarotropicState>;
physics::SurfacePoint<Formulation> point{};
point.fields.get<Enthalpy>() = std::numeric_limits<double>::max();
const auto result = physics::evaluate_pressure_surface<Formulation>(model, condition, point);
REQUIRE_FALSE(result);
CHECK(result.error().reason == physics::SurfaceFailureReason::non_finite_linearization);
}
SECTION("Finite negative carrier values retain their declared residual") {
using Formulation = physics::PressureSurfaceFormulation<OffsetEOS, BarotropicState>;
physics::SurfacePoint<Formulation> point{};
point.fields.get<Enthalpy>() = -0.2;
const auto result = physics::evaluate_pressure_surface<Formulation>(OffsetEOS{}, condition, point);
REQUIRE(result);
CHECK(result->residual == -0.2 - (0.3 + 2.0 * 0.4));
}
}
TEST_CASE("A rejected surface batch hides all previous results and permits a valid retry", "[surface][unit]") {
GeneralEOS model;
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(physics::make_pressure_surface_graph<GeneralFormulation>(), general_context(model, 0.4, {1.7, 1.9}, {0.2, 0.3}, {0.1, 0.2}));
REQUIRE(cache.try_prepare(general_context(model, 0.4, {1.7, 1.9}, {0.2, 0.3}, {0.1, 0.2})));
auto bad_enthalpy = std::vector<double>{1.7, 1.9};
auto bad_entropy = std::vector<double>{0.2, 0.3};
SECTION("Carrier rejection after a reusable EOS response") {
bad_enthalpy[1] = std::numeric_limits<double>::quiet_NaN();
}
SECTION("EOS rejection after the first sample has been evaluated") {
bad_entropy[1] = std::numeric_limits<double>::infinity();
}
const auto rejected = cache.try_prepare(general_context(model, 0.4, bad_enthalpy, bad_entropy, {0.1, 0.2}));
REQUIRE_FALSE(rejected);
CHECK(rejected.error().error.sample == 1);
CHECK_FALSE(cache.ready());
CHECK(cache.preparation_count() == 1);
CHECK_THROWS_AS(cache.get<Linearization>(), std::logic_error);
CHECK_THROWS_AS(cache.get<physics::keys::EOSResponse>(), std::logic_error);
REQUIRE(cache.try_prepare(general_context(model, 0.4, {1.8, 2.0}, {0.2, 0.3}, {0.1, 0.2})));
CHECK(cache.preparation_count() == 2);
REQUIRE(cache.get<Linearization>().size() == 2);
CHECK(cache.get<Linearization>()[0].residual == 1.8 - (2.0 * 0.4 + 3.0 * 0.2 + 5.0 * 0.1));
}
TEST_CASE("Surface graph rejects mismatched columns instead of broadcasting a state-dependent relation", "[surface][unit]") {
GeneralEOS model;
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(physics::make_pressure_surface_graph<GeneralFormulation>(), general_context(model, 0.4, {1.7, 1.9}, {0.2, 0.3}, {0.1, 0.2}));
SECTION("EOS columns disagree") {
const auto result = cache.try_prepare(general_context(model, 0.4, {1.7, 1.9}, {0.2, 0.3}, {0.1}));
REQUIRE_FALSE(result);
CHECK(result.error().error.reason == physics::SurfaceFailureReason::inconsistent_sample_count);
CHECK(model.evaluations == 0);
}
SECTION("A singleton state-dependent response is not a constant") {
const auto result = cache.try_prepare(general_context(model, 0.4, {1.7, 1.9}, {0.2}, {0.1}));
REQUIRE_FALSE(result);
CHECK(result.error().error.reason == physics::SurfaceFailureReason::inconsistent_sample_count);
}
SECTION("Empty EOS columns") {
const auto result = cache.try_prepare(general_context(model, 0.4, {}, {}, {}));
REQUIRE_FALSE(result);
CHECK(result.error().error.reason == physics::SurfaceFailureReason::empty_surface);
}
CHECK_FALSE(cache.ready());
REQUIRE(cache.try_prepare(general_context(model, 0.4, {1.7, 1.9}, {0.2, 0.3}, {0.1, 0.2})));
}
TEST_CASE("A pressure-only surface rejects an empty carrier batch", "[surface][unit]") {
OffsetEOS model;
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(physics::make_pressure_surface_graph<OffsetEOS, BarotropicState>(), offset_context(model, 0.0, {}));
const auto result = cache.try_prepare(offset_context(model, 0.0, {}));
REQUIRE_FALSE(result);
CHECK(result.error().error.reason == physics::SurfaceFailureReason::empty_surface);
CHECK_FALSE(cache.ready());
}
TEST_CASE("An existing EOS domain exception leaves the surface cache unavailable until recovery", "[surface][unit]") {
GasEOS model;
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(physics::make_pressure_surface_graph<GasFormulation>(), gas_context(model, 0.8, {0.4}, {1.0}));
REQUIRE(cache.try_prepare(gas_context(model, 0.8, {0.4}, {1.0})));
CHECK_THROWS_AS(cache.try_prepare(gas_context(model, 0.8, {0.4}, {0.0})), eos::EOSEvaluationError);
CHECK_FALSE(cache.ready());
CHECK_THROWS_AS(cache.get<Linearization>(), std::logic_error);
REQUIRE(cache.try_prepare(gas_context(model, 0.8, {0.4}, {2.0})));
CHECK(cache.get<Linearization>()[0].residual == 0.0);
}