Add bounded fully coupled second-order trial correction and frozen screen

This commit is contained in:
2026-09-18 09:41:00 -04:00
parent 2944dd5019
commit 0d87b8b919
14 changed files with 2442 additions and 35 deletions

View File

@@ -0,0 +1,80 @@
# Bounded coupled second-order trial correction
Branch `research/coupled-trial-correction-20260918`, based on the completed
nonlinear obstruction investigation at `2944dd5`. The configured worktree
retains its existing name, `worktrees/preconditioner-improvement`, so that
build reuse does not disturb the preceding branch or immutable executables.
## Hypothesis
At the recorded n = 1.5, J = 0 endpoint, tenfold tighter linear accuracy did
not improve the useful trial range. The full directional derivative was
consistent with the prepared residual. Gravity-only correction reduced the
remainder norm but increased its adverse projection through hydrostatic,
EOS and surface balance. The next hypothesis is that correcting the entire
trial remainder can preserve the intended Newton response over larger steps.
For the captured base residual F0 and verified Newton direction p, preserve
L(alpha) = F0 + alpha J0 p. At a prepared candidate y, solve
J(y) c = -(F(y) - L(alpha)),
then prepare and evaluate y+c. All equations, normalization factors and
ordinary full-residual acceptance thresholds remain unchanged. A separate
auxiliary true-defect check is required; the base Newton report is never
replaced by a correction report. The locally second-order argument requires
a bounded inverse, which is not assumed to hold uniformly in this model.
## Implementation contracts
- A third, defaulted compile-time trial-path policy composes with the
established enthalpy and gravity policies. Existing two-policy recipes
retain their types and behavior. The new policy is incompatible with the
eliminated-gravity path until a corresponding reduced-manifold contract
is designed; unsupported combinations fail at compile time.
- The auxiliary solve has its own backend, a finite iteration budget and an
independent application of the complete candidate Jacobian. Its initial
options are relative tolerance 0.03, absolute tolerance 1e-14, at most160
iterations and restart160.
- Limit the correction norm to alpha||p|| by default. The initial work limit
is one actual auxiliary solve per Newton iteration, and one solve per
selected candidate. Runtime expert controls change these bounds explicitly.
- Select correction only after a prepared candidate fails the existing
acceptance predicate. Reuse the exact configured metric for selecting
between original and corrected candidates. An already acceptable step
incurs no coupled solve; an unprepared candidate cannot invoke one.
- Recheck the same selected admissibility policy and actual geometry after
correction. Retain the original candidate if the auxiliary solve fails,
the norm cap rejects it, preparation/admissibility fails, or its metric
worsens. Restoring a fallback includes fresh operator preparation, not
merely copying state vectors.
- The exported numerical core and trial transaction are shared by production
and frozen diagnostics. The physics-facing model API is unchanged.
## Initial screen
The committed `coupled_trial_screen_2026-09-18.json` records complete controls
and immutable input paths. All heavy builds and numerical cases run serially.
Evidence is written to `diagnostics/coupled_trial_2026-09-18/` in the task
workspace; individual campaign directories are immutable.
Use the previously saved late eta0.03 direction, independently reapplying the
full Jacobian at the restored base before using it. This avoids conflating a
new linear solve with the new trial map. Test both raw and gravity-corrected
starting candidates at alpha1/16,1/64,1/256,1/1024. The middle two previously
prepared but failed the merit test. The largest failed pressure preparation;
the smallest already decreased the residual and must not trigger correction.
The seed control uses its saved eta0.03 direction and alpha1 with the
gravity-corrected path. Its candidate already passes; it should preserve that
result without auxiliary coupled work. Each frozen alpha is an independent
trial with a fresh per-iteration budget, not a simulated live line search.
Record inputs/factors/layout, F0,p,J0p, the actual auxiliary RHS and J(y)c,
candidate and returned state/residual vectors, true defects, norm-cap gates,
selection/fallback decisions, restoration invariants and complete work.
Useful frozen progress is a prerequisite for a bounded nonlinear campaign.
Any accepted model must also be checked for virial balance and internal
admissibility; a lower residual alone is insufficient.
Implementation and test results are pending.

View File

@@ -0,0 +1,138 @@
[
{
"id": "late_curvature_coupled",
"arguments": [
"--index",
"1.5",
"--angular-momentum",
"0",
"--no-central",
"--max-newton",
"0",
"--coupled-trial-probe",
"--strategy",
"curvature-coupled",
"--probe-state",
"/Users/tboudreaux/.codex/.chatgpt-projects/g-p-6a6b3f2194ec8191bebcd59075897f1d/diagnostics/nonlinear_step_2026-09-18/late_v1/n1p5_late_eta003/nonlinear_step_physical_base.txt",
"--probe-direction",
"/Users/tboudreaux/.codex/.chatgpt-projects/g-p-6a6b3f2194ec8191bebcd59075897f1d/diagnostics/nonlinear_step_2026-09-18/late_v1/n1p5_late_eta003/nonlinear_step_direction_full.txt",
"--linear-tol",
"0.03",
"--restart",
"160",
"--preconditioner-variant",
"gravity-schur",
"--schur-sweeps",
"1",
"--schur-relaxation",
"0.5",
"--gravity-tol",
"1e-10",
"--gravity-iterations",
"300",
"--gravity-restart",
"80",
"--coupled-tol",
"0.03",
"--coupled-iterations",
"160",
"--coupled-restart",
"160",
"--coupled-max-ratio",
"1",
"--coupled-attempts",
"1"
]
},
{
"id": "late_raw_coupled",
"arguments": [
"--index",
"1.5",
"--angular-momentum",
"0",
"--no-central",
"--max-newton",
"0",
"--coupled-trial-probe",
"--strategy",
"coupled",
"--probe-state",
"/Users/tboudreaux/.codex/.chatgpt-projects/g-p-6a6b3f2194ec8191bebcd59075897f1d/diagnostics/nonlinear_step_2026-09-18/late_v1/n1p5_late_eta003/nonlinear_step_physical_base.txt",
"--probe-direction",
"/Users/tboudreaux/.codex/.chatgpt-projects/g-p-6a6b3f2194ec8191bebcd59075897f1d/diagnostics/nonlinear_step_2026-09-18/late_v1/n1p5_late_eta003/nonlinear_step_direction_full.txt",
"--linear-tol",
"0.03",
"--restart",
"160",
"--preconditioner-variant",
"gravity-schur",
"--schur-sweeps",
"1",
"--schur-relaxation",
"0.5",
"--gravity-tol",
"1e-10",
"--gravity-iterations",
"300",
"--gravity-restart",
"80",
"--coupled-tol",
"0.03",
"--coupled-iterations",
"160",
"--coupled-restart",
"160",
"--coupled-max-ratio",
"1",
"--coupled-attempts",
"1"
]
},
{
"id": "seed_curvature_coupled",
"arguments": [
"--index",
"1.5",
"--angular-momentum",
"0",
"--no-central",
"--max-newton",
"0",
"--coupled-trial-probe",
"--strategy",
"curvature-coupled",
"--probe-state",
"/Users/tboudreaux/.codex/.chatgpt-projects/g-p-6a6b3f2194ec8191bebcd59075897f1d/diagnostics/nonlinear_step_2026-09-18/controls_v1/n1p5_seed_eta003/nonlinear_step_physical_base.txt",
"--probe-direction",
"/Users/tboudreaux/.codex/.chatgpt-projects/g-p-6a6b3f2194ec8191bebcd59075897f1d/diagnostics/nonlinear_step_2026-09-18/controls_v1/n1p5_seed_eta003/nonlinear_step_direction_full.txt",
"--linear-tol",
"0.03",
"--restart",
"160",
"--preconditioner-variant",
"gravity-schur",
"--schur-sweeps",
"1",
"--schur-relaxation",
"0.5",
"--gravity-tol",
"1e-10",
"--gravity-iterations",
"300",
"--gravity-restart",
"80",
"--coupled-tol",
"0.03",
"--coupled-iterations",
"160",
"--coupled-restart",
"160",
"--coupled-max-ratio",
"1",
"--coupled-attempts",
"1",
"--coupled-seed-control"
]
}
]

