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.
155 lines
9.1 KiB
C++
155 lines
9.1 KiB
C++
#include <limits>
|
|
#include <memory>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
#include "mfem/mesh/mesh.hpp"
|
|
#include "serif/tests/solver/virial.hpp"
|
|
|
|
namespace {
|
|
namespace diagnostics = serif::tests::solver;
|
|
namespace quantities = serif::dimensions;
|
|
namespace eos = serif::eos;
|
|
|
|
/** @brief A pressure-only EOS demonstrates that diagnostics need no enthalpy or polytropic parameters. */
|
|
struct PressureOnlyEOS {
|
|
using Relations = eos::relations::RelationCatalog<eos::relations::PressureFromDensity>;
|
|
enum class Mode { regular, negative, nonfinite, domain_failure, finite_surface };
|
|
Mode mode{Mode::regular};
|
|
|
|
/** @brief Supply an exactly integrable pressure or a deliberate validation failure. */
|
|
[[nodiscard]] quantities::PressureValue evaluate(eos::relations::PressureFromDensity, const quantities::DensityValue density) const {
|
|
if (mode == Mode::finite_surface) { return quantities::PressureValue{1.0 + density.value() * density.value()}; }
|
|
if (density.value() == 0.0) { return quantities::PressureValue{0.0}; }
|
|
if (mode == Mode::domain_failure) { throw eos::EOSEvaluationError{"Deliberate pressure domain rejection."}; }
|
|
if (mode == Mode::negative) { return quantities::PressureValue{-1.0}; }
|
|
if (mode == Mode::nonfinite) { return quantities::PressureValue{std::numeric_limits<double>::quiet_NaN()}; }
|
|
return quantities::PressureValue{density.value() * density.value()};
|
|
}
|
|
};
|
|
|
|
/** @brief Put the complete unit-cube boundary in the fixture's stellar-surface attribute. */
|
|
[[nodiscard]] mfem::Mesh material_cube() {
|
|
auto mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON);
|
|
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { mesh.GetBdrElement(boundary)->SetAttribute(1); }
|
|
mesh.SetAttributes();
|
|
return mesh;
|
|
}
|
|
|
|
/** @brief Use exact tensor-polynomial density on a translated physical cube with a genuine vacuum trace. */
|
|
struct VirialFixture {
|
|
mfem::Mesh mesh{material_cube()};
|
|
mfem::H1_FECollection collection{diagnostics::Density::FunctionalSpace::order, 3};
|
|
mfem::H1_FECollection potential_collection{diagnostics::Potential::FunctionalSpace::order, 3};
|
|
mfem::FiniteElementSpace scalar{&mesh, &collection};
|
|
mfem::FiniteElementSpace potential_space{&mesh, &potential_collection};
|
|
mfem::FiniteElementSpace vector{&mesh, &collection, 3, mfem::Ordering::byVDIM};
|
|
serif::discretization::backend::mfem::TermBinding<diagnostics::Density, diagnostics::Schema> binding{scalar};
|
|
mfem::GridFunction density{&scalar}, potential{&potential_space}, compactification{&scalar}, displacement{&vector};
|
|
serif::mapping::DomainMapper mapper{{}, std::make_unique<serif::mapping::compactification::KelvinCompactification>(
|
|
serif::utils::user_options::KelvinCompactificationOptions{})};
|
|
serif::mapping::GridFunctionMappingEvaluator geometry{mapper, displacement, compactification};
|
|
|
|
/** @brief Project rho=216 product[x_i(1-x_i)] and translate the physical domain by (2,-1,1/2). */
|
|
VirialFixture() {
|
|
mfem::FunctionCoefficient profile([](const mfem::Vector& point) {
|
|
double value = 216.0;
|
|
for (int axis = 0; axis < 3; ++axis) { value *= point[axis] * (1.0 - point[axis]); }
|
|
return value;
|
|
});
|
|
density.ProjectCoefficient(profile);
|
|
const auto boundary = serif::discretization::backend::mfem::select_boundary_dofs<
|
|
serif::discretization::domain::StellarSurfaceBoundary>(binding);
|
|
for (const int dof : boundary.reduced_dofs()) { density[dof] = 0.0; }
|
|
potential = -2.0;
|
|
compactification = 0.0;
|
|
mfem::VectorFunctionCoefficient shift(3, [](const mfem::Vector&, mfem::Vector& value) {
|
|
value.SetSize(3);
|
|
value[0] = 2.0; value[1] = -1.0; value[2] = 0.5;
|
|
});
|
|
displacement.ProjectCoefficient(shift);
|
|
}
|
|
};
|
|
}
|
|
|
|
TEST_CASE("Vacuum virial integrates tilted physical rotation about a displaced axis", "[solver][virial][unit]") {
|
|
VirialFixture fixture;
|
|
const serif::equations::RigidRotation rotation{{0.2, 0.4, -0.4}, {-1.0, 0.25, 0.75}};
|
|
const auto result = diagnostics::measure_vacuum_virial(fixture.binding, fixture.density, fixture.potential,
|
|
fixture.geometry, PressureOnlyEOS{}, rotation);
|
|
REQUIRE(result);
|
|
REQUIRE(result->boundary_dofs > 0);
|
|
// Each normalized coordinate has mean 1/2 and variance 1/20. With the physical translation,
|
|
// mean minus axis origin is (3.5,-.75,.25); T=1/2[|omega|^2(2/20+|d|^2)-(omega.d)^2]=2.2905.
|
|
for (const auto& sample : result->samples) {
|
|
CHECK_THAT(sample.mass, Catch::Matchers::WithinAbs(1.0, 2.0e-13));
|
|
CHECK_THAT(sample.volume, Catch::Matchers::WithinAbs(1.0, 2.0e-13));
|
|
CHECK_THAT(sample.kinetic_energy, Catch::Matchers::WithinAbs(2.2905, 2.0e-12));
|
|
CHECK_THAT(sample.pressure_integral, Catch::Matchers::WithinAbs(1.728, 2.0e-12));
|
|
CHECK_THAT(sample.potential_energy, Catch::Matchers::WithinAbs(-1.0, 2.0e-13));
|
|
CHECK_THAT(sample.gravity_work, Catch::Matchers::WithinAbs(0.0, 2.0e-12));
|
|
CHECK_THAT(sample.energy_virial(), Catch::Matchers::WithinAbs(8.765, 2.0e-11));
|
|
CHECK_THAT(sample.force_virial(), Catch::Matchers::WithinAbs(9.765, 2.0e-11));
|
|
}
|
|
CHECK(result->quadrature_allowance < 1.0e-11);
|
|
CHECK_FALSE(result->passes()); // This analytically integrable candidate is deliberately not an equilibrium.
|
|
}
|
|
|
|
TEST_CASE("Vacuum virial rejects unsupported thermodynamics and an uncertified surface", "[solver][virial][unit]") {
|
|
VirialFixture fixture;
|
|
PressureOnlyEOS equation_of_state;
|
|
SECTION("Negative pressure") { equation_of_state.mode = PressureOnlyEOS::Mode::negative; }
|
|
SECTION("Nonfinite pressure") { equation_of_state.mode = PressureOnlyEOS::Mode::nonfinite; }
|
|
SECTION("EOS domain rejection") { equation_of_state.mode = PressureOnlyEOS::Mode::domain_failure; }
|
|
SECTION("Finite pressure at zero density") { equation_of_state.mode = PressureOnlyEOS::Mode::finite_surface; }
|
|
SECTION("Even a tiny nonzero density trace is not silently clipped") {
|
|
const auto boundary = serif::discretization::backend::mfem::select_boundary_dofs<
|
|
serif::discretization::domain::StellarSurfaceBoundary>(fixture.binding);
|
|
fixture.density[boundary.reduced_dofs()[0]] = 1.0e-17;
|
|
}
|
|
SECTION("Negative represented interior density") { fixture.density *= -1.0; }
|
|
SECTION("Zero binding energy has no usable normalization") { fixture.potential = 0.0; }
|
|
SECTION("Missing stellar boundary cannot certify a vacuum trace") {
|
|
for (int face = 0; face < fixture.mesh.GetNBE(); ++face) { fixture.mesh.GetBdrElement(face)->SetAttribute(2); }
|
|
fixture.mesh.SetAttributes();
|
|
}
|
|
const auto result = diagnostics::measure_vacuum_virial(fixture.binding, fixture.density, fixture.potential,
|
|
fixture.geometry, equation_of_state, serif::equations::RigidRotation{{0.0, 0.0, 0.0}});
|
|
REQUIRE_FALSE(result);
|
|
CHECK_FALSE(result.error().empty());
|
|
}
|
|
|
|
TEST_CASE("Vacuum virial certifies the entire infinity potential trace", "[solver][virial][unit]") {
|
|
VirialFixture fixture;
|
|
serif::discretization::backend::mfem::TermBinding<diagnostics::Potential, diagnostics::Schema> potential_binding{fixture.potential_space};
|
|
fixture.potential = 0.0;
|
|
CHECK_FALSE(diagnostics::has_infinity_zero_gauge(potential_binding, fixture.potential));
|
|
fixture.mesh.GetBdrElement(0)->SetAttribute(2);
|
|
fixture.mesh.SetAttributes();
|
|
CHECK(diagnostics::has_infinity_zero_gauge(potential_binding, fixture.potential));
|
|
fixture.potential = -2.0;
|
|
CHECK_FALSE(diagnostics::has_infinity_zero_gauge(potential_binding, fixture.potential));
|
|
fixture.potential = 0.0;
|
|
const auto boundary = serif::discretization::backend::mfem::select_boundary_dofs<
|
|
serif::discretization::domain::InfinitySurfaceBoundary>(potential_binding);
|
|
fixture.potential[boundary.reduced_dofs()[boundary.size() / 2]] = 1.0e-17;
|
|
CHECK_FALSE(diagnostics::has_infinity_zero_gauge(potential_binding, fixture.potential));
|
|
}
|
|
|
|
TEST_CASE("Vacuum virial acceptance reserves room for integration uncertainty", "[solver][virial][unit]") {
|
|
diagnostics::VacuumVirialAssessment assessment{};
|
|
auto& final = assessment.samples.back();
|
|
final.potential_energy = -1.0;
|
|
final.pressure_integral = 1.0 / 3.0;
|
|
assessment.quadrature_allowance = 1.0e-8;
|
|
CHECK(assessment.passes());
|
|
final.pressure_integral = (1.0 + 9.95e-7) / 3.0;
|
|
CHECK_FALSE(assessment.passes()); // A point estimate below 1e-6 does not leave enough integration margin.
|
|
final.pressure_integral = 1.0 / 3.0;
|
|
assessment.quadrature_allowance = 1.01e-7;
|
|
CHECK_FALSE(assessment.passes()); // The separately bounded quadrature requirement also applies at exact virial balance.
|
|
assessment.quadrature_allowance = 0.0;
|
|
final.potential_energy = 0.0;
|
|
CHECK_FALSE(assessment.passes());
|
|
}
|