#include #include #include "serif/tests/equations/test_equations.hpp" #include "serif/eos/models/polytropic.hpp" #include "serif/geometry/surface/automatic.hpp" #include "serif/backend/mfem/geometry/surface/pressure_boundary.hpp" #include "serif/tests/surface/boundary_fixture.hpp" #include "serif/tests/surface/models.hpp" #include "serif/utils/dependencies/computation_cache.hpp" namespace { using namespace serif; using namespace serif::tests::surface; namespace backend = physics::backend::mfem; using ContinuousDensity = ScalarTerm; using Pressure = ScalarTerm; struct ForwardOnly : GasEOS { using Relations = eos::relations::RelationCatalog; }; struct SymbolicEOS { }; } TEST_CASE("Automatic isobaric compilation preserves the polytropic zero-pressure unit derivative", "[surface][automatic]") { using Fields = dependencies::KeyList; using Selection = physics::AutomaticPressureSurface>; using Formulation = Selection::Formulation; const eos::models::Polytrope equation_of_state{3.0, 0.25}; physics::SurfacePoint point{}; point.fields.get() = 0.0; const auto result = physics::evaluate_pressure_surface(equation_of_state, physics::Isobaric{quantities::PressureValue{0.0}}, point); REQUIRE(result); CHECK(result->residual == 0.0); CHECK(result->field_derivatives.get() == 1.0); CHECK(Formulation::resolves_carrier); } TEST_CASE("Automatic isobaric compilation uses inverse or direct EOS relations without enthalpy", "[surface][automatic]") { using Fields = dependencies::KeyList; using Rows = dependencies::KeyList; const auto check = [](const EOS& equation_of_state) { using Formulation = typename physics::AutomaticPressureSurface::Formulation; physics::SurfacePoint point{}; point.fields.template get() = 0.4; point.fields.template get() = 2.0; const auto result = physics::evaluate_pressure_surface(equation_of_state, physics::Isobaric{quantities::PressureValue{0.8}}, point); REQUIRE(result); CHECK(result->residual == 0.0); constexpr double step = 1.0e-6; auto plus = point; auto minus = point; plus.fields.template get() += step * 0.3; minus.fields.template get() -= step * 0.3; plus.fields.template get() -= step * 0.2; minus.fields.template get() += step * 0.2; const auto forward = physics::evaluate_pressure_surface(equation_of_state, physics::Isobaric{quantities::PressureValue{0.8}}, plus); const auto backward = physics::evaluate_pressure_surface(equation_of_state, physics::Isobaric{quantities::PressureValue{0.8}}, minus); REQUIRE(forward); REQUIRE(backward); const double derivative = result->field_derivatives.template get() * 0.3 - result->field_derivatives.template get() * 0.2; CHECK_THAT((forward->residual - backward->residual) / (2.0 * step), Catch::Matchers::WithinAbs(derivative, 2.0e-9)); }; check(GasEOS{}); check(ForwardOnly{}); } TEST_CASE("A pressure unknown needs no EOS relations or EOS graph input", "[surface][automatic]") { using Fields = dependencies::KeyList; using Formulation = physics::AutomaticPressureSurface::Formulation; physics::SurfacePoint point{}; point.fields.get() = 0.2; const auto single_point = physics::evaluate_pressure_surface(SymbolicEOS{}, physics::Isobaric{quantities::PressureValue{0.2}}, point); REQUIRE(single_point); CHECK(single_point->residual == 0.0); CHECK(single_point->field_derivatives.get() == 1.0); const auto inputs = [](const double target) { return dependencies::make_dependency_context(physics::pressure_condition(physics::Isobaric{quantities::PressureValue{target}}), physics::boundary_values({0.0, 0.2, 0.8})); }; auto cache = dependencies::make_computation_cache(physics::make_pressure_surface_graph(), inputs(0.2)); REQUIRE(cache.try_prepare(inputs(0.2))); const auto& values = cache.get(); REQUIRE(values.size() == 3); CHECK(values[0].residual == -0.2); CHECK(values[1].residual == 0.0); CHECK_THAT(values[2].residual, Catch::Matchers::WithinAbs(0.6, 1.0e-15)); for (const auto& value : values) { CHECK(value.field_derivatives.get() == 1.0); } REQUIRE(cache.try_prepare(inputs(0.8))); CHECK(cache.get()[2].residual == 0.0); const auto unchanged = cache.try_prepare(inputs(0.8)); REQUIRE(unchanged); CHECK(unchanged->computed_count() == 0); } TEST_CASE("Automatic pressure-field boundaries preserve interior rows and exact transpose actions", "[surface][automatic][backend]") { namespace fixture = serif::tests::surface::boundary; namespace coefficients = operators::backend::mfem; using Fields = dependencies::KeyList; using Formulation = physics::AutomaticPressureSurface::Formulation; fixture::Fixture mesh; discretization::backend::mfem::TermBinding binding{mesh.linear_space}; const ::mfem::Vector pressure = fixture::sequence(binding.size(), 0.4, 0.2); const ::mfem::Vector direction = fixture::sequence(binding.size(), -0.1, 0.7); const ::mfem::Vector dual = fixture::sequence(binding.size(), 0.3, 0.4); const auto state = coefficients::make_term_coefficients(coefficients::bind(binding, pressure)); const auto directions = coefficients::make_term_coefficients(coefficients::bind(binding, direction)); const auto boundary = backend::make_pressure_boundary(state); const SymbolicEOS equation_of_state; const auto eos_input = dependencies::borrow(equation_of_state, dependencies::track_by([](const SymbolicEOS&) { return 0; })); const auto inputs = backend::make_pressure_boundary_inputs(boundary, eos_input, physics::Isobaric{quantities::PressureValue{0.2}}, state); auto cache = dependencies::make_computation_cache(backend::make_pressure_boundary_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const auto& linearization = cache.get(); ::mfem::Vector residual(binding.size()), action(binding.size()), transpose(binding.size()); residual = 4.0; action = 0.0; transpose = 0.0; backend::apply_residual_rows(*boundary, linearization, residual); backend::apply_jacobian_rows(*boundary, linearization, directions, action); backend::add_jacobian_transpose_rows(*boundary, linearization, dual, transpose); const ::mfem::Vector selected = fixture::boundary_mask(binding); for (int row = 0; row < binding.size(); ++row) { if (selected[row] != 0.0) { CHECK_THAT(residual[row], Catch::Matchers::WithinAbs(pressure[row] - 0.2, 1.0e-14)); CHECK_THAT(action[row], Catch::Matchers::WithinAbs(direction[row], 1.0e-14)); CHECK_THAT(transpose[row], Catch::Matchers::WithinAbs(dual[row], 1.0e-14)); } else { CHECK(residual[row] == 4.0); CHECK(action[row] == 0.0); CHECK(transpose[row] == 0.0); } } CHECK_THAT(action * dual, Catch::Matchers::WithinAbs(direction * transpose, 1.0e-14)); }