View File

@@ -0,0 +1,471 @@
#pragma once
// Included after import mean_field. The experiment owns observations only;
// the prepared production transaction owns correction and fallback decisions.
#include "plateau_nonlinear_step_diagnostics.hpp"
#include <optional>
#include <vector>
namespace experiment::plateau_diagnostics {
namespace coupled_trial_detail {
// Expose only the production workspace operations needed by this frozen probe.
// Keeping the affine gravity gate here would let the comparison silently drift
// away from the trial path used by the nonlinear solver.
template <class State>
struct GravityWorkspace final : mean_field::solver::detail::StellarGravityWorkspace<State> {
using Base = mean_field::solver::detail::StellarGravityWorkspace<State>;
using Base::Base;
using Base::ApplyCorrection;
using Base::BeginGravityAttempt;
using Base::Refresh;
using Base::desiredResidual;
using Base::fullCorrection;
using Base::gravityCorrection;
using Base::lastAffineDisagreement;
using Base::lastPreparedDefect;
using Base::lastPreparedTargetThreshold;
using Base::lastSolve;
using Base::subsystem;
using Base::trialSolveSucceeded;
};
}
struct CoupledTrialProbeOptions final {
double outerLinearTolerance{.03};
mean_field::solver::nonlinear::GravitySolveOptions gravity{};
mean_field::solver::CoupledTrialCorrectionOptions coupled{};
bool gravityPreconditionPath{true};
bool seedControl{false};
double sufficientDecrease{1.e-4};
};
template <class State>
void ProbeCoupledTrial(State &state, const mfem::Vector &normalizedBase,
const mfem::Vector &suppliedDirection, const std::filesystem::path &output,
CoupledTrialProbeOptions options = {}) {
using namespace mean_field;
namespace d = nonlinear_step_detail;
using GravityWorkspace = coupled_trial_detail::GravityWorkspace<State>;
using GravityCorrection = solver::PreparedGravityCurvatureCorrection<typename GravityWorkspace::Subsystem>;
using Transaction = solver::detail::PreparedCoupledTrialTransaction<State>;
const auto communicator = state.Problem().GetCommunicator();
int ranks = 0;
MPI_Comm_size(communicator, &ranks);
const int size = state.Problem().StateSize();
if (ranks != 1 || size != state.Problem().EquationSize() || normalizedBase.Size() != size ||
suppliedDirection.Size() != size || !d::Finite(normalizedBase) || !d::Finite(suppliedDirection) ||
!std::isfinite(options.outerLinearTolerance) || options.outerLinearTolerance <= 0. ||
options.outerLinearTolerance >= 1. || !std::isfinite(options.sufficientDecrease) ||
options.sufficientDecrease <= 0. || options.sufficientDecrease >= 1.)
throw std::invalid_argument("Coupled-trial probes require one rank, compatible finite vectors and valid controls.");
options.gravity.Validate();
options.coupled.Validate();
const auto start = d::Clock::now();
const double nan = std::numeric_limits<double>::quiet_NaN();
const mfem::Vector base(normalizedBase), direction(suppliedDirection);
const auto &normalization = state.normalizedOperator->GetNormalization();
const mfem::Vector factors(normalization.StateFactors()), residualFactors(normalization.ResidualFactors());
const mfem::Vector savedAccepted(state.acceptedNormalizedState),
savedAcceptedResidual(state.acceptedNormalizedResidual), savedPhysical(state.AcceptedPhysicalState()),
savedCorrection(state.normalizedCorrection), savedPhysicalCorrection(state.physicalCorrection),
savedVolumeDirection(state.volumeDisplacementDirection);
const double savedMinimumJacobian = state.acceptedMinimumJacobianDeterminant;
std::filesystem::create_directories(output);
const auto file = [&](const std::string &name) { return output / ("coupled_trial_" + name); };
auto summary = d::Open(file("summary.csv"));
auto preparations = d::Open(file("preparations.csv"));
auto trials = d::Open(file("trials.csv"));
auto blocks = d::Open(file("blocks.csv"));
auto manifest = d::Open(file("layout.csv"));
summary << "metric,value\n";
preparations << "sequence,kind,sample,alpha,prepared,rejection_reason,rejection_stage,seconds,residual_norm\n";
trials << "sample,alpha,gravity_path,raw_prepared,raw_rejection_reason,raw_rejection_stage,raw_norm,raw_merit,"
"gravity_attempted,gravity_reported_gate,gravity_prepared_gate,gravity_target_defect,"
"gravity_affine_disagreement,gravity_target_threshold,before_prepared,before_usable,before_norm,"
"before_merit,before_accepted,soc_called,soc_status,soc_attempted,aux_status,aux_reported_gate,"
"aux_iterations,aux_rhs_norm,aux_reported_true_norm,aux_verified_true_norm,aux_verified_gate,"
"aux_threshold,aux_verification_applied,correction_norm,correction_cap,correction_ratio,"
"corrected_metric_observed,corrected_norm,corrected_merit,corrected_accepted,selected_prepared,"
"selected_norm,selected_merit,selected_accepted,selected_is_original,fallback_residual_difference,"
"raw_seconds,gravity_seconds,soc_seconds,soc_refresh_seconds,soc_solve_seconds,"
"soc_verification_seconds,soc_preparation_seconds,soc_preparations,soc_restorations,"
"aux_operator_applications,aux_inverse_applications,verified_action_defect_norm,"
"capture_seconds,total_sample_seconds,aux_core_status,rhs_saved,correction_saved,verified_vectors_saved\n";
blocks << "sample,alpha,stage,block,offset,size,residual_norm\n";
manifest << "kind,block,offset,size\n";
for (const auto &value : state.Problem().GetManifest().valueBlocks())
manifest << "value," << value.stableId << ',' << value.offset << ',' << value.size << '\n';
for (const auto &row : state.Problem().GetManifest().residualBlocks())
manifest << "residual," << row.stableId << ',' << row.offset << ',' << row.size << '\n';
manifest.flush();
const auto metric = [&](const char *name, auto value) { summary << name << ',' << value << '\n'; };
metric("schema_version", 1);
metric("independent_frozen_alphas", 1);
metric("accepted_commits", 0);
metric("outer_linear_tolerance", options.outerLinearTolerance);
metric("sufficient_decrease", options.sufficientDecrease);
metric("gravity_path", options.gravityPreconditionPath);
metric("seed_control", options.seedControl);
metric("coupled_maximum_correction_ratio", options.coupled.maximumCorrectionRatio);
metric("coupled_relative_tolerance", options.coupled.control.relativeTolerance);
metric("coupled_absolute_tolerance", options.coupled.control.absoluteTolerance);
metric("coupled_maximum_iterations", options.coupled.control.maximumIterations);
metric("coupled_restart", options.coupled.backend.restartLength);
metric("gravity_relative_tolerance", options.gravity.control.relativeTolerance);
metric("gravity_absolute_tolerance", options.gravity.control.absoluteTolerance);
metric("gravity_maximum_iterations", options.gravity.control.maximumIterations);
metric("gravity_restart", options.gravity.backend.restartLength);
metric("polynomial_direction_repairs", 0);
metric("sampled_enthalpy_policy", 1);
metric("coupled_attempt_budget_per_independent_alpha", 1);
metric("soc_status_not_attempted", int(solver::CoupledTrialAttemptStatus::not_attempted));
metric("soc_status_original_accepted", int(solver::CoupledTrialAttemptStatus::original_accepted));
metric("soc_status_budget_exhausted", int(solver::CoupledTrialAttemptStatus::budget_exhausted));
metric("soc_status_kept", int(solver::CoupledTrialAttemptStatus::kept));
metric("soc_status_linear_failure", int(solver::CoupledTrialAttemptStatus::linear_failure));
metric("soc_status_norm_limit", int(solver::CoupledTrialAttemptStatus::norm_limit));
metric("soc_status_non_finite", int(solver::CoupledTrialAttemptStatus::non_finite));
metric("soc_status_inadmissible", int(solver::CoupledTrialAttemptStatus::inadmissible));
metric("soc_status_not_improved", int(solver::CoupledTrialAttemptStatus::not_improved));
std::vector<double> alphas = options.seedControl ? std::vector<double>{1.}
: std::vector<double>{1. / 16., 1. / 64., 1. / 256., 1. / 1024.};
for (std::size_t sample = 0; sample < alphas.size(); ++sample)
summary << "alpha_" << sample << ',' << alphas[sample] << '\n';
mfem::Vector residual(size), baseAction(size);
bool captured = false, haveBaseAction = false, finished = false;
int preparationsCount = 0, refreshes = 0, directActions = 0;
double preparationSeconds = 0., refreshSeconds = 0., actionSeconds = 0., restorationError = 0.;
const auto prepare = [&](const mfem::Vector &candidate, const char *kind, int sample, double alpha) {
state.trialNormalizedState = candidate;
state.normalizedOperator->DenormalizeState(candidate, state.candidatePhysicalState);
const auto started = d::Clock::now();
auto result = state.PrepareTrial();
const double seconds = d::Seconds(started);
preparationSeconds += seconds;
preparations << ++preparationsCount << ',' << kind << ',' << sample << ',' << alpha << ',' << bool(result)
<< ',' << (result ? -1 : int(result.error().reason)) << ','
<< (result ? -1 : int(result.error().stage)) << ',' << seconds << ','
<< (result ? d::Norm(state.trialNormalizedResidual) : nan) << '\n';
preparations.flush();
if (result && !d::Finite(state.trialNormalizedResidual))
throw std::runtime_error("Coupled-trial preparation produced a nonfinite residual.");
return result;
};
const auto refresh = [&]() {
const auto started = d::Clock::now();
state.physicalInverse->Refresh();
refreshSeconds += d::Seconds(started);
++refreshes;
};
const auto restore = [&](const char *kind) {
if (!prepare(base, kind, -1, 0.)) throw std::runtime_error("The coupled-trial base cannot be restored.");
if (!captured) { residual = state.trialNormalizedResidual; captured = true; }
restorationError = std::max(restorationError, d::Difference(residual, state.trialNormalizedResidual));
refresh();
};
const auto action = [&](const mfem::Vector &input, mfem::Vector &result) {
const auto started = d::Clock::now();
state.normalizedOperator->Mult(input, result);
actionSeconds += d::Seconds(started);
++directActions;
if (!d::Finite(result)) throw std::runtime_error("The coupled-trial full-J action is nonfinite.");
};
const auto evaluate = [&](const mfem::Vector &value) {
return solver::nonlinear::getMetric(solver::nonlinear::NormalizedResidualMetric{}, value, communicator);
};
const auto blockRows = [&](std::size_t sample, double alpha, const char *stage, const mfem::Vector &value) {
blocks << sample << ',' << alpha << ',' << stage << ",all,0," << size << ',' << d::Norm(value) << '\n';
for (const auto &row : state.Problem().GetManifest().residualBlocks())
blocks << sample << ',' << alpha << ',' << stage << ',' << row.stableId << ',' << row.offset << ','
<< row.size << ',' << d::Norm(value, row.offset, row.size) << '\n';
blocks.flush();
};
const auto finish = [&]() {
// Output is append-only evidence. A failed cleanup must not retry a
// previously written filename or replace the original failure record.
finished = true;
const bool directionsNeverChanged = d::Identical(savedCorrection, state.normalizedCorrection) &&
d::Identical(savedPhysicalCorrection, state.physicalCorrection) &&
d::Identical(savedVolumeDirection, state.volumeDisplacementDirection);
state.normalizedCorrection = savedCorrection;
state.physicalCorrection = savedPhysicalCorrection;
state.volumeDisplacementDirection = savedVolumeDirection;
restore("final_base_restore");
d::Save(file("restored_residual.txt"), state.trialNormalizedResidual);
mfem::Vector restoredAction(size);
action(direction, restoredAction);
d::Save(file("restored_action.txt"), restoredAction);
const bool acceptedIdentical = d::Identical(savedAccepted, state.acceptedNormalizedState) &&
d::Identical(savedAcceptedResidual, state.acceptedNormalizedResidual) &&
d::Identical(savedPhysical, state.AcceptedPhysicalState()) &&
savedMinimumJacobian == state.acceptedMinimumJacobianDeterminant;
const bool factorsIdentical = d::Identical(factors, normalization.StateFactors()) &&
d::Identical(residualFactors, normalization.ResidualFactors());
const bool directionsIdentical = d::Identical(savedCorrection, state.normalizedCorrection) &&
d::Identical(savedPhysicalCorrection, state.physicalCorrection) &&
d::Identical(savedVolumeDirection, state.volumeDisplacementDirection);
metric("accepted_buffers_bitwise_identical", acceptedIdentical);
metric("normalization_factors_bitwise_identical", factorsIdentical);
metric("direction_buffers_bitwise_identical", directionsIdentical);
metric("direction_buffers_unchanged_before_restoration", directionsNeverChanged);
metric("maximum_base_restoration_error", restorationError);
metric("restored_base_action_error", haveBaseAction ? d::Difference(baseAction, restoredAction) : nan);
metric("fresh_operator_and_inverse_current", state.normalizedOperator->IsPrepared() &&
state.physicalInverse->IsCurrent() && state.normalizedInverse->IsCurrent());
metric("explicit_probe_preparations", preparationsCount);
metric("explicit_probe_preparation_seconds", preparationSeconds);
metric("explicit_probe_inverse_refreshes", refreshes);
metric("explicit_probe_inverse_refresh_seconds", refreshSeconds);
metric("explicit_probe_operator_applications", directActions);
metric("explicit_probe_operator_seconds", actionSeconds);
metric("total_seconds", d::Seconds(start));
summary.flush();
if (!acceptedIdentical || !factorsIdentical || !directionsIdentical || !directionsNeverChanged)
throw std::runtime_error("The coupled-trial probe changed accepted buffers, directions or normalization.");
};
try {
restore("initial_base_restore");
mfem::Vector physicalBase(size);
state.normalizedOperator->DenormalizeState(base, physicalBase);
action(direction, baseAction);
haveBaseAction = true;
mfem::Vector defect(baseAction);
defect += residual;
const auto baseMetric = evaluate(residual);
const double outerThreshold = options.outerLinearTolerance * baseMetric.residualNorm;
const bool outerGate = d::Norm(defect) <= outerThreshold;
d::Save(file("base_normalized.txt"), base);
d::Save(file("base_physical.txt"), physicalBase);
d::Save(file("base_residual.txt"), residual);
d::Save(file("state_factors.txt"), factors);
d::Save(file("residual_factors.txt"), residualFactors);
d::Save(file("direction.txt"), direction);
d::Save(file("base_action.txt"), baseAction);
d::Save(file("base_defect.txt"), defect);
metric("base_residual_norm", baseMetric.residualNorm);
metric("base_merit", baseMetric.merit);
metric("direction_norm", d::Norm(direction));
metric("base_merit_slope", d::Dot(residual, baseAction, 0, size));
metric("outer_verified_defect_norm", d::Norm(defect));
metric("outer_verified_relative_defect", d::Ratio(d::Norm(defect), baseMetric.residualNorm));
metric("outer_verified_threshold", outerThreshold);
metric("outer_verified_gate", outerGate);
metric("outer_gate_failure_skips_trials", !outerGate);
summary.flush();
if (!outerGate) { metric("probe_exception", 0); finish(); return; }
Transaction transaction(state, options.coupled, 1);
std::optional<GravityWorkspace> gravity;
std::optional<GravityCorrection> gravityCorrection;
if (options.gravityPreconditionPath) {
gravity.emplace(state, options.gravity);
gravityCorrection.emplace(gravity->subsystem);
metric("compact_gravity_operator", gravity->subsystem.UsesCompactOperator());
metric("compact_gravity_inverse", gravity->subsystem.UsesCompactPreconditioner());
}
for (std::size_t sample = 0; sample < alphas.size(); ++sample) {
const auto sampleStart = d::Clock::now();
const double alpha = alphas[sample];
const std::string suffix = "_a" + std::to_string(sample) + ".txt";
restore("capture_base");
const auto captureStart = d::Clock::now();
// These are independent frozen trials. Resetting here does not
// represent several attempts inside one production line search.
transaction.CaptureBase(residual, direction);
transaction.BeginTrial();
if (gravity) {
gravity->subsystem.Refresh();
gravityCorrection->CaptureBase(residual, direction);
gravity->BeginGravityAttempt();
}
const double captureSeconds = d::Seconds(captureStart);
mfem::Vector prediction(residual), rawState(base);
prediction.Add(alpha, baseAction);
rawState.Add(alpha, direction);
d::Save(file("prediction" + suffix), prediction);
d::Save(file("raw_state" + suffix), rawState);
const auto rawStart = d::Clock::now();
auto preparation = prepare(rawState, "raw", static_cast<int>(sample), alpha);
const double rawSeconds = d::Seconds(rawStart);
const bool rawPrepared = bool(preparation);
const int rawReason = rawPrepared ? -1 : int(preparation.error().reason);
const int rawStage = rawPrepared ? -1 : int(preparation.error().stage);
solver::nonlinear::MetricEvaluation rawMetric{nan, nan};
if (rawPrepared) {
rawMetric = evaluate(state.trialNormalizedResidual);
d::Save(file("raw_residual" + suffix), state.trialNormalizedResidual);
blockRows(sample, alpha, "raw", state.trialNormalizedResidual);
}
bool gravityAttempted = false, gravityReportedGate = !gravity, gravityPreparedGate = !gravity;
double gravitySeconds = 0.;
if (rawPrepared && gravity) {
const auto gravityStart = d::Clock::now();
gravity->Refresh(state);
gravityCorrection->BuildLinearPrediction(alpha, gravity->desiredResidual);
gravityAttempted = true;
gravity->lastSolve = gravityCorrection->SolveCorrection(
alpha, state.trialNormalizedResidual, gravity->gravityCorrection, options.gravity.control);
gravity->trialSolveSucceeded = gravity->lastSolve.Converged();
gravityReportedGate = gravity->trialSolveSucceeded;
if (gravityReportedGate) {
// This is the same production affine-gravity assessment,
// including its contribution-aware arithmetic allowance.
preparation = gravity->ApplyCorrection(state);
gravityPreparedGate = bool(preparation) && gravity->TrialSolveSucceeded();
d::Save(file("gravity_correction" + suffix), gravity->fullCorrection);
}
gravitySeconds = d::Seconds(gravityStart);
}
const bool beforePrepared = bool(preparation);
const bool usable = beforePrepared && gravityReportedGate && gravityPreparedGate;
mfem::Vector beforeState(state.trialNormalizedState), beforeResidual;
solver::nonlinear::MetricEvaluation beforeMetric{nan, nan};
if (beforePrepared) {
beforeResidual = state.trialNormalizedResidual;
beforeMetric = evaluate(beforeResidual);
d::Save(file("before_state" + suffix), beforeState);
d::Save(file("before_residual" + suffix), beforeResidual);
blockRows(sample, alpha, "before", beforeResidual);
}
const auto accepts = [&](const solver::nonlinear::MetricEvaluation &candidate) {
return std::isfinite(candidate.merit) && candidate.merit >= 0. &&
candidate.merit <= (1. - options.sufficientDecrease * alpha) * baseMetric.merit;
};
const auto better = [](const auto &candidate, const auto &original) {
return std::isfinite(candidate.merit) && candidate.merit >= 0. &&
std::isfinite(candidate.residualNorm) && candidate.residualNorm >= 0. &&
candidate.merit < original.merit;
};
const bool beforeAccepted = usable && accepts(beforeMetric);
auto selectedMetric = beforeMetric;
bool correctedObserved = false;
mfem::Vector observedCorrectedState, observedCorrectedResidual;
solver::nonlinear::MetricEvaluation observedCorrectedMetric{nan, nan};
const auto observeMetric = [&](const mfem::Vector &value) {
const auto result = evaluate(value);
// This callback observes the actual prepared correction before
// a possible merit-based rollback. It neither reconstructs nor
// reapplies the remedy and does not change its selection rule.
if (!d::Identical(state.trialNormalizedState, beforeState)) {
observedCorrectedState = state.trialNormalizedState;
observedCorrectedResidual = value;
observedCorrectedMetric = result;
correctedObserved = true;
}
return result;
};
const auto previousDiagnostics = transaction.GetDiagnostics();
double socSeconds = 0.;
if (usable) {
const auto socStart = d::Clock::now();
transaction.TryImprove(state, alpha, preparation, selectedMetric, observeMetric,
accepts, better, [](const mfem::Vector &) { return true; });
socSeconds = d::Seconds(socStart);
}
const auto &diagnostics = transaction.GetDiagnostics();
const auto &report = diagnostics.currentSolve;
const bool attempted = report.has_value();
const bool rhsSaved = attempted && d::Finite(transaction.GetCore().GetRightHandSide());
const bool correctionSaved = attempted && d::Finite(transaction.GetCorrection());
const bool verifiedVectorsSaved = attempted && report->verificationApplied && rhsSaved &&
d::Finite(transaction.GetCore().GetVerifiedAction()) && d::Finite(transaction.GetCore().GetVerifiedDefect());
double savedDefectNorm = nan;
if (attempted) {
if (rhsSaved) d::Save(file("soc_rhs" + suffix), transaction.GetCore().GetRightHandSide());
if (correctionSaved) d::Save(file("soc_correction" + suffix), transaction.GetCorrection());
if (verifiedVectorsSaved) {
d::Save(file("soc_action" + suffix), transaction.GetCore().GetVerifiedAction());
mfem::Vector savedDefect(transaction.GetCore().GetVerifiedAction());
savedDefect -= transaction.GetCore().GetRightHandSide();
savedDefectNorm = d::Norm(savedDefect);
d::Save(file("soc_defect" + suffix), savedDefect);
}
}
if (correctedObserved) {
d::Save(file("corrected_state" + suffix), observedCorrectedState);
d::Save(file("corrected_residual" + suffix), observedCorrectedResidual);
mfem::Vector physical(size);
state.normalizedOperator->DenormalizeState(observedCorrectedState, physical);
d::Save(file("corrected_physical" + suffix), physical);
blockRows(sample, alpha, "corrected", observedCorrectedResidual);
}
const bool selectedPrepared = bool(preparation);
const bool selectedIsOriginal = beforePrepared && d::Identical(beforeState, state.trialNormalizedState);
double fallbackDifference = nan;
if (selectedPrepared) {
selectedMetric = evaluate(state.trialNormalizedResidual);
d::Save(file("selected_state" + suffix), state.trialNormalizedState);
d::Save(file("selected_residual" + suffix), state.trialNormalizedResidual);
d::Save(file("selected_physical" + suffix), state.candidatePhysicalState);
blockRows(sample, alpha, "selected", state.trialNormalizedResidual);
if (selectedIsOriginal) fallbackDifference = d::Difference(beforeResidual, state.trialNormalizedResidual);
}
const bool selectedAccepted = usable && selectedPrepared && accepts(selectedMetric);
trials << sample << ',' << alpha << ',' << options.gravityPreconditionPath << ',' << rawPrepared << ','
<< rawReason << ',' << rawStage << ',' << rawMetric.residualNorm << ',' << rawMetric.merit << ','
<< gravityAttempted << ',' << gravityReportedGate << ',' << gravityPreparedGate << ','
<< (gravityAttempted ? gravity->lastPreparedDefect : nan) << ','
<< (gravityAttempted ? gravity->lastAffineDisagreement : nan) << ','
<< (gravityAttempted ? gravity->lastPreparedTargetThreshold : nan) << ','
<< beforePrepared << ',' << usable << ',' << beforeMetric.residualNorm << ',' << beforeMetric.merit
<< ',' << beforeAccepted << ',' << usable << ',' << int(diagnostics.status) << ',' << attempted
<< ',' << (attempted ? int(report->linearSolve.status) : -1) << ','
<< (attempted && report->linearSolve.Converged()) << ','
<< (attempted ? report->linearSolve.iterations : 0) << ','
<< (attempted ? report->linearSolve.rightHandSideNorm : nan) << ','
<< (attempted ? report->linearSolve.trueResidualNorm : nan) << ','
<< (attempted ? report->verifiedTrueResidualNorm : nan) << ','
<< (attempted && report->verifiedGate) << ','
<< (attempted ? report->convergenceThreshold : nan) << ','
<< (attempted && report->verificationApplied) << ','
<< (attempted ? report->correctionNorm : nan) << ','
<< (attempted ? report->maximumCorrectionNorm : nan) << ','
<< (attempted ? d::Ratio(report->correctionNorm, alpha * d::Norm(direction)) : nan) << ','
<< correctedObserved << ',' << observedCorrectedMetric.residualNorm << ','
<< observedCorrectedMetric.merit << ',' << (correctedObserved && accepts(observedCorrectedMetric)) << ','
<< selectedPrepared << ',' << selectedMetric.residualNorm << ',' << selectedMetric.merit << ','
<< selectedAccepted << ',' << selectedIsOriginal << ',' << fallbackDifference << ','
<< rawSeconds << ',' << gravitySeconds << ',' << socSeconds << ','
<< diagnostics.refreshSeconds - previousDiagnostics.refreshSeconds << ','
<< diagnostics.solveSeconds - previousDiagnostics.solveSeconds << ','
<< diagnostics.verificationSeconds - previousDiagnostics.verificationSeconds << ','
<< diagnostics.preparationSeconds - previousDiagnostics.preparationSeconds << ','
<< diagnostics.preparations - previousDiagnostics.preparations << ','
<< diagnostics.restorations - previousDiagnostics.restorations << ','
<< (attempted ? report->linearSolve.operatorApplications : 0) << ','
<< (attempted ? report->linearSolve.inversePreconditionerApplications : 0) << ','
<< savedDefectNorm << ',' << captureSeconds << ',' << d::Seconds(sampleStart) << ','
<< (attempted ? int(report->status) : -1) << ',' << rhsSaved << ',' << correctionSaved << ','
<< verifiedVectorsSaved << '\n';
trials.flush();
summary.flush();
}
const auto &totals = transaction.GetDiagnostics();
metric("soc_attempts", totals.attempts);
metric("soc_kept", totals.kept);
metric("soc_fallbacks", totals.fallbacks);
metric("soc_iterations", totals.iterations);
metric("soc_solve_seconds", totals.solveSeconds);
metric("soc_verification_seconds", totals.verificationSeconds);
metric("soc_preparation_seconds", totals.preparationSeconds);
metric("soc_refresh_seconds", totals.refreshSeconds);
metric("soc_preparations", totals.preparations);
metric("soc_restorations", totals.restorations);
if (gravity) {
metric("gravity_solves", gravity->subsystem.GetSolveCount());
metric("gravity_iterations", gravity->subsystem.GetIterationCount());
metric("gravity_failures", gravity->subsystem.GetFailureCount());
metric("gravity_solve_seconds", gravity->subsystem.GetSolveSeconds());
}
} catch (...) {
const auto failure = std::current_exception();
metric("probe_exception", 1);
if (!finished) finish();
std::rethrow_exception(failure);
}
metric("probe_exception", 0);
finish();
}
} // namespace experiment::plateau_diagnostics

