feat(newton): first newton solver implementation

This commit is contained in:
2026-09-08 06:36:39 -04:00
parent 76818f2f82
commit b3c04d507a
98 changed files with 20397 additions and 11040 deletions

View File

@@ -0,0 +1,868 @@
module;
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:solver.linear_backend;
export import :preconditioning.backend;
export namespace mean_field::solver {
enum class LinearSolveStatus : std::uint8_t {
converged,
maximum_iterations,
breakdown,
non_finite,
backend_failure
};
struct LinearSolveControl final {
double relativeTolerance{1.0e-8};
double absoluteTolerance{0.0};
int maximumIterations{100};
void Validate() const {
if (!std::isfinite(relativeTolerance) || relativeTolerance < 0.0) {
throw std::invalid_argument("A linear solve requires a finite, non-negative relative tolerance.");
}
if (!std::isfinite(absoluteTolerance) || absoluteTolerance < 0.0) {
throw std::invalid_argument("A linear solve requires a finite, non-negative absolute tolerance.");
}
if (maximumIterations <= 0) {
throw std::invalid_argument("A linear solve requires at least one permitted iteration.");
}
}
[[nodiscard]] double ConvergenceThreshold(const double globalRightHandSideNorm) const {
Validate();
if (!std::isfinite(globalRightHandSideNorm) || globalRightHandSideNorm < 0.0) {
throw std::invalid_argument("A linear solve requires a finite, non-negative right-hand-side norm.");
}
const double relativeThreshold = relativeTolerance * globalRightHandSideNorm;
if (!std::isfinite(relativeThreshold)) {
throw std::invalid_argument("The linear relative convergence threshold must be finite.");
}
return absoluteTolerance > relativeThreshold ? absoluteTolerance : relativeThreshold;
}
};
/*
* Numerical termination is data, not an exception. Implementations throw
* for invalid controls, configuration, dimensions, or violated lifetime
* contracts. All reported norms are communicator-global Euclidean norms.
* Solve uses the incoming correction as its initial guess and overwrites
* it with the final correction, so initialResidualNorm is ||b - A x_0||.
* Convergence remains relative to the right-hand side rather than the
* quality of a particular initial guess:
*
* ||b - A x|| <= max(absoluteTolerance,
* relativeTolerance * ||b||).
*
* For a zero right-hand side, relativeTrueResidualNorm is zero exactly
* when the true residual is zero and positive infinity otherwise. The
* true-residual fields are distinct from the backend's recurrence so
* callers never have to infer one from the other. This is deliberately
* fixed-size: recording a history is an optional backend concern whose
* storage must be owned and reserved by the prepared runtime, not allocated
* while Solve is active.
*/
struct LinearSolveReport final {
LinearSolveStatus status{LinearSolveStatus::backend_failure};
LinearSolveControl control{};
int iterations{0};
int restarts{0};
double rightHandSideNorm{0.0};
double initialResidualNorm{0.0};
double reportedResidualNorm{0.0};
double trueResidualNorm{0.0};
double relativeTrueResidualNorm{0.0};
// Includes MFEM's initial-guess residual application and the
// post-solve application used to verify the true residual.
std::uint64_t operatorApplications{0};
std::uint64_t inversePreconditionerApplications{0};
double solveSeconds{0.0};
// These totals include only completed applications. Operator time also
// includes the post-solve true-residual verification application.
double operatorSeconds{0.0};
double inversePreconditionerSeconds{0.0};
[[nodiscard]] bool Converged() const noexcept {
return status == LinearSolveStatus::converged;
}
};
struct LinearBackendConfigurationTag { };
template <typename Candidate>
concept LinearBackendConfiguration =
std::derived_from<std::remove_cvref_t<Candidate>, LinearBackendConfigurationTag> &&
std::move_constructible<std::remove_cvref_t<Candidate>> && requires {
requires std::same_as<
std::remove_cv_t<decltype(std::remove_cvref_t<Candidate>::supportedPreconditionerContract)>,
preconditioning::ApplicationContract>;
typename std::integral_constant<
preconditioning::ApplicationContract, std::remove_cvref_t<Candidate>::supportedPreconditionerContract>;
requires(
std::remove_cvref_t<Candidate>::supportedPreconditionerContract ==
preconditioning::ApplicationContract::stationary_linear ||
std::remove_cvref_t<Candidate>::supportedPreconditionerContract ==
preconditioning::ApplicationContract::flexible
);
};
} // namespace mean_field::solver
namespace mean_field::solver::detail {
template <typename Candidate>
concept StaticPreconditionerContractDeclared = requires { &std::remove_cvref_t<Candidate>::applicationContract; };
template <typename Candidate>
concept ExactStaticPreconditionerContract = requires {
requires std::same_as<
std::remove_cv_t<decltype(std::remove_cvref_t<Candidate>::applicationContract)>,
preconditioning::ApplicationContract>;
typename std::integral_constant<
preconditioning::ApplicationContract, std::remove_cvref_t<Candidate>::applicationContract>;
requires(
std::remove_cvref_t<Candidate>::applicationContract ==
preconditioning::ApplicationContract::stationary_linear ||
std::remove_cvref_t<Candidate>::applicationContract == preconditioning::ApplicationContract::flexible
);
};
template <typename Candidate, typename = void> struct StaticPreconditionerContract {
static constexpr bool declared = StaticPreconditionerContractDeclared<Candidate>;
static constexpr bool registered = false;
static constexpr preconditioning::ApplicationContract value =
preconditioning::ApplicationContract::stationary_linear;
};
template <typename Candidate>
struct StaticPreconditionerContract<Candidate, std::enable_if_t<ExactStaticPreconditionerContract<Candidate>>> {
static constexpr bool declared = true;
static constexpr auto value = std::remove_cvref_t<Candidate>::applicationContract;
static constexpr bool registered = true;
};
template <typename Candidate>
concept BackendPreconditionerContractDeclared = requires { typename std::remove_cvref_t<Candidate>::BackendType; };
template <typename Backend>
concept ExactRegisteredBackendPreconditionerContract = requires {
requires preconditioning::backend::Registered<std::remove_cvref_t<Backend>>;
requires std::same_as<
std::remove_cv_t<
decltype(preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract)>,
preconditioning::ApplicationContract>;
typename std::integral_constant<
preconditioning::ApplicationContract,
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract>;
requires(
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract ==
preconditioning::ApplicationContract::stationary_linear ||
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract ==
preconditioning::ApplicationContract::flexible
);
};
template <typename Candidate>
concept ExactBackendPreconditionerContract =
BackendPreconditionerContractDeclared<Candidate> &&
ExactRegisteredBackendPreconditionerContract<typename std::remove_cvref_t<Candidate>::BackendType>;
template <typename Candidate, typename = void> struct BackendPreconditionerContract {
static constexpr bool declared = BackendPreconditionerContractDeclared<Candidate>;
static constexpr bool registered = false;
static constexpr preconditioning::ApplicationContract value =
preconditioning::ApplicationContract::stationary_linear;
};
template <typename Candidate>
struct BackendPreconditionerContract<Candidate, std::enable_if_t<ExactBackendPreconditionerContract<Candidate>>> {
private:
using Backend = typename std::remove_cvref_t<Candidate>::BackendType;
public:
static constexpr bool declared = true;
static constexpr bool registered = true;
static constexpr preconditioning::ApplicationContract value =
preconditioning::backend::Traits<std::remove_cvref_t<Backend>>::applicationContract;
};
template <typename Candidate> struct DirectPreconditionerContractAudit final {
private:
using StaticContract = StaticPreconditionerContract<Candidate>;
using BackendContract = BackendPreconditionerContract<Candidate>;
public:
static constexpr bool declarationsValid = (!StaticContract::declared || StaticContract::registered) &&
(!BackendContract::declared || BackendContract::registered);
static constexpr bool sourcesAgree = !StaticContract::registered || !BackendContract::registered ||
StaticContract::value == BackendContract::value;
static constexpr bool registered =
declarationsValid && sourcesAgree && (StaticContract::registered || BackendContract::registered);
static constexpr preconditioning::ApplicationContract value = [] {
if constexpr (StaticContract::registered) {
return StaticContract::value;
} else if constexpr (BackendContract::registered) {
return BackendContract::value;
} else {
return preconditioning::ApplicationContract::stationary_linear;
}
}();
};
template <typename Candidate>
concept PhysicalInversePreconditionerContractDeclared =
requires(const std::remove_cvref_t<Candidate> &candidate) { candidate.GetPhysicalInverse(); };
template <typename Candidate>
using PhysicalInverseType =
std::remove_cvref_t<decltype(std::declval<const std::remove_cvref_t<Candidate> &>().GetPhysicalInverse())>;
template <typename Candidate>
concept ExactPhysicalInversePreconditionerContract =
PhysicalInversePreconditionerContractDeclared<Candidate> &&
DirectPreconditionerContractAudit<PhysicalInverseType<Candidate>>::registered;
template <typename Candidate, typename = void> struct PhysicalInversePreconditionerContract {
static constexpr bool declared = PhysicalInversePreconditionerContractDeclared<Candidate>;
static constexpr bool registered = false;
static constexpr preconditioning::ApplicationContract value =
preconditioning::ApplicationContract::stationary_linear;
};
template <typename Candidate>
struct PhysicalInversePreconditionerContract<
Candidate,
std::enable_if_t<ExactPhysicalInversePreconditionerContract<Candidate>>> {
using PhysicalInverse = PhysicalInverseType<Candidate>;
using ContractAudit = DirectPreconditionerContractAudit<PhysicalInverse>;
static constexpr bool declared = true;
static constexpr bool registered = true;
static constexpr preconditioning::ApplicationContract value = ContractAudit::value;
};
template <typename Candidate> struct LinearPreconditionerContractAudit final {
private:
using StaticContract = StaticPreconditionerContract<Candidate>;
using BackendContract = BackendPreconditionerContract<Candidate>;
using PhysicalContract = PhysicalInversePreconditionerContract<Candidate>;
public:
static constexpr bool declarationsValid = (!StaticContract::declared || StaticContract::registered) &&
(!BackendContract::declared || BackendContract::registered) &&
(!PhysicalContract::declared || PhysicalContract::registered);
static constexpr bool sourcesAgree = (!StaticContract::registered || !BackendContract::registered ||
StaticContract::value == BackendContract::value) &&
(!StaticContract::registered || !PhysicalContract::registered ||
StaticContract::value == PhysicalContract::value) &&
(!BackendContract::registered || !PhysicalContract::registered ||
BackendContract::value == PhysicalContract::value);
static constexpr bool registered =
declarationsValid && sourcesAgree &&
(StaticContract::registered || BackendContract::registered || PhysicalContract::registered);
static constexpr preconditioning::ApplicationContract value = [] {
if constexpr (StaticContract::registered) {
return StaticContract::value;
} else if constexpr (BackendContract::registered) {
return BackendContract::value;
} else if constexpr (PhysicalContract::registered) {
return PhysicalContract::value;
} else {
return preconditioning::ApplicationContract::stationary_linear;
}
}();
};
} // namespace mean_field::solver::detail
export namespace mean_field::solver {
template <typename Candidate>
concept LinearPreconditionerApplicationContractAvailable =
detail::LinearPreconditionerContractAudit<std::remove_cvref_t<Candidate>>::registered;
template <LinearPreconditionerApplicationContractAvailable Candidate>
inline constexpr preconditioning::ApplicationContract linearPreconditionerApplicationContract =
detail::LinearPreconditionerContractAudit<std::remove_cvref_t<Candidate>>::value;
template <typename Configuration, typename Preconditioner>
concept LinearBackendPreconditionerCompatible =
LinearBackendConfiguration<Configuration> && LinearPreconditionerApplicationContractAvailable<Preconditioner> &&
(std::remove_cvref_t<Configuration>::supportedPreconditionerContract ==
preconditioning::ApplicationContract::flexible ||
linearPreconditionerApplicationContract<std::remove_cvref_t<Preconditioner>> ==
preconditioning::ApplicationContract::stationary_linear);
/*
* A prepared backend is bound once to the exact operator and inverse that
* its owner keeps at stable addresses and identifies the communicator on
* which it operates. The communicator supplied to preparation is borrowed;
* a backend may retain it or own a congruent duplicate. The handle returned
* by GetCommunicator is borrowed from the backend and must not be freed by
* the caller. Every prepared backend must be destroyed before MPI_Finalize.
* It owns its numerical workspaces; neither copyability nor movability is
* required. Solve treats a caller-provided, correctly sized correction
* vector as its initial guess and overwrites it with the final correction.
*/
template <typename Candidate, typename Operator, typename Preconditioner>
concept PreparedLinearBackendFor = std::derived_from<std::remove_cvref_t<Operator>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver> &&
std::destructible<std::remove_cvref_t<Candidate>> &&
requires(
std::remove_cvref_t<Candidate> &prepared,
const std::remove_cvref_t<Candidate> &constantPrepared,
const mfem::Vector &rightHandSide,
mfem::Vector &correction,
const LinearSolveControl &control
) {
{
constantPrepared.GetOperator()
} -> std::same_as<const std::remove_cvref_t<Operator> &>;
{
constantPrepared.GetPreconditioner()
} -> std::same_as<const std::remove_cvref_t<Preconditioner> &>;
{ constantPrepared.GetCommunicator() } -> std::same_as<MPI_Comm>;
{ constantPrepared.IsReady() } -> std::same_as<bool>;
{ constantPrepared.RightHandSideSize() } -> std::same_as<int>;
{ constantPrepared.CorrectionSize() } -> std::same_as<int>;
{
prepared.Solve(rightHandSide, correction, control)
} -> std::same_as<LinearSolveReport>;
};
/*
* `prepareLinearBackend` is intentionally unqualified in this detection
* boundary. A third-party configuration supplies its overload beside the
* configuration type and ADL discovers it without a library registry.
*/
template <typename Configuration, typename Operator, typename Preconditioner>
concept LinearBackendRuntimeAvailableFor =
LinearBackendPreconditionerCompatible<Configuration, Preconditioner> &&
std::derived_from<std::remove_cvref_t<Operator>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver> &&
requires(
std::remove_cvref_t<Configuration> configuration,
const std::remove_cvref_t<Operator> &operation,
std::remove_cvref_t<Preconditioner> &preconditioner,
MPI_Comm communicator
) {
requires std::same_as<
decltype(prepareLinearBackend(std::move(configuration), operation, preconditioner, communicator)),
std::remove_cvref_t<
decltype(prepareLinearBackend(std::move(configuration), operation, preconditioner, communicator))>>;
{
prepareLinearBackend(std::move(configuration), operation, preconditioner, communicator)
} -> PreparedLinearBackendFor<std::remove_cvref_t<Operator>, std::remove_cvref_t<Preconditioner>>;
};
template <LinearBackendConfiguration Configuration, typename Operator, typename Preconditioner>
requires LinearBackendRuntimeAvailableFor<Configuration, Operator, Preconditioner>
using PreparedLinearBackendType = std::remove_cvref_t<decltype(prepareLinearBackend(
std::declval<std::remove_cvref_t<Configuration> &&>(),
std::declval<const std::remove_cvref_t<Operator> &>(),
std::declval<std::remove_cvref_t<Preconditioner> &>(),
std::declval<MPI_Comm>()
))>;
} // namespace mean_field::solver
export namespace mean_field::solver::linear {
struct FGMRESOptions final {
int restartLength{50};
int printLevel{-1};
void Validate() const {
if (restartLength <= 0) {
throw std::invalid_argument("MFEM FGMRES requires a positive restart length.");
}
if (printLevel < -1 || printLevel > 3) {
throw std::invalid_argument("MFEM FGMRES print level must be between -1 and 3.");
}
}
};
class FGMRES final : public LinearBackendConfigurationTag {
public:
static constexpr preconditioning::ApplicationContract supportedPreconditionerContract =
preconditioning::ApplicationContract::flexible;
FGMRES() = default;
explicit FGMRES(FGMRESOptions options) : m_options(std::move(options)) {
m_options.Validate();
}
[[nodiscard]] const FGMRESOptions &GetOptions() const noexcept {
return m_options;
}
private:
FGMRESOptions m_options{};
};
namespace detail {
template <typename Operation>
requires std::derived_from<std::remove_cvref_t<Operation>, mfem::Operator>
class CountedOperator final : public mfem::Operator {
public:
explicit CountedOperator(const Operation &operation)
: mfem::Operator(
operation.Height(),
operation.Width()
),
m_operation(std::addressof(operation)) {
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
const auto start = std::chrono::steady_clock::now();
m_operation->Mult(input, output);
m_seconds += std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
++m_applications;
}
void Reset() const noexcept {
m_applications = 0;
m_seconds = 0.0;
}
[[nodiscard]] std::uint64_t Applications() const noexcept {
return m_applications;
}
[[nodiscard]] double Seconds() const noexcept {
return m_seconds;
}
private:
const Operation *m_operation;
mutable std::uint64_t m_applications{0};
mutable double m_seconds{0.0};
};
template <typename Operation, typename Preconditioner>
requires std::derived_from<std::remove_cvref_t<Operation>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver>
class CountedPreconditioner final : public mfem::Solver {
public:
CountedPreconditioner(
const Operation &operation,
Preconditioner &preconditioner,
const CountedOperator<Operation> &countedOperation
)
: mfem::Solver(
preconditioner.Height(),
preconditioner.Width(),
false
),
m_operation(std::addressof(operation)),
m_preconditioner(std::addressof(preconditioner)),
m_countedOperation(std::addressof(countedOperation)) {
m_preconditioner->iterative_mode = false;
}
void SetOperator(const mfem::Operator &operation) override {
if (std::addressof(operation) != m_countedOperation) {
throw std::invalid_argument("The MFEM FGMRES preconditioner received an unexpected operator.");
}
m_preconditioner->SetOperator(*m_operation);
if (m_preconditioner->Height() != Height() || m_preconditioner->Width() != Width()) {
throw std::invalid_argument(
"The MFEM FGMRES preconditioner changed dimensions while binding its operator."
);
}
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
const auto start = std::chrono::steady_clock::now();
m_preconditioner->Mult(input, output);
m_seconds += std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
++m_applications;
}
void Reset() const noexcept {
m_applications = 0;
m_seconds = 0.0;
}
[[nodiscard]] std::uint64_t Applications() const noexcept {
return m_applications;
}
[[nodiscard]] double Seconds() const noexcept {
return m_seconds;
}
private:
const Operation *m_operation;
Preconditioner *m_preconditioner;
const CountedOperator<Operation> *m_countedOperation;
mutable std::uint64_t m_applications{0};
mutable double m_seconds{0.0};
};
[[nodiscard]] inline bool MpiIsUsable() noexcept {
int initialized = 0;
int finalized = 0;
return MPI_Initialized(&initialized) == MPI_SUCCESS && initialized != 0 &&
MPI_Finalized(&finalized) == MPI_SUCCESS && finalized == 0;
}
[[nodiscard]] inline bool AllRanksAgree(
const bool localValue,
const MPI_Comm communicator
) {
int local = localValue ? 1 : 0;
int global = 0;
if (MPI_Allreduce(&local, &global, 1, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not perform a communicator-wide validity check.");
}
return global != 0;
}
inline void RequireCollectivelyIdenticalConfiguration(
const FGMRESOptions &options,
const LinearSolveControl &control,
const MPI_Comm communicator
) {
const std::array<double, 2> localRealValues{control.relativeTolerance, control.absoluteTolerance};
std::array<double, 2> minimumRealValues{};
std::array<double, 2> maximumRealValues{};
const std::array<int, 3> localIntegerValues{
control.maximumIterations, options.restartLength, options.printLevel
};
std::array<int, 3> minimumIntegerValues{};
std::array<int, 3> maximumIntegerValues{};
if (MPI_Allreduce(
localRealValues.data(), minimumRealValues.data(), static_cast<int>(localRealValues.size()),
MPI_DOUBLE, MPI_MIN, communicator
) != MPI_SUCCESS ||
MPI_Allreduce(
localRealValues.data(), maximumRealValues.data(), static_cast<int>(localRealValues.size()),
MPI_DOUBLE, MPI_MAX, communicator
) != MPI_SUCCESS ||
MPI_Allreduce(
localIntegerValues.data(), minimumIntegerValues.data(), static_cast<int>(localIntegerValues.size()),
MPI_INT, MPI_MIN, communicator
) != MPI_SUCCESS ||
MPI_Allreduce(
localIntegerValues.data(), maximumIntegerValues.data(), static_cast<int>(localIntegerValues.size()),
MPI_INT, MPI_MAX, communicator
) != MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not validate its distributed configuration.");
}
if (minimumRealValues != maximumRealValues || minimumIntegerValues != maximumIntegerValues) {
throw std::invalid_argument(
"MFEM FGMRES requires identical options and solve controls on every communicator rank."
);
}
}
[[nodiscard]] inline bool LocallyFinite(const mfem::Vector &values) {
for (int index = 0; index < values.Size(); ++index) {
if (!std::isfinite(values(index))) {
return false;
}
}
return true;
}
[[nodiscard]] inline double GlobalNorm(
const mfem::Vector &values,
const MPI_Comm communicator
) {
const double localNorm = values.Norml2();
const double localNormSquared = localNorm * localNorm;
double globalNormSquared = 0.0;
if (MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, communicator) !=
MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not reduce a global vector norm.");
}
if (!std::isfinite(globalNormSquared) || globalNormSquared < 0.0) {
return std::numeric_limits<double>::quiet_NaN();
}
return std::sqrt(globalNormSquared);
}
[[nodiscard]] inline double MaximumRankValue(
const double localValue,
const MPI_Comm communicator
) {
double maximumValue = 0.0;
if (MPI_Allreduce(&localValue, &maximumValue, 1, MPI_DOUBLE, MPI_MAX, communicator) != MPI_SUCCESS) {
throw std::runtime_error("MFEM FGMRES could not reduce a communicator-wide timing measurement.");
}
return maximumValue;
}
template <typename Candidate> [[nodiscard]] bool RuntimeDependencyIsCurrent(const Candidate &candidate) {
if constexpr (requires {
{ candidate.IsCurrent() } -> std::same_as<bool>;
}) {
return candidate.IsCurrent();
} else if constexpr (requires {
{ candidate.IsPrepared() } -> std::same_as<bool>;
}) {
return candidate.IsPrepared();
} else {
return true;
}
}
template <typename Operation, typename Preconditioner>
requires std::derived_from<std::remove_cvref_t<Operation>, mfem::Operator> &&
std::derived_from<std::remove_cvref_t<Preconditioner>, mfem::Solver>
class PreparedFGMRES final {
private:
using Clock = std::chrono::steady_clock;
public:
PreparedFGMRES(
FGMRESOptions options,
const Operation &operation,
Preconditioner &preconditioner,
const MPI_Comm communicator
)
: m_options(std::move(options)),
m_operation(std::addressof(operation)),
m_preconditioner(std::addressof(preconditioner)),
m_communicator(communicator),
m_countedOperation(operation),
m_countedPreconditioner(
operation,
preconditioner,
m_countedOperation
),
m_solver(communicator),
m_rightHandSide(operation.Height()),
m_operationAction(operation.Height()),
m_trueResidual(operation.Height()) {
m_options.Validate();
if (!MpiIsUsable()) {
throw std::logic_error("MFEM FGMRES requires initialized MPI that has not been finalized.");
}
if (m_communicator == MPI_COMM_NULL) {
throw std::invalid_argument("MFEM FGMRES requires a non-null MPI communicator.");
}
if (operation.Height() <= 0 || operation.Width() <= 0 || operation.Height() != operation.Width() ||
preconditioner.Height() != operation.Width() || preconditioner.Width() != operation.Height()) {
throw std::invalid_argument(
"MFEM FGMRES requires compatible square operator and preconditioner dimensions."
);
}
m_rightHandSide = 0.0;
m_operationAction = 0.0;
m_trueResidual = 0.0;
m_solver.SetPreconditioner(m_countedPreconditioner);
m_solver.SetOperator(m_countedOperation);
m_solver.SetKDim(m_options.restartLength);
m_solver.SetPrintLevel(m_options.printLevel);
m_solver.iterative_mode = true;
}
PreparedFGMRES(const PreparedFGMRES &) = delete;
PreparedFGMRES &operator=(const PreparedFGMRES &) = delete;
PreparedFGMRES(PreparedFGMRES &&) = delete;
PreparedFGMRES &operator=(PreparedFGMRES &&) = delete;
[[nodiscard]] const Operation &GetOperator() const noexcept {
return *m_operation;
}
[[nodiscard]] const Preconditioner &GetPreconditioner() const noexcept {
return *m_preconditioner;
}
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept {
return m_communicator;
}
[[nodiscard]] bool IsReady() const {
return m_operation != nullptr && m_preconditioner != nullptr && m_communicator != MPI_COMM_NULL &&
m_operation->Height() == m_operation->Width() &&
m_preconditioner->Height() == m_operation->Width() &&
m_preconditioner->Width() == m_operation->Height() &&
m_rightHandSide.Size() == m_operation->Height() &&
m_operationAction.Size() == m_operation->Height() &&
m_trueResidual.Size() == m_operation->Height() && RuntimeDependencyIsCurrent(*m_operation) &&
RuntimeDependencyIsCurrent(*m_preconditioner);
}
[[nodiscard]] int RightHandSideSize() const noexcept {
return m_operation->Height();
}
[[nodiscard]] int CorrectionSize() const noexcept {
return m_operation->Width();
}
[[nodiscard]] LinearSolveReport Solve(
const mfem::Vector &rightHandSide,
mfem::Vector &correction,
const LinearSolveControl &control
) {
bool localConfigurationIsValid = true;
try {
m_options.Validate();
control.Validate();
} catch (const std::invalid_argument &) {
localConfigurationIsValid = false;
}
if (!AllRanksAgree(localConfigurationIsValid, m_communicator)) {
throw std::invalid_argument(
"MFEM FGMRES requires valid options and solve controls on every communicator rank."
);
}
if (!localConfigurationIsValid) {
throw std::invalid_argument("MFEM FGMRES received invalid options or solve controls.");
}
RequireCollectivelyIdenticalConfiguration(m_options, control, m_communicator);
if (!AllRanksAgree(IsReady(), m_communicator)) {
throw std::logic_error(
"MFEM FGMRES requires a complete, current prepared runtime on every communicator rank."
);
}
if (!AllRanksAgree(
rightHandSide.Size() == RightHandSideSize() && correction.Size() == CorrectionSize(),
m_communicator
)) {
throw std::invalid_argument(
"MFEM FGMRES received incompatible linear-system vectors on at least one rank."
);
}
if (!AllRanksAgree(LocallyFinite(rightHandSide), m_communicator) ||
!AllRanksAgree(LocallyFinite(correction), m_communicator)) {
throw std::invalid_argument(
"MFEM FGMRES requires finite right-hand side and initial-guess values."
);
}
m_rightHandSide = rightHandSide;
const double rightHandSideNorm = GlobalNorm(m_rightHandSide, m_communicator);
const double threshold = control.ConvergenceThreshold(rightHandSideNorm);
m_countedOperation.Reset();
m_countedPreconditioner.Reset();
m_solver.SetRelTol(0.0);
m_solver.SetAbsTol(threshold);
m_solver.SetMaxIter(control.maximumIterations);
const Clock::time_point start = Clock::now();
m_solver.Mult(m_rightHandSide, correction);
const double solveSeconds =
MaximumRankValue(std::chrono::duration<double>(Clock::now() - start).count(), m_communicator);
const bool correctionIsFinite = AllRanksAgree(LocallyFinite(correction), m_communicator);
double trueResidualNorm = std::numeric_limits<double>::quiet_NaN();
if (correctionIsFinite) {
m_countedOperation.Mult(correction, m_operationAction);
m_trueResidual = m_rightHandSide;
m_trueResidual -= m_operationAction;
if (AllRanksAgree(LocallyFinite(m_trueResidual), m_communicator)) {
trueResidualNorm = GlobalNorm(m_trueResidual, m_communicator);
}
}
const double initialResidualNorm = m_solver.GetInitialNorm();
const double reportedResidualNorm = m_solver.GetFinalNorm();
const std::uint64_t krylovIterationCount = m_countedPreconditioner.Applications();
if (krylovIterationCount > static_cast<std::uint64_t>(std::numeric_limits<int>::max())) {
throw std::overflow_error("MFEM FGMRES reported more Krylov iterations than can be represented.");
}
const int iterations = static_cast<int>(krylovIterationCount);
// A restart is an additional Krylov cycle entered after the
// initial cycle, not the residual check at a cycle boundary.
const int restarts = iterations > 0 ? (iterations - 1) / m_options.restartLength : 0;
const bool numericalValuesAreFinite = correctionIsFinite && std::isfinite(initialResidualNorm) &&
std::isfinite(reportedResidualNorm) &&
std::isfinite(trueResidualNorm);
LinearSolveStatus status = LinearSolveStatus::backend_failure;
if (!numericalValuesAreFinite) {
status = LinearSolveStatus::non_finite;
} else if (trueResidualNorm <= threshold) {
status = LinearSolveStatus::converged;
} else if (!m_solver.GetConverged() && iterations >= control.maximumIterations) {
status = LinearSolveStatus::maximum_iterations;
}
const double relativeTrueResidualNorm =
rightHandSideNorm > 0.0 ? trueResidualNorm / rightHandSideNorm
: (trueResidualNorm == 0.0 ? 0.0 : std::numeric_limits<double>::infinity());
const double operatorSeconds = MaximumRankValue(m_countedOperation.Seconds(), m_communicator);
const double inversePreconditionerSeconds =
MaximumRankValue(m_countedPreconditioner.Seconds(), m_communicator);
return {
.status = status,
.control = control,
.iterations = iterations,
.restarts = restarts,
.rightHandSideNorm = rightHandSideNorm,
.initialResidualNorm = initialResidualNorm,
.reportedResidualNorm = reportedResidualNorm,
.trueResidualNorm = trueResidualNorm,
.relativeTrueResidualNorm = relativeTrueResidualNorm,
.operatorApplications = m_countedOperation.Applications(),
.inversePreconditionerApplications = m_countedPreconditioner.Applications(),
.solveSeconds = solveSeconds,
.operatorSeconds = operatorSeconds,
.inversePreconditionerSeconds = inversePreconditionerSeconds
};
}
private:
FGMRESOptions m_options;
const Operation *m_operation;
Preconditioner *m_preconditioner;
MPI_Comm m_communicator;
CountedOperator<Operation> m_countedOperation;
CountedPreconditioner<Operation, Preconditioner> m_countedPreconditioner;
mfem::FGMRESSolver m_solver;
mfem::Vector m_rightHandSide;
mfem::Vector m_operationAction;
mfem::Vector m_trueResidual;
};
} // namespace detail
template <
typename Operation,
typename Preconditioner>
requires std::derived_from<
std::remove_cvref_t<Operation>,
mfem::Operator> &&
std::derived_from<
std::remove_cvref_t<Preconditioner>,
mfem::Solver>
[[nodiscard]] auto prepareLinearBackend(
FGMRES configuration,
const Operation &operation,
Preconditioner &preconditioner,
const MPI_Comm communicator
) {
return detail::PreparedFGMRES<Operation, Preconditioner>{
configuration.GetOptions(), operation, preconditioner, communicator
};
}
} // namespace mean_field::solver::linear

