Skip to content

Commit ed71f2c

Browse files
committed
export from google3
1 parent 219ce59 commit ed71f2c

6 files changed

Lines changed: 347 additions & 63 deletions

File tree

.allstar/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2010-2025 Google LLC
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
exports_files(
15+
glob(["**"]),
16+
visibility = ["//ortools/open_source:__subpackages__"],
17+
)

ortools/lp_data/README.md

Whitespace-only changes.

ortools/pdlp/primal_dual_hybrid_gradient.cc

Lines changed: 169 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14+
// We solve a QP, which we call the "original QP", by applying preprocessing
15+
// including presolve and rescaling, which produces a new QP that we call the
16+
// "working QP". We then solve the working QP using the Primal Dual Hybrid
17+
// Gradient algorithm (PDHG). The optimality criteria are evaluated using the
18+
// original QP. There are three main modules in this file:
19+
// * The free function `PrimalDualHybridGradient()`, which is the user API, and
20+
// is responsible for input validation that doesn't use
21+
// ShardedQuadraticProgram, creating a `PreprocessSolver`, and calling
22+
// `PreprocessSolver::PreprocessAndSolve()`.
23+
// * The class `PreprocessSolver`, which is responsible for everything that
24+
// touches the original QP, including input validation that uses
25+
// ShardedQuadraticProgram, the preprocessing, converting solutions to the
26+
// working QP back to solutions to the original QP, and termination checks. It
27+
// also creates a `Solver` object and calls `Solver::Solve()`.
28+
// * The class `Solver`, which is responsible for everything that only touches
29+
// the working QP. It keeps a pointer to `PreprocessSolver` and calls methods
30+
// on it when it needs access to the original QP, e.g. termination checks.
31+
// When feasibility polishing is enabled the main solve's `Solver` object
32+
// creates additional `Solver` objects periodically to do the feasibility
33+
// polishing (in `Solver::TryPrimalPolishing()` and
34+
// `Solver::TryDualPolishing()`).
35+
// The main reason for having two separate classes `PreprocessSolver` and
36+
// `Solver` is the fact that feasibility polishing mode uses a single
37+
// `PreprocessSolver` object with multiple `Solver` objects.
38+
1439
#include "ortools/pdlp/primal_dual_hybrid_gradient.h"
1540

1641
#include <algorithm>
@@ -313,6 +338,7 @@ SolverResult ConstructSolverResult(VectorXd primal_solution,
313338
.solve_log = std::move(solve_log)};
314339
}
315340

