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.
139 lines
7.9 KiB
C++
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);
|
|
}
|