View File

@@ -0,0 +1,579 @@
module;
#include <cmath>
#include <concepts>
#include <cstdint>
#include <exception>
#include <functional>
#include <optional>
#include <span>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:solver.newton;
export import :solver.linear_backend;
export namespace mean_field::solver::nonlinear {
/*
* Backtracking counts the full Newton trial as its first trial. A
* contraction is applied only after that candidate has been rejected.
*/
struct BacktrackingOptions final {
double initialStepLength{1.0};
double contractionFactor{0.5};
double fractionToBoundarySafety{0.9};
double sufficientDecrease{1.0e-4};
double minimumStepLength{1.0e-8};
int maximumTrials{20};
void Validate() const {
if (!std::isfinite(initialStepLength) || initialStepLength <= 0.0) {
throw std::invalid_argument("Newton backtracking requires a finite, positive initial step length.");
}
if (!std::isfinite(contractionFactor) || contractionFactor <= 0.0 || contractionFactor >= 1.0) {
throw std::invalid_argument(
"Newton backtracking requires a finite contraction factor strictly between zero and one."
);
}
if (!std::isfinite(fractionToBoundarySafety) || fractionToBoundarySafety <= 0.0 ||
fractionToBoundarySafety >= 1.0) {
throw std::invalid_argument(
"Newton backtracking requires a finite fraction-to-boundary safety factor strictly between zero "
"and one."
);
}
if (!std::isfinite(sufficientDecrease) || sufficientDecrease <= 0.0 || sufficientDecrease >= 1.0) {
throw std::invalid_argument(
"Newton backtracking requires a finite sufficient-decrease factor strictly between zero and one."
);
}
if (!std::isfinite(minimumStepLength) || minimumStepLength <= 0.0 ||
minimumStepLength > initialStepLength) {
throw std::invalid_argument(
"Newton backtracking requires a finite, positive minimum step no larger than the initial step."
);
}
if (maximumTrials <= 0) {
throw std::invalid_argument("Newton backtracking requires at least one permitted trial.");
}
}
};
struct NewtonOptions final {
double relativeTolerance{1.0e-8};
double absoluteTolerance{0.0};
int maximumIterations{50};
LinearSolveControl linearSolve{};
BacktrackingOptions backtracking{};
void Validate() const {
if (!std::isfinite(relativeTolerance) || relativeTolerance < 0.0) {
throw std::invalid_argument("A Newton solve requires a finite, non-negative relative tolerance.");
}
if (!std::isfinite(absoluteTolerance) || absoluteTolerance < 0.0) {
throw std::invalid_argument("A Newton solve requires a finite, non-negative absolute tolerance.");
}
if (maximumIterations <= 0) {
throw std::invalid_argument("A Newton solve requires at least one permitted iteration.");
}
linearSolve.Validate();
backtracking.Validate();
}
[[nodiscard]] double ConvergenceThreshold(const double initialResidualNorm) const {
Validate();
if (!std::isfinite(initialResidualNorm) || initialResidualNorm < 0.0) {
throw std::invalid_argument(
"A Newton convergence threshold requires a finite, non-negative initial residual norm."
);
}
const double relativeThreshold = relativeTolerance * initialResidualNorm;
if (!std::isfinite(relativeThreshold)) {
throw std::invalid_argument("The Newton relative convergence threshold must be finite.");
}
return absoluteTolerance > relativeThreshold ? absoluteTolerance : relativeThreshold;
}
};
/*
* The MVP globalization merit is phi(x) = 0.5 ||F_normalized(x)||^2.
* A metric customization receives the solve communicator and must return
* communicator-consistent values or throw collectively. The Newton engine
* reduces every predicate that drives control flow, but it cannot make a
* rank-local exception inside an arbitrary callback collective-safe.
*/
struct NormalizedResidualMetric final { };
struct MetricEvaluation final {
double residualNorm{0.0};
double merit{0.0};
};
[[nodiscard]] inline MetricEvaluation getMetric(
const NormalizedResidualMetric &,
const mfem::Vector &normalizedResidual,
const MPI_Comm communicator
) {
if (communicator == MPI_COMM_NULL) {
throw std::invalid_argument("A nonlinear metric requires a valid communicator.");
}
double localSquaredNorm = 0.0;
for (int index = 0; index < normalizedResidual.Size(); ++index) {
const double value = static_cast<double>(normalizedResidual(index));
localSquaredNorm += value * value;
}
double globalSquaredNorm = 0.0;
if (MPI_Allreduce(&localSquaredNorm, &globalSquaredNorm, 1, MPI_DOUBLE, MPI_SUM, communicator) != MPI_SUCCESS) {
throw std::runtime_error("The nonlinear metric could not reduce the normalized residual norm.");
}
const double residualNorm = std::sqrt(globalSquaredNorm);
return {.residualNorm = residualNorm, .merit = 0.5 * residualNorm * residualNorm};
}
template <typename Metric = NormalizedResidualMetric>
requires std::move_constructible<std::remove_cvref_t<Metric>>
class Newton final {
public:
using MetricType = std::remove_cvref_t<Metric>;
Newton()
requires std::default_initializable<MetricType>
: Newton(
NewtonOptions{},
MetricType{}
) {
}
explicit Newton(NewtonOptions options)
requires std::default_initializable<MetricType>
: Newton(
std::move(options),
MetricType{}
) {
}
Newton(
NewtonOptions options,
MetricType metric
)
: m_options(std::move(options)),
m_metric(std::move(metric)) {
m_options.Validate();
}
[[nodiscard]] const NewtonOptions &options() const noexcept {
return m_options;
}
[[nodiscard]] const MetricType &metric() const noexcept {
return m_metric;
}
private:
NewtonOptions m_options;
[[no_unique_address]] MetricType m_metric;
};
Newton() -> Newton<NormalizedResidualMetric>;
Newton(NewtonOptions) -> Newton<NormalizedResidualMetric>;
template <typename Metric>
Newton(
NewtonOptions,
Metric
) -> Newton<std::remove_cvref_t<Metric>>;
template <typename Candidate> struct IsNewtonConfiguration : std::false_type { };
template <typename Metric> struct IsNewtonConfiguration<Newton<Metric>> : std::true_type { };
template <typename Candidate>
concept NewtonConfiguration = IsNewtonConfiguration<std::remove_cvref_t<Candidate>>::value;
enum class IterationDisposition : std::uint8_t {
unspecified,
accepted,
converged,
inadmissible_state,
non_finite_state,
non_finite_residual,
linear_solve_failure,
globalization_failure,
stagnation,
iteration_limit
};
/*
* Event spans borrow solver workspaces and are valid only for the duration
* of the callback. Copy values that must outlive the callback.
*/
struct BeforeIteration final {
int iteration{0};
double initialResidualNorm{0.0};
double residualNorm{0.0};
double relativeResidualNorm{0.0};
double merit{0.0};
MPI_Comm communicator{MPI_COMM_NULL};
std::span<const mfem::real_t> physicalState{};
std::span<const mfem::real_t> normalizedState{};
std::span<const mfem::real_t> normalizedResidual{};
};
enum class LineSearchTrialDisposition : std::uint8_t {
accepted,
inadmissible_state,
non_finite_state,
non_finite_residual,
insufficient_decrease
};
struct AfterLineSearchTrial final {
int iteration{0};
int trial{0};
double stepLength{0.0};
LineSearchTrialDisposition disposition{LineSearchTrialDisposition::insufficient_decrease};
std::string_view rejectionSource{};
std::optional<MetricEvaluation> metric{};
std::optional<double> minimumJacobianDeterminant{};
double preparationSeconds{0.0};
double metricSeconds{0.0};
MPI_Comm communicator{MPI_COMM_NULL};
std::span<const mfem::real_t> candidatePhysicalState{};
std::span<const mfem::real_t> candidateNormalizedState{};
std::span<const mfem::real_t> candidateNormalizedResidual{};
};
struct AfterIteration final {
int iteration{0};
IterationDisposition disposition{IterationDisposition::unspecified};
bool stepAccepted{false};
double acceptedStepLength{0.0};
int lineSearchTrials{0};
double initialResidualNorm{0.0};
double previousResidualNorm{0.0};
double residualNorm{0.0};
double relativeResidualNorm{0.0};
double merit{0.0};
double iterationSeconds{0.0};
double lineSearchSeconds{0.0};
double trialPreparationSeconds{0.0};
double metricEvaluationSeconds{0.0};
double preconditionerRefreshSeconds{0.0};
double rollbackSeconds{0.0};
std::optional<LinearSolveReport> linearSolve{};
MPI_Comm communicator{MPI_COMM_NULL};
std::span<const mfem::real_t> physicalState{};
std::span<const mfem::real_t> normalizedState{};
std::span<const mfem::real_t> normalizedResidual{};
};
struct NoObserver final { };
template <typename Candidate>
concept BeforeIterationCallback = std::invocable<Candidate &, const BeforeIteration &> &&
std::same_as<std::invoke_result_t<Candidate &, const BeforeIteration &>, void>;
template <typename Candidate>
concept AfterIterationCallback = std::invocable<Candidate &, const AfterIteration &> &&
std::same_as<std::invoke_result_t<Candidate &, const AfterIteration &>, void>;
template <typename Candidate>
concept LineSearchTrialCallback =
std::invocable<Candidate &, const AfterLineSearchTrial &> &&
std::same_as<std::invoke_result_t<Candidate &, const AfterLineSearchTrial &>, void>;
template <BeforeIterationCallback BeforeCallback, AfterIterationCallback AfterCallback>
class CallbackObserver final {
public:
CallbackObserver(
BeforeCallback before,
AfterCallback after
)
: m_before(std::move(before)),
m_after(std::move(after)) {
}
void beforeIteration(const BeforeIteration &event) noexcept(std::is_nothrow_invocable_v<
BeforeCallback &,
const BeforeIteration &>) {
std::invoke(m_before, event);
}
void afterIteration(const AfterIteration &event) noexcept(std::is_nothrow_invocable_v<
AfterCallback &,
const AfterIteration &>) {
std::invoke(m_after, event);
}
private:
[[no_unique_address]] BeforeCallback m_before;
[[no_unique_address]] AfterCallback m_after;
};
template <
typename BeforeCallback,
typename AfterCallback>
requires BeforeIterationCallback<std::decay_t<BeforeCallback>> &&
AfterIterationCallback<std::decay_t<AfterCallback>> &&
std::constructible_from<
std::decay_t<BeforeCallback>,
BeforeCallback> &&
std::constructible_from<
std::decay_t<AfterCallback>,
AfterCallback>
[[nodiscard]] auto makeObserver(
BeforeCallback &&before,
AfterCallback &&after
) {
return CallbackObserver<std::decay_t<BeforeCallback>, std::decay_t<AfterCallback>>{
std::forward<BeforeCallback>(before), std::forward<AfterCallback>(after)
};
}
template <
BeforeIterationCallback BeforeCallback,
LineSearchTrialCallback TrialCallback,
AfterIterationCallback AfterCallback>
class DetailedCallbackObserver final {
public:
DetailedCallbackObserver(
BeforeCallback before,
TrialCallback trial,
AfterCallback after
)
: m_before(std::move(before)),
m_trial(std::move(trial)),
m_after(std::move(after)) {
}
void beforeIteration(const BeforeIteration &event) noexcept(std::is_nothrow_invocable_v<
BeforeCallback &,
const BeforeIteration &>) {
std::invoke(m_before, event);
}
void afterLineSearchTrial(const AfterLineSearchTrial &event) noexcept(std::is_nothrow_invocable_v<
TrialCallback &,
const AfterLineSearchTrial &>) {
std::invoke(m_trial, event);
}
void afterIteration(const AfterIteration &event) noexcept(std::is_nothrow_invocable_v<
AfterCallback &,
const AfterIteration &>) {
std::invoke(m_after, event);
}
private:
[[no_unique_address]] BeforeCallback m_before;
[[no_unique_address]] TrialCallback m_trial;
[[no_unique_address]] AfterCallback m_after;
};
template <
typename BeforeCallback,
typename TrialCallback,
typename AfterCallback>
requires BeforeIterationCallback<std::decay_t<BeforeCallback>> &&
LineSearchTrialCallback<std::decay_t<TrialCallback>> &&
AfterIterationCallback<std::decay_t<AfterCallback>> &&
std::constructible_from<
std::decay_t<BeforeCallback>,
BeforeCallback> &&
std::constructible_from<
std::decay_t<TrialCallback>,
TrialCallback> &&
std::constructible_from<
std::decay_t<AfterCallback>,
AfterCallback>
[[nodiscard]] auto makeObserver(
BeforeCallback &&before,
TrialCallback &&trial,
AfterCallback &&after
) {
return DetailedCallbackObserver<
std::decay_t<BeforeCallback>, std::decay_t<TrialCallback>, std::decay_t<AfterCallback>>{
std::forward<BeforeCallback>(before), std::forward<TrialCallback>(trial), std::forward<AfterCallback>(after)
};
}
namespace detail {
[[nodiscard]] inline double NextBacktrackingStepLength(
const double rejectedStepLength,
const double acceptedMinimumJacobianDeterminant,
const std::optional<double> rejectedMinimumJacobianDeterminant,
const bool rejectedByInvertedGeometry,
const BacktrackingOptions &options
) noexcept {
const double contractedStepLength = rejectedStepLength * options.contractionFactor;
if (!rejectedByInvertedGeometry || !rejectedMinimumJacobianDeterminant.has_value() ||
!std::isfinite(acceptedMinimumJacobianDeterminant) || acceptedMinimumJacobianDeterminant <= 0.0 ||
!std::isfinite(*rejectedMinimumJacobianDeterminant) || *rejectedMinimumJacobianDeterminant > 0.0) {
return contractedStepLength;
}
const double determinantChange = acceptedMinimumJacobianDeterminant - *rejectedMinimumJacobianDeterminant;
if (!std::isfinite(determinantChange) || determinantChange <= 0.0) {
return contractedStepLength;
}
const double estimatedBoundaryStep =
rejectedStepLength * acceptedMinimumJacobianDeterminant / determinantChange;
const double safeguardedStep = options.fractionToBoundarySafety * estimatedBoundaryStep;
if (!std::isfinite(safeguardedStep) || safeguardedStep <= 0.0 || safeguardedStep >= rejectedStepLength) {
return contractedStepLength;
}
/*
* Keep the configured backtracking ladder intact. The geometry
* certificate is used only to skip rungs that its local boundary
* estimate says are unsafe; it does not introduce a new trial
* length between two rungs. This preserves the candidates that
* ordinary backtracking would eventually test while avoiding the
* expensive preparation of the skipped, inverted geometries.
*/
if (contractedStepLength <= safeguardedStep) {
return contractedStepLength;
}
const double rung =
std::ceil(std::log(safeguardedStep / rejectedStepLength) / std::log(options.contractionFactor));
double skippedStep = rejectedStepLength * std::pow(options.contractionFactor, rung);
if (!std::isfinite(skippedStep) || skippedStep <= 0.0 || skippedStep >= rejectedStepLength) {
return contractedStepLength;
}
if (skippedStep > safeguardedStep) {
skippedStep *= options.contractionFactor;
}
return skippedStep;
}
template <typename Observer>
inline constexpr bool isNoObserver = std::same_as<std::remove_cvref_t<Observer>, NoObserver>;
template <typename Observer>
concept ObservesBeforeIteration =
!isNoObserver<Observer> &&
requires(std::remove_reference_t<Observer> &observer, const BeforeIteration &event) {
{ observer.beforeIteration(event) } -> std::same_as<void>;
};
template <typename Observer>
concept ObservesAfterIteration =
!isNoObserver<Observer> &&
requires(std::remove_reference_t<Observer> &observer, const AfterIteration &event) {
{ observer.afterIteration(event) } -> std::same_as<void>;
};
template <typename Observer>
concept ObservesLineSearchTrial =
!isNoObserver<Observer> &&
requires(std::remove_reference_t<Observer> &observer, const AfterLineSearchTrial &event) {
{ observer.afterLineSearchTrial(event) } -> std::same_as<void>;
};
template <typename Callback>
void InvokeObserverHookCollectively(
const MPI_Comm communicator,
const char *remoteFailureMessage,
Callback &&callback
) {
std::exception_ptr localFailure;
try {
std::invoke(std::forward<Callback>(callback));
} catch (...) {
localFailure = std::current_exception();
}
const int localFailureFlag = localFailure != nullptr ? 1 : 0;
int globalFailureFlag = 0;
if (MPI_Allreduce(&localFailureFlag, &globalFailureFlag, 1, MPI_INT, MPI_MAX, communicator) !=
MPI_SUCCESS) {
if (localFailure != nullptr) {
std::rethrow_exception(localFailure);
}
throw std::runtime_error("The nonlinear solver could not synchronize an observer callback.");
}
if (globalFailureFlag != 0) {
if (localFailure != nullptr) {
std::rethrow_exception(localFailure);
}
throw std::runtime_error(remoteFailureMessage);
}
}
template <typename Observer>
void InvokeBeforeIteration(
Observer &observer,
const BeforeIteration &event
) {
if constexpr (ObservesBeforeIteration<Observer>) {
if constexpr (noexcept(observer.beforeIteration(event))) {
observer.beforeIteration(event);
} else {
InvokeObserverHookCollectively(
event.communicator, "An observer before-iteration callback failed on another rank.",
[&observer, &event] { observer.beforeIteration(event); }
);
}
}
}
template <typename Observer>
void InvokeAfterIteration(
Observer &observer,
const AfterIteration &event
) {
if constexpr (ObservesAfterIteration<Observer>) {
if constexpr (noexcept(observer.afterIteration(event))) {
observer.afterIteration(event);
} else {
InvokeObserverHookCollectively(
event.communicator, "An observer after-iteration callback failed on another rank.",
[&observer, &event] { observer.afterIteration(event); }
);
}
}
}
template <typename Observer>
void InvokeAfterLineSearchTrial(
Observer &observer,
const AfterLineSearchTrial &event
) {
if constexpr (ObservesLineSearchTrial<Observer>) {
if constexpr (noexcept(observer.afterLineSearchTrial(event))) {
observer.afterLineSearchTrial(event);
} else {
InvokeObserverHookCollectively(
event.communicator, "An observer line-search callback failed on another rank.",
[&observer, &event] { observer.afterLineSearchTrial(event); }
);
}
}
}
} // namespace detail
/*
* Observers run synchronously on every solve rank. Ordinary callback
* exceptions are synchronized before the solver proceeds, so all ranks can
* unwind together; explicitly noexcept callbacks bypass that synchronization.
* A callback must still not enter an MPI collective on only a subset of
* ranks. A before/after pair is guaranteed for iterations that finish by
* returning an evaluation report. Infrastructure exceptions unwind
* immediately and do not promise an after callback.
*/
template <typename Candidate>
concept NewtonObserver = detail::isNoObserver<Candidate> || detail::ObservesBeforeIteration<Candidate> ||
detail::ObservesLineSearchTrial<Candidate> || detail::ObservesAfterIteration<Candidate>;
} // namespace mean_field::solver::nonlinear

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
export module mean_field:solver.stellar_equilibrium;
export import :solver.stellar_structure;
export import :solver.stellar_context;

