1456 lines
61 KiB
C++
1456 lines
61 KiB
C++
#include <algorithm>
|
|
#include <cmath>
|
|
#include <concepts>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <numbers>
|
|
#include <ranges>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <catch2/catch_approx.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include <mfem.hpp>
|
|
#include <mpi.h>
|
|
|
|
import mean_field;
|
|
import test_helpers;
|
|
|
|
namespace stellar_solver_architecture_test {
|
|
class TwoByTwoOperator final : public mfem::Operator {
|
|
public:
|
|
TwoByTwoOperator() : mfem::Operator(2) {
|
|
}
|
|
|
|
void Mult(
|
|
const mfem::Vector &input,
|
|
mfem::Vector &output
|
|
) const override {
|
|
output.SetSize(2);
|
|
output(0) = 4.0 * input(0) + input(1);
|
|
output(1) = 2.0 * input(0) + 3.0 * input(1);
|
|
}
|
|
};
|
|
|
|
class IdentityInverse final : public mfem::Solver {
|
|
public:
|
|
static constexpr mean_field::preconditioning::ApplicationContract applicationContract =
|
|
mean_field::preconditioning::ApplicationContract::flexible;
|
|
|
|
IdentityInverse() : mfem::Solver(2) {
|
|
}
|
|
|
|
void SetOperator(const mfem::Operator &operation) override {
|
|
if (operation.Height() != 2 || operation.Width() != 2) {
|
|
throw std::invalid_argument("The identity inverse requires a two-by-two operation.");
|
|
}
|
|
}
|
|
|
|
void Mult(
|
|
const mfem::Vector &input,
|
|
mfem::Vector &output
|
|
) const override {
|
|
output = input;
|
|
}
|
|
};
|
|
|
|
struct LifetimeProbe final {
|
|
const void *problemIdentity{nullptr};
|
|
bool backendDestroyed{false};
|
|
bool dependenciesCurrentAtDestruction{false};
|
|
std::vector<double> incomingCorrectionNorms;
|
|
std::vector<double> returnedCorrectionNorms;
|
|
};
|
|
|
|
struct ScriptedBackend final : mean_field::solver::LinearBackendConfigurationTag {
|
|
static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract =
|
|
mean_field::preconditioning::ApplicationContract::flexible;
|
|
|
|
std::shared_ptr<LifetimeProbe> probe;
|
|
double correctionValue{0.0};
|
|
bool resizeCorrection{false};
|
|
|
|
ScriptedBackend() = default;
|
|
|
|
explicit ScriptedBackend(
|
|
std::shared_ptr<LifetimeProbe> lifetimeProbe,
|
|
const double scriptedCorrectionValue = 0.0,
|
|
const bool resizeScriptedCorrection = false
|
|
)
|
|
: probe(std::move(lifetimeProbe)),
|
|
correctionValue(scriptedCorrectionValue),
|
|
resizeCorrection(resizeScriptedCorrection) {
|
|
}
|
|
};
|
|
|
|
template <typename Operation, typename Preconditioner> class PreparedScriptedBackend final {
|
|
public:
|
|
PreparedScriptedBackend(
|
|
const Operation &operation,
|
|
Preconditioner &preconditioner,
|
|
const MPI_Comm communicator,
|
|
std::shared_ptr<LifetimeProbe> probe,
|
|
const double correctionValue,
|
|
const bool resizeCorrection
|
|
)
|
|
: m_operation(std::addressof(operation)),
|
|
m_preconditioner(std::addressof(preconditioner)),
|
|
m_communicator(communicator),
|
|
m_probe(std::move(probe)),
|
|
m_correctionValue(correctionValue),
|
|
m_resizeCorrection(resizeCorrection) {
|
|
if (m_probe != nullptr) {
|
|
m_probe->problemIdentity = std::addressof(operation.GetProblem());
|
|
}
|
|
}
|
|
|
|
PreparedScriptedBackend(const PreparedScriptedBackend &) = delete;
|
|
PreparedScriptedBackend &operator=(const PreparedScriptedBackend &) = delete;
|
|
PreparedScriptedBackend(PreparedScriptedBackend &&) = delete;
|
|
PreparedScriptedBackend &operator=(PreparedScriptedBackend &&) = delete;
|
|
|
|
~PreparedScriptedBackend() {
|
|
if (m_probe != nullptr) {
|
|
m_probe->backendDestroyed = true;
|
|
m_probe->dependenciesCurrentAtDestruction = m_operation != nullptr && m_preconditioner != nullptr &&
|
|
m_operation->IsPrepared() && m_preconditioner->IsCurrent();
|
|
}
|
|
}
|
|
|
|
[[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 noexcept {
|
|
return m_operation != nullptr && m_preconditioner != nullptr && m_communicator != MPI_COMM_NULL &&
|
|
m_operation->IsPrepared() && m_preconditioner->IsCurrent();
|
|
}
|
|
|
|
[[nodiscard]] int RightHandSideSize() const noexcept {
|
|
return m_operation->Height();
|
|
}
|
|
|
|
[[nodiscard]] int CorrectionSize() const noexcept {
|
|
return m_operation->Width();
|
|
}
|
|
|
|
[[nodiscard]] mean_field::solver::LinearSolveReport Solve(
|
|
const mfem::Vector &rightHandSide,
|
|
mfem::Vector &correction,
|
|
const mean_field::solver::LinearSolveControl &control
|
|
) {
|
|
control.Validate();
|
|
if (rightHandSide.Size() != RightHandSideSize() || correction.Size() != CorrectionSize()) {
|
|
throw std::invalid_argument("The scripted backend received incompatible vectors.");
|
|
}
|
|
if (m_resizeCorrection) {
|
|
correction.SetSize(CorrectionSize() + 1);
|
|
correction = 0.0;
|
|
return {
|
|
.status = mean_field::solver::LinearSolveStatus::converged,
|
|
.control = control,
|
|
.iterations = 1,
|
|
.initialResidualNorm = 1.0,
|
|
.reportedResidualNorm = 0.0,
|
|
.trueResidualNorm = 0.0,
|
|
.relativeTrueResidualNorm = 0.0
|
|
};
|
|
}
|
|
|
|
mfem::Vector operationAction(m_operation->Height());
|
|
m_operation->Mult(correction, operationAction);
|
|
mfem::Vector initialResidual(rightHandSide);
|
|
initialResidual -= operationAction;
|
|
const double initialResidualNorm = GlobalNorm(initialResidual);
|
|
if (m_probe != nullptr) {
|
|
m_probe->incomingCorrectionNorms.push_back(GlobalNorm(correction));
|
|
}
|
|
|
|
correction = m_correctionValue;
|
|
if (m_probe != nullptr) {
|
|
m_probe->returnedCorrectionNorms.push_back(GlobalNorm(correction));
|
|
}
|
|
m_operation->Mult(correction, operationAction);
|
|
mfem::Vector trueResidual(rightHandSide);
|
|
trueResidual -= operationAction;
|
|
const double rightHandSideNorm = GlobalNorm(rightHandSide);
|
|
const double trueResidualNorm = GlobalNorm(trueResidual);
|
|
const bool converged = trueResidualNorm <= control.ConvergenceThreshold(rightHandSideNorm);
|
|
return {
|
|
.status = converged ? mean_field::solver::LinearSolveStatus::converged
|
|
: mean_field::solver::LinearSolveStatus::maximum_iterations,
|
|
.control = control,
|
|
.iterations = 1,
|
|
.restarts = 0,
|
|
.initialResidualNorm = initialResidualNorm,
|
|
.reportedResidualNorm = trueResidualNorm,
|
|
.trueResidualNorm = trueResidualNorm,
|
|
.relativeTrueResidualNorm =
|
|
rightHandSideNorm == 0.0 ? (trueResidualNorm == 0.0 ? 0.0 : std::numeric_limits<double>::infinity())
|
|
: trueResidualNorm / rightHandSideNorm,
|
|
.operatorApplications = 2,
|
|
.inversePreconditionerApplications = 1
|
|
};
|
|
}
|
|
|
|
private:
|
|
[[nodiscard]] double GlobalNorm(const mfem::Vector &values) const {
|
|
const double localNorm = values.Norml2();
|
|
const double localNormSquared = localNorm * localNorm;
|
|
double globalNormSquared = 0.0;
|
|
if (MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, m_communicator) !=
|
|
MPI_SUCCESS) {
|
|
throw std::runtime_error("The scripted backend could not reduce a residual norm.");
|
|
}
|
|
return std::sqrt(globalNormSquared);
|
|
}
|
|
|
|
const Operation *m_operation;
|
|
Preconditioner *m_preconditioner;
|
|
MPI_Comm m_communicator;
|
|
std::shared_ptr<LifetimeProbe> m_probe;
|
|
double m_correctionValue;
|
|
bool m_resizeCorrection;
|
|
};
|
|
|
|
template <
|
|
typename Operation,
|
|
typename Preconditioner>
|
|
[[nodiscard]] auto prepareLinearBackend(
|
|
ScriptedBackend configuration,
|
|
const Operation &operation,
|
|
Preconditioner &preconditioner,
|
|
const MPI_Comm communicator
|
|
) {
|
|
return PreparedScriptedBackend<Operation, Preconditioner>{
|
|
operation,
|
|
preconditioner,
|
|
communicator,
|
|
std::move(configuration.probe),
|
|
configuration.correctionValue,
|
|
configuration.resizeCorrection
|
|
};
|
|
}
|
|
|
|
struct ZeroMetric final { };
|
|
|
|
[[nodiscard]] mean_field::solver::nonlinear::MetricEvaluation getMetric(
|
|
const ZeroMetric &,
|
|
const mfem::Vector &,
|
|
MPI_Comm
|
|
) {
|
|
return {.residualNorm = 0.0, .merit = 0.0};
|
|
}
|
|
|
|
struct MetricSequenceState final {
|
|
std::vector<mean_field::solver::nonlinear::MetricEvaluation> evaluations;
|
|
std::size_t next{0};
|
|
};
|
|
|
|
struct SequencedMetric final {
|
|
std::shared_ptr<MetricSequenceState> state;
|
|
};
|
|
|
|
[[nodiscard]] mean_field::solver::nonlinear::MetricEvaluation getMetric(
|
|
const SequencedMetric &metric,
|
|
const mfem::Vector &,
|
|
MPI_Comm
|
|
) {
|
|
if (metric.state == nullptr || metric.state->next >= metric.state->evaluations.size()) {
|
|
throw std::runtime_error("The sequenced metric exhausted its scripted evaluations.");
|
|
}
|
|
return metric.state->evaluations[metric.state->next++];
|
|
}
|
|
|
|
struct ThrowingTrialMetric final {
|
|
std::shared_ptr<int> calls;
|
|
};
|
|
|
|
[[nodiscard]] mean_field::solver::nonlinear::MetricEvaluation getMetric(
|
|
const ThrowingTrialMetric &metric,
|
|
const mfem::Vector &,
|
|
MPI_Comm
|
|
) {
|
|
if ((*metric.calls)++ == 0) {
|
|
return {.residualNorm = 1.0, .merit = 0.5};
|
|
}
|
|
throw std::runtime_error("scripted metric infrastructure failure");
|
|
}
|
|
|
|
struct MalformedObserver final {
|
|
void beforeIteraton(const mean_field::solver::nonlinear::BeforeIteration &) {
|
|
}
|
|
};
|
|
|
|
struct NonFiniteCorrectionProbe final {
|
|
int solveCalls{0};
|
|
bool secondIncomingCorrectionWasZero{false};
|
|
};
|
|
|
|
struct NonFiniteThenFailingBackend final : mean_field::solver::LinearBackendConfigurationTag {
|
|
static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract =
|
|
mean_field::preconditioning::ApplicationContract::flexible;
|
|
|
|
std::shared_ptr<NonFiniteCorrectionProbe> probe;
|
|
|
|
NonFiniteThenFailingBackend() = default;
|
|
|
|
explicit NonFiniteThenFailingBackend(std::shared_ptr<NonFiniteCorrectionProbe> failureProbe)
|
|
: probe(std::move(failureProbe)) {
|
|
}
|
|
};
|
|
|
|
template <typename Operation, typename Preconditioner> class PreparedNonFiniteThenFailingBackend final {
|
|
public:
|
|
PreparedNonFiniteThenFailingBackend(
|
|
const Operation &operation,
|
|
Preconditioner &preconditioner,
|
|
const MPI_Comm communicator,
|
|
std::shared_ptr<NonFiniteCorrectionProbe> probe
|
|
)
|
|
: m_operation(std::addressof(operation)),
|
|
m_preconditioner(std::addressof(preconditioner)),
|
|
m_communicator(communicator),
|
|
m_probe(std::move(probe)) {
|
|
}
|
|
|
|
[[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 noexcept {
|
|
return m_operation != nullptr && m_preconditioner != nullptr && m_communicator != MPI_COMM_NULL;
|
|
}
|
|
|
|
[[nodiscard]] int RightHandSideSize() const noexcept {
|
|
return m_operation->Height();
|
|
}
|
|
|
|
[[nodiscard]] int CorrectionSize() const noexcept {
|
|
return m_operation->Width();
|
|
}
|
|
|
|
[[nodiscard]] mean_field::solver::LinearSolveReport Solve(
|
|
const mfem::Vector &rightHandSide,
|
|
mfem::Vector &correction,
|
|
const mean_field::solver::LinearSolveControl &control
|
|
) {
|
|
control.Validate();
|
|
if (rightHandSide.Size() != RightHandSideSize() || correction.Size() != CorrectionSize()) {
|
|
throw std::invalid_argument("The non-finite test backend received incompatible vectors.");
|
|
}
|
|
if (m_probe == nullptr) {
|
|
throw std::logic_error("The non-finite test backend requires its probe.");
|
|
}
|
|
|
|
++m_probe->solveCalls;
|
|
if (m_probe->solveCalls == 1) {
|
|
correction = std::numeric_limits<double>::quiet_NaN();
|
|
return {.status = mean_field::solver::LinearSolveStatus::non_finite, .control = control};
|
|
}
|
|
|
|
m_probe->secondIncomingCorrectionWasZero = true;
|
|
for (int index = 0; index < correction.Size(); ++index) {
|
|
m_probe->secondIncomingCorrectionWasZero =
|
|
m_probe->secondIncomingCorrectionWasZero && correction(index) == 0.0;
|
|
}
|
|
correction = 0.0;
|
|
return {.status = mean_field::solver::LinearSolveStatus::maximum_iterations, .control = control};
|
|
}
|
|
|
|
private:
|
|
const Operation *m_operation;
|
|
Preconditioner *m_preconditioner;
|
|
MPI_Comm m_communicator;
|
|
std::shared_ptr<NonFiniteCorrectionProbe> m_probe;
|
|
};
|
|
|
|
template <
|
|
typename Operation,
|
|
typename Preconditioner>
|
|
[[nodiscard]] auto prepareLinearBackend(
|
|
NonFiniteThenFailingBackend configuration,
|
|
const Operation &operation,
|
|
Preconditioner &preconditioner,
|
|
const MPI_Comm communicator
|
|
) {
|
|
return PreparedNonFiniteThenFailingBackend<Operation, Preconditioner>{
|
|
operation, preconditioner, communicator, std::move(configuration.probe)
|
|
};
|
|
}
|
|
|
|
struct ThrowingRefreshProbe final {
|
|
int refreshCalls{0};
|
|
};
|
|
|
|
struct ThrowingRefreshPrescription final : mean_field::preconditioning::StellarPreconditionerPrescriptionTag {
|
|
std::shared_ptr<ThrowingRefreshProbe> probe;
|
|
|
|
ThrowingRefreshPrescription() = default;
|
|
|
|
explicit ThrowingRefreshPrescription(std::shared_ptr<ThrowingRefreshProbe> refreshProbe)
|
|
: probe(std::move(refreshProbe)) {
|
|
}
|
|
};
|
|
|
|
template <typename Problem> class ThrowingRefreshInverse final : public mfem::Solver {
|
|
public:
|
|
static constexpr mean_field::preconditioning::ApplicationContract applicationContract =
|
|
mean_field::preconditioning::ApplicationContract::flexible;
|
|
|
|
ThrowingRefreshInverse(
|
|
const Problem &problem,
|
|
std::shared_ptr<ThrowingRefreshProbe> probe
|
|
)
|
|
: mfem::Solver(
|
|
problem.StateSize(),
|
|
problem.EquationSize()
|
|
),
|
|
m_problem(std::addressof(problem)),
|
|
m_preparationGeneration(problem.GetPreparationGeneration()),
|
|
m_probe(std::move(probe)) {
|
|
}
|
|
|
|
ThrowingRefreshInverse(const ThrowingRefreshInverse &) = delete;
|
|
ThrowingRefreshInverse &operator=(const ThrowingRefreshInverse &) = delete;
|
|
ThrowingRefreshInverse(ThrowingRefreshInverse &&) = delete;
|
|
ThrowingRefreshInverse &operator=(ThrowingRefreshInverse &&) = delete;
|
|
|
|
void SetOperator(const mfem::Operator &operation) override {
|
|
if (std::addressof(operation) != std::addressof(m_problem->GetLinearizationOperator())) {
|
|
throw std::invalid_argument("The throwing refresh inverse cannot bind another problem.");
|
|
}
|
|
}
|
|
|
|
void Mult(
|
|
const mfem::Vector &input,
|
|
mfem::Vector &output
|
|
) const override {
|
|
output = input;
|
|
}
|
|
|
|
[[nodiscard]] const Problem &GetProblem() const noexcept {
|
|
return *m_problem;
|
|
}
|
|
|
|
[[nodiscard]] bool IsCurrent() const noexcept {
|
|
return m_problem != nullptr && m_problem->IsPrepared() &&
|
|
m_preparationGeneration == m_problem->GetPreparationGeneration();
|
|
}
|
|
|
|
void Refresh() {
|
|
if (m_problem == nullptr || !m_problem->IsPrepared() || m_probe == nullptr) {
|
|
throw std::logic_error("The throwing refresh inverse has incomplete state.");
|
|
}
|
|
++m_probe->refreshCalls;
|
|
if (m_probe->refreshCalls == 1) {
|
|
throw std::runtime_error("scripted preconditioner refresh failure");
|
|
}
|
|
m_preparationGeneration = m_problem->GetPreparationGeneration();
|
|
}
|
|
|
|
private:
|
|
const Problem *m_problem;
|
|
std::uint64_t m_preparationGeneration;
|
|
std::shared_ptr<ThrowingRefreshProbe> m_probe;
|
|
};
|
|
|
|
template <typename Problem>
|
|
[[nodiscard]] auto prepareStellarPreconditioner(
|
|
ThrowingRefreshPrescription prescription,
|
|
const Problem &problem
|
|
) {
|
|
return ThrowingRefreshInverse<Problem>{problem, std::move(prescription.probe)};
|
|
}
|
|
|
|
struct ObserverRecord final {
|
|
std::vector<char> order;
|
|
int beforeCalls{0};
|
|
int afterCalls{0};
|
|
int trialCalls{0};
|
|
std::vector<mean_field::solver::nonlinear::IterationDisposition> dispositions;
|
|
std::vector<mean_field::solver::nonlinear::LineSearchTrialDisposition> trialDispositions;
|
|
std::vector<std::string> trialRejectionSources;
|
|
std::vector<double> trialStepLengths;
|
|
std::vector<double> acceptedStepLengths;
|
|
std::vector<int> lineSearchTrials;
|
|
std::vector<std::vector<mfem::real_t>> beforeNormalizedStates;
|
|
std::vector<std::vector<mfem::real_t>> afterNormalizedStates;
|
|
mean_field::solver::nonlinear::IterationDisposition disposition{
|
|
mean_field::solver::nonlinear::IterationDisposition::unspecified
|
|
};
|
|
bool accepted{true};
|
|
};
|
|
|
|
struct AfterOnlyObserver final {
|
|
void afterIteration(const mean_field::solver::nonlinear::AfterIteration &) {
|
|
}
|
|
};
|
|
|
|
struct TrialOnlyObserver final {
|
|
void afterLineSearchTrial(const mean_field::solver::nonlinear::AfterLineSearchTrial &) {
|
|
}
|
|
};
|
|
|
|
[[nodiscard]] mean_field::fem::FEM makeFiniteElements() {
|
|
auto arguments = test_utils::setup_args();
|
|
return mean_field::fem::setup_fem(arguments.mesh_file, arguments, 0);
|
|
}
|
|
|
|
[[nodiscard]] double matchingCentralDensity() {
|
|
constexpr double radius = mean_field::utils::RADIUS;
|
|
return std::numbers::pi_v<double> * mean_field::utils::MASS / (4.0 * radius * radius * radius);
|
|
}
|
|
|
|
[[nodiscard]] auto makeModel() {
|
|
using namespace mean_field;
|
|
constexpr double radius = utils::RADIUS;
|
|
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
|
|
return model::StellarModel(
|
|
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
|
|
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{utils::MASS}}),
|
|
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{matchingCentralDensity()}})
|
|
);
|
|
}
|
|
|
|
[[nodiscard]] auto makeGeneratedRotationModel() {
|
|
using namespace mean_field;
|
|
constexpr double radius = utils::RADIUS;
|
|
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
|
|
return model::StellarModel(
|
|
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
|
|
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{utils::MASS}}),
|
|
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.05}}),
|
|
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{matchingCentralDensity()}})
|
|
);
|
|
}
|
|
|
|
template <typename Candidate>
|
|
concept HasSolutionAccessor = requires(Candidate &candidate) { candidate.solution(); };
|
|
|
|
template <typename Candidate>
|
|
concept HasStructureViewAccessor = requires(Candidate &candidate) { candidate.structureView(); };
|
|
|
|
template <typename Context, typename Newton>
|
|
concept MakesSolverFromRvalue =
|
|
requires(Context &&context, Newton newton) { mean_field::solver::make(std::move(context), std::move(newton)); };
|
|
|
|
template <typename Context, typename Newton, typename Observer>
|
|
concept MakesSolverWithObserver = requires(Context &context, Newton newton, Observer observer) {
|
|
mean_field::solver::make(context, std::move(newton), std::move(observer));
|
|
};
|
|
|
|
template <typename Report>
|
|
concept ReadsDiagnosticsFromRvalue = requires(Report report) { std::move(report).diagnostics(); };
|
|
} // namespace stellar_solver_architecture_test
|
|
|
|
TEST_CASE(
|
|
"Newton Options And The Default Residual Metric Enforce Their Numerical Contracts",
|
|
"[solver][newton][options][metric][unit]"
|
|
) {
|
|
using namespace mean_field;
|
|
using Catch::Approx;
|
|
|
|
solver::nonlinear::NewtonOptions options{
|
|
.relativeTolerance = 0.25,
|
|
.absoluteTolerance = 0.5,
|
|
.maximumIterations = 1,
|
|
.linearSolve = {.relativeTolerance = 0.0, .absoluteTolerance = 0.0, .maximumIterations = 1},
|
|
.backtracking = {
|
|
.initialStepLength = 1.0,
|
|
.contractionFactor = 0.5,
|
|
.fractionToBoundarySafety = 0.9,
|
|
.sufficientDecrease = 1.0e-4,
|
|
.minimumStepLength = 1.0e-8,
|
|
.maximumTrials = 1
|
|
}
|
|
};
|
|
CHECK_NOTHROW(options.Validate());
|
|
CHECK(options.ConvergenceThreshold(4.0) == Approx(1.0));
|
|
options.absoluteTolerance = 2.0;
|
|
CHECK(options.ConvergenceThreshold(4.0) == Approx(2.0));
|
|
CHECK_THROWS_AS(options.ConvergenceThreshold(-1.0), std::invalid_argument);
|
|
CHECK_THROWS_AS(options.ConvergenceThreshold(std::numeric_limits<double>::infinity()), std::invalid_argument);
|
|
|
|
auto invalidBacktracking = solver::nonlinear::BacktrackingOptions{};
|
|
invalidBacktracking.initialStepLength = 0.0;
|
|
CHECK_THROWS_AS(invalidBacktracking.Validate(), std::invalid_argument);
|
|
invalidBacktracking = {};
|
|
invalidBacktracking.contractionFactor = 1.0;
|
|
CHECK_THROWS_AS(invalidBacktracking.Validate(), std::invalid_argument);
|
|
invalidBacktracking = {};
|
|
invalidBacktracking.fractionToBoundarySafety = 1.0;
|
|
CHECK_THROWS_AS(invalidBacktracking.Validate(), std::invalid_argument);
|
|
invalidBacktracking = {};
|
|
invalidBacktracking.sufficientDecrease = 0.0;
|
|
CHECK_THROWS_AS(invalidBacktracking.Validate(), std::invalid_argument);
|
|
invalidBacktracking = {};
|
|
invalidBacktracking.minimumStepLength = 2.0;
|
|
CHECK_THROWS_AS(invalidBacktracking.Validate(), std::invalid_argument);
|
|
invalidBacktracking = {};
|
|
invalidBacktracking.maximumTrials = 0;
|
|
CHECK_THROWS_AS(invalidBacktracking.Validate(), std::invalid_argument);
|
|
|
|
auto invalidNewton = solver::nonlinear::NewtonOptions{};
|
|
invalidNewton.relativeTolerance = -1.0;
|
|
CHECK_THROWS_AS(invalidNewton.Validate(), std::invalid_argument);
|
|
invalidNewton = {};
|
|
invalidNewton.absoluteTolerance = std::numeric_limits<double>::infinity();
|
|
CHECK_THROWS_AS(invalidNewton.Validate(), std::invalid_argument);
|
|
invalidNewton = {};
|
|
invalidNewton.maximumIterations = 0;
|
|
CHECK_THROWS_AS(invalidNewton.Validate(), std::invalid_argument);
|
|
invalidNewton = {};
|
|
invalidNewton.linearSolve.maximumIterations = 0;
|
|
CHECK_THROWS_AS(invalidNewton.Validate(), std::invalid_argument);
|
|
|
|
const auto backtracking = solver::nonlinear::BacktrackingOptions{};
|
|
CHECK(
|
|
solver::nonlinear::detail::NextBacktrackingStepLength(
|
|
1.0, 1.0, std::optional<double>{-3.0}, true, backtracking
|
|
) == Approx(0.125)
|
|
);
|
|
CHECK(
|
|
solver::nonlinear::detail::NextBacktrackingStepLength(
|
|
1.0, 1.0, std::optional<double>{-10.0826}, true, backtracking
|
|
) == Approx(0.0625)
|
|
);
|
|
CHECK(
|
|
solver::nonlinear::detail::NextBacktrackingStepLength(1.0, 1.0, std::nullopt, true, backtracking) == Approx(0.5)
|
|
);
|
|
CHECK(
|
|
solver::nonlinear::detail::NextBacktrackingStepLength(
|
|
1.0, 1.0, std::optional<double>{-3.0}, false, backtracking
|
|
) == Approx(0.5)
|
|
);
|
|
invalidNewton = {};
|
|
invalidNewton.backtracking.maximumTrials = 0;
|
|
CHECK_THROWS_AS(invalidNewton.Validate(), std::invalid_argument);
|
|
|
|
mfem::Vector residual(2);
|
|
residual(0) = 3.0;
|
|
residual(1) = 4.0;
|
|
int communicatorSize = 0;
|
|
REQUIRE(MPI_Comm_size(MPI_COMM_WORLD, &communicatorSize) == MPI_SUCCESS);
|
|
const auto metric =
|
|
solver::nonlinear::getMetric(solver::nonlinear::NormalizedResidualMetric{}, residual, MPI_COMM_WORLD);
|
|
const double expectedNorm = 5.0 * std::sqrt(static_cast<double>(communicatorSize));
|
|
CHECK(metric.residualNorm == Approx(expectedNorm));
|
|
CHECK(metric.merit == Approx(0.5 * expectedNorm * expectedNorm));
|
|
CHECK_THROWS_AS(
|
|
solver::nonlinear::getMetric(solver::nonlinear::NormalizedResidualMetric{}, residual, MPI_COMM_NULL),
|
|
std::invalid_argument
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"MFEM FGMRES Honors Restart Configuration And A Supplied Initial Guess",
|
|
"[solver][linear][fgmres][restart][warm-start]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
using Catch::Approx;
|
|
|
|
CHECK_THROWS_AS(solver::linear::FGMRES(solver::linear::FGMRESOptions{.restartLength = 0}), std::invalid_argument);
|
|
CHECK_THROWS_AS(
|
|
solver::linear::FGMRES(solver::linear::FGMRESOptions{.restartLength = 2, .printLevel = 4}),
|
|
std::invalid_argument
|
|
);
|
|
|
|
TwoByTwoOperator operation;
|
|
IdentityInverse inverse;
|
|
constexpr int restartLength = 1;
|
|
auto backend = solver::linear::prepareLinearBackend(
|
|
solver::linear::FGMRES({.restartLength = restartLength, .printLevel = -1}), operation, inverse, MPI_COMM_WORLD
|
|
);
|
|
REQUIRE(backend.IsReady());
|
|
|
|
mfem::Vector rightHandSide(2);
|
|
rightHandSide(0) = 1.0;
|
|
rightHandSide(1) = 2.0;
|
|
mfem::Vector correction(2);
|
|
correction = 0.0;
|
|
const solver::LinearSolveControl control{
|
|
.relativeTolerance = 1.0e-12, .absoluteTolerance = 1.0e-14, .maximumIterations = 200
|
|
};
|
|
const auto coldReport = backend.Solve(rightHandSide, correction, control);
|
|
REQUIRE(coldReport.Converged());
|
|
CHECK(correction(0) == Approx(0.1).margin(1.0e-11));
|
|
CHECK(correction(1) == Approx(0.6).margin(1.0e-11));
|
|
CHECK(coldReport.trueResidualNorm <= control.ConvergenceThreshold(std::sqrt(5.0)));
|
|
int communicatorSize = 0;
|
|
REQUIRE(MPI_Comm_size(MPI_COMM_WORLD, &communicatorSize) == MPI_SUCCESS);
|
|
CHECK(coldReport.rightHandSideNorm == Approx(std::sqrt(5.0 * static_cast<double>(communicatorSize))));
|
|
CHECK(coldReport.operatorApplications > 0);
|
|
CHECK(coldReport.inversePreconditionerApplications > 0);
|
|
CHECK(coldReport.iterations == static_cast<int>(coldReport.inversePreconditionerApplications));
|
|
CHECK(coldReport.iterations > restartLength);
|
|
CHECK(coldReport.iterations <= control.maximumIterations);
|
|
CHECK(coldReport.restarts == (coldReport.iterations - 1) / restartLength);
|
|
CHECK(coldReport.restarts > 0);
|
|
CHECK(coldReport.solveSeconds >= 0.0);
|
|
CHECK(coldReport.operatorSeconds >= 0.0);
|
|
CHECK(coldReport.inversePreconditionerSeconds >= 0.0);
|
|
|
|
correction(0) += 1.0e-5;
|
|
correction(1) -= 1.0e-5;
|
|
const auto warmReport = backend.Solve(rightHandSide, correction, control);
|
|
REQUIRE(warmReport.Converged());
|
|
CHECK(warmReport.initialResidualNorm < coldReport.initialResidualNorm);
|
|
CHECK(correction(0) == Approx(0.1).margin(1.0e-11));
|
|
CHECK(correction(1) == Approx(0.6).margin(1.0e-11));
|
|
}
|
|
|
|
TEST_CASE(
|
|
"A User-Owned Context Certifies Views Only Through Evaluation Reports",
|
|
"[solver][architecture][ownership][report][view]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto probe = std::make_shared<LifetimeProbe>();
|
|
const void *problemIdentity = nullptr;
|
|
|
|
{
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{probe}
|
|
);
|
|
using Context = std::remove_cvref_t<decltype(context)>;
|
|
using Newton = decltype(solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
using Report = typename Context::EvaluationReport;
|
|
|
|
STATIC_CHECK_FALSE(std::copy_constructible<Context>);
|
|
STATIC_CHECK_FALSE(std::move_constructible<Context>);
|
|
STATIC_CHECK_FALSE(std::default_initializable<Report>);
|
|
STATIC_CHECK_FALSE(std::default_initializable<typename Report::StructureView>);
|
|
STATIC_CHECK_FALSE(std::default_initializable<typename Report::CheckpointView>);
|
|
STATIC_CHECK_FALSE(ReadsDiagnosticsFromRvalue<Report>);
|
|
STATIC_CHECK_FALSE(HasSolutionAccessor<Context>);
|
|
STATIC_CHECK_FALSE(HasStructureViewAccessor<Context>);
|
|
STATIC_CHECK_FALSE(MakesSolverFromRvalue<Context, Newton>);
|
|
STATIC_CHECK_FALSE(MakesSolverWithObserver<Context, Newton, MalformedObserver>);
|
|
STATIC_CHECK(solver::nonlinear::detail::ObservesAfterIteration<AfterOnlyObserver>);
|
|
STATIC_CHECK_FALSE(solver::nonlinear::detail::ObservesBeforeIteration<AfterOnlyObserver>);
|
|
STATIC_CHECK(solver::nonlinear::detail::ObservesLineSearchTrial<TrialOnlyObserver>);
|
|
STATIC_CHECK_FALSE(solver::nonlinear::detail::ObservesAfterIteration<TrialOnlyObserver>);
|
|
|
|
REQUIRE(context.isReady());
|
|
CHECK_FALSE(context.hasActiveSolver());
|
|
problemIdentity = probe->problemIdentity;
|
|
REQUIRE(problemIdentity != nullptr);
|
|
|
|
auto firstReport = [&] {
|
|
auto solver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
REQUIRE(solver.isReady());
|
|
CHECK(context.hasActiveSolver());
|
|
CHECK_THROWS_AS(
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{})),
|
|
std::logic_error
|
|
);
|
|
return solver.evaluate();
|
|
}();
|
|
|
|
CHECK_FALSE(context.hasActiveSolver());
|
|
REQUIRE(firstReport.converged());
|
|
CHECK(firstReport.completedNonlinearIterations() == 0);
|
|
CHECK(firstReport.initialResidualNorm() == 0.0);
|
|
CHECK_THROWS_AS(firstReport.failure(), std::logic_error);
|
|
|
|
auto firstView = firstReport.structureView();
|
|
auto firstCheckpoint = firstReport.checkpointView();
|
|
REQUIRE(firstView.valid());
|
|
REQUIRE(firstCheckpoint.valid());
|
|
STATIC_CHECK(std::same_as<decltype(firstView.state()), std::span<const mfem::real_t>>);
|
|
REQUIRE_FALSE(firstView.state().empty());
|
|
CHECK(std::ranges::all_of(firstView.state(), [](const auto value) { return std::isfinite(value); }));
|
|
CHECK(
|
|
firstView.model().template specification<constraint::FixedCentralDensity>().targetDensity() ==
|
|
dimensions::DensityValue{matchingCentralDensity()}
|
|
);
|
|
REQUIRE(firstView.prescribedRotation().has_value());
|
|
CHECK(firstView.rotation().angular_velocity()(0) == 0.0);
|
|
CHECK(firstView.rotation().angular_velocity()(1) == 0.0);
|
|
CHECK(firstView.rotation().angular_velocity()(2) == 0.0);
|
|
REQUIRE_FALSE(firstView.stateDescriptors().empty());
|
|
REQUIRE_FALSE(firstView.stateBlock(utils::blocks::density_field.mass_term).empty());
|
|
|
|
const auto destination =
|
|
std::filesystem::temp_directory_path() /
|
|
("mean_field_unimplemented_" + std::to_string(reinterpret_cast<std::uintptr_t>(std::addressof(context))));
|
|
REQUIRE_FALSE(std::filesystem::exists(destination));
|
|
CHECK_THROWS_AS(firstView.capture(), std::logic_error);
|
|
CHECK_THROWS_AS(firstCheckpoint.capture(), std::logic_error);
|
|
CHECK_THROWS_AS(equilibrium::serialize(firstView, destination), std::logic_error);
|
|
CHECK_THROWS_AS(equilibrium::serialize(firstCheckpoint, destination), std::logic_error);
|
|
CHECK_FALSE(std::filesystem::exists(destination));
|
|
|
|
auto secondReport = [&] {
|
|
auto solver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
return solver.evaluate();
|
|
}();
|
|
REQUIRE(secondReport.converged());
|
|
REQUIRE(secondReport.structureView().valid());
|
|
CHECK_FALSE(firstView.valid());
|
|
CHECK_FALSE(firstCheckpoint.valid());
|
|
CHECK_THROWS_AS(firstView.state(), std::logic_error);
|
|
CHECK_THROWS_AS(firstReport.structureView(), std::logic_error);
|
|
CHECK_THROWS_AS(equilibrium::serialize(firstView, destination), std::logic_error);
|
|
|
|
CHECK_FALSE(probe->backendDestroyed);
|
|
REQUIRE(context.isReady());
|
|
}
|
|
|
|
CHECK(probe->problemIdentity == problemIdentity);
|
|
CHECK(probe->backendDestroyed);
|
|
CHECK(probe->dependenciesCurrentAtDestruction);
|
|
|
|
auto expiredResult = [] {
|
|
auto finiteElements = makeFiniteElements();
|
|
if (!finiteElements.okay()) {
|
|
throw std::runtime_error("The expired-view test could not build its finite-element model.");
|
|
}
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{}
|
|
);
|
|
auto solver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
auto report = solver.evaluate();
|
|
auto view = report.structureView();
|
|
return std::pair{std::move(report), std::move(view)};
|
|
}();
|
|
REQUIRE(expiredResult.first.converged());
|
|
CHECK_FALSE(expiredResult.second.valid());
|
|
CHECK_THROWS_AS(expiredResult.second.state(), std::logic_error);
|
|
CHECK_THROWS_AS(expiredResult.first.structureView(), std::logic_error);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Damped Newton Pairs Observer Hooks And Returns The Last Accepted Checkpoint On Failure",
|
|
"[solver][newton][observer][checkpoint][backtracking]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{}
|
|
);
|
|
|
|
auto record = std::make_shared<ObserverRecord>();
|
|
auto observer = solver::nonlinear::makeObserver(
|
|
[record, token = std::make_unique<int>(19)](const solver::nonlinear::BeforeIteration &event) {
|
|
CHECK(*token == 19);
|
|
CHECK_FALSE(event.physicalState.empty());
|
|
++record->beforeCalls;
|
|
record->order.push_back('B');
|
|
},
|
|
[record](const solver::nonlinear::AfterIteration &event) {
|
|
++record->afterCalls;
|
|
record->order.push_back('A');
|
|
record->disposition = event.disposition;
|
|
record->accepted = event.stepAccepted;
|
|
CHECK(event.linearSolve.has_value());
|
|
CHECK_FALSE(event.normalizedResidual.empty());
|
|
}
|
|
);
|
|
|
|
auto newton = solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{
|
|
.relativeTolerance = 1.0e-12,
|
|
.absoluteTolerance = 0.0,
|
|
.maximumIterations = 2,
|
|
.linearSolve =
|
|
{.relativeTolerance = 0.0,
|
|
.absoluteTolerance = std::numeric_limits<double>::max(),
|
|
.maximumIterations = 1},
|
|
.backtracking = {
|
|
.initialStepLength = 1.0,
|
|
.contractionFactor = 0.5,
|
|
.sufficientDecrease = 1.0e-4,
|
|
.minimumStepLength = 0.5,
|
|
.maximumTrials = 1
|
|
}
|
|
}
|
|
);
|
|
|
|
auto report = [&] {
|
|
auto solver = solver::make(context, std::move(newton), std::move(observer));
|
|
return solver.evaluate();
|
|
}();
|
|
|
|
CHECK_FALSE(context.hasActiveSolver());
|
|
CHECK_FALSE(report.converged());
|
|
CHECK(report.failure().reason == solver::StellarEquilibriumFailureReason::globalization_failure);
|
|
CHECK(report.diagnostics().attemptedNonlinearIterations == 1);
|
|
CHECK(report.diagnostics().acceptedNonlinearIterations == 0);
|
|
CHECK(report.diagnostics().totalLineSearchTrials == 1);
|
|
CHECK_THROWS_AS(report.structureView(), std::logic_error);
|
|
auto checkpoint = report.lastAcceptedCheckpointView();
|
|
REQUIRE(checkpoint.valid());
|
|
REQUIRE_FALSE(checkpoint.state().empty());
|
|
CHECK(context.isReady());
|
|
|
|
CHECK(record->beforeCalls == 1);
|
|
CHECK(record->afterCalls == 1);
|
|
CHECK(record->order == std::vector<char>{'B', 'A'});
|
|
CHECK(record->disposition == solver::nonlinear::IterationDisposition::globalization_failure);
|
|
CHECK_FALSE(record->accepted);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Generated Rotation Is Visible Through A Certified Structure View",
|
|
"[solver][architecture][rotation][view]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto context = solver::makeContext(
|
|
makeGeneratedRotationModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{}
|
|
);
|
|
auto solver = solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
auto report = solver.evaluate();
|
|
|
|
REQUIRE(report.converged());
|
|
auto view = report.structureView();
|
|
CHECK_FALSE(view.prescribedRotation().has_value());
|
|
const auto rotation = view.rotation();
|
|
REQUIRE(rotation.angular_velocity().Size() == 3);
|
|
CHECK(rotation.angular_velocity()(0) == 0.0);
|
|
CHECK(rotation.angular_velocity()(1) == 0.0);
|
|
CHECK(rotation.angular_velocity()(2) > 0.0);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Damped Newton Reuses Its Step History And Scales The Next Linear Warm Start",
|
|
"[solver][newton][backtracking][warm-start][observer]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
using Catch::Approx;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto backendProbe = std::make_shared<LifetimeProbe>();
|
|
constexpr double correctionValue = 1.0e-10;
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{backendProbe, correctionValue}
|
|
);
|
|
|
|
auto metricState = std::make_shared<MetricSequenceState>();
|
|
metricState->evaluations = {
|
|
{.residualNorm = 4.0, .merit = 8.0},
|
|
{.residualNorm = 5.0, .merit = 12.5},
|
|
{.residualNorm = 4.5, .merit = 10.125},
|
|
{.residualNorm = 2.0, .merit = 2.0},
|
|
{.residualNorm = 0.0, .merit = 0.0}
|
|
};
|
|
auto observerRecord = std::make_shared<ObserverRecord>();
|
|
auto observer = solver::nonlinear::makeObserver(
|
|
[observerRecord](const solver::nonlinear::BeforeIteration &event) {
|
|
++observerRecord->beforeCalls;
|
|
observerRecord->order.push_back('B');
|
|
observerRecord->beforeNormalizedStates.emplace_back(
|
|
event.normalizedState.begin(), event.normalizedState.end()
|
|
);
|
|
},
|
|
[observerRecord](const solver::nonlinear::AfterLineSearchTrial &event) {
|
|
++observerRecord->trialCalls;
|
|
observerRecord->trialDispositions.push_back(event.disposition);
|
|
observerRecord->trialRejectionSources.emplace_back(event.rejectionSource);
|
|
observerRecord->trialStepLengths.push_back(event.stepLength);
|
|
CHECK_FALSE(event.candidatePhysicalState.empty());
|
|
CHECK_FALSE(event.candidateNormalizedState.empty());
|
|
CHECK_FALSE(event.candidateNormalizedResidual.empty());
|
|
CHECK(event.metric.has_value());
|
|
CHECK(event.minimumJacobianDeterminant.has_value());
|
|
CHECK(event.preparationSeconds >= 0.0);
|
|
CHECK(event.metricSeconds >= 0.0);
|
|
},
|
|
[observerRecord](const solver::nonlinear::AfterIteration &event) {
|
|
++observerRecord->afterCalls;
|
|
observerRecord->order.push_back('A');
|
|
observerRecord->dispositions.push_back(event.disposition);
|
|
observerRecord->acceptedStepLengths.push_back(event.acceptedStepLength);
|
|
observerRecord->lineSearchTrials.push_back(event.lineSearchTrials);
|
|
observerRecord->afterNormalizedStates.emplace_back(
|
|
event.normalizedState.begin(), event.normalizedState.end()
|
|
);
|
|
CHECK(event.iterationSeconds >= 0.0);
|
|
CHECK(event.lineSearchSeconds >= 0.0);
|
|
CHECK(event.trialPreparationSeconds >= 0.0);
|
|
CHECK(event.metricEvaluationSeconds >= 0.0);
|
|
CHECK(event.preconditionerRefreshSeconds >= 0.0);
|
|
CHECK(event.rollbackSeconds >= 0.0);
|
|
}
|
|
);
|
|
auto newton = solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{
|
|
.relativeTolerance = 1.0e-12,
|
|
.absoluteTolerance = 0.0,
|
|
.maximumIterations = 3,
|
|
.linearSolve =
|
|
{.relativeTolerance = 0.0,
|
|
.absoluteTolerance = std::numeric_limits<double>::max(),
|
|
.maximumIterations = 2},
|
|
.backtracking =
|
|
{.initialStepLength = 1.0,
|
|
.contractionFactor = 0.5,
|
|
.sufficientDecrease = 1.0e-4,
|
|
.minimumStepLength = 0.25,
|
|
.maximumTrials = 3}
|
|
},
|
|
SequencedMetric{metricState}
|
|
);
|
|
|
|
auto equilibriumSolver = solver::make(context, std::move(newton), std::move(observer));
|
|
const auto report = equilibriumSolver.evaluate();
|
|
|
|
REQUIRE(report.converged());
|
|
CHECK(report.diagnostics().attemptedNonlinearIterations == 2);
|
|
CHECK(report.diagnostics().acceptedNonlinearIterations == 2);
|
|
CHECK(report.diagnostics().totalLineSearchTrials == 4);
|
|
CHECK(report.diagnostics().inadmissibleLineSearchTrials == 0);
|
|
CHECK(report.diagnostics().nonFiniteLineSearchTrials == 0);
|
|
CHECK(report.diagnostics().insufficientDecreaseTrials == 2);
|
|
CHECK(report.diagnostics().totalLinearSolveSeconds >= 0.0);
|
|
CHECK(report.diagnostics().totalLineSearchSeconds >= 0.0);
|
|
CHECK(report.diagnostics().totalTrialPreparationSeconds >= 0.0);
|
|
CHECK(report.diagnostics().totalMetricEvaluationSeconds >= 0.0);
|
|
CHECK(report.diagnostics().totalPreconditionerRefreshSeconds >= 0.0);
|
|
CHECK(report.diagnostics().totalRollbackSeconds >= 0.0);
|
|
CHECK(metricState->next == metricState->evaluations.size());
|
|
REQUIRE(observerRecord->beforeCalls == 2);
|
|
REQUIRE(observerRecord->afterCalls == 2);
|
|
REQUIRE(observerRecord->trialCalls == 4);
|
|
CHECK(observerRecord->order == std::vector<char>{'B', 'A', 'B', 'A'});
|
|
CHECK(
|
|
observerRecord->dispositions ==
|
|
std::vector{
|
|
solver::nonlinear::IterationDisposition::accepted, solver::nonlinear::IterationDisposition::converged
|
|
}
|
|
);
|
|
CHECK(observerRecord->lineSearchTrials == std::vector<int>{3, 1});
|
|
CHECK(
|
|
observerRecord->trialDispositions == std::vector{
|
|
solver::nonlinear::LineSearchTrialDisposition::insufficient_decrease,
|
|
solver::nonlinear::LineSearchTrialDisposition::insufficient_decrease,
|
|
solver::nonlinear::LineSearchTrialDisposition::accepted,
|
|
solver::nonlinear::LineSearchTrialDisposition::accepted
|
|
}
|
|
);
|
|
CHECK(
|
|
observerRecord->trialRejectionSources == std::vector<std::string>{"residual metric", "residual metric", "", ""}
|
|
);
|
|
CHECK(observerRecord->trialStepLengths == std::vector<double>{1.0, 0.5, 0.25, 0.5});
|
|
REQUIRE(observerRecord->acceptedStepLengths.size() == 2);
|
|
CHECK(observerRecord->acceptedStepLengths[0] == Approx(0.25));
|
|
CHECK(observerRecord->acceptedStepLengths[1] == Approx(0.5));
|
|
|
|
REQUIRE(observerRecord->beforeNormalizedStates.size() == 2);
|
|
REQUIRE(observerRecord->afterNormalizedStates.size() == 2);
|
|
REQUIRE(observerRecord->beforeNormalizedStates[0].size() == observerRecord->afterNormalizedStates[0].size());
|
|
for (std::size_t index = 0; index < observerRecord->beforeNormalizedStates[0].size(); ++index) {
|
|
CHECK(
|
|
observerRecord->afterNormalizedStates[0][index] - observerRecord->beforeNormalizedStates[0][index] ==
|
|
Approx(0.25 * correctionValue).margin(1.0e-14)
|
|
);
|
|
CHECK(
|
|
observerRecord->beforeNormalizedStates[1][index] ==
|
|
Approx(observerRecord->afterNormalizedStates[0][index]).margin(1.0e-14)
|
|
);
|
|
}
|
|
REQUIRE(backendProbe->incomingCorrectionNorms.size() == 2);
|
|
REQUIRE(backendProbe->returnedCorrectionNorms.size() == 2);
|
|
CHECK(backendProbe->incomingCorrectionNorms[0] == 0.0);
|
|
CHECK(
|
|
backendProbe->incomingCorrectionNorms[1] ==
|
|
Approx(0.75 * backendProbe->returnedCorrectionNorms[0]).margin(1.0e-14)
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"An Accepted Final Newton Step Reports The Iteration Limit To Its Observer",
|
|
"[solver][newton][iteration-limit][observer][checkpoint]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{nullptr, 1.0e-10}
|
|
);
|
|
auto metricState = std::make_shared<MetricSequenceState>();
|
|
metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}};
|
|
auto observerRecord = std::make_shared<ObserverRecord>();
|
|
auto observer = solver::nonlinear::makeObserver(
|
|
[](const solver::nonlinear::BeforeIteration &) { },
|
|
[observerRecord](const solver::nonlinear::AfterIteration &event) {
|
|
++observerRecord->afterCalls;
|
|
observerRecord->disposition = event.disposition;
|
|
observerRecord->accepted = event.stepAccepted;
|
|
}
|
|
);
|
|
auto newton = solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{
|
|
.relativeTolerance = 1.0e-12,
|
|
.absoluteTolerance = 0.0,
|
|
.maximumIterations = 1,
|
|
.linearSolve =
|
|
{.relativeTolerance = 0.0,
|
|
.absoluteTolerance = std::numeric_limits<double>::max(),
|
|
.maximumIterations = 1},
|
|
.backtracking = {.maximumTrials = 1}
|
|
},
|
|
SequencedMetric{metricState}
|
|
);
|
|
auto equilibriumSolver = solver::make(context, std::move(newton), std::move(observer));
|
|
const auto report = equilibriumSolver.evaluate();
|
|
|
|
REQUIRE_FALSE(report.converged());
|
|
CHECK(report.failure().reason == solver::StellarEquilibriumFailureReason::iteration_limit);
|
|
CHECK(report.completedNonlinearIterations() == 1);
|
|
CHECK(report.diagnostics().attemptedNonlinearIterations == 1);
|
|
CHECK(report.diagnostics().totalLineSearchTrials == 1);
|
|
REQUIRE(report.lastAcceptedCheckpointView().valid());
|
|
CHECK(observerRecord->afterCalls == 1);
|
|
CHECK(observerRecord->disposition == solver::nonlinear::IterationDisposition::iteration_limit);
|
|
CHECK(observerRecord->accepted);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Metric Infrastructure Failures Propagate And Restore The Accepted Context State",
|
|
"[solver][newton][exception-safety][rollback][metric]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{nullptr, 1.0e-10}
|
|
);
|
|
|
|
std::vector<mfem::real_t> baseline;
|
|
{
|
|
auto baselineSolver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
const auto baselineReport = baselineSolver.evaluate();
|
|
const auto baselineView = baselineReport.structureView();
|
|
const auto baselineSpan = baselineView.state();
|
|
baseline.assign(baselineSpan.begin(), baselineSpan.end());
|
|
}
|
|
|
|
auto calls = std::make_shared<int>(0);
|
|
auto options = solver::nonlinear::NewtonOptions{
|
|
.relativeTolerance = 0.0,
|
|
.absoluteTolerance = 0.0,
|
|
.maximumIterations = 1,
|
|
.linearSolve =
|
|
{.relativeTolerance = 0.0, .absoluteTolerance = std::numeric_limits<double>::max(), .maximumIterations = 1},
|
|
.backtracking = {.maximumTrials = 1}
|
|
};
|
|
{
|
|
auto throwingSolver = solver::make(context, solver::nonlinear::Newton(options, ThrowingTrialMetric{calls}));
|
|
CHECK_THROWS_WITH(throwingSolver.evaluate(), "scripted metric infrastructure failure");
|
|
}
|
|
REQUIRE(context.isReady());
|
|
|
|
auto recoveredSolver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
const auto recoveredReport = recoveredSolver.evaluate();
|
|
REQUIRE(recoveredReport.converged());
|
|
const auto recoveredView = recoveredReport.structureView();
|
|
const auto recovered = recoveredView.state();
|
|
REQUIRE(recovered.size() == baseline.size());
|
|
CHECK(std::equal(recovered.begin(), recovered.end(), baseline.begin(), baseline.end()));
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Observer Callback Exceptions Preserve Their Local Cause",
|
|
"[solver][newton][observer][exception-safety]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
auto observer = solver::nonlinear::makeObserver(
|
|
[](const solver::nonlinear::BeforeIteration &) { throw std::runtime_error("before observer failure"); },
|
|
[](const solver::nonlinear::AfterLineSearchTrial &) { throw std::runtime_error("trial observer failure"); },
|
|
[](const solver::nonlinear::AfterIteration &) { throw std::runtime_error("after observer failure"); }
|
|
);
|
|
|
|
CHECK_THROWS_WITH(
|
|
solver::nonlinear::detail::InvokeBeforeIteration(
|
|
observer, solver::nonlinear::BeforeIteration{.communicator = MPI_COMM_WORLD}
|
|
),
|
|
"before observer failure"
|
|
);
|
|
CHECK_THROWS_WITH(
|
|
solver::nonlinear::detail::InvokeAfterLineSearchTrial(
|
|
observer, solver::nonlinear::AfterLineSearchTrial{.communicator = MPI_COMM_WORLD}
|
|
),
|
|
"trial observer failure"
|
|
);
|
|
CHECK_THROWS_WITH(
|
|
solver::nonlinear::detail::InvokeAfterIteration(
|
|
observer, solver::nonlinear::AfterIteration{.communicator = MPI_COMM_WORLD}
|
|
),
|
|
"after observer failure"
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"A Trial Observer Exception Restores The Accepted Context State",
|
|
"[solver][newton][observer][exception-safety][rollback]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{nullptr, 1.0e-10}
|
|
);
|
|
|
|
std::vector<mfem::real_t> baseline;
|
|
{
|
|
auto baselineSolver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
const auto baselineReport = baselineSolver.evaluate();
|
|
const auto baselineView = baselineReport.structureView();
|
|
const auto baselineSpan = baselineView.state();
|
|
baseline.assign(baselineSpan.begin(), baselineSpan.end());
|
|
}
|
|
|
|
auto metricState = std::make_shared<MetricSequenceState>();
|
|
metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}};
|
|
auto observer = solver::nonlinear::makeObserver(
|
|
[](const solver::nonlinear::BeforeIteration &) { },
|
|
[](const solver::nonlinear::AfterLineSearchTrial &) {
|
|
throw std::runtime_error("trial observer safe-shutdown failure");
|
|
},
|
|
[](const solver::nonlinear::AfterIteration &) { }
|
|
);
|
|
auto newton = solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{
|
|
.relativeTolerance = 0.0,
|
|
.absoluteTolerance = 0.0,
|
|
.maximumIterations = 1,
|
|
.linearSolve =
|
|
{.relativeTolerance = 0.0,
|
|
.absoluteTolerance = std::numeric_limits<double>::max(),
|
|
.maximumIterations = 1},
|
|
.backtracking = {.maximumTrials = 1}
|
|
},
|
|
SequencedMetric{metricState}
|
|
);
|
|
{
|
|
auto throwingSolver = solver::make(context, std::move(newton), std::move(observer));
|
|
CHECK_THROWS_WITH(throwingSolver.evaluate(), "trial observer safe-shutdown failure");
|
|
}
|
|
REQUIRE(context.isReady());
|
|
|
|
auto recoveredSolver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
const auto recoveredReport = recoveredSolver.evaluate();
|
|
REQUIRE(recoveredReport.converged());
|
|
const auto recoveredView = recoveredReport.structureView();
|
|
const auto recovered = recoveredView.state();
|
|
REQUIRE(recovered.size() == baseline.size());
|
|
CHECK(std::equal(recovered.begin(), recovered.end(), baseline.begin(), baseline.end()));
|
|
}
|
|
|
|
TEST_CASE(
|
|
"A Failed Nonfinite Linear Correction Cannot Poison The Next Evaluation Warm Start",
|
|
"[solver][newton][linear][warm-start][reuse]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto probe = std::make_shared<NonFiniteCorrectionProbe>();
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), NonFiniteThenFailingBackend{probe}
|
|
);
|
|
auto metricState = std::make_shared<MetricSequenceState>();
|
|
metricState->evaluations = {{.residualNorm = 1.0, .merit = 0.5}, {.residualNorm = 1.0, .merit = 0.5}};
|
|
auto equilibriumSolver = solver::make(
|
|
context, solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{.maximumIterations = 1}, SequencedMetric{metricState}
|
|
)
|
|
);
|
|
|
|
const auto firstReport = equilibriumSolver.evaluate();
|
|
REQUIRE_FALSE(firstReport.converged());
|
|
CHECK(firstReport.failure().reason == solver::StellarEquilibriumFailureReason::linear_solve_failure);
|
|
const auto secondReport = equilibriumSolver.evaluate();
|
|
REQUIRE_FALSE(secondReport.converged());
|
|
CHECK(secondReport.failure().reason == solver::StellarEquilibriumFailureReason::linear_solve_failure);
|
|
CHECK(probe->solveCalls == 2);
|
|
CHECK(probe->secondIncomingCorrectionWasZero);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Newton Rejects A Linear Backend That Changes Its Correction Workspace Size",
|
|
"[solver][newton][linear][contract][exception-safety]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), ScriptedBackend{nullptr, 0.0, true}
|
|
);
|
|
auto metricState = std::make_shared<MetricSequenceState>();
|
|
metricState->evaluations = {{.residualNorm = 1.0, .merit = 0.5}};
|
|
auto equilibriumSolver = solver::make(
|
|
context, solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{.maximumIterations = 1}, SequencedMetric{metricState}
|
|
)
|
|
);
|
|
|
|
CHECK_THROWS_WITH(
|
|
equilibriumSolver.evaluate(), "A prepared linear backend changed the Newton correction vector's required size."
|
|
);
|
|
REQUIRE(context.isReady());
|
|
}
|
|
|
|
TEST_CASE(
|
|
"A Preconditioner Commit Failure Rolls Back Before Accepted State Mutation",
|
|
"[solver][newton][exception-safety][rollback][preconditioner]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto refreshProbe = std::make_shared<ThrowingRefreshProbe>();
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
ThrowingRefreshPrescription{refreshProbe}, ScriptedBackend{nullptr, 1.0e-10}
|
|
);
|
|
|
|
std::vector<mfem::real_t> baseline;
|
|
{
|
|
auto baselineSolver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
const auto baselineReport = baselineSolver.evaluate();
|
|
const auto baselineView = baselineReport.structureView();
|
|
const auto baselineSpan = baselineView.state();
|
|
baseline.assign(baselineSpan.begin(), baselineSpan.end());
|
|
}
|
|
|
|
auto metricState = std::make_shared<MetricSequenceState>();
|
|
metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}};
|
|
auto newton = solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{
|
|
.relativeTolerance = 0.0,
|
|
.absoluteTolerance = 0.0,
|
|
.maximumIterations = 1,
|
|
.linearSolve =
|
|
{.relativeTolerance = 0.0,
|
|
.absoluteTolerance = std::numeric_limits<double>::max(),
|
|
.maximumIterations = 1},
|
|
.backtracking = {.maximumTrials = 1}
|
|
},
|
|
SequencedMetric{metricState}
|
|
);
|
|
{
|
|
auto throwingSolver = solver::make(context, std::move(newton));
|
|
CHECK_THROWS_WITH(throwingSolver.evaluate(), "scripted preconditioner refresh failure");
|
|
}
|
|
CHECK(refreshProbe->refreshCalls == 2);
|
|
REQUIRE(context.isReady());
|
|
|
|
auto recoveredSolver =
|
|
solver::make(context, solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{}));
|
|
const auto recoveredReport = recoveredSolver.evaluate();
|
|
REQUIRE(recoveredReport.converged());
|
|
const auto recoveredView = recoveredReport.structureView();
|
|
const auto recovered = recoveredView.state();
|
|
REQUIRE(recovered.size() == baseline.size());
|
|
CHECK(std::equal(recovered.begin(), recovered.end(), baseline.begin(), baseline.end()));
|
|
}
|
|
|
|
TEST_CASE(
|
|
"The Production Stellar Context Reaches MFEM FGMRES Through The Default Newton Metric",
|
|
"[solver][newton][linear][fgmres][integration][wiring]"
|
|
) {
|
|
using namespace mean_field;
|
|
using namespace stellar_solver_architecture_test;
|
|
|
|
auto finiteElements = makeFiniteElements();
|
|
REQUIRE(finiteElements.okay());
|
|
auto context = solver::makeContext(
|
|
makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)},
|
|
preconditioning::makePreconditioner(), solver::linear::FGMRES({.restartLength = 4, .printLevel = -1})
|
|
);
|
|
auto equilibriumSolver = solver::make(
|
|
context, solver::nonlinear::Newton(
|
|
solver::nonlinear::NewtonOptions{
|
|
.relativeTolerance = 0.0,
|
|
.absoluteTolerance = 0.0,
|
|
.maximumIterations = 1,
|
|
.linearSolve = {.relativeTolerance = 1.0e-2, .absoluteTolerance = 0.0, .maximumIterations = 1},
|
|
.backtracking = {.maximumTrials = 1}
|
|
}
|
|
)
|
|
);
|
|
|
|
const auto report = equilibriumSolver.evaluate();
|
|
const auto &diagnostics = report.diagnostics();
|
|
CHECK(diagnostics.initialResidualNorm > 0.0);
|
|
CHECK(diagnostics.attemptedNonlinearIterations == 1);
|
|
REQUIRE(diagnostics.lastLinearSolve.has_value());
|
|
CHECK(diagnostics.lastLinearSolve->control.maximumIterations == 1);
|
|
CHECK(diagnostics.lastLinearSolve->operatorApplications > 0);
|
|
if (report.converged()) {
|
|
CHECK(report.structureView().valid());
|
|
} else {
|
|
CHECK(report.lastAcceptedCheckpointView().valid());
|
|
}
|
|
}
|