Add prepared Schur gravity reduction and contract tests
This commit is contained in:
327
libmeanfield/interface/solver/gravity_reduction.cppm
Normal file
327
libmeanfield/interface/solver/gravity_reduction.cppm
Normal file
@@ -0,0 +1,327 @@
|
||||
module;
|
||||
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
export module mean_field:solver.gravity_reduction;
|
||||
|
||||
export import :solver.gravity_subsystem;
|
||||
export import :solver.linear_backend;
|
||||
|
||||
export namespace mean_field::solver {
|
||||
template <typename Candidate>
|
||||
concept PreparedGravityReductionSubsystem = requires(
|
||||
Candidate &subsystem,
|
||||
const Candidate &constantSubsystem,
|
||||
const mfem::Vector &rightHandSide,
|
||||
mfem::Vector &correction,
|
||||
const LinearSolveControl &control
|
||||
) {
|
||||
{ constantSubsystem.GetPartition() } -> std::same_as<const GravityBlockPartition &>;
|
||||
{ constantSubsystem.IsCurrent() } -> std::same_as<bool>;
|
||||
{ constantSubsystem.GetCommunicator() } -> std::same_as<MPI_Comm>;
|
||||
requires std::derived_from<
|
||||
std::remove_cvref_t<decltype(constantSubsystem.GetFullOperator())>, mfem::Operator>;
|
||||
{ subsystem.Solve(rightHandSide, correction, control) } -> std::same_as<LinearSolveReport>;
|
||||
};
|
||||
|
||||
/*
|
||||
* MFEM's void Mult boundary cannot return a failed inner solve. Keep that
|
||||
* failure distinguishable from a contract violation so an outer backend
|
||||
* can stop the numerical attempt without accepting an inaccurate Schur
|
||||
* action. The Try methods below retain the ordinary report-based path.
|
||||
*/
|
||||
class GravityReductionFailure final : public std::runtime_error {
|
||||
public:
|
||||
explicit GravityReductionFailure(LinearSolveReport report)
|
||||
: std::runtime_error("A reduced-gravity action failed its inner true-residual tolerance."),
|
||||
m_report(std::move(report)) {
|
||||
}
|
||||
|
||||
[[nodiscard]] const LinearSolveReport &GetReport() const noexcept {
|
||||
return m_report;
|
||||
}
|
||||
|
||||
private:
|
||||
LinearSolveReport m_report;
|
||||
};
|
||||
|
||||
/*
|
||||
* For J = [F_u F_z; G_u G_z], this applies F_u - F_z G_z^{-1} G_u.
|
||||
* Neither the full operator nor its preparation may change during an
|
||||
* application. All coordinates, including specification borders, come
|
||||
* from the same partition used by the gravity subsystem.
|
||||
*
|
||||
* A tolerance-stopped inner iteration approximates this linear operator;
|
||||
* flexible outer preconditioning does not make an inexact operator exact.
|
||||
* Its owner must verify a lifted correction with the full Jacobian and
|
||||
* choose an inner tolerance commensurate with the outer accuracy target.
|
||||
*/
|
||||
template <PreparedGravityReductionSubsystem Subsystem>
|
||||
class GravityReducedOperator final : public mfem::Operator {
|
||||
public:
|
||||
GravityReducedOperator(
|
||||
Subsystem &subsystem,
|
||||
LinearSolveControl innerControl
|
||||
)
|
||||
: mfem::Operator(
|
||||
subsystem.GetPartition().RetainedResidualSize(),
|
||||
subsystem.GetPartition().RetainedValueSize()
|
||||
),
|
||||
m_subsystem(std::addressof(subsystem)),
|
||||
m_innerControl(std::move(innerControl)),
|
||||
m_fullDirection(subsystem.GetPartition().RootValueSize()),
|
||||
m_fullGravityDirection(subsystem.GetPartition().RootValueSize()),
|
||||
m_fullAction(subsystem.GetPartition().RootResidualSize()),
|
||||
m_fullGravityAction(subsystem.GetPartition().RootResidualSize()),
|
||||
m_gravityRightHandSide(subsystem.GetPartition().GravityResidualSize()),
|
||||
m_gravityCorrection(subsystem.GetPartition().GravityValueSize()),
|
||||
m_retainedResult(subsystem.GetPartition().RetainedResidualSize()),
|
||||
m_retainedGravityAction(subsystem.GetPartition().RetainedResidualSize()) {
|
||||
m_innerControl.Validate();
|
||||
const auto &partition = GetPartition();
|
||||
const auto &operation = subsystem.GetFullOperator();
|
||||
if (operation.Width() != partition.RootValueSize() ||
|
||||
operation.Height() != partition.RootResidualSize()) {
|
||||
throw std::invalid_argument("The reduced-gravity partition does not match its full operator.");
|
||||
}
|
||||
}
|
||||
|
||||
GravityReducedOperator(const GravityReducedOperator &) = delete;
|
||||
GravityReducedOperator &operator=(const GravityReducedOperator &) = delete;
|
||||
GravityReducedOperator(GravityReducedOperator &&) = delete;
|
||||
GravityReducedOperator &operator=(GravityReducedOperator &&) = delete;
|
||||
|
||||
[[nodiscard]] const GravityBlockPartition &GetPartition() const noexcept {
|
||||
return m_subsystem->GetPartition();
|
||||
}
|
||||
|
||||
[[nodiscard]] const Subsystem &GetSubsystem() const noexcept {
|
||||
return *m_subsystem;
|
||||
}
|
||||
|
||||
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept {
|
||||
return m_subsystem->GetCommunicator();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsCurrent() const noexcept {
|
||||
return m_subsystem->IsCurrent();
|
||||
}
|
||||
|
||||
[[nodiscard]] const LinearSolveControl &GetInnerSolveControl() const noexcept {
|
||||
return m_innerControl;
|
||||
}
|
||||
|
||||
[[nodiscard]] const LinearSolveReport &GetLastInnerSolve() const noexcept {
|
||||
return m_lastInnerSolve;
|
||||
}
|
||||
|
||||
[[nodiscard]] LinearSolveReport TryMult(
|
||||
const mfem::Vector &input,
|
||||
mfem::Vector &output
|
||||
) const {
|
||||
RequireCurrent();
|
||||
const auto &partition = GetPartition();
|
||||
partition.InjectRetainedValues(input, m_fullDirection);
|
||||
FullMult(m_fullDirection, m_fullAction);
|
||||
partition.GatherGravityResiduals(m_fullAction, m_gravityRightHandSide);
|
||||
if (!SolveGravity()) {
|
||||
return m_lastInnerSolve;
|
||||
}
|
||||
partition.InjectGravityValues(m_gravityCorrection, m_fullGravityDirection);
|
||||
FullMult(m_fullGravityDirection, m_fullGravityAction);
|
||||
partition.GatherRetainedResiduals(m_fullAction, m_retainedResult);
|
||||
partition.GatherRetainedResiduals(m_fullGravityAction, m_retainedGravityAction);
|
||||
m_retainedResult -= m_retainedGravityAction;
|
||||
output = m_retainedResult;
|
||||
return m_lastInnerSolve;
|
||||
}
|
||||
|
||||
void Mult(
|
||||
const mfem::Vector &input,
|
||||
mfem::Vector &output
|
||||
) const override {
|
||||
const auto report = TryMult(input, output);
|
||||
if (!report.Converged()) {
|
||||
throw GravityReductionFailure(report);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Condensing a nonzero gravity residual preserves equivalence to the
|
||||
* monolithic Newton equations. Dropping it merely because gravity was
|
||||
* approximately completed would turn inner error into an outer bias.
|
||||
*/
|
||||
[[nodiscard]] LinearSolveReport TryCondenseRightHandSide(
|
||||
const mfem::Vector &fullRightHandSide,
|
||||
mfem::Vector &reducedRightHandSide
|
||||
) const {
|
||||
RequireCurrent();
|
||||
const auto &partition = GetPartition();
|
||||
partition.GatherGravityResiduals(fullRightHandSide, m_gravityRightHandSide);
|
||||
if (!SolveGravity()) {
|
||||
return m_lastInnerSolve;
|
||||
}
|
||||
partition.InjectGravityValues(m_gravityCorrection, m_fullGravityDirection);
|
||||
FullMult(m_fullGravityDirection, m_fullGravityAction);
|
||||
partition.GatherRetainedResiduals(fullRightHandSide, m_retainedResult);
|
||||
partition.GatherRetainedResiduals(m_fullGravityAction, m_retainedGravityAction);
|
||||
m_retainedResult -= m_retainedGravityAction;
|
||||
reducedRightHandSide = m_retainedResult;
|
||||
return m_lastInnerSolve;
|
||||
}
|
||||
|
||||
[[nodiscard]] LinearSolveReport TryLiftCorrection(
|
||||
const mfem::Vector &retainedCorrection,
|
||||
const mfem::Vector &fullRightHandSide,
|
||||
mfem::Vector &fullCorrection
|
||||
) const {
|
||||
RequireCurrent();
|
||||
const auto &partition = GetPartition();
|
||||
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);
|
||||
}
|
||||
|
||||
[[nodiscard]] LinearSolveReport TryLiftTangent(
|
||||
const mfem::Vector &retainedDirection,
|
||||
mfem::Vector &fullDirection
|
||||
) const {
|
||||
RequireCurrent();
|
||||
const auto &partition = GetPartition();
|
||||
partition.InjectRetainedValues(retainedDirection, m_fullDirection);
|
||||
FullMult(m_fullDirection, m_fullAction);
|
||||
partition.GatherGravityResiduals(m_fullAction, m_gravityRightHandSide);
|
||||
m_gravityRightHandSide *= -1.0;
|
||||
return FinishLift(fullDirection);
|
||||
}
|
||||
|
||||
private:
|
||||
void RequireCurrent() const {
|
||||
if (!IsCurrent()) {
|
||||
throw std::logic_error("A reduced-gravity action requires the current prepared gravity subsystem.");
|
||||
}
|
||||
}
|
||||
|
||||
void FullMult(
|
||||
const mfem::Vector &input,
|
||||
mfem::Vector &output
|
||||
) const {
|
||||
m_subsystem->GetFullOperator().Mult(input, output);
|
||||
RequireCurrent();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool SolveGravity() const {
|
||||
// A previous Schur action is not a suitable initial guess for an
|
||||
// unrelated Krylov direction and can make the action history dependent.
|
||||
m_gravityCorrection = 0.0;
|
||||
m_lastInnerSolve = m_subsystem->Solve(m_gravityRightHandSide, m_gravityCorrection, m_innerControl);
|
||||
RequireCurrent();
|
||||
if (m_lastInnerSolve.Converged()) {
|
||||
if (!std::isfinite(m_lastInnerSolve.rightHandSideNorm) ||
|
||||
!std::isfinite(m_lastInnerSolve.trueResidualNorm)) {
|
||||
m_lastInnerSolve.status = LinearSolveStatus::non_finite;
|
||||
} else if (m_lastInnerSolve.rightHandSideNorm < 0.0 || m_lastInnerSolve.trueResidualNorm < 0.0 ||
|
||||
m_lastInnerSolve.trueResidualNorm >
|
||||
m_innerControl.ConvergenceThreshold(m_lastInnerSolve.rightHandSideNorm)) {
|
||||
m_lastInnerSolve.status = LinearSolveStatus::backend_failure;
|
||||
}
|
||||
}
|
||||
return m_lastInnerSolve.Converged();
|
||||
}
|
||||
|
||||
[[nodiscard]] LinearSolveReport FinishLift(mfem::Vector &fullCorrection) const {
|
||||
if (!SolveGravity()) {
|
||||
return m_lastInnerSolve;
|
||||
}
|
||||
GetPartition().AddGravityValues(m_gravityCorrection, m_fullDirection);
|
||||
fullCorrection = m_fullDirection;
|
||||
return m_lastInnerSolve;
|
||||
}
|
||||
|
||||
Subsystem *m_subsystem;
|
||||
LinearSolveControl m_innerControl;
|
||||
mutable mfem::Vector m_fullDirection;
|
||||
mutable mfem::Vector m_fullGravityDirection;
|
||||
mutable mfem::Vector m_fullAction;
|
||||
mutable mfem::Vector m_fullGravityAction;
|
||||
mutable mfem::Vector m_gravityRightHandSide;
|
||||
mutable mfem::Vector m_gravityCorrection;
|
||||
mutable mfem::Vector m_retainedResult;
|
||||
mutable mfem::Vector m_retainedGravityAction;
|
||||
mutable LinearSolveReport m_lastInnerSolve;
|
||||
};
|
||||
|
||||
/*
|
||||
* The retained block of an exact full inverse equals the inverse Schur
|
||||
* complement. Restricting a supplied approximate inverse preserves that
|
||||
* relationship without assembling a second approximate physics operator.
|
||||
*/
|
||||
template <PreparedGravityReductionSubsystem Subsystem, typename FullInverse>
|
||||
requires std::derived_from<FullInverse, mfem::Solver> &&
|
||||
LinearPreconditionerApplicationContractAvailable<FullInverse>
|
||||
class GravityReducedPreconditioner final : public mfem::Solver {
|
||||
public:
|
||||
static constexpr auto applicationContract = linearPreconditionerApplicationContract<FullInverse>;
|
||||
|
||||
GravityReducedPreconditioner(
|
||||
const GravityReducedOperator<Subsystem> &operation,
|
||||
FullInverse &fullInverse
|
||||
)
|
||||
: mfem::Solver(operation.Width(), operation.Height(), false),
|
||||
m_operation(std::addressof(operation)),
|
||||
m_fullInverse(std::addressof(fullInverse)),
|
||||
m_fullRightHandSide(operation.GetPartition().RootResidualSize()),
|
||||
m_fullCorrection(operation.GetPartition().RootValueSize()) {
|
||||
if (fullInverse.Width() != m_fullRightHandSide.Size() ||
|
||||
fullInverse.Height() != m_fullCorrection.Size()) {
|
||||
throw std::invalid_argument("A reduced-gravity preconditioner requires a compatible full inverse.");
|
||||
}
|
||||
}
|
||||
|
||||
void SetOperator(const mfem::Operator &operation) override {
|
||||
if (std::addressof(operation) != m_operation) {
|
||||
throw std::invalid_argument("The reduced-gravity preconditioner cannot bind a different operator.");
|
||||
}
|
||||
}
|
||||
|
||||
void Mult(
|
||||
const mfem::Vector &input,
|
||||
mfem::Vector &output
|
||||
) const override {
|
||||
if (!m_operation->IsCurrent()) {
|
||||
throw std::logic_error("The reduced-gravity preconditioner requires the current prepared subsystem.");
|
||||
}
|
||||
const auto &partition = m_operation->GetPartition();
|
||||
partition.InjectRetainedResiduals(input, m_fullRightHandSide);
|
||||
m_fullInverse->Mult(m_fullRightHandSide, m_fullCorrection);
|
||||
if (!m_operation->IsCurrent()) {
|
||||
throw std::logic_error("The full inverse changed the reduced-gravity preparation.");
|
||||
}
|
||||
partition.GatherRetainedValues(m_fullCorrection, output);
|
||||
}
|
||||
|
||||
[[nodiscard]] const FullInverse &GetFullInverse() const noexcept {
|
||||
return *m_fullInverse;
|
||||
}
|
||||
|
||||
private:
|
||||
const GravityReducedOperator<Subsystem> *m_operation;
|
||||
FullInverse *m_fullInverse;
|
||||
mutable mfem::Vector m_fullRightHandSide;
|
||||
mutable mfem::Vector m_fullCorrection;
|
||||
};
|
||||
} // namespace mean_field::solver
|
||||
Reference in New Issue
Block a user