From a2871e11e21527d8c7b8d8d436cb0f543bf93cd6 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Thu, 17 Sep 2026 11:01:13 -0400 Subject: [PATCH] Clear unavailable gravity measurements before each attempt --- .../GRAVITY_PREPARED_ROUNDOFF_2026-09-17.md | 41 +++++++++++++++++-- .../interface/solver/newton_numerics.cppm | 8 ++++ .../solver/stellar_newton_strategy.cppm | 21 ++++++++-- tests/solver/stellar_numerical_strategies.cpp | 14 +++++++ 4 files changed, 77 insertions(+), 7 deletions(-) diff --git a/experiments/GRAVITY_PREPARED_ROUNDOFF_2026-09-17.md b/experiments/GRAVITY_PREPARED_ROUNDOFF_2026-09-17.md index ed615e0..d8c9d49 100644 --- a/experiments/GRAVITY_PREPARED_ROUNDOFF_2026-09-17.md +++ b/experiments/GRAVITY_PREPARED_ROUNDOFF_2026-09-17.md @@ -33,9 +33,30 @@ Two separate requirements remain: Diagnostics retain the raw affine discrepancy, raw target defect, arithmetic estimate, and resulting target threshold separately. Nonfinite or negative -norm inputs fail the assessment. A failed preparation leaves these measurements -unavailable rather than reusing the preceding trial's values. Diagnostics also -report whether compact gravity operator and inverse applications were selected. +norm inputs fail the assessment. Each gravity attempt clears its correction +norm and prepared-assessment measurements before the raw candidate preparation. +An early preparation or inner-solve failure therefore leaves unmeasured values +as NaN, rather than reusing the preceding attempt's values. Initial gravity +completion follows the same rule. A failure in the corrected preparation can +still retain that attempt's measured correction norm. + +The last completed inner solve's true defect and cumulative solve/iteration/ +failure/time counters have a different lifetime: a candidate that fails before +calling the inner solver does not erase them. A line-search candidate rejected +before invoking the gravity strategy does not begin an auxiliary attempt and +leaves all last-attempt diagnostics intact. Diagnostic consumers must distinguish +these meanings. Diagnostics also report whether compact gravity operator and +inverse applications were selected. + +`lastLinearReportSpace` labels the retained `lastLinearSolve` report from the +outer strategy's `Solve`. A later positivity-direction relift can fail while +that outer report remains converged in reduced coordinates. The evaluation then +reports `linear_solve_failure`; the auxiliary failure is reflected by the gravity +failure counter and last completed inner true defect. Changing only the report +space would incorrectly relabel the retained reduced-system norms. The full +lifted Newton gate precedes feasibility repair; a repaired direction must pass +its relift, descent check, and nonlinear acceptance, but is not claimed to solve +the original Newton equations. Three focused tests, appended to `tests/solver/gravity_reduction.cpp`, cover large cancelling contributions, rejection of defects beyond either gate, @@ -43,3 +64,17 @@ nonfinite measurements, and scaling over twenty-four orders of magnitude. The real stellar rejection must still be reproduced and reassessed; passing a synthetic arithmetic test alone establishes neither the hypothesis nor stellar convergence. + + +## Reporting regression + +Branch `research/gravity-trial-diagnostics-20260917` moves this reset to a shared +attempt-entry helper, used by both trial strategies and initial completion. +Existing production rollback fixtures now assert finite measurements after a +completed curvature assessment, and unavailable prepared/correction fields after +a failed initial inner solve while retaining its finite true defect and counters. +No new stellar fixture or numerical behavior is introduced. The source-level +success-then-early-failure path is covered by the unconditional shared reset; +a separate expensive campaign is not justified for this reporting change. +Build/run of the added assertions is deferred until the running immutable +production campaign completes. diff --git a/libmeanfield/interface/solver/newton_numerics.cppm b/libmeanfield/interface/solver/newton_numerics.cppm index 42e37fc..c5fec12 100644 --- a/libmeanfield/interface/solver/newton_numerics.cppm +++ b/libmeanfield/interface/solver/newton_numerics.cppm @@ -81,9 +81,17 @@ export namespace mean_field::solver::nonlinear { std::uint64_t gravityPreparedFailures{0}; bool compactGravityOperator{false}; bool compactGravityInverse{false}; + // Coordinates of the evaluation's lastLinearSolve report. A later + // feasibility relift or trial solve has its own auxiliary diagnostics + // and does not replace that report or relabel its stored norms. LinearReportSpace lastLinearReportSpace{LinearReportSpace::full_system}; double gravitySolveSeconds{0.0}; + // Last completed auxiliary solve; a later failure before Solve leaves + // this value and the cumulative counters unchanged. double lastGravityTrueResidual{0.0}; + // Current gravity attempt: NaN until that attempt measures the value. + // Candidates rejected before invoking gravity leave its prior attempt + // untouched because no auxiliary work was attempted. double lastGravityCorrectionNorm{0.0}; double lastGravityPreparedDefect{0.0}; double lastGravityAffineDisagreement{0.0}; diff --git a/libmeanfield/interface/solver/stellar_newton_strategy.cppm b/libmeanfield/interface/solver/stellar_newton_strategy.cppm index fd8fc55..abaff16 100644 --- a/libmeanfield/interface/solver/stellar_newton_strategy.cppm +++ b/libmeanfield/interface/solver/stellar_newton_strategy.cppm @@ -140,6 +140,17 @@ namespace mean_field::solver::detail { } protected: + void BeginGravityAttempt() noexcept { + trialSolveSucceeded = true; + // These measurements belong to this attempt. An early preparation + // or inner-solve failure must not publish the preceding trial's + // successful assessment. The last completed solve and cumulative + // counters deliberately retain their separate lifetime. + lastCorrectionNorm = lastPreparedDefect = lastAffineDisagreement = + lastArithmeticAllowance = lastPreparedTargetThreshold = + std::numeric_limits::quiet_NaN(); + } + void Refresh(State &state) { // Preparing a trial invalidates both normalized and physical // inverses. Refreshing only the restricted wrapper would hide this. @@ -157,8 +168,6 @@ namespace mean_field::solver::detail { } auto ApplyCorrection(State &state) { - lastPreparedDefect = lastAffineDisagreement = lastArithmeticAllowance = - lastPreparedTargetThreshold = std::numeric_limits::quiet_NaN(); const auto &partition = subsystem.GetPartition(); partition.InjectGravityValues(gravityCorrection, fullCorrection); subsystem.GetOperator().Mult(gravityCorrection, gravityAction); @@ -249,7 +258,7 @@ namespace mean_field::solver::detail { return true; } auto PrepareTrial(State &state, const double alpha) { - this->trialSolveSucceeded = true; + this->BeginGravityAttempt(); auto preparation = state.PrepareTrial(); if (!preparation) { return preparation; } this->Refresh(state); @@ -281,6 +290,7 @@ namespace mean_field::solver::detail { m_rhs(m_operator.Height()), m_correction(m_operator.Width()) { } bool Initialize(State &state, nonlinear::NumericalStrategyDiagnostics &diagnostics) { + this->BeginGravityAttempt(); const auto &partition = this->subsystem.GetPartition(); partition.GatherGravityResiduals(state.acceptedNormalizedResidual, this->gravityResidual); diagnostics.initialGravityResidual = StrategyNorm(this->gravityResidual, this->subsystem.GetCommunicator()); @@ -343,11 +353,14 @@ namespace mean_field::solver::detail { this->subsystem.GetPartition().GatherRetainedValues(state.normalizedCorrection, m_correction); this->lastSolve = m_operator.TryLiftCorrection(m_correction, state.linearRightHandSide, state.normalizedCorrection); + // Keep the report space attached to the outer Solve result; this + // later auxiliary failure is reported through DirectionSolveFailed + // and the gravity counters/last completed inner defect. return this->lastSolve.Converged(); } auto PrepareTrial(State &state, double) { - this->trialSolveSucceeded = true; + this->BeginGravityAttempt(); auto preparation = state.PrepareTrial(); if (!preparation) { return preparation; } this->Refresh(state); diff --git a/tests/solver/stellar_numerical_strategies.cpp b/tests/solver/stellar_numerical_strategies.cpp index 3c43735..ac3a37e 100644 --- a/tests/solver/stellar_numerical_strategies.cpp +++ b/tests/solver/stellar_numerical_strategies.cpp @@ -196,6 +196,11 @@ TEST_CASE("Production gravity strategies retain optional model borders and resto CHECK(report.diagnostics().lastLinearSolve->iterations == 0); CHECK(report.diagnostics().numericalStrategies.gravitySolves > 0); CHECK(report.diagnostics().numericalStrategies.gravityFailures == 0); + CHECK(std::isfinite(report.diagnostics().numericalStrategies.lastGravityCorrectionNorm)); + CHECK(std::isfinite(report.diagnostics().numericalStrategies.lastGravityPreparedDefect)); + CHECK(std::isfinite(report.diagnostics().numericalStrategies.lastGravityAffineDisagreement)); + CHECK(std::isfinite(report.diagnostics().numericalStrategies.lastGravityArithmeticAllowance)); + CHECK(std::isfinite(report.diagnostics().numericalStrategies.lastGravityPreparedTargetThreshold)); CHECK(report.lastAcceptedCheckpointView().valid()); } REQUIRE(context.isReady()); @@ -237,6 +242,15 @@ TEST_CASE("A failed initial gravity completion preserves the accepted stellar ch CHECK(report.diagnostics().numericalStrategies.initialGravityResidual > 0.0); CHECK(report.diagnostics().numericalStrategies.gravitySolves == 1); CHECK(report.diagnostics().numericalStrategies.gravityFailures == 1); + const auto &auxiliary = report.diagnostics().numericalStrategies; + // A completed but unsuccessful inner solve has a meaningful true + // defect. No correction/prepared assessment occurred in this attempt. + CHECK(std::isfinite(auxiliary.lastGravityTrueResidual)); + CHECK(std::isnan(auxiliary.lastGravityCorrectionNorm)); + CHECK(std::isnan(auxiliary.lastGravityPreparedDefect)); + CHECK(std::isnan(auxiliary.lastGravityAffineDisagreement)); + CHECK(std::isnan(auxiliary.lastGravityArithmeticAllowance)); + CHECK(std::isnan(auxiliary.lastGravityPreparedTargetThreshold)); CHECK(report.diagnostics().acceptedNonlinearIterations == 0); CHECK(report.lastAcceptedCheckpointView().valid()); }