#include #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 partial_derivative(EnthalpyFromPressureAndComposition, quantities::WithRespectTo, quantities::PressureValue, quantities::EntropyValue, quantities::QuantityValue) const { return quantities::PartialDerivative{std::numeric_limits::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::quiet_NaN(), std::numeric_limits::infinity(), -std::numeric_limits::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(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::infinity()}; using Formulation = physics::PressureSurfaceFormulation; physics::SurfacePoint point{}; point.fields.get() = 0.4; const auto result = physics::evaluate_pressure_surface(model, condition, point); REQUIRE_FALSE(result); CHECK(result.error().reason == physics::SurfaceFailureReason::non_finite_eos_response); } SECTION("EOS partial") { using Formulation = physics::PressureSurfaceFormulation; const auto result = physics::evaluate_pressure_surface(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::max()}; using Formulation = physics::PressureSurfaceFormulation; physics::SurfacePoint point{}; point.fields.get() = std::numeric_limits::max(); const auto result = physics::evaluate_pressure_surface(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; physics::SurfacePoint point{}; point.fields.get() = -0.2; const auto result = physics::evaluate_pressure_surface(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::make_pressure_surface_graph(), 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{1.7, 1.9}; auto bad_entropy = std::vector{0.2, 0.3}; SECTION("Carrier rejection after a reusable EOS response") { bad_enthalpy[1] = std::numeric_limits::quiet_NaN(); } SECTION("EOS rejection after the first sample has been evaluated") { bad_entropy[1] = std::numeric_limits::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(), std::logic_error); CHECK_THROWS_AS(cache.get(), 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().size() == 2); CHECK(cache.get()[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::make_pressure_surface_graph(), 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::make_pressure_surface_graph(), 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::make_pressure_surface_graph(), 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(), std::logic_error); REQUIRE(cache.try_prepare(gas_context(model, 0.8, {0.4}, {2.0}))); CHECK(cache.get()[0].residual == 0.0); }