Add bounded coupled damping and prediction-based attempt selection
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
@@ -162,6 +163,12 @@ namespace coupled_trial_correction_test {
|
||||
static_assert(CoupledTrialInverse<ExactInverse>);
|
||||
static_assert(!CoupledTrialOperation<mfem::Operator>);
|
||||
static_assert(!CoupledTrialInverse<mfem::Solver>);
|
||||
static_assert(CoupledCorrectionSearch<FullCoupledStep>);
|
||||
static_assert(CoupledCorrectionSearch<BacktrackedCoupledStep>);
|
||||
static_assert(!CoupledCorrectionSearch<int>);
|
||||
static_assert(CoupledAttemptCriterion<EveryRejectedCoupledAttempt>);
|
||||
static_assert(CoupledAttemptCriterion<NearAcceptanceCoupledAttempt>);
|
||||
static_assert(!CoupledAttemptCriterion<int>);
|
||||
static_assert(LinearBackendRuntimeAvailableFor<TestBackend, DenseOperation, ExactInverse>);
|
||||
using ScriptedCorrection = PreparedCoupledTrialCorrection<DenseOperation, ExactInverse, TestBackend>;
|
||||
} // namespace coupled_trial_correction_test
|
||||
@@ -435,6 +442,11 @@ namespace coupled_trial_correction_test {
|
||||
int preparations{0};
|
||||
bool quarticBarrier{false}, rejectCorrectedPreparation{false}, nonfiniteCorrectedResidual{false};
|
||||
bool throwCorrectedPreparation{false};
|
||||
bool cubicSearchModel{false};
|
||||
bool rejectPositivePreparation{false};
|
||||
double barrierMultiplier{1.0};
|
||||
int throwAtPreparation{-1};
|
||||
std::vector<double> preparedCoordinates;
|
||||
|
||||
ToyState() {
|
||||
trialNormalizedState = 0.0;
|
||||
@@ -449,23 +461,37 @@ namespace coupled_trial_correction_test {
|
||||
[[nodiscard]] std::expected<void, int> PrepareTrial() {
|
||||
++preparations;
|
||||
candidatePhysicalState = trialNormalizedState;
|
||||
preparedCoordinates.push_back(trialNormalizedState(0));
|
||||
if (preparations == throwAtPreparation) {
|
||||
operation.prepared = false;
|
||||
throw std::runtime_error("Synthetic delayed preparation failure.");
|
||||
}
|
||||
if (throwCorrectedPreparation && trialNormalizedState(0) < -2.0) {
|
||||
operation.prepared = false;
|
||||
throw std::runtime_error("Synthetic corrected preparation failure.");
|
||||
}
|
||||
if (rejectCorrectedPreparation && trialNormalizedState(0) < -2.0) {
|
||||
if ((rejectCorrectedPreparation && trialNormalizedState(0) < -2.0) ||
|
||||
(rejectPositivePreparation && trialNormalizedState(0) > 0.0)) {
|
||||
operation.prepared = false;
|
||||
return std::unexpected(7);
|
||||
}
|
||||
trialNormalizedResidual = coupledResidual(trialNormalizedState);
|
||||
auto matrix = coupledJacobian(trialNormalizedState);
|
||||
if (cubicSearchModel) {
|
||||
// At alpha=.5, c=(1.5,0,0) overshoots badly at full scale.
|
||||
// Its quarter step improves the ordinary residual merit.
|
||||
const double x = trialNormalizedState(0);
|
||||
trialNormalizedResidual(0) = 1.0 + x + 10.0*x*x + 11.0*x*x*x;
|
||||
matrix = identity(3);
|
||||
matrix(0, 0) = 1.0 + 20.0*x + 33.0*x*x;
|
||||
}
|
||||
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);
|
||||
trialNormalizedResidual(0) += barrierMultiplier*x*x*(x+.5)*(x+.5);
|
||||
matrix(0, 0) += barrierMultiplier*2.0*x*(x+.5)*(2.0*x+.5);
|
||||
}
|
||||
operation.Prepare(matrix);
|
||||
if (nonfiniteCorrectedResidual && trialNormalizedState(0) < -2.0)
|
||||
@@ -789,3 +815,365 @@ TEST_CASE("Exceptions after coupled mutation restore the original prepared candi
|
||||
REQUIRE(preparation.has_value());
|
||||
checkRestoredTrial(state, originalState, originalResidual, originalAction);
|
||||
}
|
||||
|
||||
TEST_CASE("Damping one verified coupled correction accepts a nonlinear step that its full scale misses",
|
||||
"[solver][coupled_trial_correction][backtracked]") {
|
||||
using namespace coupled_trial_correction_test;
|
||||
using Catch::Approx;
|
||||
ToyState state;
|
||||
state.cubicSearchModel = true;
|
||||
SECTION("Finite full and half corrections overshoot the nonlinear residual") { }
|
||||
SECTION("Rejected full and half preparations are followed by a valid smaller correction") {
|
||||
state.rejectPositivePreparation = true;
|
||||
}
|
||||
solver::detail::PreparedCoupledTrialTransaction<ToyState, BacktrackedCoupledStep> transaction(state, exactOptions());
|
||||
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
||||
auto preparation = state.PrepareAlpha(.5);
|
||||
auto metric = toyMetric(state.trialNormalizedResidual);
|
||||
REQUIRE(metric.merit == Approx(1.5703125));
|
||||
const auto accepts = [](const auto &candidate) { return candidate.merit <= 1.5*(1.0-1.e-4*.5); };
|
||||
REQUIRE_FALSE(accepts(metric));
|
||||
const int initialActions = state.operation.applications, initialInverses = state.inverse.applications;
|
||||
transaction.BeginTrial();
|
||||
transaction.TryImprove(state, .5, preparation, metric, toyMetric, accepts, toyImproves, admissibleToy);
|
||||
const auto &diagnostics = transaction.GetDiagnostics();
|
||||
REQUIRE(diagnostics.currentSolve.has_value());
|
||||
REQUIRE(diagnostics.currentSolve->Admissible());
|
||||
REQUIRE(diagnostics.currentSolve->verifiedGate);
|
||||
CHECK(diagnostics.status == CoupledTrialAttemptStatus::kept);
|
||||
CHECK(diagnostics.selectedBeta == .25);
|
||||
CHECK(diagnostics.attempts == 1);
|
||||
CHECK(diagnostics.kept == 1);
|
||||
CHECK(diagnostics.fallbacks == 0);
|
||||
CHECK(diagnostics.candidateCount == 3);
|
||||
REQUIRE(diagnostics.candidates.size() == 3);
|
||||
CHECK(diagnostics.candidates[0].beta == 1.0);
|
||||
CHECK_FALSE(diagnostics.candidates[0].accepted);
|
||||
if (state.rejectPositivePreparation) {
|
||||
CHECK(diagnostics.candidates[0].status == CoupledTrialCandidateStatus::inadmissible);
|
||||
CHECK(diagnostics.candidates[1].status == CoupledTrialCandidateStatus::inadmissible);
|
||||
} else CHECK(diagnostics.candidates[0].merit > diagnostics.originalMerit);
|
||||
CHECK(diagnostics.candidates[1].beta == .5);
|
||||
CHECK_FALSE(diagnostics.candidates[1].accepted);
|
||||
CHECK(diagnostics.candidates[2].beta == .25);
|
||||
CHECK(diagnostics.candidates[2].accepted);
|
||||
CHECK(diagnostics.preparations == 3);
|
||||
CHECK(diagnostics.restorations == 0);
|
||||
CHECK(transaction.GetCorrection()(0) == Approx(1.5));
|
||||
CHECK(transaction.GetSelectedCorrection()(0) == Approx(.375));
|
||||
CHECK(state.trialNormalizedState(0) == Approx(-.125));
|
||||
CHECK(state.trialNormalizedResidual(0) == Approx(1.009765625));
|
||||
CHECK(accepts(metric));
|
||||
// Candidate preparations change J but must not launch another solve or
|
||||
// verify a scaled correction against a different candidate Jacobian.
|
||||
CHECK(state.operation.applications-initialActions == diagnostics.currentSolve->linearSolve.operatorApplications+1);
|
||||
CHECK(state.inverse.applications-initialInverses == diagnostics.currentSolve->linearSolve.inversePreconditionerApplications);
|
||||
CHECK(diagnostics.iterations == static_cast<std::uint64_t>(diagnostics.currentSolve->linearSolve.iterations));
|
||||
CHECK(state.operation.IsPrepared());
|
||||
CHECK_FALSE(state.inverse.IsCurrent());
|
||||
checkOuterData(state);
|
||||
transaction.BeginTrial();
|
||||
CHECK(std::isnan(transaction.GetDiagnostics().selectedBeta));
|
||||
CHECK(transaction.GetDiagnostics().candidateCount == 0);
|
||||
CHECK(transaction.GetDiagnostics().candidates.empty());
|
||||
CHECK_FALSE(transaction.GetDiagnostics().currentSolve.has_value());
|
||||
CHECK(transaction.GetSelectedCorrection().Norml2() == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("A bounded coupled search freshly restores its best earlier beta when no beta passes Armijo",
|
||||
"[solver][coupled_trial_correction][backtracked][rollback]") {
|
||||
using namespace coupled_trial_correction_test;
|
||||
using Catch::Approx;
|
||||
ToyState state;
|
||||
state.cubicSearchModel = true;
|
||||
solver::detail::PreparedCoupledTrialTransaction<ToyState, BacktrackedCoupledStep> transaction(state, exactOptions());
|
||||
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
||||
auto preparation = state.PrepareAlpha(.5);
|
||||
auto metric = toyMetric(state.trialNormalizedResidual);
|
||||
// This stringent but valid Armijo parameter excludes the quarter step;
|
||||
// it remains the best full-merit improvement among all four proposals.
|
||||
const auto accepts = [](const auto &candidate) { return candidate.merit <= 1.5*(1.0-.99*.5); };
|
||||
transaction.BeginTrial();
|
||||
transaction.TryImprove(state, .5, preparation, metric, toyMetric, accepts, toyImproves, admissibleToy);
|
||||
const auto &diagnostics = transaction.GetDiagnostics();
|
||||
CHECK(diagnostics.status == CoupledTrialAttemptStatus::kept);
|
||||
CHECK(diagnostics.selectedBeta == .25);
|
||||
CHECK(diagnostics.candidateCount == 4);
|
||||
REQUIRE(diagnostics.candidates.size() == 4);
|
||||
for (const auto &candidate : diagnostics.candidates) CHECK_FALSE(candidate.accepted);
|
||||
CHECK(diagnostics.candidates[2].merit < diagnostics.candidates[3].merit);
|
||||
CHECK_FALSE(accepts(metric));
|
||||
CHECK(metric.merit < diagnostics.originalMerit);
|
||||
CHECK(diagnostics.preparations == 5);
|
||||
CHECK(diagnostics.restorations == 1);
|
||||
CHECK(diagnostics.fallbacks == 0);
|
||||
REQUIRE(state.preparedCoordinates.size() == 6);
|
||||
CHECK(state.preparedCoordinates[4] == Approx(-.3125));
|
||||
CHECK(state.preparedCoordinates[5] == Approx(-.125));
|
||||
CHECK(state.trialNormalizedState(0) == Approx(-.125));
|
||||
CHECK(state.candidatePhysicalState(0) == Approx(-.125));
|
||||
CHECK(state.trialNormalizedResidual(0) == Approx(1.009765625));
|
||||
CHECK(state.operation.Matrix()(0, 0) == Approx(1.0+20.0*(-.125)+33.0*.125*.125));
|
||||
CHECK(state.operation.IsPrepared());
|
||||
checkOuterData(state);
|
||||
}
|
||||
|
||||
TEST_CASE("Every rejected damping proposal falls back to the original prepared trial",
|
||||
"[solver][coupled_trial_correction][backtracked][rollback]") {
|
||||
using namespace coupled_trial_correction_test;
|
||||
ToyState state;
|
||||
bool rejectAdmissibility = false, invalidMetric = false;
|
||||
auto expected = CoupledTrialAttemptStatus::not_improved;
|
||||
SECTION("Every nonlinear correction has worse full merit") {
|
||||
state.quarticBarrier = true;
|
||||
state.barrierMultiplier = 100.0;
|
||||
}
|
||||
SECTION("Every beta fails admissibility before preparation") {
|
||||
rejectAdmissibility = true;
|
||||
expected = CoupledTrialAttemptStatus::inadmissible;
|
||||
}
|
||||
SECTION("A finite merit cannot hide a nonfinite metric norm") {
|
||||
invalidMetric = true;
|
||||
expected = CoupledTrialAttemptStatus::non_finite;
|
||||
}
|
||||
solver::detail::PreparedCoupledTrialTransaction<ToyState, BacktrackedCoupledStep> 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;
|
||||
transaction.BeginTrial();
|
||||
transaction.TryImprove(state, .5, preparation, metric,
|
||||
[&](const mfem::Vector &residual) {
|
||||
if (invalidMetric && state.trialNormalizedState(0) != originalState(0))
|
||||
return nonlinear::MetricEvaluation{.residualNorm = std::numeric_limits<double>::quiet_NaN(), .merit = .1};
|
||||
return toyMetric(residual);
|
||||
}, toyAccepts, toyImproves, [rejectAdmissibility](const mfem::Vector &) { return !rejectAdmissibility; });
|
||||
const auto &diagnostics = transaction.GetDiagnostics();
|
||||
CHECK(diagnostics.status == expected);
|
||||
REQUIRE(diagnostics.currentSolve.has_value());
|
||||
CHECK(diagnostics.currentSolve->Admissible());
|
||||
CHECK(diagnostics.attempts == 1);
|
||||
CHECK(diagnostics.candidateCount == 4);
|
||||
CHECK(diagnostics.kept == 0);
|
||||
CHECK(diagnostics.fallbacks == 1);
|
||||
CHECK(diagnostics.restorations == 1);
|
||||
CHECK(diagnostics.preparations == (rejectAdmissibility ? 1 : 5));
|
||||
CHECK(std::isnan(diagnostics.selectedBeta));
|
||||
CHECK(transaction.GetSelectedCorrection().Norml2() == 0.0);
|
||||
CHECK(metric.merit == originalMerit);
|
||||
REQUIRE(preparation.has_value());
|
||||
checkRestoredTrial(state, originalState, originalResidual, originalAction);
|
||||
}
|
||||
|
||||
TEST_CASE("Exceptions during later damping or best-candidate replay restore the original trial",
|
||||
"[solver][coupled_trial_correction][backtracked][rollback][exception]") {
|
||||
using namespace coupled_trial_correction_test;
|
||||
ToyState state;
|
||||
state.cubicSearchModel = true;
|
||||
enum class FailureSite { later_admissibility, later_metric, replay_preparation, replay_metric };
|
||||
FailureSite site = FailureSite::later_admissibility;
|
||||
SECTION("The second admissibility callback throws") { site = FailureSite::later_admissibility; }
|
||||
SECTION("The second candidate metric throws") { site = FailureSite::later_metric; }
|
||||
SECTION("Repreparing the earlier best invalidates J and throws") { site = FailureSite::replay_preparation; }
|
||||
SECTION("The freshly replayed best metric throws") { site = FailureSite::replay_metric; }
|
||||
solver::detail::PreparedCoupledTrialTransaction<ToyState, BacktrackedCoupledStep> 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;
|
||||
if (site == FailureSite::replay_preparation) state.throwAtPreparation = state.preparations+5;
|
||||
int metricCalls = 0, admissibilityCalls = 0;
|
||||
transaction.BeginTrial();
|
||||
const auto attempt = [&] {
|
||||
transaction.TryImprove(state, .5, preparation, metric,
|
||||
[&](const mfem::Vector &residual) {
|
||||
++metricCalls;
|
||||
if ((site == FailureSite::later_metric && metricCalls == 2) ||
|
||||
(site == FailureSite::replay_metric && metricCalls == 5))
|
||||
throw std::runtime_error("Synthetic damping metric failure.");
|
||||
return toyMetric(residual);
|
||||
}, [](const auto &candidate) { return candidate.merit <= 1.5*(1.0-.99*.5); }, toyImproves,
|
||||
[&](const mfem::Vector &) {
|
||||
if (++admissibilityCalls == 2 && site == FailureSite::later_admissibility)
|
||||
throw std::runtime_error("Synthetic later admissibility failure.");
|
||||
return true;
|
||||
});
|
||||
};
|
||||
CHECK_THROWS_AS(attempt(), std::runtime_error);
|
||||
CHECK(transaction.GetDiagnostics().kept == 0);
|
||||
CHECK(std::isnan(transaction.GetDiagnostics().selectedBeta));
|
||||
CHECK(transaction.GetSelectedCorrection().Norml2() == 0.0);
|
||||
CHECK(metric.merit == originalMerit);
|
||||
REQUIRE(preparation.has_value());
|
||||
checkRestoredTrial(state, originalState, originalResidual, originalAction);
|
||||
}
|
||||
|
||||
TEST_CASE("Near-acceptance selection defers work without spending the captured direction budget",
|
||||
"[solver][coupled_trial_correction][backtracked][budget]") {
|
||||
using namespace coupled_trial_correction_test;
|
||||
using Catch::Approx;
|
||||
ToyState state;
|
||||
solver::detail::PreparedCoupledTrialTransaction<ToyState, BacktrackedCoupledStep,
|
||||
NearAcceptanceCoupledAttempt> transaction(state, exactOptions());
|
||||
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
||||
auto preparation = state.PrepareAlpha(.5);
|
||||
auto metric = toyMetric(state.trialNormalizedResidual);
|
||||
const int actions = state.operation.applications, inverses = state.inverse.applications, preparations = state.preparations;
|
||||
transaction.BeginTrial();
|
||||
transaction.TryImprove(state, .5, preparation, metric, toyMetric,
|
||||
[](const auto &candidate) { return candidate.merit <= 1.5*(1.0-1.e-4*.5); }, toyImproves, admissibleToy);
|
||||
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::prediction_deferred);
|
||||
CHECK(transaction.GetDiagnostics().baseMerit == Approx(1.5));
|
||||
CHECK(transaction.GetDiagnostics().linearPredictionMerit == Approx(.375));
|
||||
CHECK(transaction.GetDiagnostics().predictedDecrease == Approx(1.125));
|
||||
CHECK(transaction.GetDiagnostics().predictionExcessRatio == Approx(26.0/9.0));
|
||||
CHECK(transaction.GetDiagnostics().deferredAttempts == 1);
|
||||
CHECK(transaction.GetDiagnostics().attempts == 0);
|
||||
CHECK_FALSE(transaction.GetDiagnostics().currentSolve.has_value());
|
||||
CHECK(state.operation.applications == actions);
|
||||
CHECK(state.inverse.applications == inverses);
|
||||
CHECK(state.preparations == preparations);
|
||||
|
||||
preparation = state.PrepareAlpha(.25);
|
||||
metric = toyMetric(state.trialNormalizedResidual);
|
||||
REQUIRE_FALSE(toyAccepts(metric));
|
||||
transaction.BeginTrial();
|
||||
CHECK(std::isnan(transaction.GetDiagnostics().predictionExcessRatio));
|
||||
transaction.TryImprove(state, .25, preparation, metric, toyMetric, toyAccepts, toyImproves, admissibleToy);
|
||||
CHECK(transaction.GetDiagnostics().predictionExcessRatio == Approx(1.0/84.0));
|
||||
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::kept);
|
||||
CHECK(transaction.GetDiagnostics().selectedBeta == 1.0);
|
||||
CHECK(transaction.GetDiagnostics().attempts == 1);
|
||||
CHECK(transaction.GetDiagnostics().deferredAttempts == 1);
|
||||
CHECK(metric.merit == Approx(.84375));
|
||||
CHECK(toyAccepts(metric));
|
||||
|
||||
preparation = state.PrepareAlpha(.25);
|
||||
metric = toyMetric(state.trialNormalizedResidual);
|
||||
transaction.BeginTrial();
|
||||
transaction.TryImprove(state, .25, preparation, metric, toyMetric, toyAccepts, toyImproves, admissibleToy);
|
||||
CHECK(transaction.GetDiagnostics().status == CoupledTrialAttemptStatus::budget_exhausted);
|
||||
CHECK(transaction.GetDiagnostics().attempts == 1);
|
||||
CHECK_FALSE(transaction.GetDiagnostics().currentSolve.has_value());
|
||||
CHECK(transaction.GetDiagnostics().candidateCount == 0);
|
||||
CHECK(transaction.GetDiagnostics().candidates.empty());
|
||||
CHECK(std::isnan(transaction.GetDiagnostics().selectedBeta));
|
||||
CHECK(transaction.GetSelectedCorrection().Norml2() == 0.0);
|
||||
checkOuterData(state);
|
||||
}
|
||||
|
||||
TEST_CASE("Near-acceptance selection skips acceptable trials and requires a predicted decrease",
|
||||
"[solver][coupled_trial_correction][backtracked][budget]") {
|
||||
using namespace coupled_trial_correction_test;
|
||||
ToyState state;
|
||||
solver::detail::PreparedCoupledTrialTransaction<ToyState, BacktrackedCoupledStep,
|
||||
NearAcceptanceCoupledAttempt> transaction(state, exactOptions());
|
||||
mfem::Vector direction(state.normalizedCorrection);
|
||||
bool accepted = false;
|
||||
SECTION("An already acceptable original never starts auxiliary work") { accepted = true; }
|
||||
SECTION("A zero predicted decrease cannot define a near-acceptance allowance") { direction = 0.0; }
|
||||
transaction.CaptureBase(state.acceptedResidual, direction);
|
||||
auto preparation = state.PrepareAlpha(accepted ? .05 : 0.0);
|
||||
auto metric = toyMetric(state.trialNormalizedResidual);
|
||||
transaction.BeginTrial();
|
||||
transaction.TryImprove(state, .5, preparation, metric, toyMetric,
|
||||
[](const auto &candidate) { return candidate.merit <= 1.5*(1.0-1.e-4*.5); }, toyImproves, admissibleToy);
|
||||
CHECK(transaction.GetDiagnostics().status == (accepted ? CoupledTrialAttemptStatus::original_accepted :
|
||||
CoupledTrialAttemptStatus::prediction_deferred));
|
||||
CHECK(transaction.GetDiagnostics().attempts == 0);
|
||||
CHECK_FALSE(transaction.GetDiagnostics().currentSolve.has_value());
|
||||
CHECK(transaction.GetDiagnostics().candidateCount == 0);
|
||||
CHECK(transaction.GetSelectedCorrection().Norml2() == 0.0);
|
||||
if (!accepted) CHECK(transaction.GetDiagnostics().predictedDecrease == 0.0);
|
||||
checkOuterData(state);
|
||||
}
|
||||
|
||||
TEST_CASE("Damped coupled candidates make collective admissibility and metric decisions",
|
||||
"[solver][coupled_trial_correction][backtracked][mpi]") {
|
||||
using namespace coupled_trial_correction_test;
|
||||
using Catch::Approx;
|
||||
int rank = 0;
|
||||
REQUIRE(MPI_Comm_rank(MPI_COMM_WORLD, &rank) == MPI_SUCCESS);
|
||||
ToyState state;
|
||||
state.problem.communicator = MPI_COMM_WORLD;
|
||||
state.cubicSearchModel = true;
|
||||
bool invalidMetric = false, strictAcceptance = false;
|
||||
SECTION("One rank rejects the first two admissibility checks") { }
|
||||
SECTION("One rank reports a nonfinite quarter-step metric") { invalidMetric = true; }
|
||||
SECTION("One rank requires the full bounded search before keeping its best") { strictAcceptance = true; }
|
||||
solver::detail::PreparedCoupledTrialTransaction<ToyState, BacktrackedCoupledStep> transaction(state, exactOptions());
|
||||
transaction.CaptureBase(state.acceptedResidual, state.normalizedCorrection);
|
||||
auto preparation = state.PrepareAlpha(.5);
|
||||
auto metric = toyMetric(state.trialNormalizedResidual);
|
||||
transaction.BeginTrial();
|
||||
transaction.TryImprove(state, .5, preparation, metric,
|
||||
[&](const mfem::Vector &residual) {
|
||||
if (invalidMetric && rank == 0 && std::abs(state.trialNormalizedState(0)+.125) < 1.e-12)
|
||||
return nonlinear::MetricEvaluation{.residualNorm = std::numeric_limits<double>::quiet_NaN(), .merit = .1};
|
||||
return toyMetric(residual);
|
||||
}, [&](const auto &candidate) {
|
||||
return candidate.merit <= 1.5*(1.0-(strictAcceptance && rank == 0 ? .99 : 1.e-4)*.5);
|
||||
}, toyImproves, [&](const mfem::Vector &candidate) {
|
||||
return invalidMetric || strictAcceptance || rank != 0 || candidate(0) <= 0.0;
|
||||
});
|
||||
const auto &diagnostics = transaction.GetDiagnostics();
|
||||
REQUIRE(diagnostics.currentSolve.has_value());
|
||||
REQUIRE(diagnostics.currentSolve->Admissible());
|
||||
CHECK(diagnostics.status == CoupledTrialAttemptStatus::kept);
|
||||
CHECK(diagnostics.attempts == 1);
|
||||
CHECK(diagnostics.selectedBeta == (invalidMetric ? .125 : .25));
|
||||
CHECK(diagnostics.candidateCount == (invalidMetric || strictAcceptance ? 4 : 3));
|
||||
REQUIRE(diagnostics.candidates.size() == diagnostics.candidateCount);
|
||||
if (invalidMetric) CHECK(diagnostics.candidates[2].status == CoupledTrialCandidateStatus::non_finite);
|
||||
else if (strictAcceptance) CHECK_FALSE(diagnostics.candidates[2].accepted);
|
||||
else {
|
||||
CHECK(diagnostics.candidates[0].status == CoupledTrialCandidateStatus::inadmissible);
|
||||
CHECK(diagnostics.candidates[1].status == CoupledTrialCandidateStatus::inadmissible);
|
||||
CHECK(diagnostics.preparations == 1);
|
||||
}
|
||||
CHECK(state.trialNormalizedState(0) == Approx(invalidMetric ? -.3125 : -.125));
|
||||
checkOuterData(state);
|
||||
}
|
||||
|
||||
TEST_CASE("Damped coupled search controls reject invalid or inconsistent work budgets collectively",
|
||||
"[solver][coupled_trial_correction][backtracked][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);
|
||||
ToyState state;
|
||||
state.problem.communicator = MPI_COMM_WORLD;
|
||||
BacktrackedCoupledStep search;
|
||||
NearAcceptanceCoupledAttempt selection;
|
||||
SECTION("A rank-local zero candidate budget cannot bound a useful search") { if (rank == 0) search.maximumSteps = 0; }
|
||||
SECTION("A rank-local nondecreasing scale is invalid") { if (rank == 0) search.reduction = 1.0; }
|
||||
SECTION("A rank-local zero scale is invalid") { if (rank == 0) search.reduction = 0.0; }
|
||||
SECTION("A rank-local nonfinite scale is invalid") { if (rank == 0) search.reduction = std::numeric_limits<double>::quiet_NaN(); }
|
||||
SECTION("A nominally finite budget whose last scale underflows is invalid") {
|
||||
if (rank == 0) search.maximumSteps = 4096;
|
||||
}
|
||||
SECTION("A rank-local zero acceptance allowance is invalid") { if (rank == 0) selection.maximumExcessRatio = 0.0; }
|
||||
SECTION("A rank-local nonfinite allowance is invalid") { if (rank == 0) selection.maximumExcessRatio = std::numeric_limits<double>::infinity(); }
|
||||
SECTION("Different valid candidate budgets cannot share a collective search") {
|
||||
if (ranks < 2) return;
|
||||
search.maximumSteps = rank == 0 ? 4 : 3;
|
||||
}
|
||||
SECTION("Different valid reductions cannot share a collective search") {
|
||||
if (ranks < 2) return;
|
||||
search.reduction = rank == 0 ? .5 : .25;
|
||||
}
|
||||
SECTION("Different valid allowances cannot select one global attempt") {
|
||||
if (ranks < 2) return;
|
||||
selection.maximumExcessRatio = rank == 0 ? 1.0 : 2.0;
|
||||
}
|
||||
const auto create = [&] {
|
||||
return solver::detail::PreparedCoupledTrialTransaction(state, exactOptions(), 1, search, selection);
|
||||
};
|
||||
CHECK_THROWS_AS(create(), std::invalid_argument);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user