Add bounded coupled damping and prediction-based attempt selection

This commit is contained in:
2026-09-18 10:45:45 -04:00
parent 46249d0758
commit 422c46bcbf
11 changed files with 2296 additions and 101 deletions

View File

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