View File

@@ -0,0 +1,185 @@
#pragma once
// Include after import mean_field. Physical replay is deliberately separate
// from timed trial selection: these quadratures do not influence acceptance.
#include "plateau_physical_diagnostics.hpp"
#include <cstring>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <type_traits>
#include <vector>
namespace experiment::plateau_diagnostics {
namespace coupled_physical_detail {
inline bool Finite(const mfem::Vector &values) {
for (int i = 0; i < values.Size(); ++i)
if (!std::isfinite(values(i))) return false;
return true;
}
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);
}
inline mfem::Vector Load(const std::filesystem::path &path, int size) {
std::ifstream input(path);
long long storedSize = -1;
if (!(input >> storedSize) || storedSize != size)
throw std::invalid_argument("Coupled physical snapshot has an incompatible size: " + path.string());
mfem::Vector values(size);
for (int i = 0; i < size; ++i)
if (!(input >> values(i)) || !std::isfinite(values(i)))
throw std::invalid_argument("Coupled physical snapshot is incomplete or nonfinite: " + path.string());
std::string trailing;
if (input >> trailing || input.bad())
throw std::invalid_argument("Coupled physical snapshot has trailing data or a read error: " + path.string());
return values;
}
// PhysicalState's structural constructor uses these historical accepted-state
// member names. This adapter supplies an explicitly prepared, uncommitted
// snapshot instead; it never aliases a writable accepted vector. Reusing that
// constructor preserves the same reduced-field scatter and Piola transforms.
template <class State>
struct PreparedSnapshotView final {
State &state;
decltype(std::declval<State &>().normalizedOperator) &normalizedOperator;
const mfem::Vector &acceptedNormalizedResidual;
explicit PreparedSnapshotView(State &source)
: state(source), normalizedOperator(source.normalizedOperator),
acceptedNormalizedResidual(source.trialNormalizedResidual) {
if (!normalizedOperator->IsPrepared() || !Finite(acceptedNormalizedResidual))
throw std::logic_error("Physical snapshot reconstruction requires a fresh prepared trial.");
}
decltype(auto) Problem() const { return state.Problem(); }
const mfem::Vector &AcceptedPhysicalState() const { return normalizedOperator->GetPhysicalState(); }
};
} // namespace coupled_physical_detail
template <class State>
void MeasureCoupledTrialSnapshots(State &state, const mean_field::fem::FEM &fem,
const std::filesystem::path &output, double n, double K) {
namespace d = coupled_physical_detail;
int ranks = 0;
MPI_Comm_size(state.Problem().GetCommunicator(), &ranks);
if (ranks != 1 || !std::isfinite(n) || n < 1. || !std::isfinite(K) || K <= 0.)
throw std::invalid_argument("Coupled physical replay requires one rank, finite n>=1 and finite K>0.");
constexpr std::string_view prefix = "coupled_trial_selected_physical_a";
constexpr std::string_view suffix = ".txt";
std::vector<std::pair<int, std::filesystem::path>> snapshots;
for (const auto &entry : std::filesystem::directory_iterator(output)) {
const std::string name = entry.path().filename().string();
if (!entry.is_regular_file() || !name.starts_with(prefix) || !name.ends_with(suffix)) continue;
const std::string digits = name.substr(prefix.size(), name.size() - prefix.size() - suffix.size());
if (digits.empty() || !std::all_of(digits.begin(), digits.end(), [](char ch) { return ch >= '0' && ch <= '9'; }))
throw std::invalid_argument("Coupled physical snapshot has an invalid sample index: " + name);
snapshots.emplace_back(std::stoi(digits), entry.path());
}
std::sort(snapshots.begin(), snapshots.end(), [](const auto &a, const auto &b) { return a.first < b.first; });
for (std::size_t i = 1; i < snapshots.size(); ++i)
if (snapshots[i - 1].first == snapshots[i].first)
throw std::invalid_argument("Coupled physical replay found duplicate sample indices.");
const auto path = output / "coupled_trial_physical.csv";
if (std::filesystem::exists(path))
throw std::invalid_argument("Coupled physical replay output already exists: " + path.string());
std::ofstream stream(path);
stream.exceptions(std::ios::failbit | std::ios::badbit);
stream << std::setprecision(17) << "sample,source,quadrature,metric,value\n";
const auto write = [&](int sample, const std::string &source, int quadrature, const std::string &metric, double value) {
stream << sample << ',' << source << ',' << quadrature << ',' << metric << ',' << value << '\n';
};
const mfem::Vector accepted(state.acceptedNormalizedState), acceptedResidual(state.acceptedNormalizedResidual),
acceptedPhysical(state.AcceptedPhysicalState()), correction(state.normalizedCorrection),
physicalCorrection(state.physicalCorrection), volumeDirection(state.volumeDisplacementDirection);
const auto &normalization = state.normalizedOperator->GetNormalization();
const mfem::Vector stateFactors(normalization.StateFactors()), residualFactors(normalization.ResidualFactors());
const double minimumJacobian = state.acceptedMinimumJacobianDeterminant;
const auto acceptedUnchanged = [&] {
return d::Identical(accepted, state.acceptedNormalizedState) &&
d::Identical(acceptedResidual, state.acceptedNormalizedResidual) &&
d::Identical(acceptedPhysical, state.AcceptedPhysicalState()) &&
minimumJacobian == state.acceptedMinimumJacobianDeterminant;
};
bool restorationAttempted = false;
const auto restore = [&] {
restorationAttempted = true;
const bool before = acceptedUnchanged();
state.RestoreAccepted();
const bool after = acceptedUnchanged();
const bool factorsUnchanged = d::Identical(stateFactors, normalization.StateFactors()) &&
d::Identical(residualFactors, normalization.ResidualFactors());
const bool directionsUnchanged = d::Identical(correction, state.normalizedCorrection) &&
d::Identical(physicalCorrection, state.physicalCorrection) &&
d::Identical(volumeDirection, state.volumeDisplacementDirection);
const bool ready = state.normalizedOperator->IsPrepared() && state.physicalInverse->IsCurrent() &&
state.normalizedInverse->IsCurrent();
write(-1, "restoration", 0, "accepted_buffers_unchanged_before_restoration", before);
write(-1, "restoration", 0, "accepted_buffers_unchanged_after_restoration", after);
write(-1, "restoration", 0, "normalization_factors_unchanged", factorsUnchanged);
write(-1, "restoration", 0, "direction_buffers_unchanged", directionsUnchanged);
write(-1, "restoration", 0, "accepted_operator_and_inverse_current", ready);
stream.flush();
if (!before || !after || !factorsUnchanged || !directionsUnchanged || !ready)
throw std::runtime_error("Coupled physical replay failed its accepted-state restoration invariant.");
};
try {
const double targetMass = state.Problem().GetStellarModel()
.template specification<mean_field::models::FixedTotalMass>().targetMass().value();
for (const auto &[sample, snapshotPath] : snapshots) {
const std::string source = snapshotPath.filename().string();
const auto saved = d::Load(snapshotPath, state.Problem().StateSize());
state.normalizedOperator->NormalizeState(saved, state.trialNormalizedState);
state.normalizedOperator->DenormalizeState(state.trialNormalizedState, state.candidatePhysicalState);
const auto preparation = state.PrepareTrial();
write(sample, source, 0, "fresh_preparation_passed", bool(preparation));
if (!preparation) {
write(sample, source, 0, "rejection_reason", int(preparation.error().reason));
write(sample, source, 0, "rejection_stage", int(preparation.error().stage));
throw std::runtime_error("Selected coupled trial is inadmissible during physical replay: " + source);
}
const auto &prepared = state.normalizedOperator->GetPhysicalState();
if (prepared.Size() != saved.Size() || !d::Finite(prepared) || !d::Finite(state.trialNormalizedResidual))
throw std::runtime_error("Selected coupled trial produced invalid prepared physical data: " + source);
long double squaredDifference = 0.;
double maximumDifference = 0., maximumScaledDifference = 0.;
for (int i = 0; i < saved.Size(); ++i) {
const double difference = std::abs(prepared(i) - saved(i));
squaredDifference += static_cast<long double>(difference) * difference;
maximumDifference = std::max(maximumDifference, difference);
maximumScaledDifference = std::max(maximumScaledDifference, difference / std::max(1., std::abs(saved(i))));
}
d::PreparedSnapshotView<State> view(state);
experiment::polytrope_validation::PhysicalState physical(view, fem);
for (const int quadrature : {14, 18}) {
auto measurements = MeasurePhysical(physical, n, K, quadrature);
measurements["saved_prepared_l2_difference"] = static_cast<double>(std::sqrt(squaredDifference));
measurements["saved_prepared_maximum_difference"] = maximumDifference;
measurements["saved_prepared_maximum_scaled_difference"] = maximumScaledDifference;
measurements["minimum_preparation_jacobian"] = preparation->minimumJacobianDeterminant;
measurements["normalized_bordered_residual"] = physical.normalizedBorderedResidualNorm;
measurements["normalized_unbordered_residual"] = physical.normalizedUnborderedResidualNorm;
measurements["normalized_border_action"] = physical.normalizedCentralBorderActionNorm;
measurements["central_border"] = physical.centralBorder;
measurements["target_mass"] = targetMass;
measurements["mass_absolute_error"] = std::abs(measurements.at("mass") - targetMass);
for (const auto &[metric, value] : measurements) write(sample, source, quadrature, metric, value);
stream.flush();
}
}
write(-1, "replay", 0, "selected_snapshots_measured", static_cast<double>(snapshots.size()));
restore();
} catch (...) {
const auto failure = std::current_exception();
if (!restorationAttempted) restore();
std::rethrow_exception(failure);
}
}
} // namespace experiment::plateau_diagnostics

