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.
316342class 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.
508535class 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+
22582324std::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-
25852655std::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
27092820TerminationCriteria 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.
29433062SolverResult PrimalDualHybridGradient (
29443063 QuadraticProgram qp, const PrimalDualHybridGradientParams& params,
29453064 std::optional<PrimalAndDualSolution> initial_solution,
0 commit comments