feat(newton): first newton solver implementation
This commit is contained in:
868
libmeanfield/interface/solver/linear_backend.cppm
Normal file
868
libmeanfield/interface/solver/linear_backend.cppm
Normal 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
|
||||
579
libmeanfield/interface/solver/newton.cppm
Normal file
579
libmeanfield/interface/solver/newton.cppm
Normal 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
|
||||
1642
libmeanfield/interface/solver/stellar_context.cppm
Normal file
1642
libmeanfield/interface/solver/stellar_context.cppm
Normal file
File diff suppressed because it is too large
Load Diff
4
libmeanfield/interface/solver/stellar_equilibrium.cppm
Normal file
4
libmeanfield/interface/solver/stellar_equilibrium.cppm
Normal file
@@ -0,0 +1,4 @@
|
||||
export module mean_field:solver.stellar_equilibrium;
|
||||
|
||||
export import :solver.stellar_structure;
|
||||
export import :solver.stellar_context;
|
||||
60
libmeanfield/interface/solver/stellar_equilibrium_types.cppm
Normal file
60
libmeanfield/interface/solver/stellar_equilibrium_types.cppm
Normal 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
|
||||
486
libmeanfield/interface/solver/stellar_structure.cppm
Normal file
486
libmeanfield/interface/solver/stellar_structure.cppm
Normal 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
|
||||
Reference in New Issue
Block a user