View File

@@ -24,6 +24,8 @@ import mean_field;
#include "plateau_shape_curvature_diagnostics.hpp"
#include "plateau_numerical_strategy_diagnostics.hpp"
#include "plateau_nonlinear_step_diagnostics.hpp"
#include "plateau_coupled_trial_diagnostics.hpp"
#include "plateau_coupled_trial_physical.hpp"
#include "plateau_gravity_application_diagnostics.hpp"
#include "plateau_positivity_strategy_diagnostics.hpp"
#include "plateau_polynomial_certificate.hpp"
@@ -37,6 +39,9 @@ struct Options {
bool probes = false, noCentral = false;
bool strategyProbe = false, positivityProbe = false, positivityCertificate = false;
bool preconditionerProbe = false, nonlinearStepProbe = false;
bool coupledTrialProbe = false, coupledSeedControl = false;
double coupledTolerance = .03, coupledMaximumRatio = 1.;
int coupledIterations = 160, coupledRestart = 160, coupledAttempts = 1;
std::string preconditionerVariant = "independent";
int schurCorrectionSweeps = 1;
double schurRelaxation = 0.5;
@@ -103,6 +108,20 @@ Options Parse(int argc, char **argv) {
o.preconditionerProbe = true;
else if (a == "--nonlinear-step-probe")
o.nonlinearStepProbe = true;
else if (a == "--coupled-trial-probe")
o.coupledTrialProbe = true;
else if (a == "--coupled-seed-control")
o.coupledSeedControl = true;
else if (a == "--coupled-tol")
o.coupledTolerance = std::stod(v());
else if (a == "--coupled-iterations")
o.coupledIterations = std::stoi(v());
else if (a == "--coupled-restart")
o.coupledRestart = std::stoi(v());
else if (a == "--coupled-max-ratio")
o.coupledMaximumRatio = std::stod(v());
else if (a == "--coupled-attempts")
o.coupledAttempts = std::stoi(v());
else if (a == "--preconditioner-variant")
o.preconditionerVariant = v();
else if (a == "--schur-sweeps")
@@ -139,17 +158,17 @@ Options Parse(int argc, char **argv) {
throw std::invalid_argument("Invalid options");
if (o.probeRestart != 40 && o.probeRestart != 160)
throw std::invalid_argument("--probe-restart must be 40 or 160");
if ((o.pressureProbe || o.shapeCurvature || o.surfaceTrace || o.strategyProbe || o.positivityProbe) && (o.probeState.empty() || o.probeDirection.empty()))
if ((o.pressureProbe || o.shapeCurvature || o.surfaceTrace || o.strategyProbe || o.positivityProbe || o.coupledTrialProbe) && (o.probeState.empty() || o.probeDirection.empty()))
throw std::invalid_argument("Focused probes require --probe-state and --probe-direction");
if (o.positivityCertificate && o.probeState.empty())
throw std::invalid_argument("--positivity-certificate requires --probe-state");
if (o.positivityCertificate && (o.probes || o.pressureProbe || o.shapeCurvature || o.surfaceTrace ||
o.strategyProbe || o.positivityProbe || o.preconditionerProbe || o.nonlinearStepProbe || !o.probeDirection.empty() || !o.replayCandidate.empty()))
o.strategyProbe || o.positivityProbe || o.preconditionerProbe || o.nonlinearStepProbe || o.coupledTrialProbe || !o.probeDirection.empty() || !o.replayCandidate.empty()))
throw std::invalid_argument("--positivity-certificate is a certificate-only mode; omit other probe options");
if (o.preconditionerProbe) {
experiment::plateau_diagnostics::ParsePreconditionerBenchmarkVariant(o.preconditionerVariant);
if (o.maxNewton != 0 || o.probes || o.pressureProbe || o.shapeCurvature || o.surfaceTrace ||
o.strategyProbe || o.positivityProbe || o.positivityCertificate || o.nonlinearStepProbe || !o.probeDirection.empty() ||
o.strategyProbe || o.positivityProbe || o.positivityCertificate || o.nonlinearStepProbe || o.coupledTrialProbe || !o.probeDirection.empty() ||
!o.replayCandidate.empty() || !o.captureIterations.empty() || o.strategy != "baseline")
throw std::invalid_argument("--preconditioner-probe requires --max-newton 0 and no nonlinear/probe strategies");
if (!std::isfinite(o.linearTolerance) || o.linearTolerance <= 0.0)
@@ -157,25 +176,37 @@ Options Parse(int argc, char **argv) {
}
if (o.nonlinearStepProbe) {
if (o.maxNewton != 0 || o.probes || o.pressureProbe || o.shapeCurvature || o.surfaceTrace ||
o.strategyProbe || o.positivityProbe || o.positivityCertificate || o.preconditionerProbe ||
o.strategyProbe || o.positivityProbe || o.positivityCertificate || o.preconditionerProbe || o.coupledTrialProbe ||
!o.probeDirection.empty() || !o.replayCandidate.empty() || !o.captureIterations.empty() ||
o.strategy != "baseline")
throw std::invalid_argument("--nonlinear-step-probe requires --max-newton 0 and no other probes or strategy; it compares raw and curvature paths itself");
if (!std::isfinite(o.linearTolerance) || o.linearTolerance <= 0.0)
throw std::invalid_argument("A nonlinear-step probe requires a finite positive --linear-tol");
}
if (o.coupledTrialProbe) {
if (o.maxNewton != 0 || o.probes || o.pressureProbe || o.shapeCurvature || o.surfaceTrace ||
o.strategyProbe || o.positivityProbe || o.positivityCertificate || o.preconditionerProbe ||
o.nonlinearStepProbe || !o.replayCandidate.empty() || !o.captureIterations.empty() ||
o.coupledAttempts != 1 || (o.strategy != "coupled" && o.strategy != "curvature-coupled"))
throw std::invalid_argument("--coupled-trial-probe requires --max-newton 0 and strategy coupled or curvature-coupled, without other probes");
if (!std::isfinite(o.linearTolerance) || o.linearTolerance <= 0.)
throw std::invalid_argument("A coupled trial probe requires a finite positive outer --linear-tol");
} else if (o.coupledSeedControl) {
throw std::invalid_argument("--coupled-seed-control requires --coupled-trial-probe");
}
if (!o.preconditionerProbe) {
if (o.preconditionerVariant != "independent" && o.preconditionerVariant != "gravity-schur" &&
o.preconditionerVariant != "stellar-ldu" && o.preconditionerVariant != "stellar-ldu-gravity-schur")
throw std::invalid_argument("Newton preconditioner variants are independent, gravity-schur, stellar-ldu, or stellar-ldu-gravity-schur");
if (o.preconditionerVariant != "independent") {
if (o.probes || o.pressureProbe || o.shapeCurvature || o.surfaceTrace || o.strategyProbe ||
o.positivityProbe || o.positivityCertificate || (!o.probeState.empty() && !o.nonlinearStepProbe) || !o.probeDirection.empty() ||
o.positivityProbe || o.positivityCertificate || (!o.probeState.empty() && !o.nonlinearStepProbe && !o.coupledTrialProbe) ||
(!o.probeDirection.empty() && !o.coupledTrialProbe) ||
!o.replayCandidate.empty())
throw std::invalid_argument("A custom Newton preconditioner cannot be combined with frozen or certificate probes");
if (o.strategy != "baseline" && o.strategy != "positivity" && o.strategy != "curvature" &&
o.strategy != "positivity-curvature")
throw std::invalid_argument("Custom Newton preconditioners support baseline, positivity, curvature, or positivity-curvature");
o.strategy != "positivity-curvature" && o.strategy != "coupled" && o.strategy != "curvature-coupled")
throw std::invalid_argument("Unsupported custom Newton preconditioner strategy");
if (!std::isfinite(o.linearTolerance) || o.linearTolerance <= 0.0 || o.schurCorrectionSweeps < 0 ||
!std::isfinite(o.schurRelaxation) || o.schurRelaxation <= 0.0)
throw std::invalid_argument("Invalid custom Newton preconditioner controls");
@@ -184,10 +215,14 @@ Options Parse(int argc, char **argv) {
if ((!o.probeDirection.empty() || !o.replayCandidate.empty()) && o.probeState.empty())
throw std::invalid_argument("A supplied direction or candidate requires --probe-state");
if (o.strategy != "baseline" && o.strategy != "positivity" && o.strategy != "curvature" &&
o.strategy != "reduced" && o.strategy != "positivity-curvature" && o.strategy != "positivity-reduced")
o.strategy != "reduced" && o.strategy != "positivity-curvature" && o.strategy != "positivity-reduced" &&
o.strategy != "coupled" && o.strategy != "curvature-coupled")
throw std::invalid_argument("Unknown numerical strategy");
if (!std::isfinite(o.gravityTolerance) || o.gravityTolerance <= 0.0 || o.gravityIterations < 1 || o.gravityRestart < 1)
throw std::invalid_argument("Invalid gravity solve controls");
if (!std::isfinite(o.coupledTolerance) || o.coupledTolerance <= 0. || o.coupledIterations < 1 ||
o.coupledRestart < 1 || !std::isfinite(o.coupledMaximumRatio) || o.coupledMaximumRatio <= 0. || o.coupledAttempts < 1)
throw std::invalid_argument("Invalid coupled correction controls");
if (std::filesystem::exists(o.output))
throw std::runtime_error("Output directory already exists");
std::filesystem::create_directories(o.output);
@@ -453,6 +488,7 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
<< "\nM=1\nR_reference=1\nxi1=" << scales.xi << "\nmu1=" << scales.mu << "\ncentral_phase=" << !o.noCentral
<< "\nrestart=" << o.restart << "\nprobe_restart=" << o.probeRestart
<< "\nfrozen_probe_backend=" << (o.preconditionerProbe ? "direct_mfem_fgmres_preconditioner_benchmark" :
o.coupledTrialProbe ? "supplied_outer_direction_verified_with_full_J_and_auxiliary_fgmres" :
o.nonlinearStepProbe ? "prepared_fgmres_cold_with_independent_true_gate" :
o.positivityCertificate ? "polynomial_certificate_no_linear_solve" :
o.probeDirection.empty() ? "direct_mfem_fgmres_cold" : "supplied_direction_no_linear_solve")
@@ -462,6 +498,13 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
<< "\ngravity_iterations=" << o.gravityIterations << "\ngravity_restart=" << o.gravityRestart
<< "\npreconditioner_probe=" << o.preconditionerProbe
<< "\nnonlinear_step_probe=" << o.nonlinearStepProbe
<< "\ncoupled_trial_probe=" << o.coupledTrialProbe
<< "\ncoupled_seed_control=" << o.coupledSeedControl
<< "\ncoupled_tolerance=" << o.coupledTolerance
<< "\ncoupled_iterations=" << o.coupledIterations
<< "\ncoupled_restart=" << o.coupledRestart
<< "\ncoupled_maximum_ratio=" << o.coupledMaximumRatio
<< "\ncoupled_attempts_per_iteration=" << o.coupledAttempts
<< "\npreconditioner_variant=" << o.preconditionerVariant
<< "\ncontext_preconditioner_variant=" << (o.preconditionerProbe ? "independent" : o.preconditionerVariant)
<< "\ncontext_gravity_schur_sweeps="
@@ -470,6 +513,32 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
<< "\nschur_correction_sweeps=" << o.schurCorrectionSweeps << "\nschur_relaxation=" << o.schurRelaxation
<< "\ncompiler=" << __VERSION__ << "\ncompiled=" << __DATE__ << ' ' << __TIME__ << '\n';
metadata.flush();
if (o.coupledTrialProbe) {
metadata << "coupled_trial_input=physical_snapshot_and_supplied_normalized_direction\n"
<< "coupled_trial_admissibility=sampled_enthalpy_without_repair\n"
<< "coupled_trial_alphas_are_independent=1\n"
<< "probe_state=" << o.probeState << "\nprobe_direction=" << o.probeDirection << '\n';
metadata.flush();
solver::detail::StellarEquilibriumContextDiagnostics::WithState(context, [&](auto &s, const fem::FEM &f) {
const auto physical = LoadVector(o.probeState, s.AcceptedPhysicalState().Size());
mfem::Vector base;
s.normalizedOperator->NormalizeState(physical, base);
const auto direction = LoadVector(o.probeDirection, base.Size());
experiment::plateau_diagnostics::ProbeCoupledTrial(s, base, direction, o.output, {
.outerLinearTolerance = o.linearTolerance,
.gravity = {.control = {.relativeTolerance = o.gravityTolerance, .absoluteTolerance = 1e-14,
.maximumIterations = o.gravityIterations},
.backend = {.restartLength = o.gravityRestart, .printLevel = -1}},
.coupled = {.control = {.relativeTolerance = o.coupledTolerance, .absoluteTolerance = 1e-14,
.maximumIterations = o.coupledIterations},
.backend = {.restartLength = o.coupledRestart, .printLevel = -1},
.maximumCorrectionRatio = o.coupledMaximumRatio},
.gravityPreconditionPath = o.strategy == "curvature-coupled",
.seedControl = o.coupledSeedControl});
experiment::plateau_diagnostics::MeasureCoupledTrialSnapshots(s, f, o.output, o.n, scales.K);
});
return 0;
}
if (o.nonlinearStepProbe) {
metadata << "nonlinear_step_input=" << (o.probeState.empty() ? "lane_emden_seed" : "physical_snapshot")
<< "\nnonlinear_step_paths=raw,curvature\nnonlinear_step_positivity_repair=0\n";
@@ -600,6 +669,12 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
blocklog << "iteration,stage,block,normalized_norm,physical_norm,max_abs_normalized\n";
auto triallog = File(o.output / "trials.csv");
triallog << "iteration,trial,alpha,disposition,source,F,min_jacobian\n";
auto coupledLog = File(o.output / "coupled_trials.csv");
coupledLog << "iteration,trial,alpha,diagnostics_available,status,attempts_total,kept_total,fallbacks_total,"
"iterations_total,preparations_total,restorations_total,solve_seconds_total,verification_seconds_total,"
"preparation_seconds_total,refresh_seconds_total,original_merit,corrected_merit,has_solve,"
"linear_status,linear_iterations,rhs_norm,reported_true_norm,verified_true_norm,threshold,verified_gate,"
"correction_norm,maximum_correction_norm\n";
auto stateConsistency = File(o.output / "trial_state_consistency.csv");
stateConsistency << "iteration,trial,normalized_physical_roundtrip_error,relative_error\n";
mfem::Vector captureBase;
@@ -643,6 +718,23 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
std::cout << "Newton " << e.iteration << " F=" << e.residualNorm << std::endl;
},
[&](const solver::nonlinear::AfterLineSearchTrial &e) {
coupledLog << e.iteration << ',' << e.trial << ',' << e.stepLength << ',' << (e.numericalStrategies != nullptr);
if (e.numericalStrategies) {
const auto &c = e.numericalStrategies->coupledTrial;
coupledLog << ',' << static_cast<int>(c.status) << ',' << c.attempts << ',' << c.kept << ','
<< c.fallbacks << ',' << c.iterations << ',' << c.preparations << ',' << c.restorations << ','
<< c.solveSeconds << ',' << c.verificationSeconds << ',' << c.preparationSeconds << ','
<< c.refreshSeconds << ',' << c.originalMerit << ',' << c.correctedMerit << ',' << c.currentSolve.has_value();
if (c.currentSolve) {
const auto &r = *c.currentSolve;
coupledLog << ',' << static_cast<int>(r.linearSolve.status) << ',' << r.linearSolve.iterations << ','
<< r.linearSolve.rightHandSideNorm << ',' << r.linearSolve.trueResidualNorm << ','
<< r.verifiedTrueResidualNorm << ',' << r.convergenceThreshold << ',' << r.verifiedGate << ','
<< r.correctionNorm << ',' << r.maximumCorrectionNorm;
} else coupledLog << ",-1,0,nan,nan,nan,nan,0,nan,nan";
} else coupledLog << ",-1,0,0,0,0,0,0,0,0,0,0,nan,nan,0,-1,0,nan,nan,nan,nan,0,nan,nan";
coupledLog << '\n';
coupledLog.flush();
if (e.candidatePhysicalState.size() != e.candidateNormalizedState.size() ||
e.candidateNormalizedState.size() != static_cast<size_t>(stateFactors.Size()))
throw std::runtime_error("Trial observer exposed incompatible state representations");
@@ -731,6 +823,12 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
.control = {.relativeTolerance = o.gravityTolerance, .absoluteTolerance = 1e-14,
.maximumIterations = o.gravityIterations},
.backend = {.restartLength = o.gravityRestart, .printLevel = -1}};
const nl::CoupledCurvatureCorrection coupled{
.correction = {.control = {.relativeTolerance = o.coupledTolerance, .absoluteTolerance = 1e-14,
.maximumIterations = o.coupledIterations},
.backend = {.restartLength = o.coupledRestart, .printLevel = -1},
.maximumCorrectionRatio = o.coupledMaximumRatio},
.maximumAttemptsPerIteration = o.coupledAttempts};
auto run = [&](auto numerics) {
auto solve = solver::make(context, nl::Newton(options, nl::NormalizedResidualMetric{}, numerics), observer);
return solve.evaluate();
@@ -738,6 +836,8 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
auto report = [&] {
if (o.strategy == "positivity") return run(nl::NewtonNumerics{nl::PolynomialEnthalpy{}, nl::MonolithicGravity{}});
if (o.strategy == "curvature") return run(nl::NewtonNumerics{nl::SampledEnthalpy{}, nl::GravityCurvatureCorrection{gravity}});
if (o.strategy == "coupled") return run(nl::NewtonNumerics{nl::SampledEnthalpy{}, nl::MonolithicGravity{}, coupled});
if (o.strategy == "curvature-coupled") return run(nl::NewtonNumerics{nl::SampledEnthalpy{}, nl::GravityCurvatureCorrection{gravity}, coupled});
if constexpr (FullDefaultStack)
if (o.strategy == "reduced") return run(nl::NewtonNumerics{nl::SampledEnthalpy{}, nl::EliminatedGravity{gravity}});
if (o.strategy == "positivity-curvature") return run(nl::NewtonNumerics{nl::PolynomialEnthalpy{}, nl::GravityCurvatureCorrection{gravity}});
@@ -770,6 +870,17 @@ int RunContext(Context &context, const Options &o, const Scales &scales) {
metric("initial_gravity_residual", strategyDiagnostics.initialGravityResidual);
metric("completed_initial_gravity_residual", strategyDiagnostics.completedInitialGravityResidual);
metric("last_lifted_relative_true_residual", strategyDiagnostics.lastLiftedRelativeTrueResidual);
const auto &coupledDiagnostics = strategyDiagnostics.coupledTrial;
metric("coupled_attempts", coupledDiagnostics.attempts);
metric("coupled_kept", coupledDiagnostics.kept);
metric("coupled_fallbacks", coupledDiagnostics.fallbacks);
metric("coupled_iterations", coupledDiagnostics.iterations);
metric("coupled_preparations", coupledDiagnostics.preparations);
metric("coupled_restorations", coupledDiagnostics.restorations);
metric("coupled_solve_seconds", coupledDiagnostics.solveSeconds);
metric("coupled_verification_seconds", coupledDiagnostics.verificationSeconds);
metric("coupled_preparation_seconds", coupledDiagnostics.preparationSeconds);
metric("coupled_refresh_seconds", coupledDiagnostics.refreshSeconds);
converged = report.converged();
metadata << "converged=" << converged << "\naccepted_steps=" << report.completedNonlinearIterations() << '\n';
if (!converged)