#pragma once // Include after import mean_field. Root splitting removes the discontinuous // sign indicator from the integration coordinate; tangential and mapped-volume // integration still require the quadrature convergence study recorded here. #include "polytrope_physical_state.hpp" #include #include #include #include #include #include #include #include #include #include namespace experiment::plateau_diagnostics { namespace negative_density_regions_detail { using Real = long double; struct Polynomial final { Real c, b, a; [[nodiscard]] Real Value(Real t) const { return std::fma(std::fma(a, t, b), t, c); } [[nodiscard]] Real Integral(Real left, Real right) const { const Real width = right - left; // Subtracting antiderivatives loses a thin pocket's entire mass when // both antiderivatives are large. This centered quadratic identity does // not subtract nearly equal endpoint values. return width * (Value(left + width / 2) + a * width * width / 12); } [[nodiscard]] Real Scale() const { return std::max({std::abs(a), std::abs(b), std::abs(c)}); } }; struct Interval final { Real left, right; bool negative; }; inline Polynomial Interpolate(Real left, Real middle, Real right) { return {left, 4 * middle - 3 * left - right, 2 * (left + right - 2 * middle)}; } inline std::vector Partition(const Polynomial& p) { if (!std::isfinite(p.a) || !std::isfinite(p.b) || !std::isfinite(p.c)) throw std::domain_error("Nonfinite density slice polynomial."); std::vector points{0, 1}; const auto add = [&](Real root) { if (std::isfinite(root) && root > 0 && root < 1) points.push_back(root); }; const Real scale = p.Scale(); if (scale > 0) { const Real a = p.a / scale, b = p.b / scale, c = p.c / scale; // No relative cutoff turns a small quadratic into a line: a thin // negative interval can be the very effect this diagnostic must retain. if (a == 0) { if (b != 0) add(-c / b); } else { const Real discriminant = std::fma(-4 * a, c, b * b); if (discriminant == 0) { add(-b / (2 * a)); } else if (discriminant > 0) { const Real q = -Real{0.5} * (b + std::copysign(std::sqrt(discriminant), b)); // The product-of-roots form avoids cancellation at a root near // the face. q cannot vanish for a positive discriminant. add(q / a); add(c / q); } } } std::sort(points.begin(), points.end()); points.erase(std::unique(points.begin(), points.end()), points.end()); std::vector result; for (std::size_t i = 1; i < points.size(); ++i) { const Real left = points[i - 1], right = points[i]; if (!(right > left)) throw std::logic_error("Invalid density root partition."); const Real value = p.Value(left + (right - left) / 2); const bool negative = value < 0; // Two extra interior signs catch missed roots without assigning a // finite-width pocket to an exactly tangent double root. for (const Real fraction : {Real{0.25}, Real{0.75}}) { const Real check = p.Value(left + fraction * (right - left)); if ((negative && check > 0) || (!negative && check < 0)) throw std::runtime_error("Density slice root partition has inconsistent interval signs."); } result.push_back({left, right, negative}); } return result; } inline std::ofstream Open(const std::filesystem::path& path) { if (std::filesystem::exists(path)) throw std::invalid_argument("Density region output already exists: " + path.string()); std::ofstream out(path); out.exceptions(std::ios::failbit | std::ios::badbit); out << std::setprecision(20); return out; } // These are analytic, uniform-measure one-dimensional controls, independent of // FEM and geometry. The fractional/cubic data are interpolated at three Gauss // nodes, reproducing the underresolved positive-profile mechanism of interest. inline void SelfChecks(const std::filesystem::path& path) { auto out = Open(path); out << "case,negative_length,expected_negative_length,negative_mass,expected_negative_mass,passed\n"; const auto check = [&](const char* name, const Polynomial& p, Real expectedLength, Real expectedMass) { Real length = 0, mass = 0; for (const auto& interval : Partition(p)) if (interval.negative) { length += interval.right - interval.left; mass -= p.Integral(interval.left, interval.right); } const bool passed = std::abs(length - expectedLength) <= 2e-14L * std::max(Real{1e-12}, expectedLength) && std::abs(mass - expectedMass) <= 2e-13L * std::max(Real{1e-12}, expectedMass); out << name << ',' << length << ',' << expectedLength << ',' << mass << ',' << expectedMass << ',' << passed << '\n'; if (!passed) throw std::runtime_error(std::string("Density region analytic self-check failed: ") + name); }; check("positive_quadratic", {1, 0, 1}, 0, 0); check("negative_constant", {-2, 0, 0}, 1, 2); check("linear_crossing", {-0.25L, 1, 0}, 0.25L, 0.03125L); check("double_root_nonnegative", {0.25L, -1, 1}, 0, 0); check("near_linear_positive", {1, 1, 1e-28L}, 0, 0); // Exact binary coefficients isolate the root algorithm from the separate // conditioning of subtracting a tiny decimal offset from a large value. check("near_double_negative", {0.25L - std::ldexp(Real{1}, -40), -1, 1}, std::ldexp(Real{1}, -19), 4 * std::ldexp(Real{1}, -60) / 3); const Real d = std::sqrt(Real{15}) / 10; const auto fractional = [](Real t) { return t * std::sqrt(t); }; const Real y0 = fractional(Real{0.5} - d), y1 = fractional(Real{0.5}), y2 = fractional(Real{0.5} + d); const Real a = (y0 + y2 - 2 * y1) / (2 * d * d); const Real b = (y2 - y0) / (2 * d) - a; const Polynomial fractionalInterpolant{y1 - b / 2 - a / 4, b, a}; check("gauss_q2_t_power_1p5", fractionalInterpolant, 0.04548403175047501373888L, 0.0005262295634284655L); check("gauss_q2_t_power_3", {0.05L, -0.6L, 1.5L}, std::sqrt(Real{6}) / 15, std::sqrt(Real{6}) / 2250); } struct Axis final { int coordinate{0}; int orientation{1}; int face{-1}; int faceCount{0}; }; inline std::vector ChooseAxes(polytrope_validation::PhysicalState& state) { auto& mesh = *state.finiteElements.mesh; std::vector axes(static_cast(mesh.GetNE())); for (int face = 0; face < mesh.GetNumFaces(); ++face) { int left = -1, right = -1; mesh.GetFaceElements(face, &left, &right); const bool leftStar = left >= 0 && state.isStellar(left); const bool rightStar = right >= 0 && state.isStellar(right); if (leftStar == rightStar) continue; const int element = leftStar ? left : right; auto& axis = axes[static_cast(element)]; ++axis.faceCount; if (axis.face >= 0) continue; auto* transformation = mesh.GetFaceElementTransformations(face); if (!transformation) throw std::runtime_error("Missing density region face transformation."); mfem::IntegrationPoint facePoint, point; facePoint.Set2(0.5, 0.5); if (leftStar) transformation->Loc1.Transform(facePoint, point); else transformation->Loc2.Transform(facePoint, point); const std::array coordinates{point.x, point.y, point.z}; int selected = -1; for (int d = 0; d < 3; ++d) { if (std::abs(coordinates[d]) < 1e-12 || std::abs(coordinates[d] - 1) < 1e-12) { if (selected >= 0) throw std::runtime_error("Ambiguous density region reference face."); selected = d; } } if (selected < 0) throw std::runtime_error("Density region face is not a tensor-product cell face."); axis.coordinate = selected; axis.orientation = coordinates[selected] < 0.5 ? 1 : -1; axis.face = face; } return axes; } inline mfem::IntegrationPoint Point(const Axis& axis, double u, double v, Real t) { std::array coordinate{}; coordinate[axis.coordinate] = static_cast(axis.orientation > 0 ? t : 1 - t); coordinate[(axis.coordinate + 1) % 3] = u; coordinate[(axis.coordinate + 2) % 3] = v; mfem::IntegrationPoint result; result.Set3(coordinate[0], coordinate[1], coordinate[2]); return result; } struct Measure final { Real volume{0}, mass{0}, negativeVolume{0}, negativeMass{0}; std::size_t lines{0}, negativeIntervals{0}, points{0}; double minimumRho{std::numeric_limits::infinity()}; double witnessH{std::numeric_limits::quiet_NaN()}; double witnessT{std::numeric_limits::quiet_NaN()}, witnessU{witnessT}, witnessV{witnessT}; std::array witnessPosition{}; int witnessElement{-1}; double negativeRadiusMin{std::numeric_limits::infinity()}, negativeRadiusMax{-negativeRadiusMin}; Real negativeTMin{std::numeric_limits::infinity()}, negativeTMax{-negativeTMin}; double maximumReconstructionError{0}, maximumReconstructionTolerance{0}; void Add(const Measure& other) { volume += other.volume; mass += other.mass; negativeVolume += other.negativeVolume; negativeMass += other.negativeMass; lines += other.lines; negativeIntervals += other.negativeIntervals; points += other.points; if (other.minimumRho < minimumRho) { minimumRho = other.minimumRho; witnessH = other.witnessH; witnessT = other.witnessT; witnessU = other.witnessU; witnessV = other.witnessV; witnessPosition = other.witnessPosition; witnessElement = other.witnessElement; } negativeRadiusMin = std::min(negativeRadiusMin, other.negativeRadiusMin); negativeRadiusMax = std::max(negativeRadiusMax, other.negativeRadiusMax); negativeTMin = std::min(negativeTMin, other.negativeTMin); negativeTMax = std::max(negativeTMax, other.negativeTMax); maximumReconstructionError = std::max(maximumReconstructionError, other.maximumReconstructionError); maximumReconstructionTolerance = std::max(maximumReconstructionTolerance, other.maximumReconstructionTolerance); } }; inline Real Observed(Real value) { return std::isfinite(value) ? value : std::numeric_limits::quiet_NaN(); } inline void Header(std::ostream& out) { out << "tangent_lines,negative_intervals,integration_points,volume,mass,negative_volume,negative_mass," "negative_volume_fraction,negative_mass_fraction,minimum_density,witness_h,witness_element,witness_t,witness_u,witness_v," "witness_x,witness_y,witness_z,negative_radius_minimum,negative_radius_maximum,negative_interval_t_minimum," "negative_interval_t_maximum,maximum_reconstruction_error,maximum_reconstruction_tolerance\n"; } inline void Write(std::ostream& out, const Measure& m) { out << m.lines << ',' << m.negativeIntervals << ',' << m.points << ',' << m.volume << ',' << m.mass << ',' << m.negativeVolume << ',' << m.negativeMass << ',' << m.negativeVolume / m.volume << ',' << (m.mass != 0 ? m.negativeMass / std::abs(m.mass) : std::numeric_limits::quiet_NaN()) << ',' << m.minimumRho << ',' << m.witnessH << ',' << m.witnessElement << ',' << m.witnessT << ',' << m.witnessU << ',' << m.witnessV; for (double x : m.witnessPosition) out << ',' << x; out << ',' << Observed(m.negativeRadiusMin) << ',' << Observed(m.negativeRadiusMax) << ',' << Observed(m.negativeTMin) << ',' << Observed(m.negativeTMax) << ',' << m.maximumReconstructionError << ',' << m.maximumReconstructionTolerance << '\n'; } inline bool Identical(const mfem::Vector& a, const mfem::Vector& b) { return a.Size() == b.Size() && (a.Size() == 0 || std::memcmp(a.GetData(), b.GetData(), sizeof(mfem::real_t) * a.Size()) == 0); } } // namespace negative_density_regions_detail inline void RecordDensityNegativeRegions(polytrope_validation::PhysicalState& state, double n, double K, const std::filesystem::path& output) { namespace d = negative_density_regions_detail; using d::Real; if (!std::isfinite(n) || n < 1 || !std::isfinite(K) || K <= 0) throw std::invalid_argument("Density regions require finite n>=1 and K>0."); std::filesystem::create_directories(output); // Fail the one-dimensional arithmetic controls before any FEM evaluation. d::SelfChecks(output / "density_negative_regions_selfchecks.csv"); const auto& fem = state.finiteElements; if (!fem.mesh || !fem.densityFes || !fem.enthalpyFes || !fem.compactificationCoordinate || fem.mesh->Dimension() != 3 || !fem.mesh->Conforming() || fem.densityFes->GetVDim() != 1 || dynamic_cast(fem.densityFes->FEColl()) == nullptr || state.density.ParFESpace() != fem.densityFes.get() || state.enthalpy.ParFESpace() != fem.enthalpyFes.get()) throw std::invalid_argument("Density regions require the current scalar L2 density space on a conforming 3D mesh."); int ranks = 0; MPI_Comm_size(fem.mesh->GetComm(), &ranks); if (ranks != 1) throw std::invalid_argument("Density regions currently require one MPI rank."); for (int element = 0; element < fem.mesh->GetNE(); ++element) if (state.isStellar(element)) { const auto* fe = fem.densityFes->GetFE(element); if (!fe || fe->GetOrder() != 2 || fe->GetMapType() != mfem::FiniteElement::VALUE || fe->GetGeomType() != mfem::Geometry::CUBE || fe->GetDof() != 27) throw std::invalid_argument("Density root splitting requires scalar VALUE Q2 hexahedral density elements."); } const auto axes = d::ChooseAxes(state); const mfem::Vector savedDensity(state.density), savedEnthalpy(state.enthalpy), savedPotential(state.potential), savedGravity(state.gravityGradientReference), savedDisplacement(state.displacement); mean_field::mapping::GridFunctionMappingEvaluator mapper(state.domainMapper, state.displacement, *fem.compactificationCoordinate); mean_field::mapping::MappingPointContext mapped; auto elements = d::Open(output / "density_negative_regions_elements.csv"); elements << "tangential_order,interval_order,element,attribute,axis,orientation,surface_face,surface_face_count,"; d::Header(elements); auto summary = d::Open(output / "density_negative_regions_summary.csv"); summary << "tangential_order,interval_order,"; d::Header(summary); auto intervals = d::Open(output / "density_negative_regions_intervals.csv"); intervals << "tangential_order,element,attribute,axis,orientation,surface_face,u,v,tangential_weight,t_left,t_right," "coefficient_c,coefficient_b,coefficient_a,negative_volume,negative_mass,minimum_density,witness_h," "radius_minimum,radius_maximum,left_x,left_y,left_z,right_x,right_y,right_z\n"; constexpr int intervalOrder = 14; const auto& intervalRule = mfem::IntRules.Get(mfem::Geometry::SEGMENT, intervalOrder); if (intervalRule.GetNPoints() != 8) throw std::logic_error("Unexpected density interval Gauss rule."); for (int tangentialOrder : {14, 22, 30}) { const auto& tangentRule = mfem::IntRules.Get(mfem::Geometry::SEGMENT, tangentialOrder); if (tangentRule.GetNPoints() != tangentialOrder / 2 + 1) throw std::logic_error("Unexpected density tangential Gauss rule."); d::Measure global; for (int element = 0; element < fem.mesh->GetNE(); ++element) { if (!state.isStellar(element)) continue; const auto& axis = axes[static_cast(element)]; auto* transformation = fem.mesh->GetElementTransformation(element); d::Measure local; for (int iu = 0; iu < tangentRule.GetNPoints(); ++iu) for (int iv = 0; iv < tangentRule.GetNPoints(); ++iv) { const auto& up = tangentRule.IntPoint(iu); const auto& vp = tangentRule.IntPoint(iv); const double u = up.x, v = vp.x; const Real tangentWeight = static_cast(up.weight) * vp.weight; const auto value = [&](Real t) { const auto point = d::Point(axis, u, v, t); const double rho = state.density.GetValue(element, point); if (!std::isfinite(rho)) throw std::domain_error("Nonfinite density slice value."); return rho; }; const auto polynomial = d::Interpolate(value(0), value(Real{0.5}), value(1)); const Real tolerance = 512 * std::numeric_limits::epsilon() * polynomial.Scale(); const auto verify = [&](Real t, double actual) { const Real error = std::abs(polynomial.Value(t) - actual); local.maximumReconstructionError = std::max(local.maximumReconstructionError, static_cast(error)); local.maximumReconstructionTolerance = std::max(local.maximumReconstructionTolerance, static_cast(tolerance)); if (!std::isfinite(error) || error > tolerance) throw std::runtime_error("Frozen density slice fails independent Q2 reconstruction."); }; for (Real t : {Real{0.125}, Real{0.25}, Real{0.375}, Real{0.625}, Real{0.75}, Real{0.875}}) verify(t, value(t)); const auto parts = d::Partition(polynomial); ++local.lines; const auto evaluate = [&](Real t, d::Measure& measure, bool negativeRegion) { const auto point = d::Point(axis, u, v, t); if (mapper.EvaluatePoint(*transformation, point, mapped) != mean_field::mapping::MappingStatus::valid || mapped.compactified) throw std::domain_error("Invalid stellar density-region mapping."); const double rho = state.density.GetValue(element, point), h = state.enthalpy.GetValue(element, point); if (!std::isfinite(rho) || !std::isfinite(h)) throw std::domain_error("Nonfinite density-region witness."); verify(t, rho); if (rho < measure.minimumRho) { measure.minimumRho = rho; measure.witnessH = h; measure.witnessElement = element; measure.witnessT = static_cast(t); measure.witnessU = u; measure.witnessV = v; for (int component = 0; component < 3; ++component) measure.witnessPosition[component] = mapped.physical_position(component); } if (negativeRegion) { const double radius = mapped.physical_position.Norml2(); if (!std::isfinite(radius)) throw std::domain_error("Nonfinite density-region radius."); measure.negativeRadiusMin = std::min(measure.negativeRadiusMin, radius); measure.negativeRadiusMax = std::max(measure.negativeRadiusMax, radius); } transformation->SetIntPoint(&point); const double referenceDeterminant = transformation->Jacobian().Det(); const Real determinant = static_cast(referenceDeterminant) * mapped.mapping_determinant; if (!(referenceDeterminant > 0) || !std::isfinite(referenceDeterminant) || !(mapped.mapping_determinant > 0) || !std::isfinite(mapped.mapping_determinant) || !(determinant > 0) || !std::isfinite(determinant)) throw std::domain_error("Nonpositive or nonfinite density-region physical measure."); return determinant; }; for (const auto& part : parts) { d::Measure piece; std::array leftPosition{}, rightPosition{}; (void)evaluate(part.left, piece, part.negative); for (int component = 0; component < 3; ++component) leftPosition[component] = mapped.physical_position(component); (void)evaluate(part.right, piece, part.negative); for (int component = 0; component < 3; ++component) rightPosition[component] = mapped.physical_position(component); if (polynomial.a != 0) { const Real stationary = -polynomial.b / (2 * polynomial.a); if (stationary > part.left && stationary < part.right) (void)evaluate(stationary, piece, part.negative); } for (int it = 0; it < intervalRule.GetNPoints(); ++it) { const auto& qp = intervalRule.IntPoint(it); const Real t = part.left + (part.right - part.left) * qp.x; const Real determinant = evaluate(t, piece, part.negative); const Real weight = tangentWeight * (part.right - part.left) * qp.weight * determinant; const Real rho = polynomial.Value(t); if (!(weight > 0) || !std::isfinite(weight) || !std::isfinite(rho) || (part.negative && rho > 0) || (!part.negative && rho < 0)) throw std::runtime_error("Density region quadrature disagrees with its root partition."); piece.volume += weight; piece.mass += weight * rho; ++piece.points; } if (part.negative) { piece.negativeIntervals = 1; piece.negativeVolume = piece.volume; piece.negativeMass = -piece.mass; piece.negativeTMin = part.left; piece.negativeTMax = part.right; if (tangentialOrder == 30) { intervals << tangentialOrder << ',' << element << ',' << fem.mesh->GetAttribute(element) << ',' << axis.coordinate << ',' << axis.orientation << ',' << axis.face << ',' << u << ',' << v << ',' << tangentWeight << ',' << part.left << ',' << part.right << ',' << polynomial.c << ',' << polynomial.b << ',' << polynomial.a << ',' << piece.negativeVolume << ',' << piece.negativeMass << ',' << piece.minimumRho << ',' << piece.witnessH << ',' << piece.negativeRadiusMin << ',' << piece.negativeRadiusMax; for (double x : leftPosition) intervals << ',' << x; for (double x : rightPosition) intervals << ',' << x; intervals << '\n'; } } local.Add(piece); } } if (!(local.volume > 0) || !std::isfinite(local.volume) || !std::isfinite(local.mass) || !std::isfinite(local.negativeVolume) || !std::isfinite(local.negativeMass) || local.negativeMass < 0) throw std::runtime_error("Invalid integrated density-region measure."); elements << tangentialOrder << ',' << intervalOrder << ',' << element << ',' << fem.mesh->GetAttribute(element) << ',' << axis.coordinate << ',' << axis.orientation << ',' << axis.face << ',' << axis.faceCount << ','; d::Write(elements, local); global.Add(local); } if (!(global.volume > 0)) throw std::invalid_argument("Density region diagnostic found no stellar volume."); summary << tangentialOrder << ',' << intervalOrder << ','; d::Write(summary, global); summary.flush(); elements.flush(); intervals.flush(); } const bool unchanged = d::Identical(savedDensity, state.density) && d::Identical(savedEnthalpy, state.enthalpy) && d::Identical(savedPotential, state.potential) && d::Identical(savedGravity, state.gravityGradientReference) && d::Identical(savedDisplacement, state.displacement); if (!unchanged) throw std::logic_error("Density region observation changed a source field."); auto scope = d::Open(output / "density_negative_regions_scope.txt"); scope << "source_fields_bitwise_identical=1\ntrial_preparations=0\nmpi_ranks=1\n" << "n=" << n << "\nK=" << K << "\ndensity_basis=" << fem.densityFes->FEColl()->Name() << '\n' << "Mass uses the reconstructed Q2 polynomial, verified against direct FE values; no density or EOS clipping.\n" << "Volume weights contain one reference determinant and one deformation determinant.\n" << "All stellar cells are included; surface axes point inward from the lowest-index stellar/nonstellar face.\n" << "Other cells use reference axis0. Tangential orders14/22/30 use8/12/16 points per axis; interval order14 uses8.\n" << "Whole-cell denominators integrate every root-partition interval with the same sliced quadrature.\n" << "Negative intervals are exact for each reconstructed Q2 line up to floating arithmetic; tangential integration is sampled.\n" << "Witnesses and radial bounds are samples, not global extrema or polynomial sign certificates.\n" << "Near-double-root regions below reconstruction uncertainty are not certified; compare quadrature levels and errors.\n"; } } // namespace experiment::plateau_diagnostics