580 lines
23 KiB
C++
580 lines
23 KiB
C++
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
|