#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 incomingCorrectionNorms; std::vector returnedCorrectionNorms; }; struct ScriptedBackend final : mean_field::solver::LinearBackendConfigurationTag { static constexpr mean_field::preconditioning::ApplicationContract supportedPreconditionerContract = mean_field::preconditioning::ApplicationContract::flexible; std::shared_ptr probe; double correctionValue{0.0}; bool resizeCorrection{false}; bool surfaceCorrectionOnly{false}; ScriptedBackend() = default; explicit ScriptedBackend( std::shared_ptr lifetimeProbe, const double scriptedCorrectionValue = 0.0, const bool resizeScriptedCorrection = false, const bool scriptOnlySurfaceCorrection = false ) : probe(std::move(lifetimeProbe)), correctionValue(scriptedCorrectionValue), resizeCorrection(resizeScriptedCorrection), surfaceCorrectionOnly(scriptOnlySurfaceCorrection) { } }; template class PreparedScriptedBackend final { public: PreparedScriptedBackend( const Operation &operation, Preconditioner &preconditioner, const MPI_Comm communicator, std::shared_ptr probe, const double correctionValue, const bool resizeCorrection, const bool surfaceCorrectionOnly ) : m_operation(std::addressof(operation)), m_preconditioner(std::addressof(preconditioner)), m_communicator(communicator), m_probe(std::move(probe)), m_correctionValue(correctionValue), m_resizeCorrection(resizeCorrection), m_surfaceCorrectionOnly(surfaceCorrectionOnly) { 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)); } if (m_surfaceCorrectionOnly) { mfem::Vector physicalCorrection(CorrectionSize()); physicalCorrection = 0.0; auto physicalDirection = m_operation->GetProblem().GetManifest().stateView(physicalCorrection); mfem::Vector surfaceDirection = physicalDirection.block(mean_field::utils::blocks::surface_deformation_field.parameters_term); surfaceDirection = m_correctionValue; m_operation->NormalizeState(physicalCorrection, correction); } else { 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::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 m_probe; double m_correctionValue; bool m_resizeCorrection; bool m_surfaceCorrectionOnly; }; template < typename Operation, typename Preconditioner> [[nodiscard]] auto prepareLinearBackend( ScriptedBackend configuration, const Operation &operation, Preconditioner &preconditioner, const MPI_Comm communicator ) { return PreparedScriptedBackend{ operation, preconditioner, communicator, std::move(configuration.probe), configuration.correctionValue, configuration.resizeCorrection, configuration.surfaceCorrectionOnly }; } 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 evaluations; std::size_t next{0}; }; struct SequencedMetric final { std::shared_ptr 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 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 probe; NonFiniteThenFailingBackend() = default; explicit NonFiniteThenFailingBackend(std::shared_ptr failureProbe) : probe(std::move(failureProbe)) { } }; template class PreparedNonFiniteThenFailingBackend final { public: PreparedNonFiniteThenFailingBackend( const Operation &operation, Preconditioner &preconditioner, const MPI_Comm communicator, std::shared_ptr 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::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 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, communicator, std::move(configuration.probe) }; } struct ThrowingRefreshProbe final { int refreshCalls{0}; }; struct ThrowingRefreshPrescription final : mean_field::preconditioning::StellarPreconditionerPrescriptionTag { std::shared_ptr probe; ThrowingRefreshPrescription() = default; explicit ThrowingRefreshPrescription(std::shared_ptr refreshProbe) : probe(std::move(refreshProbe)) { } }; template 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 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 m_probe; }; template [[nodiscard]] auto prepareStellarPreconditioner( ThrowingRefreshPrescription prescription, const Problem &problem ) { return ThrowingRefreshInverse{problem, std::move(prescription.probe)}; } struct ObserverRecord final { std::vector order; int beforeCalls{0}; int afterCalls{0}; int trialCalls{0}; std::vector dispositions; std::vector trialDispositions; std::vector trialRejectionSources; std::vector trialStepLengths; std::vector acceptedStepLengths; std::vector lineSearchTrials; std::vector> beforeNormalizedStates; std::vector> 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 * 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; 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; 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 concept HasSolutionAccessor = requires(Candidate &candidate) { candidate.solution(); }; template concept HasStructureViewAccessor = requires(Candidate &candidate) { candidate.structureView(); }; template concept MakesSolverFromRvalue = requires(Context &&context, Newton newton) { mean_field::solver::make(std::move(context), std::move(newton)); }; template concept MakesSolverWithObserver = requires(Context &context, Newton newton, Observer observer) { mean_field::solver::make(context, std::move(newton), std::move(observer)); }; template 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::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::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{-3.0}, true, backtracking ) == Approx(0.125) ); CHECK( solver::nonlinear::detail::NextBacktrackingStepLength( 1.0, 1.0, std::optional{-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{-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(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(communicatorSize)))); CHECK(coldReport.operatorApplications > 0); CHECK(coldReport.inversePreconditionerApplications > 0); CHECK(coldReport.iterations == static_cast(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(); const void *problemIdentity = nullptr; { auto context = solver::makeContext( makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)}, preconditioning::makePreconditioner(), ScriptedBackend{probe} ); using Context = std::remove_cvref_t; using Newton = decltype(solver::nonlinear::Newton(solver::nonlinear::NewtonOptions{}, ZeroMetric{})); using Report = typename Context::EvaluationReport; STATIC_CHECK_FALSE(std::copy_constructible); STATIC_CHECK_FALSE(std::move_constructible); STATIC_CHECK_FALSE(std::default_initializable); STATIC_CHECK_FALSE(std::default_initializable); STATIC_CHECK_FALSE(std::default_initializable); STATIC_CHECK_FALSE(ReadsDiagnosticsFromRvalue); STATIC_CHECK_FALSE(HasSolutionAccessor); STATIC_CHECK_FALSE(HasStructureViewAccessor); STATIC_CHECK_FALSE(MakesSolverFromRvalue); STATIC_CHECK_FALSE(MakesSolverWithObserver); STATIC_CHECK(solver::nonlinear::detail::ObservesAfterIteration); STATIC_CHECK_FALSE(solver::nonlinear::detail::ObservesBeforeIteration); STATIC_CHECK(solver::nonlinear::detail::ObservesLineSearchTrial); STATIC_CHECK_FALSE(solver::nonlinear::detail::ObservesAfterIteration); 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>); REQUIRE_FALSE(firstView.state().empty()); CHECK(std::ranges::all_of(firstView.state(), [](const auto value) { return std::isfinite(value); })); CHECK( firstView.model().template specification().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::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(); auto observer = solver::nonlinear::makeObserver( [record, token = std::make_unique(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::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{'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(); 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(); 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(); 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.geometryPreflightSeconds >= 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); REQUIRE(event.geometryPreflight.has_value()); CHECK(event.geometryPreflight->sampledQuadraturePointCount > 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::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().totalGeometryPreflightSeconds >= 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); REQUIRE(report.diagnostics().lastGeometryPreflight.has_value()); CHECK(report.diagnostics().lastGeometryPreflight->sampledQuadraturePointCount > 0); CHECK(metricState->next == metricState->evaluations.size()); REQUIRE(observerRecord->beforeCalls == 2); REQUIRE(observerRecord->afterCalls == 2); REQUIRE(observerRecord->trialCalls == 4); CHECK(observerRecord->order == std::vector{'B', 'A', 'B', 'A'}); CHECK( observerRecord->dispositions == std::vector{ solver::nonlinear::IterationDisposition::accepted, solver::nonlinear::IterationDisposition::converged } ); CHECK(observerRecord->lineSearchTrials == std::vector{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{"residual metric", "residual metric", "", ""} ); CHECK(observerRecord->trialStepLengths == std::vector{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( "Newton Geometry Preflight Caps A Surface Step Before Trial Preparation", "[solver][newton][geometry][preflight][backtracking][wiring]" ) { using namespace mean_field; using namespace stellar_solver_architecture_test; using Catch::Approx; auto finiteElements = makeFiniteElements(); REQUIRE(finiteElements.okay()); auto discretization = equilibrium::makeStellarDiscretization( std::move(finiteElements), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{utils::RADIUS}, utils::G} ); auto context = solver::makeContext( makeModel(), std::move(discretization), preconditioning::makePreconditioner(), ScriptedBackend{nullptr, -10.0, false, true} ); auto metricState = std::make_shared(); metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}}; std::optional observedPreflight; auto observer = solver::nonlinear::makeObserver( [](const solver::nonlinear::BeforeIteration &) { }, [&observedPreflight](const solver::nonlinear::AfterIteration &event) { observedPreflight = event.geometryPreflight; CHECK(event.geometryPreflightSeconds >= 0.0); CHECK(event.stepAccepted); CHECK(event.lineSearchTrials == 1); } ); auto newton = solver::nonlinear::Newton( solver::nonlinear::NewtonOptions{ .relativeTolerance = 0.0, .absoluteTolerance = 0.0, .maximumIterations = 1, .linearSolve = {.relativeTolerance = 0.0, .absoluteTolerance = std::numeric_limits::max(), .maximumIterations = 1}, .backtracking = {.initialStepLength = 1.0, .contractionFactor = 0.5, .fractionToBoundarySafety = 0.5, .sufficientDecrease = 1.0e-4, .minimumStepLength = 1.0e-8, .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.diagnostics().attemptedNonlinearIterations == 1); CHECK(report.diagnostics().acceptedNonlinearIterations == 1); CHECK(report.diagnostics().totalLineSearchTrials == 1); CHECK(report.diagnostics().inadmissibleLineSearchTrials == 0); CHECK(report.diagnostics().geometryLimitedIterations == 1); REQUIRE(report.diagnostics().lastGeometryPreflight.has_value()); const auto &preflight = *report.diagnostics().lastGeometryPreflight; CHECK(preflight.limitedByGeometry); CHECK(preflight.sampledQuadraturePointCount > 0); CHECK(preflight.minimumDeterminantAtAcceptedState > 0.0); CHECK(preflight.minimumDeterminantAtMaximumStepSize <= 0.0); CHECK(preflight.stepSize > 0.0); CHECK(preflight.stepSize < 1.0); CHECK(preflight.stepSize == Approx(0.5 * preflight.boundaryStepSize)); CHECK(report.diagnostics().lastAcceptedStepLength == Approx(preflight.stepSize)); REQUIRE(observedPreflight.has_value()); CHECK(observedPreflight->stepSize == Approx(preflight.stepSize)); CHECK(report.lastAcceptedCheckpointView().valid()); } 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(); metricState->evaluations = {{.residualNorm = 2.0, .merit = 2.0}, {.residualNorm = 1.0, .merit = 0.5}}; auto observerRecord = std::make_shared(); 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::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 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(0); auto options = solver::nonlinear::NewtonOptions{ .relativeTolerance = 0.0, .absoluteTolerance = 0.0, .maximumIterations = 1, .linearSolve = {.relativeTolerance = 0.0, .absoluteTolerance = std::numeric_limits::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 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(); 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::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(); auto context = solver::makeContext( makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)}, preconditioning::makePreconditioner(), NonFiniteThenFailingBackend{probe} ); auto metricState = std::make_shared(); 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(); 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(); auto context = solver::makeContext( makeModel(), equilibrium::StellarDiscretization{std::move(finiteElements)}, ThrowingRefreshPrescription{refreshProbe}, ScriptedBackend{nullptr, 1.0e-10} ); std::vector 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(); 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::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()); } }