#pragma once #include "polytrope_analytic_reference.hpp" #include #include #include #include #include #include #include #include namespace experiment::polytrope_validation { struct AnalyticSelfCheck final { std::string name; double observed{}; double expected{}; double scale{1.0}; double tolerance{}; bool passed{}; [[nodiscard]] double AbsoluteError() const { return std::abs(observed - expected); } [[nodiscard]] double ScaledError() const { return AbsoluteError() / scale; } }; struct AnalyticSelfCheckReport final { std::vector checks; [[nodiscard]] bool Passed() const { return std::all_of(checks.begin(), checks.end(), [](const AnalyticSelfCheck &check) { return check.passed; }); } }; namespace analytic_detail { struct RadialIntegrals final { long double mass{}; long double pressure{}; long double potentialEnergy{}; long double gradientEnergy{}; long double fieldEnergy{}; long double momentOfInertia{}; }; // Independent physical radial integration: no mesh, projection, seed, // or production quadrature implementation is involved in these checks. inline RadialIntegrals IntegrateReference(const N1Reference &reference) { constexpr int intervals = 8192; constexpr long double pi = std::numbers::pi_v; const long double spacing = static_cast(reference.radius) / intervals; RadialIntegrals result; for (int index = 0; index <= intervals; ++index) { const double radius = reference.radius * (static_cast(index) / intervals); const auto values = reference.AtRadius(radius); const long double r = radius; const long double density = values.density; const long double gradient = values.radialPotentialGradient; const long double weight = (index == 0 || index == intervals) ? 1.0L : ((index % 2 == 0) ? 2.0L : 4.0L); const long double volumeWeight = weight * 4.0L * pi * r * r; result.mass += volumeWeight * density; result.pressure += volumeWeight * values.pressure; result.potentialEnergy += 0.5L * volumeWeight * density * values.potential; result.gradientEnergy -= volumeWeight * density * r * gradient; result.fieldEnergy -= volumeWeight * gradient * gradient / (8.0L * pi * reference.gravitationalConstant); result.momentOfInertia += (2.0L / 3.0L) * volumeWeight * density * r * r; } const long double factor = spacing / 3.0L; result.mass *= factor; result.pressure *= factor; result.potentialEnergy *= factor; result.gradientEnergy *= factor; result.fieldEnergy *= factor; result.momentOfInertia *= factor; // The gravitational field outside the star is not zero. Its // analytic contribution is essential to the field-energy identity. result.fieldEnergy -= static_cast(reference.gravitationalConstant) * reference.mass * reference.mass / (2.0L * reference.radius); return result; } } // namespace analytic_detail inline AnalyticSelfCheckReport RunAnalyticSelfChecks() { AnalyticSelfCheckReport report; auto check = [&](std::string name, const double observed, const double expected, const double scale, const double tolerance) { const bool passed = std::isfinite(observed) && std::isfinite(expected) && std::isfinite(scale) && scale > 0.0 && std::abs(observed - expected) <= tolerance * scale; report.checks.push_back({std::move(name), observed, expected, scale, tolerance, passed}); }; const std::array, 2> references{{ {"unit", {}}, {"nonunit", {.gravitationalConstant = 2.3, .mass = 3.7, .radius = 1.9}} }}; for (const auto &[label, reference] : references) { reference.Validate(); const double densityScale = reference.CentralDensity(); const double enthalpyScale = reference.CentralEnthalpy(); const double gradientScale = enthalpyScale / reference.radius; const double energyScale = enthalpyScale * reference.mass; const double inertiaScale = reference.mass * reference.radius * reference.radius; const auto origin = reference.AtRadius(0.0); check(label + ".origin.density", origin.density, densityScale, densityScale, 0.0); check(label + ".origin.enthalpy", origin.enthalpy, enthalpyScale, enthalpyScale, 0.0); check(label + ".origin.potential", origin.potential, -2.0 * enthalpyScale, enthalpyScale, 0.0); check(label + ".origin.enclosed_mass", origin.enclosedMass, 0.0, reference.mass, 0.0); check(label + ".origin.gradient", origin.radialPotentialGradient, 0.0, gradientScale, 0.0); constexpr double smallFraction = 1.0e-8; const auto nearOrigin = reference.AtRadius(reference.radius * smallFraction); constexpr double centralSlope = std::numbers::pi * std::numbers::pi / 3.0; check(label + ".origin.mass_cubic_coefficient", nearOrigin.enclosedMass / (reference.mass * smallFraction * smallFraction * smallFraction), centralSlope, centralSlope, 5.0e-15); check(label + ".origin.gradient_linear_coefficient", nearOrigin.radialPotentialGradient / (gradientScale * smallFraction), centralSlope, centralSlope, 5.0e-15); constexpr double tinyFraction = 1.0e-200; const auto tinyRadius = reference.AtRadius(reference.radius * tinyFraction); check(label + ".origin.gradient_without_mass_underflow_division", tinyRadius.radialPotentialGradient / (gradientScale * tinyFraction), centralSlope, centralSlope, 5.0e-15); const auto surface = reference.AtRadius(reference.radius); check(label + ".surface.density", surface.density, 0.0, densityScale, 0.0); check(label + ".surface.enthalpy", surface.enthalpy, 0.0, enthalpyScale, 0.0); check(label + ".surface.pressure", surface.pressure, 0.0, enthalpyScale * densityScale, 0.0); check(label + ".surface.potential", surface.potential, -enthalpyScale, enthalpyScale, 0.0); check(label + ".surface.enclosed_mass", surface.enclosedMass, reference.mass, reference.mass, 0.0); check(label + ".surface.gradient", surface.radialPotentialGradient, gradientScale, gradientScale, 0.0); const double innerRadius = std::nextafter(reference.radius, 0.0); const double outerRadius = std::nextafter(reference.radius, std::numeric_limits::infinity()); const auto justInside = reference.AtRadius(innerRadius); const auto justOutside = reference.AtRadius(outerRadius); check(label + ".surface.potential_join", justInside.potential, justOutside.potential, enthalpyScale, 2.0e-15); check(label + ".surface.gradient_join", justInside.radialPotentialGradient, justOutside.radialPotentialGradient, gradientScale, 3.0e-15); check(label + ".surface.theta_linear_coefficient", reference.DimensionlessTheta(innerRadius) / ((reference.radius - innerRadius) / reference.radius), 1.0, 1.0, 2.0e-15); check(label + ".surface.positive_density_inside", justInside.density > 0.0 ? 1.0 : 0.0, 1.0, 1.0, 0.0); const auto exterior = reference.AtRadius(2.0 * reference.radius); check(label + ".exterior.vacuum_density", exterior.density, 0.0, densityScale, 0.0); check(label + ".exterior.point_mass_potential", exterior.potential, -0.5 * enthalpyScale, enthalpyScale, 0.0); check(label + ".exterior.point_mass_gradient", exterior.radialPotentialGradient, 0.25 * gradientScale, gradientScale, 0.0); check(label + ".normalization.does_not_clip_negative_theta", reference.NormalizedPotential(exterior.potential), -0.5, 1.0, 0.0); check(label + ".normalization.does_not_clip_positive_potential", reference.NormalizedPotential(enthalpyScale), -2.0, 1.0, 0.0); int radialIndex = 0; for (const double fraction : {0.0, 0.1, 0.25, 0.5, 0.75, 0.99, 1.0}) { const auto values = reference.AtRadius(reference.radius * fraction); const std::string prefix = label + ".radial_" + std::to_string(radialIndex++); check(prefix + ".enthalpy_eos", values.enthalpy, 2.0 * reference.PolytropicConstant() * values.density, enthalpyScale, 2.0e-15); check(prefix + ".pressure_eos", values.pressure, reference.PolytropicConstant() * values.density * values.density, enthalpyScale * densityScale, 2.0e-15); check(prefix + ".hydrostatic_constant", values.enthalpy + values.potential, -enthalpyScale, enthalpyScale, 2.0e-15); check(prefix + ".normalized_potential", reference.NormalizedPotential(values.potential), reference.DimensionlessTheta(reference.radius * fraction), 1.0, 2.0e-15); } radialIndex = 0; for (const double fraction : {0.1, 0.25, 0.5, 0.75, 0.9}) { const double radius = reference.radius * fraction; const double spacing = 1.0e-4 * reference.radius; const auto minusTwo = reference.AtRadius(radius - 2.0 * spacing); const auto minusOne = reference.AtRadius(radius - spacing); const auto plusOne = reference.AtRadius(radius + spacing); const auto plusTwo = reference.AtRadius(radius + 2.0 * spacing); const double enthalpyDerivative = (minusTwo.enthalpy - 8.0 * minusOne.enthalpy + 8.0 * plusOne.enthalpy - plusTwo.enthalpy) / (12.0 * spacing); const double massDerivative = (minusTwo.enclosedMass - 8.0 * minusOne.enclosedMass + 8.0 * plusOne.enclosedMass - plusTwo.enclosedMass) / (12.0 * spacing); const auto values = reference.AtRadius(radius); const std::string prefix = label + ".derivative_" + std::to_string(radialIndex++); check(prefix + ".hydrostatic_balance", enthalpyDerivative + values.radialPotentialGradient, 0.0, gradientScale, 5.0e-11); check(prefix + ".enclosed_mass", massDerivative, 4.0 * std::numbers::pi * radius * radius * values.density, reference.mass / reference.radius, 5.0e-11); } const auto integrals = analytic_detail::IntegrateReference(reference); check(label + ".integral.mass", static_cast(integrals.mass), reference.mass, reference.mass, 2.0e-12); check(label + ".integral.pressure", static_cast(integrals.pressure), reference.PressureIntegral(), energyScale, 2.0e-12); check(label + ".integral.binding_from_potential", static_cast(integrals.potentialEnergy), reference.BindingEnergy(), energyScale, 2.0e-12); check(label + ".integral.binding_from_gradient", static_cast(integrals.gradientEnergy), reference.BindingEnergy(), energyScale, 2.0e-12); check(label + ".integral.binding_from_field_with_exterior", static_cast(integrals.fieldEnergy), reference.BindingEnergy(), energyScale, 2.0e-12); check(label + ".integral.binding_potential_vs_gradient", static_cast(integrals.potentialEnergy), static_cast(integrals.gradientEnergy), energyScale, 2.0e-12); check(label + ".integral.scalar_virial_nonrotating", static_cast(integrals.gradientEnergy + 3.0L * integrals.pressure), 0.0, energyScale, 2.0e-12); check(label + ".integral.axial_moment_of_inertia", static_cast(integrals.momentOfInertia), reference.MomentOfInertia(), inertiaScale, 2.0e-12); } bool rejectedNegativeRadius = false; try { (void)N1Reference{}.AtRadius(-1.0); } catch (const std::invalid_argument &) { rejectedNegativeRadius = true; } check("contract.negative_radius_rejected", rejectedNegativeRadius ? 1.0 : 0.0, 1.0, 1.0, 0.0); bool rejectedInvalidScale = false; try { N1Reference{.gravitationalConstant = -1.0}.Validate(); } catch (const std::invalid_argument &) { rejectedInvalidScale = true; } check("contract.invalid_reference_rejected", rejectedInvalidScale ? 1.0 : 0.0, 1.0, 1.0, 0.0); return report; } } // namespace experiment::polytrope_validation