Files
MeanField/experiments/polytrope_validation_measurements.hpp
Emily Boudreaux 75cc638739 perf(allocations): reduced overall allocations by 95%, increaseed jacobian applicatin by 2x
This commit uses global pre allocated work space to dramatically reduce memory usage and allocation time
2026-09-10 06:50:56 -04:00

340 lines
23 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <map>
#include <numbers>
#include <stdexcept>
#include <string>
#include "polytrope_analytic_reference.hpp"
#include "polytrope_physical_state.hpp"
namespace experiment::polytrope_validation {
using Measurements = std::map<std::string, double>;
// Exact fields evaluated on the actual mapped mesh. This checks the same
// integration and physical-point locator used for a numerical solution,
// without constructing a Newton context or using its Lane-Emden seed.
class AnalyticMeshState final {
public:
const mean_field::fem::FEM &finiteElements;
mean_field::mapping::GridFunctionMappingEvaluator mapping;
N1Reference reference;
AnalyticMeshState(const mean_field::fem::FEM &fem, const N1Reference &analytic)
: finiteElements(fem),
mapping(*fem.domainMapperStateless, *fem.displacement, *fem.compactificationCoordinate),
reference(analytic) {
}
[[nodiscard]] bool isStellar(const int element) const {
using Schema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema;
return Schema::template attribute_belongs_to<mean_field::utils::domain::Stellar>(
finiteElements.mesh->GetAttribute(element)
);
}
mean_field::mapping::MappingStatus Evaluate(
const int element, const mfem::IntegrationPoint &point, PhysicalPoint &result
) {
auto *transformation = finiteElements.mesh->GetElementTransformation(element);
transformation->SetIntPoint(&point);
const auto status = mapping.EvaluatePoint(*transformation, point, result.mapping);
if (status != mean_field::mapping::MappingStatus::valid) return status;
const double radius = result.mapping.physical_position.Norml2();
const auto analytic = reference.AtRadius(radius);
result.element = element;
result.attribute = transformation->Attribute;
result.stellarMaterial = isStellar(element);
result.rho = analytic.density;
result.h = analytic.enthalpy;
result.phi = analytic.potential;
result.rotationPotential = 0.0;
result.gravityGradientPhysical.SetSize(3);
result.enthalpyGradientPhysical.SetSize(3);
result.potentialGradientPhysical.SetSize(3);
for (int component = 0; component < 3; ++component) {
const double gradient = radius > 0.0
? analytic.radialPotentialGradient * result.mapping.physical_position(component) / radius : 0.0;
result.gravityGradientPhysical(component) = gradient;
result.potentialGradientPhysical(component) = gradient;
result.enthalpyGradientPhysical(component) = radius <= reference.radius ? -gradient : 0.0;
}
return status;
}
};
struct ErrorIntegral final {
long double errorSquared{0.0L};
long double referenceSquared{0.0L};
double maximumScaledError{0.0};
void Add(const double value, const double exact, const double weight, const double scale) {
const long double difference = static_cast<long double>(value) - exact;
errorSquared += weight * difference * difference;
referenceSquared += static_cast<long double>(weight) * exact * exact;
maximumScaledError = std::max(maximumScaledError, std::abs(value - exact) / scale);
}
[[nodiscard]] double RelativeL2() const {
return referenceSquared > 0.0L ? static_cast<double>(std::sqrt(errorSquared / referenceSquared))
: std::numeric_limits<double>::quiet_NaN();
}
};
template <typename SampleState>
Measurements MeasureVolumes(SampleState &state, const N1Reference &reference, const int quadratureOrder) {
long double volume = 0.0L, mass = 0.0L, binding = 0.0L, forceBinding = 0.0L;
long double pressureIntegral = 0.0L, enthalpyPressureIntegral = 0.0L, kinetic = 0.0L;
long double momentOfInertia = 0.0L, bernoulliOffsetMean = 0.0L, bernoulliCenteredSquared = 0.0L;
long double closureSquared = 0.0L, closureDensityInnerProduct = 0.0L;
long double gradientMismatch = 0.0L, hydrostaticGradient = 0.0L;
std::array<long double, 3> firstMoment{};
ErrorIntegral densityError, enthalpyError, potentialError, pressureError, gravityError;
double minimumDensity = std::numeric_limits<double>::infinity();
double minimumEnthalpy = std::numeric_limits<double>::infinity();
double minimumDeterminant = std::numeric_limits<double>::infinity();
double bernoulliMinimum = std::numeric_limits<double>::infinity();
double bernoulliMaximum = -std::numeric_limits<double>::infinity();
std::uint64_t samples = 0, negativeDensitySamples = 0, negativeEnthalpySamples = 0;
PhysicalPoint point;
const double densityScale = reference.CentralDensity();
const double enthalpyScale = reference.CentralEnthalpy();
const double pressureScale = reference.PolytropicConstant() * densityScale * densityScale;
const double gravityScale = reference.gravitationalConstant * reference.mass /
(reference.radius * reference.radius);
for (int element = 0; element < state.finiteElements.mesh->GetNE(); ++element) {
if (!state.isStellar(element)) continue;
auto *transformation = state.finiteElements.mesh->GetElementTransformation(element);
const auto &rule = mfem::IntRules.Get(transformation->GetGeometryType(), quadratureOrder);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto &ip = rule.IntPoint(q);
if (state.Evaluate(element, ip, point) != mean_field::mapping::MappingStatus::valid) {
throw std::runtime_error("Invalid physical mapping in polytrope volume verification.");
}
transformation->SetIntPoint(&ip);
// Explicitly check the orientation of both the reference map
// and the deformation before integrating physical volume.
const double referenceDeterminant = transformation->Jacobian().Det();
const double weight = ip.weight * referenceDeterminant * point.mapping.mapping_determinant;
if (!(referenceDeterminant > 0.0) || !std::isfinite(referenceDeterminant)) {
throw std::runtime_error("Non-positive or non-finite reference Jacobian in polytrope volume verification.");
}
if (!(weight > 0.0) || !std::isfinite(weight) || !std::isfinite(point.rho) ||
!std::isfinite(point.h) || !std::isfinite(point.phi) || !std::isfinite(point.rotationPotential)) {
throw std::runtime_error("Non-finite field or non-positive physical integration weight.");
}
++samples;
const auto &position = point.mapping.physical_position;
const double radius = position.Norml2();
const auto exact = reference.AtRadius(radius);
// Do not clamp numerical density/enthalpy. Negative values are
// reported, and pressure consistency is checked independently.
const double pressure = reference.PolytropicConstant() * point.rho * point.rho;
const double pressureFromEnthalpy = point.h * point.h / (4.0 * reference.PolytropicConstant());
const long double specificBernoulli = static_cast<long double>(point.h) + point.phi - point.rotationPotential;
volume += weight;
mass += weight * point.rho;
binding += 0.5L * weight * point.rho * point.phi;
forceBinding -= weight * point.rho * (position * point.gravityGradientPhysical);
pressureIntegral += weight * pressure;
enthalpyPressureIntegral += weight * pressureFromEnthalpy;
// RigidRotation::potential is positive +|Omega x r|^2/2.
kinetic += weight * point.rho * point.rotationPotential;
momentOfInertia += weight * point.rho * (position(0) * position(0) + position(1) * position(1));
// Weighted Welford accumulation about the fixed analytic
// Bernoulli constant C=-h_c resolves small spatial variations
// without subtracting two O(h_c^2) second moments. No fitted
// potential offset is applied to any physical field/error.
const long double bernoulliOffset = specificBernoulli + enthalpyScale;
const long double bernoulliDelta = bernoulliOffset - bernoulliOffsetMean;
bernoulliOffsetMean += (static_cast<long double>(weight) / volume) * bernoulliDelta;
bernoulliCenteredSquared += weight * bernoulliDelta * (bernoulliOffset - bernoulliOffsetMean);
const double closure = point.h - 2.0 * reference.PolytropicConstant() * point.rho;
closureSquared += weight * closure * closure;
closureDensityInnerProduct += weight * point.rho * closure;
bernoulliMinimum = std::min(bernoulliMinimum, static_cast<double>(specificBernoulli));
bernoulliMaximum = std::max(bernoulliMaximum, static_cast<double>(specificBernoulli));
minimumDensity = std::min(minimumDensity, point.rho);
minimumEnthalpy = std::min(minimumEnthalpy, point.h);
minimumDeterminant = std::min(minimumDeterminant, point.mapping.mapping_determinant);
negativeDensitySamples += point.rho < 0.0;
negativeEnthalpySamples += point.h < 0.0;
densityError.Add(point.rho, exact.density, weight, densityScale);
enthalpyError.Add(point.h, exact.enthalpy, weight, enthalpyScale);
potentialError.Add(point.phi, exact.potential, weight, enthalpyScale);
pressureError.Add(pressure, exact.pressure, weight, pressureScale);
for (int component = 0; component < 3; ++component) {
if (!std::isfinite(point.gravityGradientPhysical(component)) ||
!std::isfinite(point.potentialGradientPhysical(component)) ||
!std::isfinite(point.enthalpyGradientPhysical(component))) {
throw std::runtime_error("Non-finite physical field gradient in polytrope volume verification.");
}
firstMoment[component] += weight * point.rho * position(component);
const double exactGradient = radius > 0.0
? exact.radialPotentialGradient * position(component) / radius : 0.0;
gravityError.Add(point.gravityGradientPhysical(component), exactGradient, weight, gravityScale);
const double mismatch = point.gravityGradientPhysical(component) - point.potentialGradientPhysical(component);
gradientMismatch += weight * mismatch * mismatch;
const double hydrostatic = point.enthalpyGradientPhysical(component) + point.gravityGradientPhysical(component);
hydrostaticGradient += weight * hydrostatic * hydrostatic;
}
}
}
if (!(volume > 0.0L) || !(mass > 0.0L) || !(binding < 0.0L)) {
throw std::runtime_error("Polytrope verification requires positive stellar volume/mass and negative binding energy.");
}
Measurements result{
{"quadrature_order", static_cast<double>(quadratureOrder)}, {"stellar_samples", static_cast<double>(samples)},
{"volume", static_cast<double>(volume)}, {"mass", static_cast<double>(mass)},
{"mass_relative_error", static_cast<double>(std::abs(mass / reference.mass - 1.0L))},
{"volume_radius", static_cast<double>(std::cbrt(3.0L * volume / (4.0L * std::numbers::pi_v<long double>)))},
{"binding_energy", static_cast<double>(binding)}, {"force_binding_energy", static_cast<double>(forceBinding)},
{"pressure_integral", static_cast<double>(pressureIntegral)},
{"enthalpy_pressure_integral", static_cast<double>(enthalpyPressureIntegral)},
{"kinetic_energy", static_cast<double>(kinetic)},
{"virial_signed", static_cast<double>((2.0L * kinetic + binding + 3.0L * pressureIntegral) / std::abs(binding))},
{"virial_error", static_cast<double>(std::abs(2.0L * kinetic + binding + 3.0L * pressureIntegral) / std::abs(binding))},
{"virial_ratio", static_cast<double>((2.0L * kinetic + 3.0L * pressureIntegral) / std::abs(binding))},
{"force_virial_error", static_cast<double>(std::abs(2.0L * kinetic + forceBinding + 3.0L * pressureIntegral) / std::abs(binding))},
{"enthalpy_virial_error", static_cast<double>(std::abs(2.0L * kinetic + binding + 3.0L * enthalpyPressureIntegral) / std::abs(binding))},
{"enthalpy_force_virial_error", static_cast<double>(std::abs(2.0L * kinetic + forceBinding + 3.0L * enthalpyPressureIntegral) / std::abs(binding))},
{"gravity_energy_consistency", static_cast<double>(std::abs(binding - forceBinding) / std::abs(binding))},
{"binding_relative_error", static_cast<double>(std::abs(binding / reference.BindingEnergy() - 1.0L))},
{"pressure_integral_relative_error", static_cast<double>(std::abs(pressureIntegral / reference.PressureIntegral() - 1.0L))},
{"pressure_integral_eos_disagreement", static_cast<double>(std::abs(pressureIntegral - enthalpyPressureIntegral) / reference.PressureIntegral())},
{"closure_projection_pressure_gap", static_cast<double>(closureSquared / (4.0L * reference.PolytropicConstant()))},
{"closure_density_inner_product", static_cast<double>(closureDensityInnerProduct)},
{"closure_projection_pressure_gap_relative_defect", static_cast<double>((enthalpyPressureIntegral - pressureIntegral -
closureSquared / (4.0L * reference.PolytropicConstant())) / reference.PressureIntegral())},
{"moment_of_inertia", static_cast<double>(momentOfInertia)},
{"moment_of_inertia_relative_error", static_cast<double>(std::abs(momentOfInertia / reference.MomentOfInertia() - 1.0L))},
{"bernoulli_mean", static_cast<double>(bernoulliOffsetMean - enthalpyScale)},
{"bernoulli_mean_scaled_error", static_cast<double>(std::abs(bernoulliOffsetMean) / enthalpyScale)},
{"bernoulli_scaled_range", (bernoulliMaximum - bernoulliMinimum) / enthalpyScale},
{"bernoulli_scaled_rms_variation", static_cast<double>(std::sqrt(std::max(0.0L, bernoulliCenteredSquared / volume)) / enthalpyScale)},
{"eos_enthalpy_scaled_rms", static_cast<double>(std::sqrt(closureSquared / volume) / enthalpyScale)},
{"gravity_gradient_vs_broken_potential_gradient_scaled_rms", static_cast<double>(std::sqrt(gradientMismatch / volume) / gravityScale)},
{"nonrotating_hydrostatic_gradient_scaled_rms", static_cast<double>(std::sqrt(hydrostaticGradient / volume) / gravityScale)},
{"minimum_density", minimumDensity}, {"minimum_enthalpy", minimumEnthalpy},
{"negative_density_samples", static_cast<double>(negativeDensitySamples)},
{"negative_enthalpy_samples", static_cast<double>(negativeEnthalpySamples)},
{"minimum_mapping_determinant", minimumDeterminant}
};
result["volume_radius_relative_error"] = std::abs(result.at("volume_radius") / reference.radius - 1.0);
for (const auto &[name, error] : std::array<std::pair<const char *, const ErrorIntegral *>, 5>{{
{"density", &densityError}, {"enthalpy", &enthalpyError}, {"potential", &potentialError},
{"pressure", &pressureError}, {"gravity_gradient", &gravityError}}}) {
result[std::string(name) + "_relative_l2_error"] = error->RelativeL2();
result[std::string(name) + "_maximum_scaled_error"] = error->maximumScaledError;
}
for (int component = 0; component < 3; ++component) {
result["center_of_mass_" + std::to_string(component)] = static_cast<double>(firstMoment[component] / mass);
}
return result;
}
template <typename SampleState>
Measurements MeasureSurfaceAndCorners(SampleState &state, const N1Reference &reference, const int order) {
auto &mesh = *state.finiteElements.mesh;
PhysicalPoint mapped;
double minimumRadius = std::numeric_limits<double>::infinity(), maximumRadius = 0.0;
double maximumSurfaceEnthalpy = 0.0, maximumSurfacePotentialError = 0.0;
long double area = 0.0L, radiusIntegral = 0.0L, radiusError = 0.0L;
std::uint64_t boundarySamples = 0, invalidCornerSamples = 0, cornerSamples = 0;
double minimumCornerDeterminant = std::numeric_limits<double>::infinity();
double maximumCornerCondition = 0.0;
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
if (mesh.GetBdrAttribute(boundary) != 1) continue; // Canonical sandbox stellar surface.
// The tagged stellar surface is an interior material interface
// when a vacuum region is present. Resolve the actual mesh face
// to retain both adjacent traces in that case.
const int faceIndex = mesh.GetBdrElementFaceIndex(boundary);
if (faceIndex < 0) throw std::runtime_error("Missing stellar surface mesh face.");
auto *face = mesh.GetFaceElementTransformations(faceIndex);
if (face == nullptr || face->Elem1 == nullptr) throw std::runtime_error("Missing stellar surface transformation.");
const bool first = state.isStellar(face->Elem1No);
if (!first && (face->Elem2 == nullptr || !state.isStellar(face->Elem2No))) {
throw std::runtime_error("Stellar surface has no stellar-material trace.");
}
const int element = first ? face->Elem1No : face->Elem2No;
const auto &rule = mfem::IntRules.Get(face->GetGeometryType(), order);
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto &ip = rule.IntPoint(q);
// Field evaluation may reuse MFEM's cached element transforms;
// restore both adjacent face traces before each quadrature point.
face = mesh.GetFaceElementTransformations(faceIndex);
face->SetAllIntPoints(&ip);
const auto volumePoint = first ? face->Elem1->GetIntPoint() : face->Elem2->GetIntPoint();
mfem::DenseMatrix referenceFaceJacobian(face->Jacobian());
if (state.Evaluate(element, volumePoint, mapped) != mean_field::mapping::MappingStatus::valid) {
throw std::runtime_error("Invalid mapping on stellar surface.");
}
mfem::DenseMatrix physicalFaceJacobian(3, 2);
mfem::Mult(mapped.mapping.mapping_jacobian, referenceFaceJacobian, physicalFaceJacobian);
const double weight = ip.weight * physicalFaceJacobian.Weight();
const double radius = mapped.mapping.physical_position.Norml2();
if (!(weight > 0.0) || !std::isfinite(weight) || !std::isfinite(radius) ||
!std::isfinite(mapped.h) || !std::isfinite(mapped.phi)) {
throw std::runtime_error("Non-finite field or non-positive physical surface integration weight.");
}
area += weight;
radiusIntegral += weight * radius;
radiusError += weight * (radius - reference.radius) * (radius - reference.radius);
minimumRadius = std::min(minimumRadius, radius);
maximumRadius = std::max(maximumRadius, radius);
maximumSurfaceEnthalpy = std::max(maximumSurfaceEnthalpy, std::abs(mapped.h) / reference.CentralEnthalpy());
maximumSurfacePotentialError = std::max(maximumSurfacePotentialError,
std::abs(mapped.phi + reference.CentralEnthalpy()) / reference.CentralEnthalpy());
++boundarySamples;
}
}
for (int element = 0; element < mesh.GetNE(); ++element) {
if (!state.isStellar(element)) continue;
const auto geometry = mesh.GetElementBaseGeometry(element);
const auto &vertices = *mfem::Geometries.GetVertices(geometry);
const auto &center = mfem::Geometries.GetCenter(geometry);
for (int vertex = 0; vertex < vertices.GetNPoints(); ++vertex) {
for (const double inset : {0.0, 0.005, 0.02}) {
const auto &v = vertices.IntPoint(vertex);
mfem::IntegrationPoint ip;
ip.Set3((1.0-inset)*v.x+inset*center.x, (1.0-inset)*v.y+inset*center.y, (1.0-inset)*v.z+inset*center.z);
++cornerSamples;
if (state.Evaluate(element, ip, mapped) != mean_field::mapping::MappingStatus::valid) {
++invalidCornerSamples;
continue;
}
auto *transformation = mesh.GetElementTransformation(element);
transformation->SetIntPoint(&ip);
mfem::DenseMatrix totalJacobian(3);
mfem::Mult(mapped.mapping.mapping_jacobian, transformation->Jacobian(), totalJacobian);
minimumCornerDeterminant = std::min(minimumCornerDeterminant, mapped.mapping.mapping_determinant);
const double determinant = totalJacobian.Det();
const double smallest = totalJacobian.CalcSingularvalue(2);
const double largest = totalJacobian.CalcSingularvalue(0);
if (!(determinant > 0.0) || !std::isfinite(determinant) || !(smallest > 0.0) ||
!std::isfinite(smallest) || !std::isfinite(largest)) ++invalidCornerSamples;
else maximumCornerCondition = std::max(maximumCornerCondition, largest / smallest);
}
}
}
if (!(area > 0.0L) || !std::isfinite(area)) throw std::runtime_error("No finite positive stellar surface area.");
return {{"surface_samples", static_cast<double>(boundarySamples)}, {"surface_area", static_cast<double>(area)},
{"surface_radius_minimum", minimumRadius}, {"surface_radius_maximum", maximumRadius},
{"surface_radius_area_mean", static_cast<double>(radiusIntegral / area)},
{"surface_radius_relative_rms_error", static_cast<double>(std::sqrt(radiusError / area) / reference.radius)},
{"surface_radius_relative_range", (maximumRadius - minimumRadius) / reference.radius},
{"surface_enthalpy_maximum_scaled", maximumSurfaceEnthalpy},
{"surface_potential_maximum_scaled_error", maximumSurfacePotentialError},
{"stellar_corner_samples", static_cast<double>(cornerSamples)},
{"invalid_stellar_corner_samples", static_cast<double>(invalidCornerSamples)},
{"minimum_stellar_corner_mapping_determinant", minimumCornerDeterminant},
{"maximum_stellar_corner_element_condition", maximumCornerCondition}};
}
} // namespace experiment::polytrope_validation