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

@@ -43,8 +43,9 @@ namespace mean_field::solver::detail {
void RequireCollectivelyIdenticalNumerics(const Recipe &recipe, const MPI_Comm communicator) {
using Enthalpy = typename Recipe::EnthalpyType;
using Gravity = typename Recipe::GravityType;
std::array<int, 8> integers{};
std::array<double, 2> reals{};
using Trial = typename Recipe::TrialType;
std::array<int, 13> integers{};
std::array<double, 5> reals{};
integers[0] = std::same_as<Enthalpy, nonlinear::PolynomialEnthalpy>;
integers[1] = std::same_as<Gravity, nonlinear::GravityCurvatureCorrection> ? 1 :
std::same_as<Gravity, nonlinear::EliminatedGravity> ? 2 : 0;
@@ -57,15 +58,27 @@ namespace mean_field::solver::detail {
integers[4] = options.control.maximumIterations;
integers[5] = options.backend.restartLength;
integers[6] = options.backend.printLevel;
reals = {options.control.relativeTolerance, options.control.absoluteTolerance};
reals[0] = options.control.relativeTolerance;
reals[1] = options.control.absoluteTolerance;
}
integers[7] = std::isfinite(reals[0]) && std::isfinite(reals[1]);
std::array<int, 8> minimumIntegers{}, maximumIntegers{};
std::array<double, 2> minimumReals{}, maximumReals{};
if (MPI_Allreduce(integers.data(), minimumIntegers.data(), 8, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS ||
MPI_Allreduce(integers.data(), maximumIntegers.data(), 8, 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) {
integers[8] = std::same_as<Trial, nonlinear::CoupledCurvatureCorrection>;
if constexpr (std::same_as<Trial, nonlinear::CoupledCurvatureCorrection>) {
const auto &options = recipe.trial.correction;
integers[9] = options.control.maximumIterations;
integers[10] = options.backend.restartLength;
integers[11] = options.backend.printLevel;
integers[12] = recipe.trial.maximumAttemptsPerIteration;
reals[2] = options.control.relativeTolerance;
reals[3] = options.control.absoluteTolerance;
reals[4] = options.maximumCorrectionRatio;
}
integers[7] = std::all_of(reals.begin(), reals.end(), [](double value) { return std::isfinite(value); });
std::array<int, 13> minimumIntegers{}, maximumIntegers{};
std::array<double, 5> minimumReals{}, maximumReals{};
if (MPI_Allreduce(integers.data(), minimumIntegers.data(), 13, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS ||
MPI_Allreduce(integers.data(), maximumIntegers.data(), 13, MPI_INT, MPI_MAX, communicator) != MPI_SUCCESS ||
MPI_Allreduce(reals.data(), minimumReals.data(), 5, MPI_DOUBLE, MPI_MIN, communicator) != MPI_SUCCESS ||
MPI_Allreduce(reals.data(), maximumReals.data(), 5, MPI_DOUBLE, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("Newton could not compare numerical strategies across ranks.");
}
if (minimumIntegers != maximumIntegers || minimumReals != maximumReals ||
@@ -73,6 +86,7 @@ namespace mean_field::solver::detail {
throw std::invalid_argument("Newton numerical strategies and controls must agree on every rank.");
}
if constexpr (!std::same_as<Gravity, nonlinear::MonolithicGravity>) { recipe.gravity.gravity.Validate(); }
if constexpr (std::same_as<Trial, nonlinear::CoupledCurvatureCorrection>) { recipe.trial.Validate(); }
}
template <typename State, typename Policy> class PreparedGravityStrategy;
@@ -91,7 +105,9 @@ namespace mean_field::solver::detail {
void CopyDiagnostics(nonlinear::NumericalStrategyDiagnostics &) const { }
};
template <typename State>
// Export the prepared workspace so bounded diagnostics can exercise this
// same gravity completion and arithmetic gate without reproducing them.
export template <typename State>
class StellarGravityWorkspace {
public:
using Operation = std::remove_cvref_t<decltype(*std::declval<State>().normalizedOperator)>;
@@ -406,15 +422,34 @@ namespace mean_field::solver::detail {
PreparedStellarEnthalpyStrategy<State> m_prepared;
};
template <typename State, typename Policy> struct PreparedTrialPath;
template <typename State> struct PreparedTrialPath<State, nonlinear::UncorrectedTrial> {
PreparedTrialPath(State &, const nonlinear::UncorrectedTrial &) { }
};
template <typename State> struct PreparedTrialPath<State, nonlinear::CoupledCurvatureCorrection> {
PreparedTrialPath(State &state, const nonlinear::CoupledCurvatureCorrection &policy)
: transaction(state, policy.correction, policy.maximumAttemptsPerIteration) { }
PreparedCoupledTrialTransaction<State> transaction;
};
template <typename State, nonlinear::NewtonNumericalConfiguration Recipe>
class PreparedNewtonStrategy final : public NewtonStrategyStorage {
public:
using Enthalpy = typename Recipe::EnthalpyType;
using Gravity = typename Recipe::GravityType;
using Trial = typename Recipe::TrialType;
static constexpr bool altersDirection = std::same_as<Enthalpy, nonlinear::PolynomialEnthalpy>;
static constexpr bool correctsTrial = std::same_as<Trial, nonlinear::CoupledCurvatureCorrection>;
PreparedNewtonStrategy(State &state, const Recipe &recipe)
: m_enthalpy(state, recipe.enthalpy), m_gravity(state, recipe.gravity) { }
: m_enthalpy(state, recipe.enthalpy), m_gravity(state, recipe.gravity), m_trial(state, recipe.trial) { }
void BeginTrial(nonlinear::NumericalStrategyDiagnostics &diagnostics) {
if constexpr (correctsTrial) {
m_trial.transaction.BeginTrial();
diagnostics.coupledTrial = m_trial.transaction.GetDiagnostics();
}
}
bool CheckState(const mfem::Vector &state, nonlinear::NumericalStrategyDiagnostics &diagnostics) {
return m_enthalpy.Check(state, diagnostics);
@@ -451,6 +486,12 @@ namespace mean_field::solver::detail {
if (!std::isfinite(diagnostics.lastMeritDirectionalDerivative) ||
diagnostics.lastMeritDirectionalDerivative >= 0.0) { return false; }
}
if constexpr (correctsTrial) {
// Repair has finished and the accepted Jacobian is still current.
// Every later trial, including a gravity-corrected one, uses this
// same full affine target and the original-direction norm cap.
m_trial.transaction.CaptureBase(state.acceptedNormalizedResidual, state.normalizedCorrection);
}
return true;
}
auto PrepareTrial(State &state, const double alpha, nonlinear::NumericalStrategyDiagnostics &diagnostics) {
@@ -460,9 +501,19 @@ namespace mean_field::solver::detail {
}
bool TrialSolveSucceeded() const noexcept { return m_gravity.TrialSolveSucceeded(); }
bool DirectionSolveFailed() const noexcept { return m_directionSolveFailed; }
template <typename Preparation, typename Metric, typename Evaluate, typename Accepts, typename Better>
void TryImproveTrial(State &state, double alpha, Preparation &preparation, Metric &metric,
nonlinear::NumericalStrategyDiagnostics &diagnostics,
Evaluate &&evaluate, Accepts &&accepts, Better &&better) requires(correctsTrial) {
m_trial.transaction.TryImprove(state, alpha, preparation, metric,
std::forward<Evaluate>(evaluate), std::forward<Accepts>(accepts), std::forward<Better>(better),
[&](const mfem::Vector &candidate) { return m_enthalpy.Check(candidate, diagnostics); });
diagnostics.coupledTrial = m_trial.transaction.GetDiagnostics();
}
private:
bool m_directionSolveFailed{false};
PreparedEnthalpyStrategy<State, Enthalpy> m_enthalpy;
PreparedGravityStrategy<State, Gravity> m_gravity;
[[no_unique_address]] PreparedTrialPath<State, Trial> m_trial;
};
}