792 lines
38 KiB
C++
792 lines
38 KiB
C++
#include <algorithm>
|
|
#include <cmath>
|
|
#include <concepts>
|
|
#include <cstdint>
|
|
#include <expected>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <utility>
|
|
|
|
#include <catch2/catch_approx.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <mfem.hpp>
|
|
#include <mpi.h>
|
|
|
|
import mean_field;
|
|
|
|
namespace coupled_trial_correction_test {
|
|
using namespace mean_field;
|
|
using namespace mean_field::solver;
|
|
|
|
class DenseOperation final : public mfem::Operator {
|
|
public:
|
|
explicit DenseOperation(mfem::DenseMatrix matrix)
|
|
: mfem::Operator(matrix.Height(), matrix.Width()), m_matrix(std::move(matrix)) { }
|
|
|
|
void Mult(const mfem::Vector &input, mfem::Vector &output) const override {
|
|
if (!prepared) throw std::logic_error("The test operator is not prepared.");
|
|
output.SetSize(Height());
|
|
m_matrix.Mult(input, output);
|
|
++applications;
|
|
}
|
|
void DenormalizeState(const mfem::Vector &normalized, mfem::Vector &physical) const { physical = normalized; }
|
|
void Prepare(mfem::DenseMatrix matrix) {
|
|
m_matrix = std::move(matrix);
|
|
prepared = true;
|
|
++generation;
|
|
}
|
|
[[nodiscard]] bool IsPrepared() const noexcept { return prepared; }
|
|
[[nodiscard]] std::uint64_t GetPreparationGeneration() const noexcept { return generation; }
|
|
[[nodiscard]] const DenseOperation &GetProblem() const noexcept { return *this; }
|
|
[[nodiscard]] const mfem::DenseMatrix &Matrix() const noexcept { return m_matrix; }
|
|
|
|
bool prepared{true};
|
|
mutable int applications{0};
|
|
std::uint64_t generation{1};
|
|
private:
|
|
mfem::DenseMatrix m_matrix;
|
|
};
|
|
|
|
class ExactInverse final : public mfem::Solver {
|
|
public:
|
|
static constexpr auto applicationContract = preconditioning::ApplicationContract::stationary_linear;
|
|
explicit ExactInverse(const DenseOperation &operation)
|
|
: mfem::Solver(operation.Width()), m_operation(operation) { Refresh(); }
|
|
void SetOperator(const mfem::Operator &) override { }
|
|
void Mult(const mfem::Vector &input, mfem::Vector &output) const override {
|
|
if (!IsCurrent()) throw std::logic_error("The test inverse is stale.");
|
|
output.SetSize(Height());
|
|
if (Height() > 0) {
|
|
mfem::DenseMatrixInverse inverse(m_operation.Matrix());
|
|
inverse.Mult(input, output);
|
|
if (Height() > 1) output(1) += extraSecondComponent * input(0);
|
|
}
|
|
++applications;
|
|
}
|
|
void Refresh() { m_generation = m_operation.GetPreparationGeneration(); }
|
|
[[nodiscard]] bool IsCurrent() const noexcept {
|
|
return m_operation.IsPrepared() && m_generation == m_operation.GetPreparationGeneration();
|
|
}
|
|
mutable int applications{0};
|
|
double extraSecondComponent{0.0};
|
|
private:
|
|
const DenseOperation &m_operation;
|
|
std::uint64_t m_generation{};
|
|
};
|
|
|
|
[[nodiscard]] mfem::DenseMatrix identity(const int size) {
|
|
mfem::DenseMatrix matrix(size);
|
|
matrix = 0.0;
|
|
for (int i = 0; i < size; ++i) matrix(i, i) = 1.0;
|
|
return matrix;
|
|
}
|
|
|
|
[[nodiscard]] mfem::Vector coupledResidual(const mfem::Vector &state) {
|
|
mfem::Vector result(3);
|
|
result(0) = 1.0 + state(0) + 10.0 * state(1) * state(2);
|
|
result(1) = 1.0 + state(1);
|
|
result(2) = 1.0 + state(2);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] mfem::DenseMatrix coupledJacobian(const mfem::Vector &state) {
|
|
auto matrix = identity(3);
|
|
matrix(0, 1) = 10.0 * state(2);
|
|
matrix(0, 2) = 10.0 * state(1);
|
|
return matrix;
|
|
}
|
|
|
|
[[nodiscard]] CoupledTrialCorrectionOptions exactOptions() {
|
|
return {.control = {.relativeTolerance = 1.e-12, .absoluteTolerance = 1.e-14, .maximumIterations = 6},
|
|
.backend = {.restartLength = 4, .printLevel = -1}, .maximumCorrectionRatio = 4.0};
|
|
}
|
|
|
|
enum class BackendBehavior { exact, inaccurate, failed_status, non_finite };
|
|
struct BackendProbe {
|
|
BackendBehavior behavior{BackendBehavior::exact};
|
|
int calls{0};
|
|
bool allInitialGuessesZero{true};
|
|
};
|
|
struct TestBackend final : LinearBackendConfigurationTag {
|
|
static constexpr auto supportedPreconditionerContract = preconditioning::ApplicationContract::flexible;
|
|
std::shared_ptr<BackendProbe> probe;
|
|
explicit TestBackend(std::shared_ptr<BackendProbe> value) : probe(std::move(value)) { }
|
|
};
|
|
|
|
template <typename Operation, typename Inverse>
|
|
class PreparedTestBackend final {
|
|
public:
|
|
PreparedTestBackend(const Operation &operation, Inverse &inverse, const MPI_Comm communicator,
|
|
std::shared_ptr<BackendProbe> probe)
|
|
: m_operation(operation), m_inverse(inverse), m_communicator(communicator), m_probe(std::move(probe)) { }
|
|
[[nodiscard]] const Operation &GetOperator() const noexcept { return m_operation; }
|
|
[[nodiscard]] const Inverse &GetPreconditioner() const noexcept { return m_inverse; }
|
|
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept { return m_communicator; }
|
|
[[nodiscard]] bool IsReady() const noexcept { return m_communicator != MPI_COMM_NULL; }
|
|
[[nodiscard]] int RightHandSideSize() const noexcept { return m_operation.Height(); }
|
|
[[nodiscard]] int CorrectionSize() const noexcept { return m_operation.Width(); }
|
|
[[nodiscard]] LinearSolveReport Solve(const mfem::Vector &rhs, mfem::Vector &correction,
|
|
const LinearSolveControl &control) {
|
|
++m_probe->calls;
|
|
for (int i = 0; i < correction.Size(); ++i)
|
|
m_probe->allInitialGuessesZero &= correction(i) == 0.0;
|
|
m_inverse.Mult(rhs, correction);
|
|
if (m_probe->behavior == BackendBehavior::inaccurate) correction *= 0.5;
|
|
if (m_probe->behavior == BackendBehavior::non_finite && correction.Size() > 0)
|
|
correction(0) = std::numeric_limits<double>::quiet_NaN();
|
|
double local = rhs * rhs, global = 0.0;
|
|
if (MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, m_communicator) != MPI_SUCCESS)
|
|
throw std::runtime_error("The test backend could not reduce its norm.");
|
|
// A backend claiming exact convergence is deliberately insufficient
|
|
// evidence: the correction owner must verify the actual J action.
|
|
return {.status = m_probe->behavior == BackendBehavior::failed_status ?
|
|
LinearSolveStatus::maximum_iterations : LinearSolveStatus::converged,
|
|
.control = control, .iterations = 1, .rightHandSideNorm = std::sqrt(global),
|
|
.trueResidualNorm = 0.0, .relativeTrueResidualNorm = 0.0};
|
|
}
|
|
private:
|
|
const Operation &m_operation;
|
|
Inverse &m_inverse;
|
|
MPI_Comm m_communicator;
|
|
std::shared_ptr<BackendProbe> m_probe;
|
|
};
|
|
|
|
template <typename Operation, typename Inverse>
|
|
[[nodiscard]] auto prepareLinearBackend(TestBackend recipe, const Operation &operation, Inverse &inverse,
|
|
const MPI_Comm communicator) {
|
|
return PreparedTestBackend<Operation, Inverse>(operation, inverse, communicator, std::move(recipe.probe));
|
|
}
|
|
|
|
static_assert(CoupledTrialOperation<DenseOperation>);
|
|
static_assert(CoupledTrialInverse<ExactInverse>);
|
|
static_assert(!CoupledTrialOperation<mfem::Operator>);
|
|
static_assert(!CoupledTrialInverse<mfem::Solver>);
|
|
static_assert(LinearBackendRuntimeAvailableFor<TestBackend, DenseOperation, ExactInverse>);
|
|
using ScriptedCorrection = PreparedCoupledTrialCorrection<DenseOperation, ExactInverse, TestBackend>;
|
|
} // namespace coupled_trial_correction_test
|
|
|
|
TEST_CASE("A coupled trial correction preserves the tangent and removes mixed nonlinear curvature",
|
|
"[solver][coupled_trial_correction]") {
|
|
using namespace coupled_trial_correction_test;
|
|
using Catch::Approx;
|
|
mfem::Vector base(3), direction(3), correction(3), prediction(3);
|
|
base = 0.0;
|
|
direction = -1.0;
|
|
const auto baseResidual = coupledResidual(base);
|
|
DenseOperation operation(coupledJacobian(base));
|
|
ExactInverse inverse(operation);
|
|
PreparedCoupledTrialCorrection prepared(operation, inverse, MPI_COMM_SELF, exactOptions());
|
|
prepared.CaptureBase(baseResidual, direction);
|
|
const auto capturedGeneration = prepared.GetBaseGeneration();
|
|
CHECK(prepared.GetBaseDirectionNorm() == Approx(std::sqrt(3.0)));
|
|
double previousScaledDisplacement = 0.0;
|
|
for (const double alpha : {.5, .25, .125, .0625}) {
|
|
CAPTURE(alpha);
|
|
mfem::Vector trial(direction);
|
|
trial *= alpha;
|
|
const auto rawResidual = coupledResidual(trial);
|
|
operation.Prepare(coupledJacobian(trial));
|
|
inverse.Refresh();
|
|
correction = 17.0;
|
|
const auto report = prepared.SolveCorrection(alpha, rawResidual, correction);
|
|
REQUIRE(report.Admissible());
|
|
REQUIRE(report.verifiedGate);
|
|
CHECK(report.linearSolve.Converged());
|
|
CHECK(report.linearSolve.iterations <= exactOptions().control.maximumIterations);
|
|
CHECK(report.verifiedTrueResidualNorm <= report.convergenceThreshold);
|
|
CHECK(correction(0) == Approx(-10.0 * alpha * alpha).margin(1.e-13));
|
|
CHECK(std::abs(correction(1)) < 1.e-13);
|
|
CHECK(std::abs(correction(2)) < 1.e-13);
|
|
trial += correction;
|
|
const auto correctedResidual = coupledResidual(trial);
|
|
prepared.BuildLinearPrediction(alpha, prediction);
|
|
mfem::Vector remainder(correctedResidual);
|
|
remainder -= prediction;
|
|
CHECK(remainder.Norml2() < 1.e-12);
|
|
CHECK(correctedResidual.Norml2() < rawResidual.Norml2());
|
|
if (alpha == .5) {
|
|
CHECK(rawResidual.Norml2() > baseResidual.Norml2());
|
|
CHECK(correctedResidual.Norml2() == Approx(.5 * baseResidual.Norml2()));
|
|
}
|
|
trial /= alpha;
|
|
trial -= direction;
|
|
const double scaledDisplacement = trial.Norml2();
|
|
CHECK(scaledDisplacement == Approx(10.0 * alpha));
|
|
if (previousScaledDisplacement > 0.0)
|
|
CHECK(scaledDisplacement == Approx(previousScaledDisplacement / 2.0));
|
|
previousScaledDisplacement = scaledDisplacement;
|
|
CHECK(prepared.GetBaseGeneration() == capturedGeneration);
|
|
for (int i = 0; i < 3; ++i) CHECK(direction(i) == -1.0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Coupled correction captures the base action while solving with the current trial Jacobian",
|
|
"[solver][coupled_trial_correction][lifetime]") {
|
|
using namespace coupled_trial_correction_test;
|
|
using Catch::Approx;
|
|
DenseOperation operation(identity(2));
|
|
ExactInverse inverse(operation);
|
|
auto probe = std::make_shared<BackendProbe>();
|
|
ScriptedCorrection prepared(operation, inverse, MPI_COMM_SELF, exactOptions(), TestBackend(probe));
|
|
mfem::Vector f(2), p(2), prediction(2), correction(2), trialResidual(2);
|
|
f = 1.0;
|
|
p = -1.0;
|
|
prepared.CaptureBase(f, p);
|
|
const auto generation = prepared.GetBaseGeneration();
|
|
auto matrix = identity(2);
|
|
matrix(0, 0) = 2.0;
|
|
matrix(1, 1) = 4.0;
|
|
operation.Prepare(matrix);
|
|
const int applications = operation.applications;
|
|
prepared.BuildLinearPrediction(.5, prediction);
|
|
CHECK(operation.applications == applications);
|
|
CHECK(prediction(0) == .5);
|
|
CHECK(prediction(1) == .5);
|
|
trialResidual = prediction;
|
|
trialResidual(0) += .2;
|
|
trialResidual(1) += .4;
|
|
REQUIRE_THROWS_AS(prepared.SolveCorrection(.5, trialResidual, correction), std::invalid_argument);
|
|
inverse.Refresh();
|
|
const auto report = prepared.SolveCorrection(.5, trialResidual, correction);
|
|
REQUIRE(report.Admissible());
|
|
CHECK(correction(0) == Approx(-.1));
|
|
CHECK(correction(1) == Approx(-.1));
|
|
CHECK(prepared.GetBaseGeneration() == generation);
|
|
CHECK(probe->allInitialGuessesZero);
|
|
prepared.Invalidate();
|
|
CHECK_FALSE(prepared.HasBase());
|
|
CHECK_THROWS_AS(prepared.BuildLinearPrediction(.5, prediction), std::invalid_argument);
|
|
}
|
|
|
|
TEST_CASE("Coupled correction independently rejects a false auxiliary convergence report",
|
|
"[solver][coupled_trial_correction][failure]") {
|
|
using namespace coupled_trial_correction_test;
|
|
DenseOperation operation(identity(2));
|
|
ExactInverse inverse(operation);
|
|
auto probe = std::make_shared<BackendProbe>();
|
|
ScriptedCorrection prepared(operation, inverse, MPI_COMM_SELF, exactOptions(), TestBackend(probe));
|
|
mfem::Vector f(2), p(2), trial(2), correction(2);
|
|
f = 1.0;
|
|
p = -1.0;
|
|
trial = .75;
|
|
prepared.CaptureBase(f, p);
|
|
SECTION("The returned correction fails the independently computed true gate") {
|
|
probe->behavior = BackendBehavior::inaccurate;
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK_FALSE(report.Admissible());
|
|
CHECK(report.status == CoupledTrialCorrectionStatus::linear_failure);
|
|
CHECK(report.linearSolve.Converged());
|
|
CHECK_FALSE(report.verifiedGate);
|
|
CHECK(report.verifiedTrueResidualNorm > report.convergenceThreshold);
|
|
}
|
|
SECTION("An accurate correction does not erase backend nonconvergence") {
|
|
probe->behavior = BackendBehavior::failed_status;
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK_FALSE(report.Admissible());
|
|
CHECK(report.status == CoupledTrialCorrectionStatus::linear_failure);
|
|
CHECK_FALSE(report.linearSolve.Converged());
|
|
}
|
|
SECTION("A nonfinite returned correction is never admissible") {
|
|
probe->behavior = BackendBehavior::non_finite;
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK_FALSE(report.Admissible());
|
|
CHECK(report.status == CoupledTrialCorrectionStatus::non_finite);
|
|
CHECK_FALSE(report.verifiedGate);
|
|
}
|
|
CHECK(probe->calls == 1);
|
|
CHECK(probe->allInitialGuessesZero);
|
|
for (int i = 0; i < 2; ++i) {
|
|
CHECK(f(i) == 1.0);
|
|
CHECK(p(i) == -1.0);
|
|
CHECK(trial(i) == .75);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Coupled correction bounds work and correction size without changing the Newton direction",
|
|
"[solver][coupled_trial_correction][safeguard]") {
|
|
using namespace coupled_trial_correction_test;
|
|
using Catch::Approx;
|
|
DenseOperation operation(identity(2));
|
|
ExactInverse inverse(operation);
|
|
auto options = exactOptions();
|
|
options.maximumCorrectionRatio = .1;
|
|
auto probe = std::make_shared<BackendProbe>();
|
|
ScriptedCorrection prepared(operation, inverse, MPI_COMM_SELF, options, TestBackend(probe));
|
|
mfem::Vector f(2), p(2), trial(2), correction(2);
|
|
f = 1.0;
|
|
p = -1.0;
|
|
trial = .9;
|
|
prepared.CaptureBase(f, p);
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK(report.status == CoupledTrialCorrectionStatus::norm_limit);
|
|
CHECK_FALSE(report.Admissible());
|
|
CHECK(report.verifiedGate);
|
|
CHECK(report.maximumCorrectionNorm == Approx(.1 * .5 * std::sqrt(2.0)));
|
|
CHECK(report.correctionNorm > report.maximumCorrectionNorm);
|
|
CHECK(report.linearSolve.iterations <= options.control.maximumIterations);
|
|
CHECK(probe->calls == 1);
|
|
CHECK(probe->allInitialGuessesZero);
|
|
CHECK(p(0) == -1.0);
|
|
CHECK(p(1) == -1.0);
|
|
}
|
|
|
|
TEST_CASE("Zero coupled remainder does not manufacture an auxiliary displacement",
|
|
"[solver][coupled_trial_correction][safeguard]") {
|
|
using namespace coupled_trial_correction_test;
|
|
DenseOperation operation(identity(2));
|
|
ExactInverse inverse(operation);
|
|
auto probe = std::make_shared<BackendProbe>();
|
|
ScriptedCorrection prepared(operation, inverse, MPI_COMM_SELF, exactOptions(), TestBackend(probe));
|
|
mfem::Vector f(2), p(2), trial(2), correction(2);
|
|
f = 1.0;
|
|
p = -1.0;
|
|
trial = .5;
|
|
correction = 900.0;
|
|
prepared.CaptureBase(f, p);
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK(report.Admissible());
|
|
CHECK(report.verifiedGate);
|
|
CHECK(correction.Norml2() == 0.0);
|
|
CHECK(report.correctionNorm == 0.0);
|
|
CHECK(report.verifiedTrueResidualNorm == 0.0);
|
|
CHECK(probe->calls <= 1);
|
|
CHECK(probe->allInitialGuessesZero);
|
|
}
|
|
|
|
TEST_CASE("Coupled correction rejects invalid controls before numerical work",
|
|
"[solver][coupled_trial_correction][configuration]") {
|
|
using namespace coupled_trial_correction_test;
|
|
auto options = exactOptions();
|
|
SECTION("A nonfinite cap cannot define a bounded correction") {
|
|
options.maximumCorrectionRatio = std::numeric_limits<double>::infinity();
|
|
}
|
|
SECTION("A negative cap has no feasible correction size") { options.maximumCorrectionRatio = -1.0; }
|
|
SECTION("A zero cap cannot enable the correction strategy") { options.maximumCorrectionRatio = 0.0; }
|
|
SECTION("The auxiliary iteration budget must be finite and positive") { options.control.maximumIterations = 0; }
|
|
SECTION("The auxiliary tolerance cannot be NaN") {
|
|
options.control.relativeTolerance = std::numeric_limits<double>::quiet_NaN();
|
|
}
|
|
CHECK_THROWS_AS(options.Validate(), std::invalid_argument);
|
|
}
|
|
|
|
TEST_CASE("Coupled correction decisions remain collective with an empty peer rank",
|
|
"[solver][coupled_trial_correction][mpi]") {
|
|
using namespace coupled_trial_correction_test;
|
|
using Catch::Approx;
|
|
int rank = 0;
|
|
REQUIRE(MPI_Comm_rank(MPI_COMM_WORLD, &rank) == MPI_SUCCESS);
|
|
// Only one rank owns this tiny synthetic problem. A distributed stop must
|
|
// not rely on every rank having a local entry to inspect.
|
|
const int localSize = rank == 0 ? 2 : 0;
|
|
DenseOperation operation(identity(localSize));
|
|
ExactInverse inverse(operation);
|
|
auto probe = std::make_shared<BackendProbe>();
|
|
ScriptedCorrection prepared(operation, inverse, MPI_COMM_WORLD, exactOptions(), TestBackend(probe));
|
|
mfem::Vector f(localSize), p(localSize), trial(localSize), correction(localSize);
|
|
f = 1.0;
|
|
p = -1.0;
|
|
trial = .75;
|
|
prepared.CaptureBase(f, p);
|
|
SECTION("Finite global norms include the owning rank exactly once") {
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK(report.Admissible());
|
|
CHECK(report.correctionNorm == Approx(std::sqrt(2.0) * .25));
|
|
CHECK(probe->calls == 1);
|
|
}
|
|
SECTION("One nonfinite trial prevents the backend call on every rank") {
|
|
if (rank == 0) trial(0) = std::numeric_limits<double>::quiet_NaN();
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK(report.status == CoupledTrialCorrectionStatus::non_finite);
|
|
CHECK_FALSE(report.Admissible());
|
|
CHECK(probe->calls == 0);
|
|
}
|
|
SECTION("One nonfinite correction fails the independently checked global gate") {
|
|
probe->behavior = BackendBehavior::non_finite;
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK(report.status == CoupledTrialCorrectionStatus::non_finite);
|
|
CHECK_FALSE(report.Admissible());
|
|
CHECK_FALSE(report.verifiedGate);
|
|
CHECK(probe->calls == 1);
|
|
}
|
|
}
|
|
|
|
namespace coupled_trial_correction_test {
|
|
struct ToyProblem final {
|
|
MPI_Comm communicator{MPI_COMM_SELF};
|
|
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept { return communicator; }
|
|
};
|
|
|
|
struct ToyState final {
|
|
DenseOperation operation{identity(3)};
|
|
ExactInverse inverse{operation};
|
|
DenseOperation *normalizedOperator{&operation};
|
|
ExactInverse *normalizedInverse{&inverse};
|
|
ExactInverse *physicalInverse{&inverse};
|
|
mfem::Vector trialNormalizedState = mfem::Vector(3);
|
|
mfem::Vector trialNormalizedResidual = mfem::Vector(3);
|
|
mfem::Vector candidatePhysicalState = mfem::Vector(3);
|
|
mfem::Vector acceptedState = mfem::Vector(3);
|
|
mfem::Vector acceptedResidual = mfem::Vector(3);
|
|
mfem::Vector normalizedCorrection = mfem::Vector(3);
|
|
LinearSolveReport outerReport{.status = LinearSolveStatus::converged, .iterations = 11,
|
|
.trueResidualNorm = .0123};
|
|
ToyProblem problem{};
|
|
int preparations{0};
|
|
bool quarticBarrier{false}, rejectCorrectedPreparation{false}, nonfiniteCorrectedResidual{false};
|
|
bool throwCorrectedPreparation{false};
|
|
|
|
ToyState() {
|
|
trialNormalizedState = 0.0;
|
|
candidatePhysicalState = 0.0;
|
|
trialNormalizedResidual = 1.0;
|
|
acceptedState = 0.0;
|
|
acceptedResidual = 1.0;
|
|
normalizedCorrection = -1.0;
|
|
}
|
|
[[nodiscard]] ToyProblem &Problem() noexcept { return problem; }
|
|
[[nodiscard]] const ToyProblem &Problem() const noexcept { return problem; }
|
|
[[nodiscard]] std::expected<void, int> PrepareTrial() {
|
|
++preparations;
|
|
candidatePhysicalState = trialNormalizedState;
|
|
if (throwCorrectedPreparation && trialNormalizedState(0) < -2.0) {
|
|
operation.prepared = false;
|
|
throw std::runtime_error("Synthetic corrected preparation failure.");
|
|
}
|
|
if (rejectCorrectedPreparation && trialNormalizedState(0) < -2.0) {
|
|
operation.prepared = false;
|
|
return std::unexpected(7);
|
|
}
|
|
trialNormalizedResidual = coupledResidual(trialNormalizedState);
|
|
auto matrix = coupledJacobian(trialNormalizedState);
|
|
if (quarticBarrier) {
|
|
// The extra term and its derivative vanish at the base and at
|
|
// alpha=.5. It makes the solved correction harmful without
|
|
// supplying the transaction an inconsistent trial Jacobian.
|
|
const double x = trialNormalizedState(0);
|
|
trialNormalizedResidual(0) += x*x*(x+.5)*(x+.5);
|
|
matrix(0, 0) += 2.0*x*(x+.5)*(2.0*x+.5);
|
|
}
|
|
operation.Prepare(matrix);
|
|
if (nonfiniteCorrectedResidual && trialNormalizedState(0) < -2.0)
|
|
trialNormalizedResidual(0) = std::numeric_limits<double>::quiet_NaN();
|
|
return {};
|
|
}
|
|
[[nodiscard]] std::expected<void, int> PrepareAlpha(const double alpha) {
|
|
trialNormalizedState = normalizedCorrection;
|
|
trialNormalizedState *= alpha;
|
|
return PrepareTrial();
|
|
}
|
|
};
|
|
|
|
[[nodiscard]] nonlinear::MetricEvaluation toyMetric(const mfem::Vector &residual) {
|
|
const double norm = residual.Norml2();
|
|
return {.residualNorm = norm, .merit = .5*norm*norm};
|
|
}
|
|
[[nodiscard]] bool toyAccepts(const nonlinear::MetricEvaluation &metric) {
|
|
return std::isfinite(metric.merit) && metric.merit < 1.5;
|
|
}
|
|
[[nodiscard]] bool toyImproves(const nonlinear::MetricEvaluation &candidate,
|
|
const nonlinear::MetricEvaluation &original) {
|
|
return std::isfinite(candidate.merit) && candidate.merit < original.merit;
|
|
}
|
|
[[nodiscard]] bool admissibleToy(const mfem::Vector &) { return true; }
|
|
|
|
void checkOuterData(const ToyState &state) {
|
|
for (int i = 0; i < 3; ++i) {
|
|
CHECK(state.acceptedState(i) == 0.0);
|
|
CHECK(state.acceptedResidual(i) == 1.0);
|
|
CHECK(state.normalizedCorrection(i) == -1.0);
|
|
}
|
|
CHECK(state.outerReport.status == LinearSolveStatus::converged);
|
|
CHECK(state.outerReport.iterations == 11);
|
|
CHECK(state.outerReport.trueResidualNorm == .0123);
|
|
}
|
|
void checkRestoredTrial(const ToyState &state, const mfem::Vector &originalState,
|
|
const mfem::Vector &originalResidual, const mfem::Vector &originalAction) {
|
|
for (int i = 0; i < 3; ++i) {
|
|
CHECK(state.trialNormalizedState(i) == originalState(i));
|
|
CHECK(state.candidatePhysicalState(i) == originalState(i));
|
|
CHECK(state.trialNormalizedResidual(i) == originalResidual(i));
|
|
}
|
|
CHECK(state.operation.IsPrepared());
|
|
CHECK(state.inverse.IsCurrent());
|
|
mfem::Vector action(3);
|
|
state.operation.Mult(state.normalizedCorrection, action);
|
|
for (int i = 0; i < 3; ++i) CHECK(action(i) == originalAction(i));
|
|
checkOuterData(state);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("The production coupled transaction keeps an improved full residual and leaves outer data alone",
|
|
"[solver][coupled_trial_correction][transaction]") {
|
|
using namespace coupled_trial_correction_test;
|
|
using Catch::Approx;
|
|
ToyState state;
|
|
solver::detail::PreparedCoupledTrialTransaction transaction(state, exactOptions());
|
|
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
|
auto preparation = state.PrepareAlpha(.5);
|
|
REQUIRE(preparation.has_value());
|
|
auto metric = toyMetric(state.trialNormalizedResidual);
|
|
transaction.BeginTrial();
|
|
transaction.TryImprove(state, .5, preparation, metric, toyMetric, toyAccepts, toyImproves, admissibleToy);
|
|
const auto &diagnostics = transaction.GetDiagnostics();
|
|
CHECK(diagnostics.status == CoupledTrialAttemptStatus::kept);
|
|
REQUIRE(diagnostics.currentSolve.has_value());
|
|
CHECK(diagnostics.currentSolve->Admissible());
|
|
CHECK(diagnostics.currentSolve->verifiedGate);
|
|
CHECK(diagnostics.attempts == 1);
|
|
CHECK(diagnostics.kept == 1);
|
|
CHECK(diagnostics.fallbacks == 0);
|
|
CHECK(diagnostics.preparations == 1);
|
|
CHECK(diagnostics.restorations == 0);
|
|
CHECK(metric.merit == Approx(.375));
|
|
CHECK(metric.merit < diagnostics.originalMerit);
|
|
REQUIRE(preparation.has_value());
|
|
for (int i = 0; i < 3; ++i) CHECK(state.trialNormalizedResidual(i) == Approx(.5));
|
|
checkOuterData(state);
|
|
transaction.BeginTrial();
|
|
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::not_attempted);
|
|
CHECK_FALSE(transaction.GetDiagnostics().currentSolve.has_value());
|
|
CHECK(std::isnan(transaction.GetDiagnostics().originalMerit));
|
|
CHECK(transaction.GetDiagnostics().attempts == 1);
|
|
}
|
|
|
|
TEST_CASE("The production coupled transaction restores rejected corrections and their prepared operators",
|
|
"[solver][coupled_trial_correction][transaction][rollback]") {
|
|
using namespace coupled_trial_correction_test;
|
|
ToyState state;
|
|
bool rejectAdmissibility = false;
|
|
CoupledTrialAttemptStatus expected = CoupledTrialAttemptStatus::inadmissible;
|
|
SECTION("Corrected physical preparation rejects the candidate") { state.rejectCorrectedPreparation = true; }
|
|
SECTION("Corrected polynomial admissibility rejects before preparation") { rejectAdmissibility = true; }
|
|
SECTION("A consistent nonlinear map makes the solved correction worse") {
|
|
state.quarticBarrier = true;
|
|
expected = CoupledTrialAttemptStatus::not_improved;
|
|
}
|
|
SECTION("A successfully prepared nonfinite residual is unusable") {
|
|
state.nonfiniteCorrectedResidual = true;
|
|
expected = CoupledTrialAttemptStatus::non_finite;
|
|
}
|
|
solver::detail::PreparedCoupledTrialTransaction transaction(state, exactOptions());
|
|
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
|
auto preparation = state.PrepareAlpha(.5);
|
|
REQUIRE(preparation.has_value());
|
|
const mfem::Vector originalState(state.trialNormalizedState), originalResidual(state.trialNormalizedResidual);
|
|
mfem::Vector originalAction(3);
|
|
state.operation.Mult(state.normalizedCorrection, originalAction);
|
|
auto metric = toyMetric(originalResidual);
|
|
const double originalMerit = metric.merit;
|
|
transaction.BeginTrial();
|
|
transaction.TryImprove(state, .5, preparation, metric, toyMetric, toyAccepts, toyImproves,
|
|
[rejectAdmissibility](const mfem::Vector &candidate) { return !rejectAdmissibility || candidate(0) >= -2.0; });
|
|
const auto &diagnostics = transaction.GetDiagnostics();
|
|
CHECK(diagnostics.status == expected);
|
|
REQUIRE(diagnostics.currentSolve.has_value());
|
|
CHECK(diagnostics.currentSolve->Admissible());
|
|
CHECK(diagnostics.attempts == 1);
|
|
CHECK(diagnostics.kept == 0);
|
|
CHECK(diagnostics.fallbacks == 1);
|
|
CHECK(diagnostics.restorations == 1);
|
|
CHECK(diagnostics.preparations == (rejectAdmissibility ? 1 : 2));
|
|
CHECK(metric.merit == originalMerit);
|
|
if (expected == CoupledTrialAttemptStatus::not_improved)
|
|
CHECK(diagnostics.correctedMerit > diagnostics.originalMerit);
|
|
REQUIRE(preparation.has_value());
|
|
checkRestoredTrial(state, originalState, originalResidual, originalAction);
|
|
}
|
|
|
|
TEST_CASE("The coupled transaction skips acceptable trials and bounds attempts per captured direction",
|
|
"[solver][coupled_trial_correction][transaction][budget]") {
|
|
using namespace coupled_trial_correction_test;
|
|
ToyState state;
|
|
state.quarticBarrier = true;
|
|
solver::detail::PreparedCoupledTrialTransaction transaction(state, exactOptions(), 1);
|
|
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
|
auto preparation = state.PrepareAlpha(.05);
|
|
auto metric = toyMetric(state.trialNormalizedResidual);
|
|
REQUIRE(toyAccepts(metric));
|
|
const int before = state.preparations;
|
|
transaction.BeginTrial();
|
|
transaction.TryImprove(state, .05, preparation, metric, toyMetric, toyAccepts, toyImproves, admissibleToy);
|
|
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::original_accepted);
|
|
CHECK(transaction.GetDiagnostics().attempts == 0);
|
|
CHECK(state.preparations == before);
|
|
CHECK_FALSE(transaction.GetDiagnostics().currentSolve.has_value());
|
|
|
|
preparation = state.PrepareAlpha(.5);
|
|
metric = toyMetric(state.trialNormalizedResidual);
|
|
transaction.BeginTrial();
|
|
transaction.TryImprove(state, .5, preparation, metric, toyMetric, toyAccepts, toyImproves, admissibleToy);
|
|
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::not_improved);
|
|
CHECK(transaction.GetDiagnostics().attempts == 1);
|
|
const auto iterations = transaction.GetDiagnostics().iterations;
|
|
transaction.BeginTrial();
|
|
transaction.TryImprove(state, .5, preparation, metric, toyMetric, toyAccepts, toyImproves, admissibleToy);
|
|
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::budget_exhausted);
|
|
CHECK(transaction.GetDiagnostics().attempts == 1);
|
|
CHECK(transaction.GetDiagnostics().iterations == iterations);
|
|
CHECK_FALSE(transaction.GetDiagnostics().currentSolve.has_value());
|
|
checkOuterData(state);
|
|
}
|
|
|
|
TEST_CASE("A rank-local corrected admissibility rejection restores the original trial everywhere",
|
|
"[solver][coupled_trial_correction][transaction][rollback][mpi]") {
|
|
using namespace coupled_trial_correction_test;
|
|
int rank = 0;
|
|
REQUIRE(MPI_Comm_rank(MPI_COMM_WORLD, &rank) == MPI_SUCCESS);
|
|
ToyState state;
|
|
state.problem.communicator = MPI_COMM_WORLD;
|
|
solver::detail::PreparedCoupledTrialTransaction transaction(state, exactOptions());
|
|
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
|
auto preparation = state.PrepareAlpha(.5);
|
|
const mfem::Vector originalState(state.trialNormalizedState), originalResidual(state.trialNormalizedResidual);
|
|
mfem::Vector originalAction(3);
|
|
state.operation.Mult(state.normalizedCorrection, originalAction);
|
|
auto metric = toyMetric(originalResidual);
|
|
transaction.BeginTrial();
|
|
transaction.TryImprove(state, .5, preparation, metric, toyMetric, toyAccepts, toyImproves,
|
|
[rank](const mfem::Vector &) { return rank != 0; });
|
|
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::inadmissible);
|
|
CHECK(transaction.GetDiagnostics().restorations == 1);
|
|
checkRestoredTrial(state, originalState, originalResidual, originalAction);
|
|
}
|
|
|
|
TEST_CASE("A coupled auxiliary gate or cap failure preserves the already prepared original candidate",
|
|
"[solver][coupled_trial_correction][transaction][failure]") {
|
|
using namespace coupled_trial_correction_test;
|
|
ToyState state;
|
|
auto options = exactOptions();
|
|
auto expected = CoupledTrialAttemptStatus::norm_limit;
|
|
SECTION("A correction outside the cap is rejected without mutation") {
|
|
options.maximumCorrectionRatio = .1;
|
|
}
|
|
SECTION("A bounded unsuccessful Krylov solve is rejected without mutation") {
|
|
// This stationary perturbation prevents one Krylov vector from spanning
|
|
// the exact correction. The failure comes from a real verified solve.
|
|
state.inverse.extraSecondComponent = .1;
|
|
options.control.maximumIterations = 1;
|
|
options.backend.restartLength = 1;
|
|
expected = CoupledTrialAttemptStatus::linear_failure;
|
|
}
|
|
solver::detail::PreparedCoupledTrialTransaction transaction(state, options);
|
|
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
|
auto preparation = state.PrepareAlpha(.5);
|
|
const mfem::Vector originalState(state.trialNormalizedState), originalResidual(state.trialNormalizedResidual);
|
|
mfem::Vector originalAction(3);
|
|
state.operation.Mult(state.normalizedCorrection, originalAction);
|
|
auto metric = toyMetric(originalResidual);
|
|
transaction.BeginTrial();
|
|
transaction.TryImprove(state, .5, preparation, metric, toyMetric, toyAccepts, toyImproves, admissibleToy);
|
|
const auto &diagnostics = transaction.GetDiagnostics();
|
|
CHECK(diagnostics.status == expected);
|
|
REQUIRE(diagnostics.currentSolve.has_value());
|
|
CHECK_FALSE(diagnostics.currentSolve->Admissible());
|
|
CHECK(diagnostics.attempts == 1);
|
|
CHECK(diagnostics.fallbacks == 1);
|
|
CHECK(diagnostics.preparations == 0);
|
|
CHECK(diagnostics.restorations == 0);
|
|
CHECK(diagnostics.iterations <= static_cast<std::uint64_t>(options.control.maximumIterations));
|
|
if (expected == CoupledTrialAttemptStatus::linear_failure)
|
|
CHECK_FALSE(diagnostics.currentSolve->verifiedGate);
|
|
checkRestoredTrial(state, originalState, originalResidual, originalAction);
|
|
}
|
|
|
|
TEST_CASE("Coupled correction options and step lengths must agree across ranks",
|
|
"[solver][coupled_trial_correction][configuration][mpi]") {
|
|
using namespace coupled_trial_correction_test;
|
|
int rank = 0, ranks = 0;
|
|
REQUIRE(MPI_Comm_rank(MPI_COMM_WORLD, &rank) == MPI_SUCCESS);
|
|
REQUIRE(MPI_Comm_size(MPI_COMM_WORLD, &ranks) == MPI_SUCCESS);
|
|
DenseOperation operation(identity(2));
|
|
ExactInverse inverse(operation);
|
|
auto options = exactOptions();
|
|
SECTION("A rank-local invalid cap fails collectively") {
|
|
if (rank == 0) options.maximumCorrectionRatio = std::numeric_limits<double>::quiet_NaN();
|
|
const auto create = [&] { return PreparedCoupledTrialCorrection(operation, inverse, MPI_COMM_WORLD, options); };
|
|
CHECK_THROWS_AS(create(), std::invalid_argument);
|
|
}
|
|
SECTION("Different valid caps fail collectively") {
|
|
if (ranks < 2) return;
|
|
options.maximumCorrectionRatio = rank == 0 ? 1.0 : 2.0;
|
|
const auto create = [&] { return PreparedCoupledTrialCorrection(operation, inverse, MPI_COMM_WORLD, options); };
|
|
CHECK_THROWS_AS(create(), std::invalid_argument);
|
|
}
|
|
SECTION("Different step lengths cannot form one global trial") {
|
|
if (ranks < 2) return;
|
|
PreparedCoupledTrialCorrection prepared(operation, inverse, MPI_COMM_WORLD, options);
|
|
mfem::Vector f(2), p(2), prediction(2);
|
|
f = 1.0;
|
|
p = -1.0;
|
|
prepared.CaptureBase(f, p);
|
|
CHECK_THROWS_AS(prepared.BuildLinearPrediction(rank == 0 ? .5 : .25, prediction), std::invalid_argument);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Finite entries with an overflowing auxiliary norm never authorize a correction",
|
|
"[solver][coupled_trial_correction][failure]") {
|
|
using namespace coupled_trial_correction_test;
|
|
DenseOperation operation(identity(2));
|
|
ExactInverse inverse(operation);
|
|
auto probe = std::make_shared<BackendProbe>();
|
|
ScriptedCorrection prepared(operation, inverse, MPI_COMM_SELF, exactOptions(), TestBackend(probe));
|
|
mfem::Vector f(2), p(2), trial(2), correction(2);
|
|
f = 1.0;
|
|
p = -1.0;
|
|
trial = 1.e200;
|
|
prepared.CaptureBase(f, p);
|
|
const auto report = prepared.SolveCorrection(.5, trial, correction);
|
|
CHECK(report.status == CoupledTrialCorrectionStatus::non_finite);
|
|
CHECK_FALSE(report.Admissible());
|
|
CHECK_FALSE(report.verifiedGate);
|
|
CHECK(probe->calls == 0);
|
|
}
|
|
|
|
TEST_CASE("Exceptions after coupled mutation restore the original prepared candidate before escaping",
|
|
"[solver][coupled_trial_correction][transaction][rollback][exception]") {
|
|
using namespace coupled_trial_correction_test;
|
|
ToyState state;
|
|
enum class FailureSite { admissibility, preparation, metric, comparison };
|
|
FailureSite site = FailureSite::admissibility;
|
|
SECTION("The admissibility callback throws") { site = FailureSite::admissibility; }
|
|
SECTION("Physical preparation invalidates its operator and throws") {
|
|
site = FailureSite::preparation;
|
|
state.throwCorrectedPreparation = true;
|
|
}
|
|
SECTION("Evaluation of the corrected metric throws") { site = FailureSite::metric; }
|
|
SECTION("Comparison of the corrected full merit throws") { site = FailureSite::comparison; }
|
|
solver::detail::PreparedCoupledTrialTransaction transaction(state, exactOptions());
|
|
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
|
auto preparation = state.PrepareAlpha(.5);
|
|
const mfem::Vector originalState(state.trialNormalizedState), originalResidual(state.trialNormalizedResidual);
|
|
mfem::Vector originalAction(3);
|
|
state.operation.Mult(state.normalizedCorrection, originalAction);
|
|
auto metric = toyMetric(originalResidual);
|
|
const double originalMerit = metric.merit;
|
|
int evaluations = 0;
|
|
transaction.BeginTrial();
|
|
const auto attempt = [&] {
|
|
transaction.TryImprove(state, .5, preparation, metric,
|
|
[&](const mfem::Vector &residual) {
|
|
if (++evaluations == 1 && site == FailureSite::metric)
|
|
throw std::runtime_error("Synthetic corrected metric failure.");
|
|
return toyMetric(residual);
|
|
}, toyAccepts,
|
|
[&](const auto &candidate, const auto &original) {
|
|
if (site == FailureSite::comparison)
|
|
throw std::runtime_error("Synthetic corrected comparison failure.");
|
|
return toyImproves(candidate, original);
|
|
}, [&](const mfem::Vector &) {
|
|
if (site == FailureSite::admissibility)
|
|
throw std::runtime_error("Synthetic corrected admissibility failure.");
|
|
return true;
|
|
});
|
|
};
|
|
CHECK_THROWS_AS(attempt(), std::runtime_error);
|
|
CHECK(transaction.GetDiagnostics().kept == 0);
|
|
CHECK(transaction.GetDiagnostics().restorations == 1);
|
|
CHECK(metric.merit == originalMerit);
|
|
REQUIRE(preparation.has_value());
|
|
checkRestoredTrial(state, originalState, originalResidual, originalAction);
|
|
}
|