Add bounded fully coupled second-order trial correction and frozen screen
This commit is contained in:
791
tests/solver/coupled_trial_correction.cpp
Normal file
791
tests/solver/coupled_trial_correction.cpp
Normal file
@@ -0,0 +1,791 @@
|
||||
#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);
|
||||
}
|
||||
Reference in New Issue
Block a user