Probe FEM gravity strategies and make reduced contracts collective

This commit is contained in:
2026-09-17 09:30:12 -04:00
parent 9e0e1bc42a
commit a7f080f265
2 changed files with 361 additions and 6 deletions

View File

@@ -0,0 +1,324 @@
#pragma once
// Include after import mean_field. The caller must hold the diagnostic state lease.
#include <algorithm>
#include <array>
#include <cmath>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <limits>
#include <stdexcept>
#include <string>
#include <type_traits>
namespace experiment::plateau_diagnostics {
struct NumericalStrategyProbeOptions final {
mean_field::solver::LinearSolveControl inner{1.e-9, 1.e-13, 160};
int restartLength{80};
int maximumCompletionCorrections{2};
double gravityCompletionTolerance{1.e-11};
};
namespace numerical_strategy_detail {
inline bool Finite(const mfem::Vector &values) {
for (int index = 0; index < values.Size(); ++index)
if (!std::isfinite(values(index))) return false;
return true;
}
inline double Difference(const mfem::Vector &left, const mfem::Vector &right) {
mfem::Vector difference(left);
difference -= right;
return difference.Norml2();
}
inline std::ofstream Open(const std::filesystem::path &path) {
if (std::filesystem::exists(path))
throw std::invalid_argument("Numerical strategy diagnostic output already exists: " + path.string());
std::ofstream output(path);
output.exceptions(std::ios::failbit | std::ios::badbit);
output << std::setprecision(17);
return output;
}
} // namespace numerical_strategy_detail
template <class State>
void ProbeNumericalStrategies(
State &state, const mean_field::fem::FEM &finiteElements,
const mfem::Vector &normalizedBase, const mfem::Vector &normalizedDirection,
const std::filesystem::path &output, NumericalStrategyProbeOptions options = {}
) {
using namespace mean_field;
namespace d = numerical_strategy_detail;
using Form = typename std::remove_cvref_t<decltype(state.Problem())>::FormType;
options.inner.Validate();
if (options.restartLength <= 0 || options.maximumCompletionCorrections <= 0 ||
!std::isfinite(options.gravityCompletionTolerance) || options.gravityCompletionTolerance <= 0.)
throw std::invalid_argument("Numerical strategy probes require valid inner and completion controls.");
int ranks = 0;
MPI_Comm_size(state.Problem().GetCommunicator(), &ranks);
if (ranks != 1 || !finiteElements.mesh || normalizedBase.Size() != state.Problem().StateSize() ||
normalizedDirection.Size() != normalizedBase.Size() || !d::Finite(normalizedBase) ||
!d::Finite(normalizedDirection))
throw std::invalid_argument("Numerical strategy probes require one rank and finite compatible root vectors.");
const mfem::Vector base(normalizedBase), direction(normalizedDirection);
const mfem::Vector savedAcceptedState(state.acceptedNormalizedState);
const mfem::Vector savedAcceptedResidual(state.acceptedNormalizedResidual);
const mfem::Vector savedAcceptedPhysical(state.AcceptedPhysicalState());
const mfem::Vector savedCorrection(state.normalizedCorrection), savedPhysicalCorrection(state.physicalCorrection);
const mfem::Vector savedVolumeDirection(state.volumeDisplacementDirection);
std::filesystem::create_directories(output);
auto summary = d::Open(output / "numerical_strategy_summary.csv");
auto preparations = d::Open(output / "numerical_strategy_preparations.csv");
auto inner = d::Open(output / "numerical_strategy_inner.csv");
auto curvature = d::Open(output / "numerical_strategy_curvature.csv");
auto completions = d::Open(output / "numerical_strategy_completions.csv");
auto differences = d::Open(output / "numerical_strategy_reduced_derivative.csv");
summary << "metric,value\n";
preparations << "sequence,kind,alpha,prepared,rejection_reason,rejection_stage,residual_norm\n";
inner << "kind,alpha,index,status,converged,iterations,right_hand_side_norm,true_residual_norm,"
"relative_true_residual_norm,threshold,operator_applications,inverse_applications,seconds\n";
curvature << "alpha,prepared,inner_converged,corrected_prepared,base_residual_norm,raw_residual_norm,"
"corrected_residual_norm,linear_prediction_norm,raw_gravity_remainder,corrected_gravity_remainder,"
"gravity_correction_norm,correction_over_alpha_squared,retained_state_change\n";
completions << "kind,alpha,completed,corrections,initial_gravity_norm,final_gravity_norm,full_residual_norm,"
"retained_state_change\n";
differences << "alpha,plus_completed,minus_completed,schur_action_norm,difference_quotient_norm,"
"difference_quotient_error,relative_error,plus_gravity_norm,minus_gravity_norm,"
"residual_subtraction_roundoff_scale\n";
summary << "inner_relative_tolerance," << options.inner.relativeTolerance
<< "\ninner_absolute_tolerance," << options.inner.absoluteTolerance
<< "\ninner_maximum_iterations," << options.inner.maximumIterations
<< "\ninner_restart_length," << options.restartLength
<< "\ngravity_completion_tolerance," << options.gravityCompletionTolerance << '\n';
const double nan = std::numeric_limits<double>::quiet_NaN();
int preparationCount = 0;
double restorationError = 0.;
mfem::Vector originalResidual(state.Problem().EquationSize());
bool haveOriginalResidual = false;
auto prepare = [&](const mfem::Vector &candidate, const std::string &kind, double alpha) {
state.trialNormalizedState = candidate;
const auto result = state.PrepareTrial();
++preparationCount;
preparations << preparationCount << ',' << kind << ',' << alpha << ',' << bool(result) << ',';
if (result)
preparations << "-1,-1," << state.trialNormalizedResidual.Norml2() << '\n';
else
preparations << int(result.error().reason) << ',' << int(result.error().stage) << ',' << nan << '\n';
preparations.flush();
if (result && !d::Finite(state.trialNormalizedResidual))
throw std::runtime_error("A numerical strategy probe produced a nonfinite prepared residual.");
return bool(result);
};
auto restore = [&]() {
if (!prepare(base, "restore_original", 0.))
throw std::runtime_error("A numerical strategy probe could not restore its frozen base.");
if (!haveOriginalResidual) {
originalResidual = state.trialNormalizedResidual;
haveOriginalResidual = true;
}
restorationError = std::max(restorationError, d::Difference(originalResidual, state.trialNormalizedResidual));
state.physicalInverse->Refresh();
};
auto finish = [&]() {
restore();
state.normalizedCorrection = savedCorrection;
state.physicalCorrection = savedPhysicalCorrection;
state.volumeDisplacementDirection = savedVolumeDirection;
const double acceptedStateChange = d::Difference(savedAcceptedState, state.acceptedNormalizedState);
const double acceptedResidualChange = d::Difference(savedAcceptedResidual, state.acceptedNormalizedResidual);
const double acceptedPhysicalChange = d::Difference(savedAcceptedPhysical, state.AcceptedPhysicalState());
summary << "maximum_original_restoration_error," << restorationError
<< "\naccepted_state_change," << acceptedStateChange
<< "\naccepted_residual_change," << acceptedResidualChange
<< "\naccepted_physical_change," << acceptedPhysicalChange
<< "\npreparations," << preparationCount << '\n';
summary.flush();
if (acceptedStateChange != 0. || acceptedResidualChange != 0. || acceptedPhysicalChange != 0.)
throw std::runtime_error("A numerical strategy probe changed an accepted state.");
};
auto recordInner = [&](const std::string &kind, double alpha, int index, const solver::LinearSolveReport &report) {
inner << kind << ',' << alpha << ',' << index << ',' << int(report.status) << ',' << report.Converged()
<< ',' << report.iterations << ',' << report.rightHandSideNorm << ',' << report.trueResidualNorm
<< ',' << report.relativeTrueResidualNorm << ','
<< (std::isfinite(report.rightHandSideNorm) && report.rightHandSideNorm >= 0.
? options.inner.ConvergenceThreshold(report.rightHandSideNorm) : nan)
<< ',' << report.operatorApplications
<< ',' << report.inversePreconditionerApplications << ',' << report.solveSeconds << '\n';
inner.flush();
};
try {
restore();
auto partition = solver::MakeGravityBlockPartition<Form>(state.Problem().GetManifest().layout());
solver::PreparedGravitySubsystem gravity(*state.normalizedOperator, *state.normalizedInverse, partition,
state.Problem().GetCommunicator(), solver::linear::FGMRES({options.restartLength, -1}));
solver::PreparedGravityCurvatureCorrection correction(gravity);
solver::GravityReducedOperator reduced(gravity, options.inner);
mfem::Vector baseAction(base.Size()), baseGravity(partition.GravityResidualSize());
mfem::Vector gravityAction(partition.GravityResidualSize()), gravityResidual(partition.GravityResidualSize());
mfem::Vector compactCorrection(partition.GravityValueSize());
mfem::Vector retainedBefore(partition.RetainedValueSize()), retainedAfter(partition.RetainedValueSize());
state.normalizedOperator->Mult(direction, baseAction);
partition.GatherGravityResiduals(originalResidual, baseGravity);
partition.GatherGravityResiduals(baseAction, gravityAction);
summary << "original_residual_norm," << originalResidual.Norml2()
<< "\noriginal_gravity_norm," << baseGravity.Norml2() << '\n';
for (const double alpha : {1., .1, .01, .001}) {
restore();
gravity.Refresh();
correction.CaptureBase(originalResidual, direction);
mfem::Vector candidate(base);
candidate.Add(alpha, direction);
const bool rawPrepared = prepare(candidate, "curvature_raw", alpha);
if (!rawPrepared) {
curvature << alpha << ",0,0,0," << originalResidual.Norml2()
<< ",nan,nan,nan,nan,nan,nan,nan,nan\n";
curvature.flush();
continue;
}
const mfem::Vector rawResidual(state.trialNormalizedResidual);
mfem::Vector prediction(originalResidual);
prediction.Add(alpha, baseAction);
partition.GatherGravityResiduals(rawResidual, gravityResidual);
gravityResidual -= baseGravity;
gravityResidual.Add(-alpha, gravityAction);
const double rawGravityRemainder = gravityResidual.Norml2();
state.physicalInverse->Refresh();
gravity.Refresh();
const auto report = correction.SolveCorrection(alpha, rawResidual, compactCorrection, options.inner);
recordInner("curvature", alpha, 0, report);
double correctedNorm = nan, correctedRemainder = nan, retainedChange = nan;
bool correctedPrepared = false;
if (report.Converged()) {
partition.GatherRetainedValues(candidate, retainedBefore);
partition.AddGravityValues(compactCorrection, candidate);
partition.GatherRetainedValues(candidate, retainedAfter);
retainedChange = d::Difference(retainedBefore, retainedAfter);
correctedPrepared = prepare(candidate, "curvature_corrected", alpha);
if (correctedPrepared) {
correctedNorm = state.trialNormalizedResidual.Norml2();
partition.GatherGravityResiduals(state.trialNormalizedResidual, gravityResidual);
gravityResidual -= baseGravity;
gravityResidual.Add(-alpha, gravityAction);
correctedRemainder = gravityResidual.Norml2();
}
}
curvature << alpha << ",1," << report.Converged() << ',' << correctedPrepared << ','
<< originalResidual.Norml2() << ',' << rawResidual.Norml2() << ',' << correctedNorm << ','
<< prediction.Norml2() << ',' << rawGravityRemainder << ',' << correctedRemainder << ','
<< compactCorrection.Norml2() << ',' << compactCorrection.Norml2() / (alpha * alpha) << ','
<< retainedChange << '\n';
curvature.flush();
}
// The residual used by a reduced nonlinear method is evaluated after
// gravity completion, so finite differences must follow that same manifold.
auto complete = [&](mfem::Vector &candidate, const std::string &kind, double alpha) {
partition.GatherRetainedValues(candidate, retainedBefore);
double initialNorm = nan, finalNorm = nan;
int completedCorrections = 0;
bool completed = false;
if (prepare(candidate, kind + "_initial", alpha)) {
for (int attempt = 0; attempt <= options.maximumCompletionCorrections; ++attempt) {
partition.GatherGravityResiduals(state.trialNormalizedResidual, gravityResidual);
finalNorm = gravityResidual.Norml2();
if (attempt == 0) initialNorm = finalNorm;
if (finalNorm <= options.gravityCompletionTolerance) {
completed = true;
break;
}
if (attempt == options.maximumCompletionCorrections) break;
state.physicalInverse->Refresh();
gravity.Refresh();
gravityResidual *= -1.;
compactCorrection = 0.;
const auto report = gravity.Solve(gravityResidual, compactCorrection, options.inner);
recordInner(kind, alpha, attempt, report);
if (!report.Converged()) break;
partition.AddGravityValues(compactCorrection, candidate);
++completedCorrections;
if (!prepare(candidate, kind + "_corrected", alpha)) {
finalNorm = nan;
break;
}
}
}
partition.GatherRetainedValues(candidate, retainedAfter);
const double retainedChange = d::Difference(retainedBefore, retainedAfter);
completions << kind << ',' << alpha << ',' << completed << ',' << completedCorrections << ','
<< initialNorm << ',' << finalNorm << ','
<< (completed ? state.trialNormalizedResidual.Norml2() : nan) << ',' << retainedChange << '\n';
completions.flush();
if (retainedChange != 0.)
throw std::runtime_error("Gravity completion changed retained coordinates.");
return completed;
};
restore();
mfem::Vector completedBase(base);
const bool baseCompleted = complete(completedBase, "reduced_base", 0.);
summary << "reduced_base_completed," << baseCompleted << '\n';
if (baseCompleted) {
state.physicalInverse->Refresh();
gravity.Refresh();
const mfem::Vector completedBaseResidual(state.trialNormalizedResidual);
mfem::Vector retainedDirection(partition.RetainedValueSize()), reducedAction(partition.RetainedResidualSize());
mfem::Vector liftedDirection(base.Size()), liftedAction(base.Size());
partition.GatherRetainedValues(direction, retainedDirection);
const auto actionReport = reduced.TryMult(retainedDirection, reducedAction);
recordInner("reduced_action", 0., 0, actionReport);
const auto liftReport = reduced.TryLiftTangent(retainedDirection, liftedDirection);
recordInner("reduced_lift", 0., 0, liftReport);
if (actionReport.Converged() && liftReport.Converged()) {
state.normalizedOperator->Mult(liftedDirection, liftedAction);
partition.GatherGravityResiduals(liftedAction, gravityResidual);
mfem::Vector retainedAction(partition.RetainedResidualSize());
partition.GatherRetainedResiduals(liftedAction, retainedAction);
summary << "reduced_action_norm," << reducedAction.Norml2()
<< "\nlifted_gravity_action_norm," << gravityResidual.Norml2()
<< "\nlifted_retained_action_difference," << d::Difference(retainedAction, reducedAction)
<< "\ncompleted_base_residual_norm," << completedBaseResidual.Norml2() << '\n';
for (const double alpha : {1.e-2, 1.e-3, 1.e-4}) {
std::array<mfem::Vector, 2> residuals{mfem::Vector(base.Size()), mfem::Vector(base.Size())};
std::array<bool, 2> completed{};
std::array<double, 2> gravityNorms{nan, nan};
for (int side = 0; side < 2; ++side) {
mfem::Vector candidate(completedBase);
candidate.Add(side == 0 ? alpha : -alpha, liftedDirection);
completed[side] = complete(candidate, side == 0 ? "reduced_plus" : "reduced_minus", alpha);
if (completed[side]) {
residuals[side] = state.trialNormalizedResidual;
partition.GatherGravityResiduals(residuals[side], gravityResidual);
gravityNorms[side] = gravityResidual.Norml2();
}
}
double quotientNorm = nan, error = nan, relativeError = nan, roundoffScale = nan;
if (completed[0] && completed[1]) {
mfem::Vector quotient(residuals[0]);
quotient -= residuals[1];
quotient /= 2. * alpha;
partition.GatherRetainedResiduals(quotient, retainedAction);
quotientNorm = retainedAction.Norml2();
error = d::Difference(retainedAction, reducedAction);
relativeError = error / std::max(reducedAction.Norml2(), std::numeric_limits<double>::min());
roundoffScale = 64. * std::numeric_limits<double>::epsilon() *
(1. + residuals[0].Norml2() + residuals[1].Norml2()) / (2. * alpha);
}
differences << alpha << ',' << completed[0] << ',' << completed[1] << ',' << reducedAction.Norml2()
<< ',' << quotientNorm << ',' << error << ',' << relativeError << ',' << gravityNorms[0]
<< ',' << gravityNorms[1] << ',' << roundoffScale << '\n';
differences.flush();
}
}
}
finish();
} catch (...) {
const auto failure = std::current_exception();
finish();
std::rethrow_exception(failure);
}
}
} // namespace experiment::plateau_diagnostics

