Add frozen negative density localization and independent EOS projection diagnostics

This commit is contained in:
2026-09-19 09:46:52 -04:00
parent 1148c099e3
commit 9c22934d52
7 changed files with 1496 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ import mean_field;
#include "shape_field_weak_diagnostics.hpp"
#include "reduced_surface_diagnostics.hpp"
#include "spherical_harmonic_surface_diagnostics.hpp"
#include "negative_density_diagnostics.hpp"
using namespace mean_field;
namespace {
@@ -45,6 +46,7 @@ struct Options {
bool preconditionerProbe = false, nonlinearStepProbe = false;
bool weakShapeProbe = false, weakShapeFinal = false;
bool reducedSurfaceProbe = false, reducedSurfaceAlgebraCheck = false;
bool densityDiagnostic = false;
int surfaceDegree = 4, surfaceMaximumOrder = -1;
std::string surfacePrescription = "nodal";
bool coupledTrialProbe = false, coupledSeedControl = false;
@@ -64,6 +66,7 @@ struct Options {
std::vector<int> captureIterations;
std::string mesh = "sandbox.smesh";
std::filesystem::path output, probeState, probeDirection, replayCandidate;
std::filesystem::path densitySnapshot;
};
Options Parse(int argc, char **argv) {
Options o;
@@ -124,6 +127,10 @@ Options Parse(int argc, char **argv) {
o.reducedSurfaceProbe = true;
else if (a == "--reduced-surface-algebra-check")
o.reducedSurfaceAlgebraCheck = true;
else if (a == "--density-diagnostic")
o.densityDiagnostic = true;
else if (a == "--density-snapshot")
o.densitySnapshot = v();
else if (a == "--surface-prescription")
o.surfacePrescription = v();
else if (a == "--surface-maximum-order")
@@ -194,6 +201,15 @@ Options Parse(int argc, char **argv) {
throw std::invalid_argument("Invalid options");
if (o.surfacePrescription != "nodal" && o.surfacePrescription != "harmonics")
throw std::invalid_argument("Surface prescriptions are nodal or harmonics");
if (!o.densitySnapshot.empty() && !o.densityDiagnostic)
throw std::invalid_argument("--density-snapshot requires --density-diagnostic");
if (o.densityDiagnostic && (o.maxNewton != 0 || o.strategy != "baseline" || o.probes ||
o.pressureProbe || o.shapeCurvature || o.surfaceTrace || o.strategyProbe || o.positivityProbe ||
o.positivityCertificate || o.preconditionerProbe || o.nonlinearStepProbe || o.reducedSurfaceProbe ||
o.reducedSurfaceAlgebraCheck || o.weakShapeProbe || o.weakShapeFinal || o.coupledTrialProbe ||
o.dampedCoupledProbe || !o.probeState.empty() || !o.probeDirection.empty() ||
!o.replayCandidate.empty() || !o.captureIterations.empty()))
throw std::invalid_argument("Density diagnostics require --max-newton 0 and no solver/probe strategies");
if (o.surfacePrescription == "harmonics") {
deformation::SphericalHarmonicSurfaceOptions surfaceOptions{.maximumDegree = o.surfaceDegree};
if (o.surfaceMaximumOrder != -1) surfaceOptions.maximumAzimuthalOrder = o.surfaceMaximumOrder;
@@ -372,6 +388,56 @@ mfem::Vector LoadVector(const std::filesystem::path &path, int expectedSize) {
return result;
}
template <class Problem>
mfem::Vector LoadDensitySnapshot(const Problem &problem, const Options &o, const Scales &scales) {
std::ifstream metadata(o.densitySnapshot / "metadata.txt");
if (!metadata) throw std::invalid_argument("Density replay requires snapshot metadata.");
std::map<std::string, std::string> saved;
std::string line;
while (std::getline(metadata, line)) {
const auto separator = line.find('=');
if (separator != std::string::npos) saved.emplace(line.substr(0, separator), line.substr(separator + 1));
}
const auto number = [&](const std::string &key, double expected) {
const double actual = std::stod(saved.at(key));
if (!std::isfinite(actual) || std::abs(actual - expected) > 1e-13 * std::max(1., std::abs(expected)))
throw std::invalid_argument("Density snapshot model mismatch: " + key);
};
number("n", o.n); number("K", scales.K); number("J", o.angularMomentum);
number("M", 1.); number("R_reference", 1.); number("central_phase", !o.noCentral);
if (saved.at("surface_prescription") != o.surfacePrescription)
throw std::invalid_argument("Density snapshot surface prescription mismatch.");
if (o.surfacePrescription == "harmonics") {
number("surface_degree", o.surfaceDegree);
number("surface_maximum_order", o.surfaceMaximumOrder);
}
// A matching vector size does not establish matching coordinates. Require
// every named state and residual block to match the current manifest.
std::map<std::string, std::pair<int, int>> expected;
for (const auto &block : problem.GetManifest().valueBlocks())
expected.emplace("value," + std::string(block.stableId), std::pair{block.offset, block.size});
for (const auto &block : problem.GetManifest().residualBlocks())
expected.emplace("residual," + std::string(block.stableId), std::pair{block.offset, block.size});
std::ifstream layout(o.densitySnapshot / "layout.csv");
if (!std::getline(layout, line) || line != "kind,block,offset,size,scale")
throw std::invalid_argument("Density replay requires the saved block layout.");
while (std::getline(layout, line)) {
std::istringstream row(line);
std::array<std::string, 5> fields;
for (auto &field : fields) if (!std::getline(row, field, ','))
throw std::invalid_argument("Malformed saved density layout.");
const auto key = fields[0] + "," + fields[1];
const auto found = expected.find(key);
if (found == expected.end() || found->second != std::pair{std::stoi(fields[2]), std::stoi(fields[3])})
throw std::invalid_argument("Density snapshot coordinate layout mismatch: " + key);
expected.erase(found);
}
if (!expected.empty() || layout.bad() || metadata.bad())
throw std::invalid_argument("Incomplete density snapshot provenance.");
return experiment::plateau_diagnostics::coupled_physical_detail::Load(
o.densitySnapshot / "accepted_state.txt", problem.StateSize());
}
template <class State>
void FocusedProbes(State &s, const fem::FEM &f, const Options &o, const mfem::Vector &physicalBase) {
mfem::Vector base;
@@ -615,6 +681,60 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
<< "\ncontext_gravity_schur_relaxation=" << (contextUsesGravitySchur ? o.schurRelaxation : 0.0)
<< "\nschur_correction_sweeps=" << o.schurCorrectionSweeps << "\nschur_relaxation=" << o.schurRelaxation
<< "\ncompiler=" << __VERSION__ << "\ncompiled=" << __DATE__ << ' ' << __TIME__ << '\n';
if (o.densityDiagnostic) {
metadata << "density_diagnostic=1\ndensity_source="
<< (o.densitySnapshot.empty() ? "lane_emden_seed" : o.densitySnapshot.string()) << '\n';
metadata.flush();
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &f) {
namespace d = experiment::plateau_diagnostics::coupled_physical_detail;
const mfem::Vector accepted(s.AcceptedPhysicalState()), normalized(s.acceptedNormalizedState),
residual(s.acceptedNormalizedResidual);
const auto snapshot = o.densitySnapshot.empty() ? mfem::Vector(accepted) : LoadDensitySnapshot(s.Problem(), o, scales);
s.normalizedOperator->NormalizeState(snapshot, s.trialNormalizedState);
if (!s.PrepareTrial()) throw std::runtime_error("Density snapshot cannot be prepared by the physical operator.");
d::PreparedSnapshotView view(s);
experiment::polytrope_validation::PhysicalState physical(view, f);
std::cout << "Frozen density diagnostic F=" << s.trialNormalizedResidual.Norml2() << std::endl;
SaveVector(o.output / "observed_state.txt", s.normalizedOperator->GetPhysicalState());
auto layout = File(o.output / "layout.csv");
layout << "kind,block,offset,size,scale\n";
for (const auto &block : s.Problem().GetManifest().valueBlocks())
layout << "value," << block.stableId << ',' << block.offset << ',' << block.size << ',' << block.scale << '\n';
for (const auto &block : s.Problem().GetManifest().residualBlocks())
layout << "residual," << block.stableId << ',' << block.offset << ',' << block.size << ',' << block.scale << '\n';
auto physicalOutput = File(o.output / "density_physical.csv");
physicalOutput << "quadrature,metric,value\n";
for (const auto &[key, value] : experiment::plateau_diagnostics::MeasurePhysical(physical, o.n, scales.K, 18))
physicalOutput << "18," << key << ',' << value << '\n';
physicalOutput.flush();
const auto &closure = s.Problem().GetPhysicalOperator().GetBarotropicClosureOperator();
auto ruleOutput = File(o.output / "production_closure_rules.csv");
ruleOutput << "element,quadrature_order,quadrature_points\n";
closure.VisitMappedGeometryRules([&](int element, const mfem::IntegrationRule &rule) {
ruleOutput << element << ',' << rule.GetOrder() << ',' << rule.GetNPoints() << '\n';
});
mfem::Vector closureResidual;
closure.BuildResidual(closureResidual);
mfem::ParGridFunction closureFull(f.densityFes.get());
const auto adapter = field::make_field_dof_grid_function_adapter<field::Density,
utils::domain::CoreEnvelopeVacuumDomainSchema>(*f.densityFes);
adapter.scatter(closureResidual, closureFull);
SaveVector(o.output / "production_closure_residual_full.txt", closureFull);
SaveVector(o.output / "observed_density_coefficients.txt", physical.density);
experiment::plateau_diagnostics::RecordNegativeDensity(physical, o.n, scales.K, o.output);
if (!d::Identical(accepted, s.AcceptedPhysicalState()) || !d::Identical(normalized, s.acceptedNormalizedState) ||
!d::Identical(residual, s.acceptedNormalizedResidual))
throw std::runtime_error("Density diagnostic changed the accepted state.");
s.RestoreAccepted();
auto invariants = File(o.output / "density_invariants.csv");
invariants << "metric,value\naccepted_physical_unchanged," << d::Identical(accepted, s.AcceptedPhysicalState())
<< "\naccepted_normalized_unchanged," << d::Identical(normalized, s.acceptedNormalizedState)
<< "\naccepted_residual_unchanged," << d::Identical(residual, s.acceptedNormalizedResidual)
<< "\nprepared_residual_norm," << physical.normalizedBorderedResidualNorm << '\n';
if (!d::Identical(accepted, s.AcceptedPhysicalState())) throw std::runtime_error("Density restoration failed.");
});
return 0;
}
if (o.weakShapeProbe || o.weakShapeFinal)
metadata << "weak_shape_direction_space=normalized_current_factors\n"
<< "weak_shape_operator=full_monolithic\n"