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.
This commit is contained in:
154
tests/solver/virial.cpp
Normal file
154
tests/solver/virial.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#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());
|
||||
}
|
||||
Reference in New Issue
Block a user