Files
MeanField/tests/solver/stellar_equilibrium_runtime.cpp

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());
}
}