From 1835262ca56efc83e9384c6aa36f71c33d35a2c2 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 01:30:23 -0800 Subject: [PATCH 01/10] squashed commit --- cpp/src/branch_and_bound/branch_and_bound.cpp | 150 +++++++++++++++--- cpp/src/branch_and_bound/pseudo_costs.cpp | 9 +- cpp/src/branch_and_bound/pseudo_costs.hpp | 1 + cpp/src/mip_heuristics/solver.cu | 4 +- 4 files changed, 142 insertions(+), 22 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 6ce9a4f4d0..52a9ef9237 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -70,6 +70,28 @@ i_t fractional_variables(const simplex_solver_settings_t& settings, return fractional.size(); } +template +i_t prune_fixed_fractional_variables(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& fractional) +{ + std::vector new_fractional; + new_fractional.reserve(fractional.size()); + + i_t num_fixed = 0; + for (i_t k = 0; k < (i_t)fractional.size(); k++) { + const i_t j = fractional[k]; + if (std::abs(lp.upper[j] - lp.lower[j]) < settings.fixed_tol) { + num_fixed++; + } else { + new_fractional.push_back(j); + } + } + + fractional = std::move(new_fractional); + return num_fixed; +} + template void full_variable_types(const user_problem_t& original_problem, const lp_problem_t& original_lp, @@ -2363,6 +2385,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_objective_, root_vstatus_, edge_norms_, + upper_bound_, pc_); } @@ -2372,6 +2395,111 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return solver_status_; } + // Exploit infeasible/fathomed branches from strong branching for bounds tightening. + // If branching down on x_j is infeasible, we can tighten lb[j] = ceil(x_j*). + // If branching up on x_j is infeasible, we can tighten ub[j] = floor(x_j*). + // With an incumbent, branches whose objective exceeds the cutoff yield the same deductions. + { + const f_t current_upper = upper_bound_.load(); + i_t num_tightened = 0; + i_t num_infeasible = 0; + i_t num_cutoff = 0; + for (i_t k = 0; k < (i_t)fractional.size(); k++) { + const i_t j = fractional[k]; + const f_t sb_down = pc_.strong_branch_down[k]; + const f_t sb_up = pc_.strong_branch_up[k]; + bool down_infeasible = std::isinf(sb_down); + bool up_infeasible = std::isinf(sb_up); + bool down_cutoff = false; + bool up_cutoff = false; + + if (!down_infeasible && std::isfinite(sb_down) && std::isfinite(current_upper)) { + down_cutoff = (sb_down + root_objective_ > current_upper + settings_.dual_tol); + down_infeasible = down_cutoff; + } + if (!up_infeasible && std::isfinite(sb_up) && std::isfinite(current_upper)) { + up_cutoff = (sb_up + root_objective_ > current_upper + settings_.dual_tol); + up_infeasible = up_cutoff; + } + + if (down_infeasible && up_infeasible) { + bool truly_infeasible = std::isinf(sb_down) && std::isinf(sb_up); + if (truly_infeasible) { + settings_.log.printf("Strong branching: both branches infeasible for variable %d\n", j); + return mip_status_t::INFEASIBLE; + } + // Might happen if the incumbent is already the optimal + settings_.log.printf("Strong branching: both branches fathomed for variable %d\n", j); + set_solution_at_root(solution, cut_info); + return mip_status_t::OPTIMAL; + } + if (down_infeasible) { + f_t new_lb = std::ceil(root_relax_soln_.x[j]); + if (new_lb > original_lp_.lower[j]) { + settings_.log.debug("SB tighten var %d: lb %e -> %e (%s)", + j, + original_lp_.lower[j], + new_lb, + down_cutoff ? "cutoff" : "infeasible"); + original_lp_.lower[j] = new_lb; + num_tightened++; + if (down_cutoff) { + num_cutoff++; + } else { + num_infeasible++; + } + } + } + if (up_infeasible) { + f_t new_ub = std::floor(root_relax_soln_.x[j]); + if (new_ub < original_lp_.upper[j]) { + settings_.log.debug("SB tighten var %d: ub %e -> %e (%s)", + j, + original_lp_.upper[j], + new_ub, + up_cutoff ? "cutoff" : "infeasible"); + original_lp_.upper[j] = new_ub; + num_tightened++; + if (up_cutoff) { + num_cutoff++; + } else { + num_infeasible++; + } + } + } + } + if (num_tightened > 0) { + settings_.log.printf( + "Strong branching bounds tightening: %d tightened (%d infeasible, %d cutoff)\n", + num_tightened, + num_infeasible, + num_cutoff); + + std::vector bounds_changed(original_lp_.num_cols, true); + std::vector row_sense; + bounds_strengthening_t sb_presolve(original_lp_, Arow_, row_sense, var_types_); + bool feasible = sb_presolve.bounds_strengthening( + settings_, bounds_changed, original_lp_.lower, original_lp_.upper); + if (!feasible) { + settings_.log.printf("Strong branching bounds propagation detected infeasibility\n"); + return mip_status_t::INFEASIBLE; + } + i_t num_fixed = prune_fixed_fractional_variables(original_lp_, settings_, fractional); + if (num_fixed > 0) { + settings_.log.printf( + "Strong branching bounds tightening: %d variables fixed (%d from propagation)\n", + num_fixed, + num_fixed - num_tightened); + num_fractional = fractional.size(); + } + + if (num_fractional == 0) { + set_solution_at_root(solution, cut_info); + return mip_status_t::OPTIMAL; + } + } + } + if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { std::vector lower_bounds; std::vector upper_bounds; @@ -2393,26 +2521,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::NUMERICAL; // We had a feasible integer solution, but bound // strengthening thinks we are infeasible. } - // Go through and check the fractional variables and remove any that are now fixed to their - // bounds - std::vector to_remove(fractional.size(), 0); - i_t num_to_remove = 0; - for (i_t k = 0; k < fractional.size(); k++) { - const i_t j = fractional[k]; - if (std::abs(original_lp_.upper[j] - original_lp_.lower[j]) < settings_.fixed_tol) { - to_remove[k] = 1; - num_to_remove++; - } - } - if (num_to_remove > 0) { - std::vector new_fractional; - new_fractional.reserve(fractional.size() - num_to_remove); - for (i_t k = 0; k < fractional.size(); k++) { - if (!to_remove[k]) { new_fractional.push_back(fractional[k]); } - } - fractional = new_fractional; - num_fractional = fractional.size(); - } + i_t num_fixed = prune_fixed_fractional_variables(original_lp_, settings_, fractional); + if (num_fixed > 0) { num_fractional = fractional.size(); } } } diff --git a/cpp/src/branch_and_bound/pseudo_costs.cpp b/cpp/src/branch_and_bound/pseudo_costs.cpp index ee7e2f7803..70f0bb06f3 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.cpp +++ b/cpp/src/branch_and_bound/pseudo_costs.cpp @@ -34,6 +34,7 @@ void strong_branch_helper(i_t start, const std::vector& root_soln, const std::vector& root_vstatus, const std::vector& edge_norms, + f_t upper_bound, pseudo_costs_t& pc) { raft::common::nvtx::range scope("BB::strong_branch_helper"); @@ -62,6 +63,7 @@ void strong_branch_helper(i_t start, if (elapsed_time > settings.time_limit) { break; } child_settings.time_limit = std::max(0.0, settings.time_limit - elapsed_time); child_settings.iteration_limit = 200; + child_settings.cut_off = upper_bound + settings.dual_tol; lp_solution_t solution(original_lp.num_rows, original_lp.num_cols); i_t iter = 0; std::vector vstatus = root_vstatus; @@ -80,7 +82,8 @@ void strong_branch_helper(i_t start, if (status == dual::status_t::DUAL_UNBOUNDED) { // LP was infeasible obj = std::numeric_limits::infinity(); - } else if (status == dual::status_t::OPTIMAL || status == dual::status_t::ITERATION_LIMIT) { + } else if (status == dual::status_t::OPTIMAL || status == dual::status_t::ITERATION_LIMIT || + status == dual::status_t::CUTOFF) { obj = compute_objective(child_problem, solution.x); } else { settings.log.debug("Thread id %2d remaining %d variable %d branch %d status %d\n", @@ -307,6 +310,7 @@ void strong_branching(const user_problem_t& original_problem, f_t root_obj, const std::vector& root_vstatus, const std::vector& edge_norms, + const omp_atomic_t& upper_bound, pseudo_costs_t& pc) { pc.resize(original_lp.num_cols); @@ -399,6 +403,7 @@ void strong_branching(const user_problem_t& original_problem, settings.num_threads, fractional.size()); f_t strong_branching_start_time = tic(); + const f_t current_upper_bound = (f_t)upper_bound; #pragma omp parallel num_threads(settings.num_threads) { @@ -432,6 +437,7 @@ void strong_branching(const user_problem_t& original_problem, root_soln, root_vstatus, edge_norms, + current_upper_bound, pc); } } @@ -786,6 +792,7 @@ template void strong_branching(const user_problem_t& o double root_obj, const std::vector& root_vstatus, const std::vector& edge_norms, + const omp_atomic_t& upper_bound, pseudo_costs_t& pc); #endif diff --git a/cpp/src/branch_and_bound/pseudo_costs.hpp b/cpp/src/branch_and_bound/pseudo_costs.hpp index 6b6c6917b6..9864b68099 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.hpp +++ b/cpp/src/branch_and_bound/pseudo_costs.hpp @@ -527,6 +527,7 @@ void strong_branching(const user_problem_t& original_problem, f_t root_obj, const std::vector& root_vstatus, const std::vector& edge_norms, + const omp_atomic_t& upper_bound, pseudo_costs_t& pc); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 235d4500d2..16262ff634 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -218,8 +218,10 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.knapsack_cuts = context.settings.knapsack_cuts; branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; + // default is reduced cost strengthening ON branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening; + context.settings.reduced_cost_strengthening < 0 ? 2 + : context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; branch_and_bound_settings.mip_batch_pdlp_strong_branching = From c6cef1d3a11e92e76a9e1c93d4dee6f94dbad2f3 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 02:11:35 -0800 Subject: [PATCH 02/10] review fixes --- cpp/src/branch_and_bound/branch_and_bound.cpp | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 52a9ef9237..61ffd6bf32 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2430,10 +2430,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Might happen if the incumbent is already the optimal settings_.log.printf("Strong branching: both branches fathomed for variable %d\n", j); - set_solution_at_root(solution, cut_info); - return mip_status_t::OPTIMAL; + bool has_incumbent = false; + mutex_upper_.lock(); + has_incumbent = incumbent_.has_incumbent; + mutex_upper_.unlock(); + assert(has_incumbent); + solver_status_ = mip_status_t::OPTIMAL; + set_final_solution(solution, upper_bound_.load()); + return solver_status_; } if (down_infeasible) { + mutex_original_lp_.lock(); f_t new_lb = std::ceil(root_relax_soln_.x[j]); if (new_lb > original_lp_.lower[j]) { settings_.log.debug("SB tighten var %d: lb %e -> %e (%s)", @@ -2449,8 +2456,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut num_infeasible++; } } + mutex_original_lp_.unlock(); } if (up_infeasible) { + mutex_original_lp_.lock(); f_t new_ub = std::floor(root_relax_soln_.x[j]); if (new_ub < original_lp_.upper[j]) { settings_.log.debug("SB tighten var %d: ub %e -> %e (%s)", @@ -2466,6 +2475,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut num_infeasible++; } } + mutex_original_lp_.unlock(); } } if (num_tightened > 0) { @@ -2493,10 +2503,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut num_fractional = fractional.size(); } - if (num_fractional == 0) { - set_solution_at_root(solution, cut_info); - return mip_status_t::OPTIMAL; - } + assert(num_fractional > 0); } } @@ -2522,7 +2529,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // strengthening thinks we are infeasible. } i_t num_fixed = prune_fixed_fractional_variables(original_lp_, settings_, fractional); - if (num_fixed > 0) { num_fractional = fractional.size(); } + if (num_fixed > 0) { + num_fractional = fractional.size(); + assert(num_fractional > 0); + } } } From 8b98bf31d914e1e7a81c7ef89dcfcdbe3c711aec Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 02:40:04 -0800 Subject: [PATCH 03/10] float arg for upper bound --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 +- cpp/src/branch_and_bound/pseudo_costs.cpp | 7 +++---- cpp/src/branch_and_bound/pseudo_costs.hpp | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 61ffd6bf32..9621d63c1d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2385,7 +2385,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_objective_, root_vstatus_, edge_norms_, - upper_bound_, + upper_bound_.load(), pc_); } diff --git a/cpp/src/branch_and_bound/pseudo_costs.cpp b/cpp/src/branch_and_bound/pseudo_costs.cpp index 70f0bb06f3..7633a33990 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.cpp +++ b/cpp/src/branch_and_bound/pseudo_costs.cpp @@ -310,7 +310,7 @@ void strong_branching(const user_problem_t& original_problem, f_t root_obj, const std::vector& root_vstatus, const std::vector& edge_norms, - const omp_atomic_t& upper_bound, + f_t upper_bound, pseudo_costs_t& pc) { pc.resize(original_lp.num_cols); @@ -403,7 +403,6 @@ void strong_branching(const user_problem_t& original_problem, settings.num_threads, fractional.size()); f_t strong_branching_start_time = tic(); - const f_t current_upper_bound = (f_t)upper_bound; #pragma omp parallel num_threads(settings.num_threads) { @@ -437,7 +436,7 @@ void strong_branching(const user_problem_t& original_problem, root_soln, root_vstatus, edge_norms, - current_upper_bound, + upper_bound, pc); } } @@ -792,7 +791,7 @@ template void strong_branching(const user_problem_t& o double root_obj, const std::vector& root_vstatus, const std::vector& edge_norms, - const omp_atomic_t& upper_bound, + double upper_bound, pseudo_costs_t& pc); #endif diff --git a/cpp/src/branch_and_bound/pseudo_costs.hpp b/cpp/src/branch_and_bound/pseudo_costs.hpp index 9864b68099..4b7a852d31 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.hpp +++ b/cpp/src/branch_and_bound/pseudo_costs.hpp @@ -527,7 +527,7 @@ void strong_branching(const user_problem_t& original_problem, f_t root_obj, const std::vector& root_vstatus, const std::vector& edge_norms, - const omp_atomic_t& upper_bound, + f_t upper_bound, pseudo_costs_t& pc); } // namespace cuopt::linear_programming::dual_simplex From ebb0a0025b89f471728840633617d3fb209a7926 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 03:30:46 -0800 Subject: [PATCH 04/10] resolve if fractionals disappear after fixings --- cpp/src/branch_and_bound/branch_and_bound.cpp | 64 ++++++++++++++++++- cpp/src/mip_heuristics/solver.cu | 2 +- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 9621d63c1d..f14e41b420 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2503,7 +2503,37 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut num_fractional = fractional.size(); } - assert(num_fractional > 0); + if (num_fractional == 0) { + lp_settings.concurrent_halt = NULL; + i_t iter = 0; + bool initialize_basis = false; + dual::status_t lp_status = dual_phase2_with_advanced_basis(2, + 0, + initialize_basis, + exploration_stats_.start_time, + original_lp_, + lp_settings, + root_vstatus_, + basis_update, + basic_list, + nonbasic_list, + root_relax_soln_, + iter, + edge_norms_); + exploration_stats_.total_lp_iters += iter; + root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + if (lp_status == dual::status_t::OPTIMAL) { + set_solution_at_root(solution, cut_info); + return mip_status_t::OPTIMAL; + } + if (lp_status == dual::status_t::TIME_LIMIT) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } + settings_.log.printf("LP re-solve after SB tightening returned status %d\n", lp_status); + return mip_status_t::NUMERICAL; + } } } @@ -2531,7 +2561,37 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t num_fixed = prune_fixed_fractional_variables(original_lp_, settings_, fractional); if (num_fixed > 0) { num_fractional = fractional.size(); - assert(num_fractional > 0); + if (num_fractional == 0) { + lp_settings.concurrent_halt = NULL; + i_t iter = 0; + bool initialize_basis = false; + dual::status_t lp_status = dual_phase2_with_advanced_basis(2, + 0, + initialize_basis, + exploration_stats_.start_time, + original_lp_, + lp_settings, + root_vstatus_, + basis_update, + basic_list, + nonbasic_list, + root_relax_soln_, + iter, + edge_norms_); + exploration_stats_.total_lp_iters += iter; + root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + if (lp_status == dual::status_t::OPTIMAL) { + set_solution_at_root(solution, cut_info); + return mip_status_t::OPTIMAL; + } + if (lp_status == dual::status_t::TIME_LIMIT) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } + settings_.log.printf("LP re-solve after RC tightening returned status %d\n", lp_status); + return mip_status_t::NUMERICAL; + } } } } diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 16262ff634..2c7f8ac1aa 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -220,7 +220,7 @@ solution_t mip_solver_t::run_solver() context.settings.strong_chvatal_gomory_cuts; // default is reduced cost strengthening ON branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening < 0 ? 2 + context.settings.reduced_cost_strengthening < 0 ? -1 : context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; From ee00179959541e0dcdb8f78337dc100b89b6be9e Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 03:30:58 -0800 Subject: [PATCH 05/10] RC 1 --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 2c7f8ac1aa..81c16334fe 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -220,7 +220,7 @@ solution_t mip_solver_t::run_solver() context.settings.strong_chvatal_gomory_cuts; // default is reduced cost strengthening ON branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening < 0 ? -1 + context.settings.reduced_cost_strengthening < 0 ? 1 : context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; From 62e75ccb58caf5665cd3ed5eb9c07130348641d2 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 03:31:09 -0800 Subject: [PATCH 06/10] RC 2 --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 81c16334fe..16262ff634 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -220,7 +220,7 @@ solution_t mip_solver_t::run_solver() context.settings.strong_chvatal_gomory_cuts; // default is reduced cost strengthening ON branch_and_bound_settings.reduced_cost_strengthening = - context.settings.reduced_cost_strengthening < 0 ? 1 + context.settings.reduced_cost_strengthening < 0 ? 2 : context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; branch_and_bound_settings.cut_min_orthogonality = context.settings.cut_min_orthogonality; From 6e42dcc7d0c521a02843f343f1638f1a574d68a2 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 06:44:08 -0800 Subject: [PATCH 07/10] check fractionality after resolve --- cpp/src/branch_and_bound/branch_and_bound.cpp | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index f14e41b420..349f0698ff 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2523,16 +2523,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.total_lp_iters += iter; root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); if (lp_status == dual::status_t::OPTIMAL) { - set_solution_at_root(solution, cut_info); - return mip_status_t::OPTIMAL; - } - if (lp_status == dual::status_t::TIME_LIMIT) { + fractional.clear(); + num_fractional = + fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); + if (num_fractional == 0) { + set_solution_at_root(solution, cut_info); + return mip_status_t::OPTIMAL; + } + } else if (lp_status == dual::status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); return solver_status_; + } else { + settings_.log.printf("LP re-solve after SB tightening returned status %d\n", lp_status); + return mip_status_t::NUMERICAL; } - settings_.log.printf("LP re-solve after SB tightening returned status %d\n", lp_status); - return mip_status_t::NUMERICAL; } } } @@ -2581,16 +2586,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.total_lp_iters += iter; root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); if (lp_status == dual::status_t::OPTIMAL) { - set_solution_at_root(solution, cut_info); - return mip_status_t::OPTIMAL; - } - if (lp_status == dual::status_t::TIME_LIMIT) { + fractional.clear(); + num_fractional = + fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); + if (num_fractional == 0) { + set_solution_at_root(solution, cut_info); + return mip_status_t::OPTIMAL; + } + } else if (lp_status == dual::status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); return solver_status_; + } else { + settings_.log.printf("LP re-solve after RC tightening returned status %d\n", lp_status); + return mip_status_t::NUMERICAL; } - settings_.log.printf("LP re-solve after RC tightening returned status %d\n", lp_status); - return mip_status_t::NUMERICAL; } } } From d776e510a51ec53ae3f248ed6fc925feac61098b Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 06:45:36 -0800 Subject: [PATCH 08/10] comment for clarity --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 349f0698ff..d5c245f058 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2503,6 +2503,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut num_fractional = fractional.size(); } + // If no fractionals remain after the fixings - perform a resolve + // to get fractionals to branch on, or return optimality if the root relaxation is integer if (num_fractional == 0) { lp_settings.concurrent_halt = NULL; i_t iter = 0; From b227763cdfd83386971bab00fd0ebc23698b8ad4 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 07:28:51 -0800 Subject: [PATCH 09/10] review comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index d5c245f058..3a23a3dce7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -71,7 +71,8 @@ i_t fractional_variables(const simplex_solver_settings_t& settings, } template -i_t prune_fixed_fractional_variables(const lp_problem_t& lp, +i_t prune_fixed_fractional_variables(const std::vector& lower_bounds, + const std::vector& upper_bounds, const simplex_solver_settings_t& settings, std::vector& fractional) { @@ -81,7 +82,7 @@ i_t prune_fixed_fractional_variables(const lp_problem_t& lp, i_t num_fixed = 0; for (i_t k = 0; k < (i_t)fractional.size(); k++) { const i_t j = fractional[k]; - if (std::abs(lp.upper[j] - lp.lower[j]) < settings.fixed_tol) { + if (std::abs(upper_bounds[j] - lower_bounds[j]) < settings.fixed_tol) { num_fixed++; } else { new_fractional.push_back(j); @@ -2487,17 +2488,30 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::vector bounds_changed(original_lp_.num_cols, true); std::vector row_sense; + std::vector new_lower; + std::vector new_upper; + mutex_original_lp_.lock(); + new_lower = original_lp_.lower; + new_upper = original_lp_.upper; + mutex_original_lp_.unlock(); bounds_strengthening_t sb_presolve(original_lp_, Arow_, row_sense, var_types_); - bool feasible = sb_presolve.bounds_strengthening( - settings_, bounds_changed, original_lp_.lower, original_lp_.upper); + bool feasible = + sb_presolve.bounds_strengthening(settings_, bounds_changed, new_lower, new_upper); + i_t num_fixed = 0; + if (feasible) { + num_fixed = prune_fixed_fractional_variables(new_lower, new_upper, settings_, fractional); + } + mutex_original_lp_.lock(); + original_lp_.lower = new_lower; + original_lp_.upper = new_upper; + mutex_original_lp_.unlock(); if (!feasible) { settings_.log.printf("Strong branching bounds propagation detected infeasibility\n"); return mip_status_t::INFEASIBLE; } - i_t num_fixed = prune_fixed_fractional_variables(original_lp_, settings_, fractional); if (num_fixed > 0) { settings_.log.printf( - "Strong branching bounds tightening: %d variables fixed (%d from propagation)\n", + "1Strong branching bounds tightening: %d variables fixed (%d from propagation)\n", num_fixed, num_fixed - num_tightened); num_fractional = fractional.size(); @@ -2551,21 +2565,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (num_fixed > 0) { std::vector bounds_changed(original_lp_.num_cols, true); std::vector row_sense; - + std::vector new_lower = lower_bounds; + std::vector new_upper = upper_bounds; bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); - - mutex_original_lp_.lock(); - original_lp_.lower = lower_bounds; - original_lp_.upper = upper_bounds; - bool feasible = node_presolve.bounds_strengthening( - settings_, bounds_changed, original_lp_.lower, original_lp_.upper); - mutex_original_lp_.unlock(); + bool feasible = + node_presolve.bounds_strengthening(settings_, bounds_changed, new_lower, new_upper); if (!feasible) { settings_.log.printf("Bound strengthening failed\n"); return mip_status_t::NUMERICAL; // We had a feasible integer solution, but bound // strengthening thinks we are infeasible. } - i_t num_fixed = prune_fixed_fractional_variables(original_lp_, settings_, fractional); + i_t num_fixed = prune_fixed_fractional_variables(new_lower, new_upper, settings_, fractional); + mutex_original_lp_.lock(); + original_lp_.lower = new_lower; + original_lp_.upper = new_upper; + mutex_original_lp_.unlock(); if (num_fixed > 0) { num_fractional = fractional.size(); if (num_fractional == 0) { From c84928b0c11c1f9d2eee6e483a35a865e6266e46 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 3 Mar 2026 07:29:02 -0800 Subject: [PATCH 10/10] bump --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3a23a3dce7..e1ff6c15f4 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2511,7 +2511,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (num_fixed > 0) { settings_.log.printf( - "1Strong branching bounds tightening: %d variables fixed (%d from propagation)\n", + "Strong branching bounds tightening: %d variables fixed (%d from propagation)\n", num_fixed, num_fixed - num_tightened); num_fractional = fractional.size();