View File

@@ -128,6 +128,7 @@ export namespace mean_field::solver {
mfem::Vector &output
) const {
RequireCurrent();
RequireInputSize(input, Width());
const auto &partition = GetPartition();
partition.InjectRetainedValues(input, m_fullDirection);
FullMult(m_fullDirection, m_fullAction);
@@ -165,6 +166,7 @@ export namespace mean_field::solver {
) const {
RequireCurrent();
const auto &partition = GetPartition();
RequireInputSize(fullRightHandSide, partition.RootResidualSize());
partition.GatherGravityResiduals(fullRightHandSide, m_gravityRightHandSide);
if (!SolveGravity()) {
return m_lastInnerSolve;
@@ -185,12 +187,11 @@ export namespace mean_field::solver {
) const {
RequireCurrent();
const auto &partition = GetPartition();
RequireInputSize(retainedCorrection, partition.RetainedValueSize());
RequireInputSize(fullRightHandSide, partition.RootResidualSize());
partition.InjectRetainedValues(retainedCorrection, m_fullDirection);
FullMult(m_fullDirection, m_fullAction);
m_fullAction *= -1.0;
if (fullRightHandSide.Size() != m_fullAction.Size()) {
throw std::invalid_argument("A lifted gravity correction requires a full residual right-hand side.");
}
m_fullAction += fullRightHandSide;
partition.GatherGravityResiduals(m_fullAction, m_gravityRightHandSide);
return FinishLift(fullCorrection);
@@ -202,6 +203,7 @@ export namespace mean_field::solver {
) const {
RequireCurrent();
const auto &partition = GetPartition();
RequireInputSize(retainedDirection, partition.RetainedValueSize());
partition.InjectRetainedValues(retainedDirection, m_fullDirection);
FullMult(m_fullDirection, m_fullAction);
partition.GatherGravityResiduals(m_fullAction, m_gravityRightHandSide);
@@ -210,12 +212,29 @@ export namespace mean_field::solver {
}
private:
[[nodiscard]] bool AllRanksSatisfy(const bool valid) const {
const int local = valid ? 1 : 0;
int global = 0;
if (MPI_Allreduce(&local, &global, 1, MPI_INT, MPI_MIN, GetCommunicator()) != MPI_SUCCESS) {
throw std::runtime_error("Reduced gravity could not verify its distributed application contract.");
}
return global != 0;
}
void RequireCurrent() const {
if (!IsCurrent()) {
// An early rank-local exception would strand the remaining ranks
// inside the next nested gravity solve.
if (!AllRanksSatisfy(IsCurrent())) {
throw std::logic_error("A reduced-gravity action requires the current prepared gravity subsystem.");
}
}
void RequireInputSize(const mfem::Vector &input, const int size) const {
if (!AllRanksSatisfy(input.Size() == size)) {
throw std::invalid_argument("A reduced-gravity action requires compatible input vector sizes.");
}
}
void FullMult(
const mfem::Vector &input,
mfem::Vector &output
@@ -302,13 +321,16 @@ export namespace mean_field::solver {
const mfem::Vector &input,
mfem::Vector &output
) const override {
if (!m_operation->IsCurrent()) {
if (!AllRanksSatisfy(m_operation->IsCurrent())) {
throw std::logic_error("The reduced-gravity preconditioner requires the current prepared subsystem.");
}
if (!AllRanksSatisfy(input.Size() == Width() && output.Size() == Height())) {
throw std::invalid_argument("The reduced-gravity inverse requires preallocated compatible vectors.");
}
const auto &partition = m_operation->GetPartition();
partition.InjectRetainedResiduals(input, m_fullRightHandSide);
m_fullInverse->Mult(m_fullRightHandSide, m_fullCorrection);
if (!m_operation->IsCurrent()) {
if (!AllRanksSatisfy(m_operation->IsCurrent())) {
throw std::logic_error("The full inverse changed the reduced-gravity preparation.");
}
partition.GatherRetainedValues(m_fullCorrection, output);
@@ -319,6 +341,15 @@ export namespace mean_field::solver {
}
private:
[[nodiscard]] bool AllRanksSatisfy(const bool valid) const {
const int local = valid ? 1 : 0;
int global = 0;
if (MPI_Allreduce(&local, &global, 1, MPI_INT, MPI_MIN, m_operation->GetCommunicator()) != MPI_SUCCESS) {
throw std::runtime_error("The reduced-gravity inverse could not verify distributed validity.");
}
return global != 0;
}
const GravityReducedOperator<Subsystem> *m_operation;
FullInverse *m_fullInverse;
mutable mfem::Vector m_fullRightHandSide;