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

164 lines
8.0 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#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<quantities::MassDensity>;
using Pressure = ScalarTerm<quantities::Pressure>;
struct ForwardOnly : GasEOS
{
using Relations = eos::relations::RelationCatalog<PressureFromDensityAndTemperature>;
};
struct SymbolicEOS
{
};
}
TEST_CASE("Automatic isobaric compilation preserves the polytropic zero-pressure unit derivative", "[surface][automatic]")
{
using Fields = dependencies::KeyList<Enthalpy, Density, blocks::barotropic_constant::mass_normalization, blocks::rotation::angular_velocity>;
using Selection = physics::AutomaticPressureSurface<eos::models::Polytrope, Fields, dependencies::KeyList<Enthalpy>>;
using Formulation = Selection::Formulation;
const eos::models::Polytrope equation_of_state{3.0, 0.25};
physics::SurfacePoint<Formulation> point{};
point.fields.get<Enthalpy>() = 0.0;
const auto result = physics::evaluate_pressure_surface<Formulation>(equation_of_state, physics::Isobaric{quantities::PressureValue{0.0}}, point);
REQUIRE(result);
CHECK(result->residual == 0.0);
CHECK(result->field_derivatives.get<Enthalpy>() == 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<ContinuousDensity, Temperature>;
using Rows = dependencies::KeyList<ContinuousDensity>;
const auto check = []<typename EOS>(const EOS& equation_of_state)
{
using Formulation = typename physics::AutomaticPressureSurface<EOS, Fields, Rows>::Formulation;
physics::SurfacePoint<Formulation> point{};
point.fields.template get<ContinuousDensity>() = 0.4;
point.fields.template get<Temperature>() = 2.0;
const auto result = physics::evaluate_pressure_surface<Formulation>(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<ContinuousDensity>() += step * 0.3;
minus.fields.template get<ContinuousDensity>() -= step * 0.3;
plus.fields.template get<Temperature>() -= step * 0.2;
minus.fields.template get<Temperature>() += step * 0.2;
const auto forward = physics::evaluate_pressure_surface<Formulation>(equation_of_state, physics::Isobaric{quantities::PressureValue{0.8}}, plus);
const auto backward = physics::evaluate_pressure_surface<Formulation>(equation_of_state, physics::Isobaric{quantities::PressureValue{0.8}}, minus);
REQUIRE(forward);
REQUIRE(backward);
const double derivative = result->field_derivatives.template get<ContinuousDensity>() * 0.3 - result->field_derivatives.template get<Temperature>() * 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<Pressure>;
using Formulation = physics::AutomaticPressureSurface<SymbolicEOS, Fields, Fields>::Formulation;
physics::SurfacePoint<Formulation> point{};
point.fields.get<Pressure>() = 0.2;
const auto single_point = physics::evaluate_pressure_surface<Formulation>(SymbolicEOS{}, physics::Isobaric{quantities::PressureValue{0.2}}, point);
REQUIRE(single_point);
CHECK(single_point->residual == 0.0);
CHECK(single_point->field_derivatives.get<Pressure>() == 1.0);
const auto inputs = [](const double target)
{
return dependencies::make_dependency_context(physics::pressure_condition(physics::Isobaric{quantities::PressureValue{target}}), physics::boundary_values<Pressure>({0.0, 0.2, 0.8}));
};
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(physics::make_pressure_surface_graph<Formulation>(), inputs(0.2));
REQUIRE(cache.try_prepare(inputs(0.2)));
const auto& values = cache.get<physics::keys::Linearization>();
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<Pressure>() == 1.0);
}
REQUIRE(cache.try_prepare(inputs(0.8)));
CHECK(cache.get<physics::keys::Linearization>()[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<Pressure>;
using Formulation = physics::AutomaticPressureSurface<SymbolicEOS, Fields, Fields>::Formulation;
fixture::Fixture mesh;
discretization::backend::mfem::TermBinding<Pressure, fixture::Schema> 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<Formulation>(state);
const SymbolicEOS equation_of_state;
const auto eos_input = dependencies::borrow<physics::keys::EquationOfState>(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<physics::SurfaceFailure>(backend::make_pressure_boundary_graph<Formulation>(), inputs);
REQUIRE(cache.try_prepare(inputs));
const auto& linearization = cache.get<physics::keys::Linearization>();
::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<Pressure>(*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));
}