View File

@@ -0,0 +1,60 @@
module;
#include <cstdint>
#include <optional>
#include <string>
export module mean_field:solver.stellar_equilibrium_types;
export import :solver.linear_backend;
export namespace mean_field::solver {
enum class StellarEquilibriumFailureReason : std::uint8_t {
unspecified,
inadmissible_state,
non_finite_state,
non_finite_residual,
linear_solve_failure,
globalization_failure,
stagnation,
iteration_limit
};
/*
* An owning, backend-neutral account of an expected numerical failure.
* Backend-specific measurements may be translated into the message or
* future common diagnostics, but are deliberately not part of this stable
* result boundary.
*/
struct StellarEquilibriumFailureReport final {
StellarEquilibriumFailureReason reason{StellarEquilibriumFailureReason::unspecified};
std::string message;
int completedNonlinearIterations{0};
std::optional<double> initialResidualNorm;
std::optional<double> finalResidualNorm;
};
/*
* Fixed-size diagnostics retained by every evaluation report. Detailed
* iteration histories belong in an observer so the default solve does not
* allocate storage proportional to the iteration count.
*/
struct StellarEquilibriumEvaluationDiagnostics final {
int attemptedNonlinearIterations{0};
int acceptedNonlinearIterations{0};
int totalLineSearchTrials{0};
int inadmissibleLineSearchTrials{0};
int nonFiniteLineSearchTrials{0};
int insufficientDecreaseTrials{0};
double initialResidualNorm{0.0};
double finalResidualNorm{0.0};
double lastAcceptedStepLength{0.0};
double totalLinearSolveSeconds{0.0};
double totalLineSearchSeconds{0.0};
double totalTrialPreparationSeconds{0.0};
double totalMetricEvaluationSeconds{0.0};
double totalPreconditionerRefreshSeconds{0.0};
double totalRollbackSeconds{0.0};
std::optional<LinearSolveReport> lastLinearSolve;
};
} // namespace mean_field::solver

