Files
MeanField/libmeanfield/interface/solver/coupled_trial_correction.cppm

636 lines
38 KiB
C++

module;
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:solver.coupled_trial_correction;
export import :solver.linear_backend;
export namespace mean_field::solver {
struct FullCoupledStep final { };
struct BacktrackedCoupledStep final {
int maximumSteps{4};
double reduction{.5};
void Validate() const {
if (maximumSteps <= 0 || !std::isfinite(reduction) || reduction <= 0.0 || reduction >= 1.0 ||
std::pow(reduction, maximumSteps - 1) == 0.0)
throw std::invalid_argument("Coupled damping requires positive resolved steps and a reduction strictly between zero and one.");
}
};
struct EveryRejectedCoupledAttempt final { };
struct NearAcceptanceCoupledAttempt final {
double maximumExcessRatio{1.0};
void Validate() const {
if (!std::isfinite(maximumExcessRatio) || maximumExcessRatio <= 0.0)
throw std::invalid_argument("A coupled prediction gate requires a finite positive excess ratio.");
}
};
template <typename Candidate>
concept CoupledCorrectionSearch = std::same_as<Candidate, FullCoupledStep> ||
std::same_as<Candidate, BacktrackedCoupledStep>;
template <typename Candidate>
concept CoupledAttemptCriterion = std::same_as<Candidate, EveryRejectedCoupledAttempt> ||
std::same_as<Candidate, NearAcceptanceCoupledAttempt>;
struct CoupledTrialCorrectionOptions final {
LinearSolveControl control{.relativeTolerance = .03, .absoluteTolerance = 1.e-14, .maximumIterations = 160};
linear::FGMRESOptions backend{.restartLength = 160, .printLevel = -1};
double maximumCorrectionRatio{1.0};
void Validate() const {
control.Validate();
backend.Validate();
if (control.relativeTolerance <= 0.0 || !std::isfinite(maximumCorrectionRatio) ||
maximumCorrectionRatio <= 0.0)
throw std::invalid_argument("Coupled trial correction requires positive relative tolerance and norm cap.");
}
};
enum class CoupledTrialCorrectionStatus : std::uint8_t { admissible, linear_failure, norm_limit, non_finite };
struct CoupledTrialCorrectionReport final {
CoupledTrialCorrectionStatus status{CoupledTrialCorrectionStatus::linear_failure};
LinearSolveReport linearSolve{};
double verifiedTrueResidualNorm{std::numeric_limits<double>::quiet_NaN()};
double convergenceThreshold{std::numeric_limits<double>::quiet_NaN()};
double correctionNorm{std::numeric_limits<double>::quiet_NaN()};
double maximumCorrectionNorm{std::numeric_limits<double>::quiet_NaN()};
double verificationSeconds{0.0};
bool verifiedGate{false};
bool verificationApplied{false};
[[nodiscard]] bool Admissible() const noexcept { return status == CoupledTrialCorrectionStatus::admissible; }
};
enum class CoupledTrialAttemptStatus : std::uint8_t {
not_attempted, original_accepted, budget_exhausted, kept, linear_failure, norm_limit,
non_finite, inadmissible, not_improved, prediction_deferred
};
enum class CoupledTrialCandidateStatus : std::uint8_t { evaluated, inadmissible, non_finite };
struct CoupledTrialCandidateReport final {
double beta{1.0};
CoupledTrialCandidateStatus status{CoupledTrialCandidateStatus::inadmissible};
double merit{std::numeric_limits<double>::quiet_NaN()};
bool accepted{false};
double preparationSeconds{0.0};
};
struct CoupledTrialDiagnostics final {
std::uint64_t attempts{0}, kept{0}, fallbacks{0}, iterations{0}, preparations{0}, restorations{0};
double solveSeconds{0.0}, verificationSeconds{0.0}, preparationSeconds{0.0}, refreshSeconds{0.0};
// These fields describe the current trial; BeginTrial clears them even
// when an earlier preparation rejection prevents another auxiliary solve.
CoupledTrialAttemptStatus status{CoupledTrialAttemptStatus::not_attempted};
std::optional<CoupledTrialCorrectionReport> currentSolve{};
double originalMerit{std::numeric_limits<double>::quiet_NaN()};
double correctedMerit{std::numeric_limits<double>::quiet_NaN()};
std::uint64_t deferredAttempts{0};
int candidateCount{0};
double selectedBeta{std::numeric_limits<double>::quiet_NaN()};
double baseMerit{std::numeric_limits<double>::quiet_NaN()};
double linearPredictionMerit{std::numeric_limits<double>::quiet_NaN()};
double predictedDecrease{std::numeric_limits<double>::quiet_NaN()};
double predictionExcessRatio{std::numeric_limits<double>::quiet_NaN()};
// Search candidates exclude the possible final replay of an earlier
// best beta. Preparations/restorations still account for that work.
std::vector<CoupledTrialCandidateReport> candidates{};
};
}
namespace mean_field::solver::detail {
template <typename Operation>
concept CoupledGenerationAvailable = requires(const Operation &operation) {
{ operation.GetPreparationGeneration() } -> std::convertible_to<std::uint64_t>;
} || requires(const Operation &operation) {
{ operation.GetProblem().GetPreparationGeneration() } -> std::convertible_to<std::uint64_t>;
};
template <CoupledGenerationAvailable Operation>
std::uint64_t CoupledGeneration(const Operation &operation) {
if constexpr (requires { operation.GetPreparationGeneration(); }) return operation.GetPreparationGeneration();
else return operation.GetProblem().GetPreparationGeneration();
}
inline bool CoupledFinite(const mfem::Vector &values) {
for (int index = 0; index < values.Size(); ++index)
if (!std::isfinite(values(index))) return false;
return true;
}
inline bool CoupledAll(bool condition, MPI_Comm communicator) {
int local = condition, global = 0;
if (MPI_Allreduce(&local, &global, 1, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS)
throw std::runtime_error("A coupled trial correction could not reduce its validity check.");
return global != 0;
}
inline double CoupledNorm(const mfem::Vector &values, MPI_Comm communicator) {
const double local = values * values;
double global = 0.0;
if (MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator) != MPI_SUCCESS)
throw std::runtime_error("A coupled trial correction could not reduce its norm.");
return std::sqrt(global);
}
inline void CoupledRequire(bool condition, MPI_Comm communicator, const char *message) {
if (!CoupledAll(condition, communicator)) throw std::invalid_argument(message);
}
inline CoupledTrialCorrectionOptions PrepareCoupledOptions(CoupledTrialCorrectionOptions options, MPI_Comm communicator) {
int initialized = 0, finalized = 0;
if (MPI_Initialized(&initialized) != MPI_SUCCESS || !initialized ||
MPI_Finalized(&finalized) != MPI_SUCCESS || finalized)
throw std::logic_error("A coupled trial correction requires initialized MPI that has not been finalized.");
if (communicator == MPI_COMM_NULL) throw std::invalid_argument("A coupled trial correction requires a communicator.");
bool valid = true;
try { options.Validate(); } catch (const std::invalid_argument &) { valid = false; }
CoupledRequire(valid, communicator, "Coupled trial correction controls are invalid on at least one rank.");
const std::array<int, 3> integers{options.control.maximumIterations, options.backend.restartLength, options.backend.printLevel};
const std::array<double, 3> reals{options.control.relativeTolerance, options.control.absoluteTolerance, options.maximumCorrectionRatio};
std::array<int, 3> imin{}, imax{};
std::array<double, 3> rmin{}, rmax{};
if (MPI_Allreduce(integers.data(), imin.data(), 3, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS ||
MPI_Allreduce(integers.data(), imax.data(), 3, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS ||
MPI_Allreduce(reals.data(), rmin.data(), 3, MPI_DOUBLE, MPI_MIN, communicator) != MPI_SUCCESS ||
MPI_Allreduce(reals.data(), rmax.data(), 3, MPI_DOUBLE, MPI_MAX, communicator) != MPI_SUCCESS)
throw std::runtime_error("Coupled trial correction controls could not be compared across ranks.");
if (imin != imax || rmin != rmax)
throw std::invalid_argument("Coupled trial correction controls must agree across ranks.");
return options;
}
template <typename Operation, typename Inverse>
CoupledTrialCorrectionOptions PrepareCoupledSystemOptions(CoupledTrialCorrectionOptions options,
const Operation &operation, const Inverse &inverse, MPI_Comm communicator) {
options = PrepareCoupledOptions(options, communicator);
CoupledRequire(operation.Height() >= 0 && operation.Width() == operation.Height() &&
inverse.Height() == operation.Width() && inverse.Width() == operation.Height(), communicator,
"A coupled correction requires compatible square operator and inverse dimensions.");
const long long localSize = operation.Height();
long long globalSize = 0;
if (MPI_Allreduce(&localSize, &globalSize, 1, MPI_LONG_LONG, MPI_SUM, communicator) != MPI_SUCCESS)
throw std::runtime_error("A coupled correction could not determine its global size.");
if (globalSize == 0) throw std::invalid_argument("A coupled correction requires a globally nonempty system.");
return options;
}
inline linear::FGMRES PrepareCoupledBackend(const CoupledTrialCorrectionOptions &options, MPI_Comm communicator) {
return linear::FGMRES(PrepareCoupledOptions(options, communicator).backend);
}
}
export namespace mean_field::solver {
template <typename Operation>
concept CoupledTrialOperation = std::derived_from<Operation, mfem::Operator> &&
detail::CoupledGenerationAvailable<Operation> && requires(const Operation &operation) {
{ operation.IsPrepared() } -> std::same_as<bool>;
};
template <typename Inverse>
concept CoupledTrialInverse = std::derived_from<Inverse, mfem::Solver> && requires(const Inverse &inverse) {
{ inverse.IsCurrent() } -> std::same_as<bool>;
};
// Capture fixes the affine target at the accepted linearization. Later
// SolveCorrection borrows the freshly prepared candidate Jacobian; neither
// operation owns or mutates a model state or the outer Newton direction.
template <CoupledTrialOperation Operation, CoupledTrialInverse Inverse,
LinearBackendConfiguration Backend = linear::FGMRES>
requires LinearBackendRuntimeAvailableFor<Backend, Operation, Inverse>
class PreparedCoupledTrialCorrection final {
public:
PreparedCoupledTrialCorrection(const Operation &operation, Inverse &inverse, MPI_Comm communicator,
CoupledTrialCorrectionOptions options = {})
requires std::same_as<Backend, linear::FGMRES>
: PreparedCoupledTrialCorrection(operation, inverse, communicator, options,
detail::PrepareCoupledBackend(options, communicator)) { }
PreparedCoupledTrialCorrection(const Operation &operation, Inverse &inverse, MPI_Comm communicator,
CoupledTrialCorrectionOptions options, Backend backend)
: m_operation(std::addressof(operation)), m_inverse(std::addressof(inverse)), m_communicator(communicator),
m_options(detail::PrepareCoupledSystemOptions(options, operation, inverse, communicator)),
m_backend(prepareLinearBackend(std::move(backend), operation, inverse, communicator)),
m_baseResidual(operation.Height()), m_baseAction(operation.Height()), m_rightHandSide(operation.Height()),
m_verifiedAction(operation.Height()), m_defect(operation.Height()) {
detail::CoupledRequire(IsCurrent(), communicator, "Coupled correction preparation requires a current operator and inverse.");
}
PreparedCoupledTrialCorrection(const PreparedCoupledTrialCorrection &) = delete;
PreparedCoupledTrialCorrection &operator=(const PreparedCoupledTrialCorrection &) = delete;
PreparedCoupledTrialCorrection(PreparedCoupledTrialCorrection &&) = delete;
PreparedCoupledTrialCorrection &operator=(PreparedCoupledTrialCorrection &&) = delete;
[[nodiscard]] bool IsCurrent() const {
return m_operation->IsPrepared() && m_inverse->IsCurrent() && m_backend.IsReady() &&
m_operation->Height() == m_operation->Width() &&
m_inverse->Height() == m_operation->Width() && m_inverse->Width() == m_operation->Height() &&
m_baseResidual.Size() == m_operation->Height() && m_baseAction.Size() == m_operation->Height();
}
[[nodiscard]] bool HasBase() const noexcept { return m_hasBase; }
[[nodiscard]] std::uint64_t GetBaseGeneration() const noexcept { return m_baseGeneration; }
[[nodiscard]] double GetBaseDirectionNorm() const noexcept { return m_directionNorm; }
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept { return m_communicator; }
[[nodiscard]] const mfem::Vector &GetRightHandSide() const noexcept { return m_rightHandSide; }
[[nodiscard]] const mfem::Vector &GetBaseResidual() const noexcept { return m_baseResidual; }
[[nodiscard]] const mfem::Vector &GetVerifiedAction() const noexcept { return m_verifiedAction; }
[[nodiscard]] const mfem::Vector &GetVerifiedDefect() const noexcept { return m_defect; }
[[nodiscard]] const auto &GetLastReport() const noexcept { return m_lastReport; }
[[nodiscard]] const CoupledTrialCorrectionOptions &GetOptions() const noexcept { return m_options; }
void Invalidate() noexcept { m_hasBase = false; }
void CaptureBase(const mfem::Vector &residual, const mfem::Vector &direction) {
m_hasBase = false;
detail::CoupledRequire(IsCurrent() && residual.Size() == m_operation->Height() &&
direction.Size() == m_operation->Width() && detail::CoupledFinite(residual) && detail::CoupledFinite(direction),
m_communicator, "Coupled correction capture requires a current base and finite compatible vectors.");
m_baseResidual = residual;
m_operation->Mult(direction, m_baseAction);
m_directionNorm = detail::CoupledNorm(direction, m_communicator);
detail::CoupledRequire(detail::CoupledFinite(m_baseAction) && std::isfinite(m_directionNorm), m_communicator,
"The captured coupled correction tangent is nonfinite.");
m_baseGeneration = detail::CoupledGeneration(*m_operation);
m_hasBase = true;
}
void BuildLinearPrediction(double alpha, mfem::Vector &prediction) const {
detail::CoupledRequire(m_hasBase && prediction.Size() == m_baseResidual.Size(), m_communicator,
"A coupled prediction requires a captured base and compatible output.");
RequireAlpha(alpha);
prediction = m_baseResidual;
prediction.Add(alpha, m_baseAction);
detail::CoupledRequire(detail::CoupledFinite(prediction), m_communicator, "The coupled affine target is nonfinite.");
}
[[nodiscard]] CoupledTrialCorrectionReport SolveCorrection(double alpha, const mfem::Vector &trialResidual,
mfem::Vector &correction) {
detail::CoupledRequire(m_hasBase && IsCurrent() && detail::CoupledGeneration(*m_operation) >= m_baseGeneration &&
trialResidual.Size() == m_operation->Height() && correction.Size() == m_operation->Width(),
m_communicator, "Coupled correction requires a captured base, current candidate and compatible vectors.");
RequireAlpha(alpha);
CoupledTrialCorrectionReport report;
const auto finish = [&]() { m_lastReport = report; return report; };
correction = 0.0;
m_verifiedAction = std::numeric_limits<double>::quiet_NaN();
m_defect = std::numeric_limits<double>::quiet_NaN();
report.maximumCorrectionNorm = m_options.maximumCorrectionRatio * alpha * m_directionNorm;
BuildLinearPrediction(alpha, m_rightHandSide);
m_rightHandSide -= trialResidual;
if (!detail::CoupledAll(detail::CoupledFinite(m_rightHandSide) && std::isfinite(report.maximumCorrectionNorm), m_communicator)) {
report.status = CoupledTrialCorrectionStatus::non_finite;
return finish();
}
const double rhsNorm = detail::CoupledNorm(m_rightHandSide, m_communicator);
const double relativeThreshold = m_options.control.relativeTolerance * rhsNorm;
if (!std::isfinite(rhsNorm) || !std::isfinite(relativeThreshold)) {
report.status = CoupledTrialCorrectionStatus::non_finite;
return finish();
}
report.convergenceThreshold = m_options.control.ConvergenceThreshold(rhsNorm);
report.linearSolve = m_backend.Solve(m_rightHandSide, correction, m_options.control);
if (!detail::CoupledAll(correction.Size() == m_operation->Width() && detail::CoupledFinite(correction), m_communicator)) {
report.status = CoupledTrialCorrectionStatus::non_finite;
return finish();
}
const auto start = std::chrono::steady_clock::now();
m_operation->Mult(correction, m_verifiedAction);
report.verificationApplied = true;
m_defect = m_verifiedAction;
m_defect -= m_rightHandSide;
const bool finite = detail::CoupledAll(detail::CoupledFinite(m_defect), m_communicator);
report.verifiedTrueResidualNorm = finite ? detail::CoupledNorm(m_defect, m_communicator) :
std::numeric_limits<double>::quiet_NaN();
report.correctionNorm = detail::CoupledNorm(correction, m_communicator);
report.verificationSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
report.verifiedGate = std::isfinite(report.convergenceThreshold) && report.convergenceThreshold >= 0.0 &&
std::isfinite(report.verifiedTrueResidualNorm) &&
report.verifiedTrueResidualNorm <= report.convergenceThreshold;
const bool solved = detail::CoupledAll(report.linearSolve.Converged(), m_communicator);
if (!finite || !std::isfinite(report.verifiedTrueResidualNorm) || !std::isfinite(report.correctionNorm))
report.status = CoupledTrialCorrectionStatus::non_finite;
else if (!solved || !report.verifiedGate) report.status = CoupledTrialCorrectionStatus::linear_failure;
else if (report.correctionNorm > report.maximumCorrectionNorm) report.status = CoupledTrialCorrectionStatus::norm_limit;
else report.status = CoupledTrialCorrectionStatus::admissible;
return finish();
}
private:
void RequireAlpha(double alpha) const {
detail::CoupledRequire(std::isfinite(alpha) && alpha >= 0.0, m_communicator,
"Coupled correction requires a finite nonnegative step length.");
double minimum = 0.0, maximum = 0.0;
if (MPI_Allreduce(&alpha, &minimum, 1, MPI_DOUBLE, MPI_MIN, m_communicator) != MPI_SUCCESS ||
MPI_Allreduce(&alpha, &maximum, 1, MPI_DOUBLE, MPI_MAX, m_communicator) != MPI_SUCCESS)
throw std::runtime_error("A coupled correction could not compare step lengths.");
if (minimum != maximum) throw std::invalid_argument("Coupled correction step lengths must agree across ranks.");
}
const Operation *m_operation;
Inverse *m_inverse;
MPI_Comm m_communicator;
CoupledTrialCorrectionOptions m_options;
PreparedLinearBackendType<Backend, Operation, Inverse> m_backend;
mfem::Vector m_baseResidual, m_baseAction, m_rightHandSide, m_verifiedAction, m_defect;
double m_directionNorm{0.0};
std::uint64_t m_baseGeneration{0};
bool m_hasBase{false};
std::optional<CoupledTrialCorrectionReport> m_lastReport{};
};
}
// The state protocol is deliberately structural: production and focused probes
// execute the same transaction while their typed callbacks choose the metric
// and enthalpy policy. This helper never reads or writes accepted-state buffers.
export namespace mean_field::solver::detail {
template <typename State, CoupledCorrectionSearch Search = FullCoupledStep,
CoupledAttemptCriterion Attempt = EveryRejectedCoupledAttempt>
class PreparedCoupledTrialTransaction final {
using Operation = std::remove_cvref_t<decltype(*std::declval<State>().normalizedOperator)>;
using Inverse = std::remove_cvref_t<decltype(*std::declval<State>().normalizedInverse)>;
using Clock = std::chrono::steady_clock;
public:
PreparedCoupledTrialTransaction(State &state, const CoupledTrialCorrectionOptions &options,
int maximumAttemptsPerIteration = 1, Search search = {}, Attempt attempt = {})
: m_core(*state.normalizedOperator, *state.normalizedInverse, state.Problem().GetCommunicator(), options),
m_maximumAttempts(maximumAttemptsPerIteration), m_search(search), m_attempt(attempt),
m_originalState(state.normalizedOperator->Width()), m_originalResidual(state.normalizedOperator->Height()),
m_originalPhysical(state.normalizedOperator->Width()), m_correction(state.normalizedOperator->Width()),
m_selectedCorrection(state.normalizedOperator->Width()),
m_prediction(std::same_as<Attempt, NearAcceptanceCoupledAttempt> ? state.normalizedOperator->Height() : 0) {
ValidatePoliciesCollectively();
m_selectedCorrection = 0.0;
m_diagnostics.candidates.reserve(MaximumSteps());
}
void CaptureBase(const mfem::Vector &residual, const mfem::Vector &direction) {
m_core.CaptureBase(residual, direction);
m_attemptsThisIteration = 0;
}
void BeginTrial() noexcept {
m_diagnostics.status = CoupledTrialAttemptStatus::not_attempted;
m_diagnostics.currentSolve.reset();
m_diagnostics.originalMerit = m_diagnostics.correctedMerit = m_diagnostics.selectedBeta =
m_diagnostics.baseMerit = m_diagnostics.linearPredictionMerit = m_diagnostics.predictedDecrease =
m_diagnostics.predictionExcessRatio = std::numeric_limits<double>::quiet_NaN();
m_diagnostics.candidateCount = 0;
m_diagnostics.candidates.clear();
m_selectedCorrection = 0.0;
}
[[nodiscard]] const CoupledTrialDiagnostics &GetDiagnostics() const noexcept { return m_diagnostics; }
[[nodiscard]] const auto &GetCore() const noexcept { return m_core; }
// The report verifies this unscaled vector. Damping changes the trial
// map, not the vector to which its original linear certificate applies.
[[nodiscard]] const mfem::Vector &GetCorrection() const noexcept { return m_correction; }
[[nodiscard]] const mfem::Vector &GetSelectedCorrection() const noexcept { return m_selectedCorrection; }
template <typename Preparation, typename Metric, typename Evaluate, typename Accepts, typename Better, typename CheckState>
void TryImprove(State &state, double alpha, Preparation &preparation, Metric &metric,
Evaluate &&evaluate, Accepts &&accepts, Better &&better, CheckState &&checkState) {
const auto communicator = m_core.GetCommunicator();
CoupledRequire(preparation.has_value() && state.normalizedOperator->IsPrepared() &&
CoupledFinite(state.trialNormalizedState) && CoupledFinite(state.trialNormalizedResidual) &&
ValidMetric(metric), communicator, "Coupled correction requires a successfully prepared original trial.");
m_diagnostics.originalMerit = metric.merit;
if (CoupledAll(accepts(metric), communicator)) {
m_diagnostics.status = CoupledTrialAttemptStatus::original_accepted;
return;
}
if (m_attemptsThisIteration >= m_maximumAttempts) {
m_diagnostics.status = CoupledTrialAttemptStatus::budget_exhausted;
return;
}
if constexpr (std::same_as<Attempt, NearAcceptanceCoupledAttempt>) {
// These are evaluations of saved residual vectors, not new
// physical candidates. No preparation, inverse refresh or
// attempt budget is spent on the prediction gate.
m_core.BuildLinearPrediction(alpha, m_prediction);
const auto baseMetric = evaluate(m_core.GetBaseResidual());
const auto predictionMetric = evaluate(m_prediction);
m_diagnostics.baseMerit = baseMetric.merit;
m_diagnostics.linearPredictionMerit = predictionMetric.merit;
m_diagnostics.predictedDecrease = baseMetric.merit - predictionMetric.merit;
const double decrease = m_diagnostics.predictedDecrease;
const double ratio = decrease > 0.0 ? (metric.merit - baseMetric.merit) / decrease :
std::numeric_limits<double>::quiet_NaN();
m_diagnostics.predictionExcessRatio = ratio;
if (!CoupledAll(ValidMetric(baseMetric) && ValidMetric(predictionMetric) && std::isfinite(decrease) &&
decrease > 0.0 && std::isfinite(ratio) && ratio <= m_attempt.maximumExcessRatio, communicator)) {
m_diagnostics.status = CoupledTrialAttemptStatus::prediction_deferred;
++m_diagnostics.deferredAttempts;
return;
}
}
m_originalState = state.trialNormalizedState;
m_originalResidual = state.trialNormalizedResidual;
m_originalPhysical = state.candidatePhysicalState;
Refresh(state);
++m_attemptsThisIteration;
++m_diagnostics.attempts;
m_diagnostics.currentSolve = m_core.SolveCorrection(alpha, m_originalResidual, m_correction);
const auto &report = *m_diagnostics.currentSolve;
m_diagnostics.iterations += static_cast<std::uint64_t>(report.linearSolve.iterations);
m_diagnostics.solveSeconds += report.linearSolve.solveSeconds;
m_diagnostics.verificationSeconds += report.verificationSeconds;
if (!report.Admissible()) {
++m_diagnostics.fallbacks;
switch (report.status) {
case CoupledTrialCorrectionStatus::linear_failure: m_diagnostics.status = CoupledTrialAttemptStatus::linear_failure; break;
case CoupledTrialCorrectionStatus::norm_limit: m_diagnostics.status = CoupledTrialAttemptStatus::norm_limit; break;
default: m_diagnostics.status = CoupledTrialAttemptStatus::non_finite; break;
}
// No candidate mutation has occurred. In particular, a small
// beta cannot rescue an unverified or over-cap full correction.
return;
}
const Metric originalMetric = metric;
bool originalRestorationStarted = false;
auto restoreOriginal = [&]() {
originalRestorationStarted = true;
state.trialNormalizedState = m_originalState;
state.candidatePhysicalState = m_originalPhysical;
auto restored = Prepare(state);
++m_diagnostics.restorations;
if (!CoupledAll(restored.has_value() && CoupledFinite(state.trialNormalizedResidual), communicator))
throw std::runtime_error("A coupled trial fallback could not reprepare its original candidate.");
preparation = std::move(restored);
Refresh(state);
metric = evaluate(state.trialNormalizedResidual);
++m_diagnostics.fallbacks;
};
const auto formCandidate = [&](double beta) {
state.trialNormalizedState = m_originalState;
state.trialNormalizedState.Add(beta, m_correction);
state.normalizedOperator->DenormalizeState(state.trialNormalizedState, state.candidatePhysicalState);
};
try {
std::optional<Metric> bestMetric;
double bestBeta = 0.0;
int bestStep = -1, lastStep = -1;
double beta = 1.0;
for (int step = 0; step < MaximumSteps(); ++step, beta *= Reduction()) {
lastStep = step;
formCandidate(beta);
m_diagnostics.candidates.push_back({.beta = beta});
++m_diagnostics.candidateCount;
auto &candidate = m_diagnostics.candidates.back();
if (!CoupledAll(CoupledFinite(state.trialNormalizedState) && CoupledFinite(state.candidatePhysicalState), communicator)) {
candidate.status = CoupledTrialCandidateStatus::non_finite;
m_diagnostics.status = CoupledTrialAttemptStatus::non_finite;
continue;
}
if (!CoupledAll(checkState(state.trialNormalizedState), communicator)) {
candidate.status = CoupledTrialCandidateStatus::inadmissible;
m_diagnostics.status = CoupledTrialAttemptStatus::inadmissible;
continue;
}
const double preparationStart = m_diagnostics.preparationSeconds;
auto corrected = Prepare(state);
candidate.preparationSeconds = m_diagnostics.preparationSeconds - preparationStart;
if (!CoupledAll(corrected.has_value(), communicator)) {
candidate.status = CoupledTrialCandidateStatus::inadmissible;
m_diagnostics.status = CoupledTrialAttemptStatus::inadmissible;
continue;
}
if (!CoupledAll(CoupledFinite(state.trialNormalizedResidual), communicator)) {
candidate.status = CoupledTrialCandidateStatus::non_finite;
m_diagnostics.status = CoupledTrialAttemptStatus::non_finite;
continue;
}
const auto correctedMetric = evaluate(state.trialNormalizedResidual);
m_diagnostics.correctedMerit = correctedMetric.merit;
if (!CoupledAll(ValidMetric(correctedMetric), communicator)) {
candidate.status = CoupledTrialCandidateStatus::non_finite;
m_diagnostics.status = CoupledTrialAttemptStatus::non_finite;
continue;
}
preparation = std::move(corrected);
candidate.status = CoupledTrialCandidateStatus::evaluated;
candidate.merit = correctedMetric.merit;
const bool improves = CoupledAll(better(correctedMetric, originalMetric), communicator);
candidate.accepted = improves && CoupledAll(accepts(correctedMetric), communicator);
if (improves && (!bestMetric || candidate.accepted || CoupledAll(better(correctedMetric, *bestMetric), communicator))) {
bestMetric = correctedMetric;
bestBeta = beta;
bestStep = step;
}
m_diagnostics.status = CoupledTrialAttemptStatus::not_improved;
// An improved but unacceptable full correction must not
// suppress the remaining, cheaper physical preparations.
if (candidate.accepted) break;
}
if (!bestMetric) {
restoreOriginal();
return;
}
if (bestStep != lastStep) {
// The current prepared operator belongs to the last beta,
// which can even be inadmissible. Replaying the selected
// state restores its residual, geometry and generation.
formCandidate(bestBeta);
if (!CoupledAll(checkState(state.trialNormalizedState), communicator)) {
m_diagnostics.status = CoupledTrialAttemptStatus::inadmissible;
restoreOriginal();
return;
}
auto restored = Prepare(state);
++m_diagnostics.restorations;
if (!CoupledAll(restored.has_value() && CoupledFinite(state.trialNormalizedResidual), communicator)) {
m_diagnostics.status = CoupledTrialAttemptStatus::inadmissible;
restoreOriginal();
return;
}
const auto restoredMetric = evaluate(state.trialNormalizedResidual);
if (!CoupledAll(ValidMetric(restoredMetric), communicator) ||
!CoupledAll(better(restoredMetric, originalMetric), communicator)) {
m_diagnostics.status = CoupledTrialAttemptStatus::not_improved;
restoreOriginal();
return;
}
preparation = std::move(restored);
bestMetric = restoredMetric;
Refresh(state);
}
metric = *bestMetric;
m_selectedCorrection = m_correction;
m_selectedCorrection *= bestBeta;
m_diagnostics.selectedBeta = bestBeta;
m_diagnostics.correctedMerit = metric.merit;
m_diagnostics.status = CoupledTrialAttemptStatus::kept;
++m_diagnostics.kept;
// A current selected trial retains the ordinary lifecycle:
// its inverse is refreshed only on commit or another solve.
} catch (...) {
// Only collectively propagated callback exceptions can be
// rolled back collectively. Failure to restore propagates to
// the owning Newton context's accepted-base restoration.
if (!originalRestorationStarted) restoreOriginal();
throw;
}
}
private:
template <typename Metric> static bool ValidMetric(const Metric &metric) {
bool valid = std::isfinite(metric.merit) && metric.merit >= 0.0;
if constexpr (requires { metric.residualNorm; })
valid = valid && std::isfinite(metric.residualNorm) && metric.residualNorm >= 0.0;
return valid;
}
[[nodiscard]] int MaximumSteps() const noexcept {
if constexpr (std::same_as<Search, BacktrackedCoupledStep>) return m_search.maximumSteps;
else return 1;
}
[[nodiscard]] double Reduction() const noexcept {
if constexpr (std::same_as<Search, BacktrackedCoupledStep>) return m_search.reduction;
else return 1.0;
}
void ValidatePoliciesCollectively() const {
const auto communicator = m_core.GetCommunicator();
bool valid = m_maximumAttempts > 0;
try {
if constexpr (std::same_as<Search, BacktrackedCoupledStep>) m_search.Validate();
if constexpr (std::same_as<Attempt, NearAcceptanceCoupledAttempt>) m_attempt.Validate();
} catch (const std::invalid_argument &) { valid = false; }
CoupledRequire(valid, communicator, "Coupled trial search or attempt controls are invalid on at least one rank.");
double excessRatio = 0.0;
if constexpr (std::same_as<Attempt, NearAcceptanceCoupledAttempt>) excessRatio = m_attempt.maximumExcessRatio;
const std::array<int, 4> integers{m_maximumAttempts, std::same_as<Search, BacktrackedCoupledStep>,
MaximumSteps(), std::same_as<Attempt, NearAcceptanceCoupledAttempt>};
const std::array<double, 2> reals{Reduction(), excessRatio};
std::array<int, 4> minimumIntegers{}, maximumIntegers{};
std::array<double, 2> minimumReals{}, maximumReals{};
if (MPI_Allreduce(integers.data(), minimumIntegers.data(), 4, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS ||
MPI_Allreduce(integers.data(), maximumIntegers.data(), 4, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS ||
MPI_Allreduce(reals.data(), minimumReals.data(), 2, MPI_DOUBLE, MPI_MIN, communicator) != MPI_SUCCESS ||
MPI_Allreduce(reals.data(), maximumReals.data(), 2, MPI_DOUBLE, MPI_MAX, communicator) != MPI_SUCCESS)
throw std::runtime_error("Coupled trial search and attempt controls could not be compared.");
if (minimumIntegers != maximumIntegers || minimumReals != maximumReals)
throw std::invalid_argument("Coupled trial search and attempt controls must agree across ranks.");
}
void Refresh(State &state) {
const auto start = Clock::now();
state.physicalInverse->Refresh();
m_diagnostics.refreshSeconds += std::chrono::duration<double>(Clock::now() - start).count();
}
auto Prepare(State &state) {
const auto start = Clock::now();
auto result = state.PrepareTrial();
m_diagnostics.preparationSeconds += std::chrono::duration<double>(Clock::now() - start).count();
++m_diagnostics.preparations;
return result;
}
PreparedCoupledTrialCorrection<Operation, Inverse> m_core;
int m_maximumAttempts, m_attemptsThisIteration{0};
[[no_unique_address]] Search m_search;
[[no_unique_address]] Attempt m_attempt;
mfem::Vector m_originalState, m_originalResidual, m_originalPhysical, m_correction, m_selectedCorrection, m_prediction;
CoupledTrialDiagnostics m_diagnostics{};
};
}