341+
// See comment at top of file.
316342
class PreprocessSolver {
317343
public:
318344
// Assumes that `qp` and `params` are valid.
@@ -505,6 +531,7 @@ class PreprocessSolver {
505531
IterationStatsCallback iteration_stats_callback_;
506532
};
507533

534+
// See comment at top of file.
508535
class Solver {
509536
public:
510537
// `preprocess_solver` should not be nullptr, and the `PreprocessSolver`
@@ -556,6 +583,15 @@ class Solver {
556583
// cause infinite and NaN values.
557584
constexpr static double kDivergentMovement = 1.0e100;
558585

586+
// The total number of iterations in feasibility polishing is at most
587+
// `4 * iterations_completed_ / kFeasibilityIterationFraction`.
588+
// One factor of two is because there are both primal and dual feasibility
589+
// polishing phases, and the other factor of two is because
590+
// `next_feasibility_polishing_iteration` increases by a factor of 2 each
591+
// feasibility polishing phase, so the sum of iteration limits is at most
592+
// twice the last value.
593+
constexpr static int kFeasibilityIterationFraction = 8;
594+
559595
// Attempts to solve primal and dual feasibility subproblems starting at the
560596
// average iterate, for at most `iteration_limit` iterations each. If
561597
// successful, returns a `SolverResult`, otherwise nullopt. Appends
@@ -2255,6 +2291,36 @@ IterationStats WorkFromFeasibilityPolishing(const SolveLog& solve_log) {
22552291
return result;
22562292
}
22572293

2294+
bool TerminationReasonIsInterrupted(const TerminationReason reason) {
2295+
return reason == TERMINATION_REASON_INTERRUPTED_BY_USER;
2296+
}
2297+
2298+
bool TerminationReasonIsWorkLimitNotInterrupted(
2299+
const TerminationReason reason) {
2300+
return reason == TERMINATION_REASON_ITERATION_LIMIT ||
2301+
reason == TERMINATION_REASON_TIME_LIMIT ||
2302+
reason == TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT;
2303+
}
2304+
2305+
// Note: `TERMINATION_REASON_INTERRUPTED_BY_USER` is treated as a work limit
2306+
// (that was determined in real-time by the user).
2307+
bool TerminationReasonIsWorkLimit(const TerminationReason reason) {
2308+
return TerminationReasonIsWorkLimitNotInterrupted(reason) ||
2309+
TerminationReasonIsInterrupted(reason);
2310+
}
2311+
2312+
bool DoFeasibilityPolishingAfterLimitsReached(
2313+
const PrimalDualHybridGradientParams& params,
2314+
const TerminationReason reason) {
2315+
if (TerminationReasonIsWorkLimitNotInterrupted(reason)) {
2316+
return params.apply_feasibility_polishing_after_limits_reached();
2317+
}
2318+
if (TerminationReasonIsInterrupted(reason)) {
2319+
return params.apply_feasibility_polishing_if_solver_is_interrupted();
2320+
}
2321+
return false;
2322+
}
2323+
22582324
std::optional<SolverResult> Solver::MajorIterationAndTerminationCheck(
22592325
const IterationType iteration_type, const bool force_numerical_termination,
22602326
const std::atomic<bool>* interrupt_solve,
@@ -2272,12 +2338,12 @@ std::optional<SolverResult> Solver::MajorIterationAndTerminationCheck(
22722338
IterationStats stats = CreateSimpleIterationStats(restart);
22732339
IterationStats full_work_stats =
22742340
AddWorkStats(stats, work_from_feasibility_polishing);
2341+
std::optional<TerminationReasonAndPointType> simple_termination_reason =
2342+
CheckSimpleTerminationCriteria(params_.termination_criteria(),
2343+
full_work_stats, interrupt_solve);
22752344
const bool check_termination =
22762345
major_iteration_cycle % params_.termination_check_frequency() == 0 ||
2277-
CheckSimpleTerminationCriteria(params_.termination_criteria(),
2278-
full_work_stats, interrupt_solve)
2279-
.has_value() ||
2280-
force_numerical_termination;
2346+
simple_termination_reason.has_value() || force_numerical_termination;
22812347
// We check termination on every major iteration.
22822348
DCHECK(!is_major_iteration || check_termination);
22832349
if (check_termination) {
@@ -2304,6 +2370,19 @@ std::optional<SolverResult> Solver::MajorIterationAndTerminationCheck(
23042370
}
23052371
// We've terminated.
23062372
if (maybe_termination_reason.has_value()) {
2373+
if (iteration_type == IterationType::kNormal &&
2374+
DoFeasibilityPolishingAfterLimitsReached(
2375+
params_, maybe_termination_reason->reason)) {
2376+
const std::optional<SolverResult> feasibility_result =
2377+
TryFeasibilityPolishing(
2378+
iterations_completed_ / kFeasibilityIterationFraction,
2379+
interrupt_solve, solve_log);
2380+
if (feasibility_result.has_value()) {
2381+
LOG(INFO) << "Returning result from feasibility polishing after "
2382+
"limits reached";
2383+
return *feasibility_result;
2384+
}
2385+
}
23072386
IterationStats terminating_full_stats =
23082387
AddWorkStats(stats, work_from_feasibility_polishing);
23092388
return PickSolutionAndConstructSolverResult(
@@ -2573,15 +2652,6 @@ FeasibilityPolishingDetails BuildFeasibilityPolishingDetails(
25732652
return details;
25742653
}
25752654

2576-
// Note: `TERMINATION_REASON_INTERRUPTED_BY_USER` is treated as a work limit
2577-
// (that was determined in real-time by the user).
2578-
bool TerminationReasonIsWorkLimit(const TerminationReason reason) {
2579-
return reason == TERMINATION_REASON_ITERATION_LIMIT ||
2580-
reason == TERMINATION_REASON_TIME_LIMIT ||
2581-
reason == TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT ||
2582-
reason == TERMINATION_REASON_INTERRUPTED_BY_USER;
2583-
}
2584-
25852655
std::optional<SolverResult> Solver::TryFeasibilityPolishing(
25862656
const int iteration_limit, const std::atomic<bool>* interrupt_solve,
25872657
SolveLog& solve_log) {
@@ -2600,12 +2670,20 @@ std::optional<SolverResult> Solver::TryFeasibilityPolishing(
26002670
// polishing, it is usually increased, and an experiment (on MIPLIB2017)
26012671
// shows that this test reduces the iteration count by 3-4% on average.
26022672
if (!ObjectiveGapMet(optimality_criteria, first_convergence_info)) {
2603-
if (params_.verbosity_level() >= 2) {
2604-
SOLVER_LOG(&preprocess_solver_->Logger(),
2605-
"Skipping feasibility polishing because the objective gap "
2606-
"is too large.");
2673+
std::optional<TerminationReasonAndPointType> simple_termination_reason =
2674+
CheckSimpleTerminationCriteria(params_.termination_criteria(),
2675+
TotalWorkSoFar(solve_log),
2676+
interrupt_solve);
2677+
if (!(simple_termination_reason.has_value() &&
2678+
DoFeasibilityPolishingAfterLimitsReached(
2679+
params_, simple_termination_reason->reason))) {
2680+
if (params_.verbosity_level() >= 2) {
2681+
SOLVER_LOG(&preprocess_solver_->Logger(),
2682+
"Skipping feasibility polishing because the objective gap "
2683+
"is too large.");
2684+
}
2685+
return std::nullopt;
26072686
}
2608-
return std::nullopt;
26092687
}
26102688

26112689
if (params_.verbosity_level() >= 2) {
@@ -2623,7 +2701,17 @@ std::optional<SolverResult> Solver::TryFeasibilityPolishing(
26232701
}
26242702
if (TerminationReasonIsWorkLimit(
26252703
primal_result.solve_log.termination_reason())) {
2626-
return std::nullopt;
2704+
// Have we also reached the overall work limit? If so, consider finishing
2705+
// the final polishing phase.
2706+
std::optional<TerminationReasonAndPointType> simple_termination_reason =
2707+
CheckSimpleTerminationCriteria(params_.termination_criteria(),
2708+
TotalWorkSoFar(solve_log),
2709+
interrupt_solve);
2710+
if (!(simple_termination_reason.has_value() &&
2711+
DoFeasibilityPolishingAfterLimitsReached(
2712+
params_, simple_termination_reason->reason))) {
2713+
return std::nullopt;
2714+
}
26272715
} else if (primal_result.solve_log.termination_reason() !=
26282716
TERMINATION_REASON_OPTIMAL) {
26292717
// Note: `TERMINATION_REASON_PRIMAL_INFEASIBLE` could happen normally, but
@@ -2651,9 +2739,29 @@ std::optional<SolverResult> Solver::TryFeasibilityPolishing(
26512739
TerminationReason_Name(dual_result.solve_log.termination_reason()));
26522740
}
26532741

2742+
IterationStats full_stats = TotalWorkSoFar(solve_log);
2743+
std::optional<TerminationReasonAndPointType> simple_termination_reason =
2744+
CheckSimpleTerminationCriteria(params_.termination_criteria(), full_stats,
2745+
interrupt_solve);
26542746
if (TerminationReasonIsWorkLimit(
26552747
dual_result.solve_log.termination_reason())) {
2656-
return std::nullopt;
2748+
// Have we also reached the overall work limit? If so, consider falling out
2749+
// of the "if" test and returning the polishing solution anyway.
2750+
if (simple_termination_reason.has_value() &&
2751+
DoFeasibilityPolishingAfterLimitsReached(
2752+
params_, simple_termination_reason->reason)) {
2753+
preprocess_solver_->ComputeConvergenceAndInfeasibilityFromWorkingSolution(
2754+
params_, primal_result.primal_solution, dual_result.dual_solution,
2755+
POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION,
2756+
full_stats.add_convergence_information(), nullptr);
2757+
return ConstructSolverResult(
2758+
std::move(primal_result.primal_solution),
2759+
std::move(dual_result.dual_solution), full_stats,
2760+
simple_termination_reason->reason,
2761+
POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION, solve_log);
2762+
} else {
2763+
return std::nullopt;
2764+
}
26572765
} else if (dual_result.solve_log.termination_reason() !=
26582766
TERMINATION_REASON_OPTIMAL) {
26592767
// Note: The comment in the corresponding location when checking the
@@ -2665,7 +2773,6 @@ std::optional<SolverResult> Solver::TryFeasibilityPolishing(
26652773
return std::nullopt;
26662774
}
26672775

2668-
IterationStats full_stats = TotalWorkSoFar(solve_log);
26692776
preprocess_solver_->ComputeConvergenceAndInfeasibilityFromWorkingSolution(
26702777
params_, primal_result.primal_solution, dual_result.dual_solution,
26712778
POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION,
@@ -2689,12 +2796,16 @@ std::optional<SolverResult> Solver::TryFeasibilityPolishing(
26892796
full_stats,
26902797
preprocess_solver_->OriginalBoundNorms(),
26912798
/*force_numerical_termination=*/false);
2692-
if (earned_termination.has_value()) {
2693-
return ConstructSolverResult(std::move(primal_result.primal_solution),
2694-
std::move(dual_result.dual_solution),
2695-
full_stats, earned_termination->reason,
2696-
POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION,
2697-
solve_log);
2799+
if (earned_termination.has_value() ||
2800+
(simple_termination_reason.has_value() &&
2801+
DoFeasibilityPolishingAfterLimitsReached(
2802+
params_, simple_termination_reason->reason))) {
2803+
return ConstructSolverResult(
2804+
std::move(primal_result.primal_solution),
2805+
std::move(dual_result.dual_solution), full_stats,
2806+
earned_termination.has_value() ? earned_termination->reason
2807+
: simple_termination_reason->reason,
2808+
POINT_TYPE_FEASIBILITY_POLISHING_SOLUTION, solve_log);
26982809
}
26992810
// Note: A typical termination check would now call
27002811
// `CheckSimpleTerminationCriteria`. However, there is no obvious iterate to
@@ -2708,15 +2819,22 @@ std::optional<SolverResult> Solver::TryFeasibilityPolishing(
27082819

27092820
TerminationCriteria ReduceWorkLimitsByPreviousWork(
27102821
TerminationCriteria criteria, const int iteration_limit,
2711-
const IterationStats& previous_work) {
2712-
criteria.set_iteration_limit(std::max(
2713-
0, std::min(iteration_limit, criteria.iteration_limit() -
2714-
previous_work.iteration_number())));
2715-
criteria.set_kkt_matrix_pass_limit(
2716-
std::max(0.0, criteria.kkt_matrix_pass_limit() -
2717-
previous_work.cumulative_kkt_matrix_passes()));
2718-
criteria.set_time_sec_limit(std::max(
2719-
0.0, criteria.time_sec_limit() - previous_work.cumulative_time_sec()));
2822+
const IterationStats& previous_work,
2823+
bool apply_feasibility_polishing_after_limits_reached) {
2824+
if (apply_feasibility_polishing_after_limits_reached) {
2825+
criteria.set_iteration_limit(iteration_limit);
2826+
criteria.set_kkt_matrix_pass_limit(std::numeric_limits<double>::infinity());
2827+
criteria.set_time_sec_limit(std::numeric_limits<double>::infinity());
2828+
} else {
2829+
criteria.set_iteration_limit(std::max(
2830+
0, std::min(iteration_limit, criteria.iteration_limit() -
2831+
previous_work.iteration_number())));
2832+
criteria.set_kkt_matrix_pass_limit(
2833+
std::max(0.0, criteria.kkt_matrix_pass_limit() -
2834+
previous_work.cumulative_kkt_matrix_passes()));
2835+
criteria.set_time_sec_limit(std::max(
2836+
0.0, criteria.time_sec_limit() - previous_work.cumulative_time_sec()));
2837+
}
27202838
return criteria;
27212839
}
27222840

@@ -2725,9 +2843,13 @@ SolverResult Solver::TryPrimalPolishing(
27252843
const std::atomic<bool>* interrupt_solve, SolveLog& solve_log) {
27262844
PrimalDualHybridGradientParams primal_feasibility_params = params_;
27272845
*primal_feasibility_params.mutable_termination_criteria() =
2728-
ReduceWorkLimitsByPreviousWork(params_.termination_criteria(),
2729-
iteration_limit,
2730-
TotalWorkSoFar(solve_log));
2846+
ReduceWorkLimitsByPreviousWork(
2847+
params_.termination_criteria(), iteration_limit,
2848+
TotalWorkSoFar(solve_log),
2849+
params_.apply_feasibility_polishing_after_limits_reached());
2850+
if (params_.apply_feasibility_polishing_if_solver_is_interrupted()) {
2851+
interrupt_solve = nullptr;
2852+
}
27312853

27322854
// This will save the original objective after the swap.
27332855
VectorXd objective;
@@ -2785,9 +2907,13 @@ SolverResult Solver::TryDualPolishing(VectorXd starting_dual_solution,
27852907
SolveLog& solve_log) {
27862908
PrimalDualHybridGradientParams dual_feasibility_params = params_;
27872909
*dual_feasibility_params.mutable_termination_criteria() =
2788-
ReduceWorkLimitsByPreviousWork(params_.termination_criteria(),
2789-
iteration_limit,
2790-
TotalWorkSoFar(solve_log));
2910+
ReduceWorkLimitsByPreviousWork(
2911+
params_.termination_criteria(), iteration_limit,
2912+
TotalWorkSoFar(solve_log),
2913+
params_.apply_feasibility_polishing_after_limits_reached());
2914+
if (params_.apply_feasibility_polishing_if_solver_is_interrupted()) {
2915+
interrupt_solve = nullptr;
2916+
}
27912917

27922918
// These will initially contain the homogenous variable and constraint
27932919
// bounds, but will contain the original variable and constraint bounds
@@ -2883,14 +3009,6 @@ SolverResult Solver::Solve(const IterationType iteration_type,
28833009
if (params_.use_feasibility_polishing() &&
28843010
iteration_type == IterationType::kNormal &&
28853011
iterations_completed_ >= next_feasibility_polishing_iteration) {
2886-
// The total number of iterations in feasibility polishing is at most
2887-
// `4 * iterations_completed_ / kFeasibilityIterationFraction`.
2888-
// One factor of two is because there are both primal and dual feasibility
2889-
// polishing phases, and the other factor of two is because
2890-
// `next_feasibility_polishing_iteration` increases by a factor of 2 each
2891-
// feasibility polishing phase, so the sum of iteration limits is at most
2892-
// twice the last value.
2893-
const int kFeasibilityIterationFraction = 8;
28943012
const std::optional<SolverResult> feasibility_result =
28953013
TryFeasibilityPolishing(
28963014
iterations_completed_ / kFeasibilityIterationFraction,
@@ -2940,6 +3058,7 @@ SolverResult PrimalDualHybridGradient(
29403058
std::move(iteration_stats_callback));
29413059
}
29423060

3061+
// See comment at top of file.
29433062
SolverResult PrimalDualHybridGradient(
29443063
QuadraticProgram qp, const PrimalDualHybridGradientParams& params,
29453064
std::optional<PrimalAndDualSolution> initial_solution,

0 commit comments

Comments
 (0)