Add checked harmonic snapshot continuation and matched linear trial diagnostics

This commit is contained in:
2026-09-19 13:43:49 -04:00
parent 77130fa603
commit 53faac1cd2
8 changed files with 582 additions and 7 deletions

View File

@@ -36,6 +36,8 @@ import mean_field;
#include "negative_density_diagnostics.hpp"
#include "negative_density_regions.hpp"
#include "density_order_projection.hpp"
#include "continuation_snapshot.hpp"
#include "harmonic_linear_trials.hpp"
using namespace mean_field;
namespace {
@@ -70,7 +72,8 @@ struct Options {
std::vector<int> captureIterations;
std::string mesh = "sandbox.smesh";
std::filesystem::path output, probeState, probeDirection, replayCandidate;
std::filesystem::path densitySnapshot;
std::filesystem::path densitySnapshot, restartSnapshot;
bool continuation = false, allowUnconvergedRestart = false, benchmarkTrials = false;
};
Options Parse(int argc, char **argv) {
Options o;
@@ -143,6 +146,14 @@ Options Parse(int argc, char **argv) {
}
else if (a == "--density-order-final")
o.densityOrderFinal = true;
else if (a == "--restart-snapshot")
o.restartSnapshot = v();
else if (a == "--continuation")
o.continuation = true;
else if (a == "--allow-unconverged-restart")
o.allowUnconvergedRestart = true;
else if (a == "--benchmark-trials")
o.benchmarkTrials = true;
else if (a == "--density-snapshot")
o.densitySnapshot = v();
else if (a == "--surface-prescription")
@@ -213,6 +224,13 @@ Options Parse(int argc, char **argv) {
}
if (o.output.empty() || o.n < 1 || o.n >= 5 || o.maxNewton < 0 || o.maxLinear < 1 || o.restart < 1)
throw std::invalid_argument("Invalid options");
if ((o.continuation || o.allowUnconvergedRestart) && o.restartSnapshot.empty())
throw std::invalid_argument("Continuation/restart permission requires --restart-snapshot");
if (!o.restartSnapshot.empty() && (o.densityDiagnostic || !o.probeState.empty() ||
!o.probeDirection.empty() || !o.replayCandidate.empty() || o.surfacePrescription != "harmonics"))
throw std::invalid_argument("Snapshot restart requires native harmonic coordinates and no legacy replay");
if (o.benchmarkTrials && !o.preconditionerProbe)
throw std::invalid_argument("--benchmark-trials requires --preconditioner-probe");
if (o.surfacePrescription != "nodal" && o.surfacePrescription != "harmonics")
throw std::invalid_argument("Surface prescriptions are nodal or harmonics");
if (!o.densitySnapshot.empty() && !o.densityDiagnostic)
@@ -234,10 +252,10 @@ Options Parse(int argc, char **argv) {
// size match is insufficient to establish modal snapshot provenance.
if (o.reducedSurfaceProbe || o.reducedSurfaceAlgebraCheck || o.weakShapeProbe || o.weakShapeFinal ||
o.probes || o.pressureProbe || o.shapeCurvature || o.surfaceTrace || o.strategyProbe ||
o.positivityProbe || o.positivityCertificate || o.preconditionerProbe || o.nonlinearStepProbe ||
o.positivityProbe || o.positivityCertificate ||
o.coupledTrialProbe || o.dampedCoupledProbe || !o.probeState.empty() || !o.probeDirection.empty() ||
!o.replayCandidate.empty() || o.strategy != "baseline")
throw std::invalid_argument("Native harmonic diagnostics currently support live baseline Newton trajectories only");
!o.replayCandidate.empty() || (o.strategy != "baseline" && o.strategy != "curvature"))
throw std::invalid_argument("Native harmonic diagnostics exclude legacy nodal probes and unsupported strategies");
} else if (o.surfaceMaximumOrder != -1) {
throw std::invalid_argument("--surface-maximum-order requires --surface-prescription harmonics");
}
@@ -656,6 +674,36 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
mfem::Vector residualFactors, stateFactors;
std::vector<int> centralRows;
int centralOffset = -1;
if (!o.restartSnapshot.empty()) {
mfem::Vector restartPhysical;
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &) {
const auto directory = std::filesystem::is_directory(o.restartSnapshot) ? o.restartSnapshot :
(std::filesystem::exists(o.restartSnapshot.parent_path() / "metadata.txt") ?
o.restartSnapshot.parent_path() : o.restartSnapshot.parent_path().parent_path());
const auto saved = experiment::continuation::ParseSnapshotMetadata(directory);
const auto number = [&](const char *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(std::string("Restart metadata mismatch: ") + key);
};
if (saved.at("surface_prescription") != o.surfacePrescription)
throw std::invalid_argument("Restart surface coordinate prescription differs");
number("surface_degree", o.surfaceDegree); number("surface_maximum_order", o.surfaceMaximumOrder);
number("density_order", field::Density::scalarOrder); number("enthalpy_order", field::Enthalpy::scalarOrder);
number("gravity_potential_order", field::Gravity::potentialOrder);
number("gravity_flux_order", field::Gravity::fluxOrder);
number("displacement_order", field::Displacement::Vector::familyOrder);
number("M", 1.); number("R_reference", 1.);
if (!o.continuation) {
number("n", o.n); number("K", scales.K); number("J", o.angularMomentum);
number("central_phase", !o.noCentral);
}
restartPhysical = experiment::continuation::LoadMappedSnapshot(s.Problem(),
o.restartSnapshot, o.continuation, o.allowUnconvergedRestart);
});
solver::detail::StellarEquilibriumContextDiagnostics::InitializePhysicalState(context, restartPhysical);
}
const bool contextUsesGravitySchur = !o.preconditionerProbe &&
(o.preconditionerVariant == "gravity-schur" || o.preconditionerVariant == "stellar-ldu-gravity-schur");
auto metadata = File(o.output / "metadata.txt");
@@ -697,6 +745,9 @@ 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';
metadata << "restart_snapshot=" << o.restartSnapshot.string() << "\ncontinuation=" << o.continuation
<< "\nallow_unconverged_restart=" << o.allowUnconvergedRestart
<< "\nbenchmark_trials=" << o.benchmarkTrials << '\n';
metadata << "density_order=" << field::Density::scalarOrder
<< "\nenthalpy_order=" << field::Enthalpy::scalarOrder
<< "\ngravity_potential_order=" << field::Gravity::potentialOrder
@@ -880,9 +931,10 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
}
// Custom Newton recipes do not instantiate the nine-variant frozen suite.
if constexpr (FullDefaultStack) if (o.preconditionerProbe) {
metadata << "benchmark_input=" << (o.probeState.empty() ? "lane_emden_seed" : "physical_snapshot") << '\n';
metadata << "benchmark_input=" << (!o.restartSnapshot.empty() ? "checked_restart_snapshot" :
o.probeState.empty() ? "lane_emden_seed" : "physical_snapshot") << '\n';
if (!o.probeState.empty()) metadata << "probe_state=" << o.probeState << '\n';
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &) {
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &f) {
if (o.probeState.empty()) {
s.trialNormalizedState = s.acceptedNormalizedState;
} else {
@@ -899,6 +951,8 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
{.variant = o.preconditionerVariant, .restart = o.restart,
.maximumIterations = o.maxLinear, .relativeTolerance = o.linearTolerance,
.schurCorrectionSweeps = o.schurCorrectionSweeps, .schurRelaxation = o.schurRelaxation});
if (o.benchmarkTrials)
experiment::plateau_diagnostics::RecordHarmonicLinearTrials(s, f, o.output, o.n);
});
// A missed linear gate is a measured outcome, not failure to execute
// the experiment. The CSV retains both recurrence and true gate results.