View File

@@ -0,0 +1,486 @@
module;
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
export module mean_field:solver.stellar_structure;
export import :operators.stellar_equilibrium_problem;
export import :solver.stellar_equilibrium_types;
export namespace mean_field::solver {
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem> class StellarEquilibriumEvaluationReport;
}
namespace mean_field::solver::detail {
enum class StellarViewCertification : std::uint8_t { unavailable, checkpoint, structure };
template <typename Problem> struct StellarStructureStorage final {
using ProblemType = std::remove_cvref_t<Problem>;
StellarStructureStorage(
std::unique_ptr<ProblemType> ownedProblem,
std::unique_ptr<mfem::Vector> acceptedPhysicalState,
std::unique_ptr<physics::RigidRotation> prescribedRotation
)
: problem(std::move(ownedProblem)),
physicalState(std::move(acceptedPhysicalState)),
rotation(std::move(prescribedRotation)) {
}
std::unique_ptr<ProblemType> problem;
std::unique_ptr<mfem::Vector> physicalState;
std::unique_ptr<physics::RigidRotation> rotation;
std::uint64_t viewGeneration{0};
StellarViewCertification certification{StellarViewCertification::unavailable};
};
template <typename Problem>
[[nodiscard]] bool IsCurrentView(
const std::weak_ptr<const StellarStructureStorage<Problem>> &candidate,
const std::uint64_t generation,
const bool requireConverged
) noexcept {
const auto storage = candidate.lock();
if (storage == nullptr || storage->problem == nullptr || storage->physicalState == nullptr ||
storage->viewGeneration != generation) {
return false;
}
if (requireConverged) {
return storage->certification == StellarViewCertification::structure;
}
return storage->certification == StellarViewCertification::checkpoint ||
storage->certification == StellarViewCertification::structure;
}
template <typename Problem>
[[nodiscard]] std::shared_ptr<const StellarStructureStorage<Problem>> RequireCurrentView(
const std::weak_ptr<const StellarStructureStorage<Problem>> &candidate,
const std::uint64_t generation,
const bool requireConverged
) {
auto storage = candidate.lock();
if (storage == nullptr || storage->problem == nullptr || storage->physicalState == nullptr ||
storage->viewGeneration != generation ||
(requireConverged && storage->certification != StellarViewCertification::structure) ||
(!requireConverged && storage->certification != StellarViewCertification::checkpoint &&
storage->certification != StellarViewCertification::structure)) {
throw std::logic_error("The stellar structure view is stale or is not certified for this result.");
}
return storage;
}
template <typename Vector> [[nodiscard]] std::span<const mfem::real_t> ReadOnlySpan(const Vector &values) noexcept {
return {values.GetData(), static_cast<std::size_t>(values.Size())};
}
template <typename Problem> struct StellarEvaluationReportAccess;
} // namespace mean_field::solver::detail
export namespace mean_field::equilibrium {
/*
* These are the future owning, self-contained values. There is no public
* construction path until deep capture and its MPI-independent storage
* schema are implemented.
*/
template <DiscretizedStellarEquilibriumProblem Problem> class StellarStructure final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
StellarStructure(const StellarStructure &) = delete;
StellarStructure &operator=(const StellarStructure &) = delete;
StellarStructure(StellarStructure &&) noexcept = default;
StellarStructure &operator=(StellarStructure &&) = delete;
~StellarStructure() = default;
private:
StellarStructure() = default;
};
template <DiscretizedStellarEquilibriumProblem Problem> class StellarCheckpoint final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
StellarCheckpoint(const StellarCheckpoint &) = delete;
StellarCheckpoint &operator=(const StellarCheckpoint &) = delete;
StellarCheckpoint(StellarCheckpoint &&) noexcept = default;
StellarCheckpoint &operator=(StellarCheckpoint &&) = delete;
~StellarCheckpoint() = default;
private:
StellarCheckpoint() = default;
};
/*
* Result views weakly observe context-owned storage. valid() remains safe
* after that context is destroyed. References and spans extracted from a
* valid view remain borrowed: the context must outlive their use, and the
* next evaluate() call invalidates them along with their originating view.
*/
template <DiscretizedStellarEquilibriumProblem Problem> class StellarStructureView final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
using ModelType = typename ProblemType::ModelType;
[[nodiscard]] bool valid() const noexcept {
return solver::detail::IsCurrentView(m_storage, m_generation, true);
}
[[nodiscard]] const ModelType &model() const & {
const auto storage = RequireStorage();
return storage->problem->GetStellarModel();
}
[[nodiscard]] const ModelType &model() const && = delete;
[[nodiscard]] MPI_Comm communicator() const & {
const auto storage = RequireStorage();
return storage->problem->GetCommunicator();
}
[[nodiscard]] MPI_Comm communicator() const && = delete;
[[nodiscard]] std::span<const mfem::real_t> state() const & {
const auto storage = RequireStorage();
return solver::detail::ReadOnlySpan(*storage->physicalState);
}
[[nodiscard]] std::span<const mfem::real_t> state() const && = delete;
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const & {
const auto storage = RequireStorage();
return storage->problem->GetManifest().valueBlocks();
}
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const && = delete;
template <typename Term>
requires requires(
const typename ProblemType::ManifestType &manifest,
const mfem::Vector &physicalState,
const Term &term
) { manifest.stateView(physicalState).block(term); }
[[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &term) const & {
const auto storage = RequireStorage();
const auto block = storage->problem->GetManifest().stateView(*storage->physicalState).block(term);
return solver::detail::ReadOnlySpan(block);
}
template <typename Term> [[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &) const && = delete;
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const & {
const auto storage = RequireStorage();
if (storage->rotation == nullptr) {
return std::nullopt;
}
return *storage->rotation;
}
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const && = delete;
[[nodiscard]] physics::RigidRotation rotation() const & {
const auto storage = RequireStorage();
return storage->problem->GetPreparedOperator().GetRotation();
}
[[nodiscard]] physics::RigidRotation rotation() const && = delete;
[[nodiscard]] StellarStructure<ProblemType> capture() const {
(void)RequireStorage();
throw std::logic_error("Capturing a self-contained StellarStructure is not implemented.");
}
private:
template <DiscretizedStellarEquilibriumProblem> friend class solver::StellarEquilibriumEvaluationReport;
using Storage = solver::detail::StellarStructureStorage<ProblemType>;
StellarStructureView(
std::weak_ptr<const Storage> storage,
const std::uint64_t generation
) noexcept
: m_storage(std::move(storage)),
m_generation(generation) {
}
[[nodiscard]] std::shared_ptr<const Storage> RequireStorage() const {
return solver::detail::RequireCurrentView(m_storage, m_generation, true);
}
std::weak_ptr<const Storage> m_storage;
std::uint64_t m_generation;
};
template <DiscretizedStellarEquilibriumProblem Problem> class StellarCheckpointView final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
using ModelType = typename ProblemType::ModelType;
[[nodiscard]] bool valid() const noexcept {
return solver::detail::IsCurrentView(m_storage, m_generation, false);
}
[[nodiscard]] const ModelType &model() const & {
const auto storage = RequireStorage();
return storage->problem->GetStellarModel();
}
[[nodiscard]] const ModelType &model() const && = delete;
[[nodiscard]] MPI_Comm communicator() const & {
const auto storage = RequireStorage();
return storage->problem->GetCommunicator();
}
[[nodiscard]] MPI_Comm communicator() const && = delete;
[[nodiscard]] std::span<const mfem::real_t> state() const & {
const auto storage = RequireStorage();
return solver::detail::ReadOnlySpan(*storage->physicalState);
}
[[nodiscard]] std::span<const mfem::real_t> state() const && = delete;
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const & {
const auto storage = RequireStorage();
return storage->problem->GetManifest().valueBlocks();
}
[[nodiscard]] std::span<const operators::RootBlockDescriptor> stateDescriptors() const && = delete;
template <typename Term>
requires requires(
const typename ProblemType::ManifestType &manifest,
const mfem::Vector &physicalState,
const Term &term
) { manifest.stateView(physicalState).block(term); }
[[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &term) const & {
const auto storage = RequireStorage();
const auto block = storage->problem->GetManifest().stateView(*storage->physicalState).block(term);
return solver::detail::ReadOnlySpan(block);
}
template <typename Term> [[nodiscard]] std::span<const mfem::real_t> stateBlock(const Term &) const && = delete;
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const & {
const auto storage = RequireStorage();
if (storage->rotation == nullptr) {
return std::nullopt;
}
return *storage->rotation;
}
[[nodiscard]] std::optional<physics::RigidRotation> prescribedRotation() const && = delete;
[[nodiscard]] physics::RigidRotation rotation() const & {
const auto storage = RequireStorage();
return storage->problem->GetPreparedOperator().GetRotation();
}
[[nodiscard]] physics::RigidRotation rotation() const && = delete;
[[nodiscard]] StellarCheckpoint<ProblemType> capture() const {
(void)RequireStorage();
throw std::logic_error("Capturing a self-contained StellarCheckpoint is not implemented.");
}
private:
template <DiscretizedStellarEquilibriumProblem> friend class solver::StellarEquilibriumEvaluationReport;
using Storage = solver::detail::StellarStructureStorage<ProblemType>;
StellarCheckpointView(
std::weak_ptr<const Storage> storage,
const std::uint64_t generation
) noexcept
: m_storage(std::move(storage)),
m_generation(generation) {
}
[[nodiscard]] std::shared_ptr<const Storage> RequireStorage() const {
return solver::detail::RequireCurrentView(m_storage, m_generation, false);
}
std::weak_ptr<const Storage> m_storage;
std::uint64_t m_generation;
};
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarStructure<Problem> &,
const std::filesystem::path &
) {
throw std::logic_error("Serializing a StellarStructure is not implemented.");
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarStructureView<Problem> &view,
const std::filesystem::path &
) {
(void)view.state();
throw std::logic_error("Serializing a StellarStructureView is not implemented.");
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarCheckpoint<Problem> &,
const std::filesystem::path &
) {
throw std::logic_error("Serializing a StellarCheckpoint is not implemented.");
}
template <DiscretizedStellarEquilibriumProblem Problem>
[[noreturn]] void serialize(
const StellarCheckpointView<Problem> &view,
const std::filesystem::path &
) {
(void)view.state();
throw std::logic_error("Serializing a StellarCheckpointView is not implemented.");
}
} // namespace mean_field::equilibrium
export namespace mean_field::solver {
template <equilibrium::DiscretizedStellarEquilibriumProblem Problem>
class StellarEquilibriumEvaluationReport final {
public:
using ProblemType = std::remove_cvref_t<Problem>;
using StructureView = equilibrium::StellarStructureView<ProblemType>;
using CheckpointView = equilibrium::StellarCheckpointView<ProblemType>;
StellarEquilibriumEvaluationReport(const StellarEquilibriumEvaluationReport &) = default;
StellarEquilibriumEvaluationReport &operator=(const StellarEquilibriumEvaluationReport &) = default;
StellarEquilibriumEvaluationReport(StellarEquilibriumEvaluationReport &&) noexcept = default;
StellarEquilibriumEvaluationReport &operator=(StellarEquilibriumEvaluationReport &&) noexcept = default;
~StellarEquilibriumEvaluationReport() = default;
[[nodiscard]] bool converged() const noexcept {
return m_converged;
}
[[nodiscard]] const StellarEquilibriumEvaluationDiagnostics &diagnostics() const & noexcept {
return m_diagnostics;
}
[[nodiscard]] const StellarEquilibriumEvaluationDiagnostics &diagnostics() const && = delete;
[[nodiscard]] int completedNonlinearIterations() const noexcept {
return m_diagnostics.acceptedNonlinearIterations;
}
[[nodiscard]] double initialResidualNorm() const noexcept {
return m_diagnostics.initialResidualNorm;
}
[[nodiscard]] double finalResidualNorm() const noexcept {
return m_diagnostics.finalResidualNorm;
}
[[nodiscard]] const StellarEquilibriumFailureReport &failure() const & {
if (!m_failure.has_value()) {
throw std::logic_error("A converged stellar-equilibrium report has no failure record.");
}
return *m_failure;
}
[[nodiscard]] const StellarEquilibriumFailureReport &failure() const && = delete;
[[nodiscard]] StructureView structureView() const {
if (!m_converged) {
throw std::logic_error("A failed stellar-equilibrium report cannot certify a structure view.");
}
StructureView view{m_storage, m_generation};
if (!view.valid()) {
throw std::logic_error("The stellar-equilibrium structure view has been invalidated.");
}
return view;
}
[[nodiscard]] CheckpointView checkpointView() const {
CheckpointView view{m_storage, m_generation};
if (!view.valid()) {
throw std::logic_error("The stellar-equilibrium checkpoint view has been invalidated.");
}
return view;
}
[[nodiscard]] CheckpointView lastAcceptedCheckpointView() const {
return checkpointView();
}
private:
friend struct detail::StellarEvaluationReportAccess<ProblemType>;
using Storage = detail::StellarStructureStorage<ProblemType>;
StellarEquilibriumEvaluationReport(
const bool converged,
StellarEquilibriumEvaluationDiagnostics diagnostics,
std::optional<StellarEquilibriumFailureReport> failure,
std::weak_ptr<const Storage> storage,
const std::uint64_t generation
)
: m_converged(converged),
m_diagnostics(std::move(diagnostics)),
m_failure(std::move(failure)),
m_storage(std::move(storage)),
m_generation(generation) {
}
bool m_converged;
StellarEquilibriumEvaluationDiagnostics m_diagnostics;
std::optional<StellarEquilibriumFailureReport> m_failure;
std::weak_ptr<const Storage> m_storage;
std::uint64_t m_generation;
};
} // namespace mean_field::solver
namespace mean_field::solver::detail {
template <typename Problem> struct StellarEvaluationReportAccess final {
using ProblemType = std::remove_cvref_t<Problem>;
using Report = StellarEquilibriumEvaluationReport<ProblemType>;
using Storage = StellarStructureStorage<ProblemType>;
[[nodiscard]] static Report Success(
const std::shared_ptr<Storage> &storage,
StellarEquilibriumEvaluationDiagnostics diagnostics
) {
if (storage == nullptr) {
throw std::invalid_argument("A stellar-equilibrium report requires owned result storage.");
}
storage->certification = StellarViewCertification::structure;
return Report{true, std::move(diagnostics), std::nullopt, storage, storage->viewGeneration};
}
[[nodiscard]] static Report Failure(
const std::shared_ptr<Storage> &storage,
StellarEquilibriumEvaluationDiagnostics diagnostics,
const StellarEquilibriumFailureReason reason,
std::string message
) {
if (storage == nullptr) {
throw std::invalid_argument("A stellar-equilibrium report requires owned result storage.");
}
storage->certification = StellarViewCertification::checkpoint;
StellarEquilibriumFailureReport failure{
.reason = reason,
.message = std::move(message),
.completedNonlinearIterations = diagnostics.acceptedNonlinearIterations,
.initialResidualNorm = diagnostics.initialResidualNorm,
.finalResidualNorm = diagnostics.finalResidualNorm
};
return Report{
false, std::move(diagnostics), std::optional<StellarEquilibriumFailureReport>{std::move(failure)},
storage, storage->viewGeneration
};
}
};
} // namespace mean_field::solver::detail