Files
MeanField_SERiF/tests/equations/barotropic_density_assembly.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

144 lines
9.5 KiB
C++

#include <cmath>
#include <catch2/catch_test_macros.hpp>
#include "serif/equations/barotropic_density.hpp"
#include "serif/tests/equations/barotropic_eos.hpp"
#include "serif/tests/equations/gravity_fixture.hpp"
namespace {
using Balance = serif::equations::BarotropicDensityBalance<serif::tests::equations::MixedPowerBarotrope>;
}
TEST_CASE("Barotropic density assembles a nonpolytropic value balance and complete ALE tangent", "[equations][barotropic_density][unit]") {
using namespace serif::tests::equations;
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
gravity::GravityFixture fixture(geometry);
auto density = fixture.density(), potential = fixture.potential();
auto surface_level = constant(1, -0.8), omega = constant(1, 0.2), displacement = fixture.displacement(0.3);
/** @brief Bind exactly the four physical inputs; the EOS supplies every thermodynamic value. */
const auto fields = [&](const auto& rho, const auto& phi, const auto& level, const auto& rotation) {
return backend::make_term_coefficients(backend::bind(fixture.density_binding, rho),
backend::bind(fixture.potential_binding, phi), backend::bind(fixture.constant_binding, level),
backend::bind(fixture.angular_velocity_binding, rotation));
};
const Balance balance{MixedPowerBarotrope{.gauge = 7.5}, serif::dimensions::PressureValue{0.3125},
{1.0, 2.0, 3.0}, {0.2, -0.1, 0.3}};
VolumeEquationFixture equation{balance, fixture.density_binding, fixture.displacement_binding,
fixture.compactification, fixture.mapper, gravity::reference_policy()};
equation.prepare(fields(density, potential, surface_level, omega), constant(displacement.Size(), 0.0), fixture.workspace);
::mfem::Vector residual;
equation.residual(fixture.workspace, residual);
/** @brief Integrate the analytic physical residual independently of the equation's evaluator. */
::mfem::FunctionCoefficient expected_balance([](const ::mfem::Vector& position) {
const double rho = 0.55 + 0.025 * position[0] - 0.010 * position[1] + 0.006 * position[2];
const double phi = -0.5 + 0.1 * position[0] * position[1] - 0.07 * position[2] * position[2];
const double x = position[0] - 0.2, y = position[1] + 0.1, z = position[2] - 0.3;
const double axial_projection = x + 2.0 * y + 3.0 * z;
const double centrifugal = 0.5 * (x * x + y * y + z * z - axial_projection * axial_projection / 14.0);
// P_surface = 5/16 fixes rho_surface = 1/2 and A_surface - gauge = 7/6.
return 2.0 * rho + (4.0 / 3.0) * rho * rho * rho - 7.0 / 6.0 + phi - 0.04 * centrifugal + 0.8;
});
::mfem::LinearForm reference(&fixture.density_space);
::mfem::Array<int> stellar{1, 1, 0};
const auto& rule = ::mfem::IntRules.Get(fixture.mesh.GetElementBaseGeometry(0), gravity::reference_order);
auto* integrator = new ::mfem::DomainLFIntegrator(expected_balance);
integrator->SetIntRule(&rule);
reference.AddDomainIntegrator(integrator, stellar);
reference.Assemble();
CHECK(relative_error(residual, fixture.density_binding.gather(reference)) < 3.0e-12);
const auto density_direction = deterministic(density.Size(), 0.1), potential_direction = deterministic(potential.Size(), 0.3);
const auto level_direction = constant(1, -0.05), omega_direction = constant(1, 0.07), displacement_direction = fixture.displacement(0.2);
equation.prepare(fields(density, potential, surface_level, omega), displacement, fixture.workspace);
::mfem::Vector action, transpose;
equation.apply_jacobian(fields(density_direction, potential_direction, level_direction, omega_direction),
displacement_direction, fixture.workspace, action);
const auto dual = deterministic(residual.Size(), 0.7);
equation.apply_jacobian_transpose(dual, fixture.workspace, transpose);
const auto direction = packed(packed(density_direction, potential_direction, level_direction), omega_direction, displacement_direction);
CHECK(std::abs(dual * action - direction * transpose) < 4.0e-11);
constexpr double step = 1.0e-6;
auto plus_density = density, minus_density = density, plus_potential = potential, minus_potential = potential;
auto plus_level = surface_level, minus_level = surface_level, plus_omega = omega, minus_omega = omega;
auto plus_displacement = displacement, minus_displacement = displacement;
plus_density.Add(step, density_direction);
minus_density.Add(-step, density_direction);
plus_potential.Add(step, potential_direction);
minus_potential.Add(-step, potential_direction);
plus_level.Add(step, level_direction);
minus_level.Add(-step, level_direction);
plus_omega.Add(step, omega_direction);
minus_omega.Add(-step, omega_direction);
plus_displacement.Add(step, displacement_direction);
minus_displacement.Add(-step, displacement_direction);
::mfem::Vector positive, negative;
equation.prepare(fields(plus_density, plus_potential, plus_level, plus_omega), plus_displacement, fixture.workspace);
equation.residual(fixture.workspace, positive);
equation.prepare(fields(minus_density, minus_potential, minus_level, minus_omega), minus_displacement, fixture.workspace);
equation.residual(fixture.workspace, negative);
positive -= negative;
positive /= 2.0 * step;
CHECK(relative_error(action, positive) < 4.0e-8);
}
}
TEST_CASE("Reconstructed density retains the field and geometry Jacobian including its transpose", "[equations][unit][reconstruction]") {
using namespace serif::tests::equations;
namespace reconstruction = serif::discretization::reconstruction;
namespace discretization_backend = serif::discretization::backend::mfem;
using Density = blocks::density::mass;
using PolytropicBalance = serif::equations::BarotropicDensityBalance<serif::eos::models::Polytrope>;
gravity::GravityFixture fixture(::mfem::Mesh::MakeCartesian3D(2, 1, 1, ::mfem::Element::HEXAHEDRON));
const double exponent = 5.0 / 3.0;
const auto reconstruction_plan = discretization_backend::make_reconstruction(
reconstruction::SurfacePower<Density>{.density_exponent = exponent}, fixture.density_binding);
discretization_backend::TermBinding<Density, Schema> density_binding(fixture.density_space, reconstruction_plan);
auto density = fixture.project(density_binding, [exponent](const ::mfem::Vector& point) {
return std::pow(point[2], exponent) * (1.0 + 0.1 * point[0]);
});
const auto density_direction = fixture.project(density_binding, [exponent](const ::mfem::Vector& point) {
return 0.1 * std::pow(point[2], exponent) * (point[0] - 0.3 * point[1]);
});
const auto potential = fixture.potential(), level = constant(1, -0.8), omega = constant(1, 0.2);
const auto displacement = fixture.displacement(0.3), displacement_direction = fixture.displacement(0.2);
const auto fields = [&](const auto& rho) {
return backend::make_term_coefficients(backend::bind(density_binding, rho),
backend::bind(fixture.potential_binding, potential), backend::bind(fixture.constant_binding, level),
backend::bind(fixture.angular_velocity_binding, omega));
};
VolumeEquationFixture equation{PolytropicBalance{serif::eos::models::Polytrope{exponent, 0.7}, serif::dimensions::PressureValue{0.0}},
density_binding, fixture.displacement_binding, fixture.compactification, fixture.mapper, gravity::reference_policy()};
equation.prepare(fields(density), displacement, fixture.workspace);
CHECK(equation.integration_order(0) == ::mfem::IntRules.Get(::mfem::Geometry::CUBE, gravity::reference_order).GetOrder());
::mfem::Vector action, transpose;
const auto potential_direction = constant(potential.Size(), 0.0), level_direction = constant(1, 0.0), omega_direction = constant(1, 0.0);
equation.apply_jacobian(backend::make_term_coefficients(backend::bind(density_binding, density_direction),
backend::bind(fixture.potential_binding, potential_direction), backend::bind(fixture.constant_binding, level_direction),
backend::bind(fixture.angular_velocity_binding, omega_direction)), displacement_direction, fixture.workspace, action);
const auto dual = deterministic(density_binding.size(), 0.7);
equation.apply_jacobian_transpose(dual, fixture.workspace, transpose);
const auto direction = packed(packed(density_direction, potential_direction, level_direction), omega_direction, displacement_direction);
CHECK(std::abs(dual * action - direction * transpose) < 4e-11);
for (const double step : {1e-5, 1e-6}) {
auto plus_density = density, minus_density = density;
auto plus_displacement = displacement, minus_displacement = displacement;
plus_density.Add(step, density_direction);
minus_density.Add(-step, density_direction);
plus_displacement.Add(step, displacement_direction);
minus_displacement.Add(-step, displacement_direction);
::mfem::Vector plus, minus;
equation.prepare(fields(plus_density), plus_displacement, fixture.workspace);
equation.residual(fixture.workspace, plus);
equation.prepare(fields(minus_density), minus_displacement, fixture.workspace);
equation.residual(fixture.workspace, minus);
plus -= minus;
plus /= 2.0 * step;
CHECK(relative_error(action, plus) < 4e-8);
}
}