From 63d1ba4e74fe6a22d51bc39330fe187e3e7a6e11 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 27 Feb 2026 22:24:50 -0800 Subject: [PATCH 01/34] Fix bugs causing primal simplex to cycle. Enable primal simplex cleanup after dual simplex Fixed the following bugs that were causing primal simplex to cycle: 1) Swapped input/output arguments in b_solve() 2) Incorrectly setting variable status of leaving variable 3) Primal step length was not limited by bounds of entering variable. Also fixed a bug/typo where the basis was reorderd twice after factorization. Added code to switch to phase I if we loose primal feasibility, and switch back to phase II once feasibility is regained. Tested on NETLIB LPs. Only 2 LPs pilot87 and pilot_ja need primal simplex to remove perturbations at the end of the dual simplex solve. Tested on the 14 MIPLIB root relaxations that need primal simplex to remove perturbations at the end of the dual simplex solve. --- cpp/src/dual_simplex/phase2.cpp | 7 +- cpp/src/dual_simplex/primal.cpp | 311 +++++++++++++++++++++----------- cpp/src/dual_simplex/solve.cpp | 8 +- 3 files changed, 219 insertions(+), 107 deletions(-) diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 2e3c1e05c5..c15f7f554c 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2367,6 +2367,9 @@ void prepare_optimality(i_t info, perturbation = 0.0; } else { settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); + settings.log.printf("Unperturbed dual infeasibility: %.2e\n", dual_infeas); + settings.log.printf("Objective: %+.16e\n", sol.user_objective); + settings.log.printf("Num updates: %d\n", ft.num_updates()); } } } @@ -3737,10 +3740,6 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, 100.0 * dense_delta_z / (sparse_delta_z + dense_delta_z)); ft.print_stats(); } - if (settings.inside_mip == 1 && settings.concurrent_halt != nullptr) { - settings.log.debug("Setting concurrent halt in Dual Simplex Phase 2\n"); - *settings.concurrent_halt = 1; - } } return status; } diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 78c7107ca3..ec7a9bc25c 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -21,7 +21,6 @@ namespace { template void set_primal_variables_on_bounds(const lp_problem_t& lp, const simplex_solver_settings_t& settings, - const std::vector& z, std::vector& vstatus, std::vector& x) { @@ -158,7 +157,9 @@ i_t ratio_test(const lp_problem_t& lp, std::vector& x, std::vector& delta_x, f_t& step_length, - i_t& basic_leaving) + i_t& basic_leaving, + i_t entering_index, + i_t direction) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -166,28 +167,51 @@ i_t ratio_test(const lp_problem_t& lp, i_t leaving_index = -1; f_t min_val = inf; constexpr f_t pivot_tol = 1e-8; + + // Entering variable can hit its opposite bound: limit step by that + if (direction > 0 && lp.upper[entering_index] < inf) { + const f_t limit = lp.upper[entering_index] - x[entering_index]; + if (limit >= 0 && limit < min_val) { + min_val = limit; + leaving_index = -1; // no basic leaves; will be handled by caller + basic_leaving = -1; + } + } else if (direction < 0 && lp.lower[entering_index] > -inf) { + const f_t limit = x[entering_index] - lp.lower[entering_index]; + if (limit >= 0 && limit < min_val) { + min_val = limit; + leaving_index = -1; + basic_leaving = -1; + } + } + for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; if (delta_x[j] == 0.0) { continue; } - if (lp.lower[j] > -inf && x[j] >= lp.lower[j] && delta_x[j] < -pivot_tol) { + if (lp.lower[j] > -inf && delta_x[j] < -pivot_tol) { // xj + step * delta_x[j] >= lp.lower[j] // step * delta_x[j] >= lp.lower[j] - x[j] // step <= (lp.lower[j] - x[j]) / delta_x[j], delta_x[j] < 0 const f_t neum = lp.lower[j] - x[j]; f_t ratio = neum / delta_x[j]; - if (ratio < min_val) { + // Already below lower and moving further (delta_x < 0): cap step at 0 + if (x[j] < lp.lower[j]) { ratio = 0; } + if (ratio >= 0 && ratio < min_val) { min_val = ratio; basic_leaving = k; leaving_index = j; } } - if (lp.upper[j] < inf && x[j] <= lp.upper[j] && delta_x[j] > pivot_tol) { + if (lp.upper[j] < inf && delta_x[j] > pivot_tol) { // xj + step * delta_x[j] <= lp.upper[j] // step * delta_x[j] <= lp.upper[j] - x[j] // step <= (lp.upper[j] - x[j]) / delta_x[j], delta_x[j] > 0 const f_t neum = lp.upper[j] - x[j]; f_t ratio = neum / delta_x[j]; - if (ratio < min_val) { + // If x[j] > upper (slightly infeasible), ratio < 0; skip so we don't use it. + // But if we're already above upper and would move further (delta_x > 0), cap step at 0. + if (x[j] > lp.upper[j]) { ratio = 0; } + if (ratio >= 0 && ratio < min_val) { min_val = ratio; basic_leaving = k; leaving_index = j; @@ -207,7 +231,7 @@ f_t primal_infeasibility(const lp_problem_t& lp, const i_t n = lp.num_cols; f_t primal_inf = 0; for (i_t j = 0; j < n; ++j) { - if (x[j] < lp.lower[j]) { + if (x[j] < lp.lower[j] - settings.primal_tol) { // x_j < l_j => -x_j > -l_j => -x_j + l_j > 0 const f_t infeas = -x[j] + lp.lower[j]; primal_inf += infeas; @@ -221,7 +245,7 @@ f_t primal_infeasibility(const lp_problem_t& lp, vstatus[j]); } } - if (x[j] > lp.upper[j]) { + if (x[j] > lp.upper[j] + settings.primal_tol) { // x_j > u_j => x_j - u_j > 0 const f_t infeas = x[j] - lp.upper[j]; primal_inf += infeas; @@ -239,12 +263,69 @@ f_t primal_infeasibility(const lp_problem_t& lp, return primal_inf; } +template +void compute_phase1_objective(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& x, + std::vector& objective) +{ + const i_t n = lp.num_cols; + for (i_t j = 0; j < n; ++j) { + if (x[j] < lp.lower[j] - settings.primal_tol) { + objective[j] = -1.0; + } else if (x[j] > lp.upper[j] + settings.primal_tol) { + objective[j] = 1.0; + } else { + objective[j] = 0.0; + } + } +} + +template +void compute_dual_variables(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& objective, + const std::vector& basic_list, + const std::vector& nonbasic_list, + basis_update_t& ft, + std::vector& c_basic, + std::vector& y, + std::vector& z) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + // Solve for y such that B'*y = c_B + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + c_basic[k] = objective[j]; + } + ft.b_transpose_solve(c_basic, y); + // zN = cN - N'*y + for (i_t k = 0; k < n - m; k++) { + const i_t j = nonbasic_list[k]; + // z_j <- c_j + z[j] = objective[j]; + + // z_j <- z_j - A(:, j)'*y + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + f_t dot = 0.0; + for (i_t p = col_start; p < col_end; ++p) { + dot += lp.A.x[p] * y[lp.A.i[p]]; + } + z[j] -= dot; + } + // zB = 0 + for (i_t k = 0; k < m; ++k) { + z[basic_list[k]] = 0.0; + } +} + } // namespace // Note this implementation of primal simplex is experimental // It is meant only to serve as a method to remove the perturbation to the objective // after dual simplex has found a primal feasible solution -// The implementation currently cycles. So is not enabled at this time. template primal_status_t primal_phase2(i_t phase, f_t start_time, @@ -308,6 +389,7 @@ primal_status_t primal_phase2(i_t phase, slacks_needed, work_estimate); if (rank == CONCURRENT_HALT_RETURN) { + settings.log.printf("Concurrent halt in primal phase2\n"); return primal_status_t::CONCURRENT_LIMIT; } else if (rank == TIME_LIMIT_RETURN) { return primal_status_t::TIME_LIMIT; @@ -352,46 +434,8 @@ primal_status_t primal_phase2(i_t phase, } } reorder_basic_list(q, basic_list); - reorder_basic_list(q, basic_list); basis_update_t ft(L, U, p); - std::vector c_basic(m); - for (i_t k = 0; k < m; ++k) { - const i_t j = basic_list[k]; - c_basic[k] = lp.objective[j]; - } - - // Solve B'*y = cB - ft.b_transpose_solve(c_basic, y); - settings.log.printf( - "|| y || %e || cB || %e\n", vector_norm_inf(y), vector_norm_inf(c_basic)); - - // zN = cN - N'*y - for (i_t k = 0; k < n - m; k++) { - const i_t j = nonbasic_list[k]; - // z_j <- c_j - z[j] = lp.objective[j]; - - // z_j <- z_j - A(:, j)'*y - const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; - f_t dot = 0.0; - for (i_t p = col_start; p < col_end; ++p) { - dot += lp.A.x[p] * y[lp.A.i[p]]; - } - z[j] -= dot; - } - // zB = 0 - for (i_t k = 0; k < m; ++k) { - z[basic_list[k]] = 0.0; - } - settings.log.printf("|| z || %e\n", vector_norm_inf(z)); - - set_primal_variables_on_bounds(lp, settings, z, vstatus, x); - - const f_t init_dual_inf = dual_infeasibility(lp, vstatus, z); - settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); - std::vector rhs = lp.rhs; // rhs = b - sum_{j : x_j = l_j} A(:, j) l(j) - sum_{j : x_j = u_j} A(:, j) * // u(j) @@ -412,6 +456,7 @@ primal_status_t primal_phase2(i_t phase, const i_t j = basic_list[k]; x[j] = xB[k]; } + set_primal_variables_on_bounds(lp, settings, vstatus, x); settings.log.printf("|| x || %e\n", vector_norm2(x)); std::vector residual = lp.rhs; @@ -421,6 +466,23 @@ primal_status_t primal_phase2(i_t phase, f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); settings.log.printf("Initial primal infeasibility %e\n", primal_inf); + std::vector objective = lp.objective; + const f_t primal_tol = settings.primal_tol; + if (primal_inf > primal_tol) { + // We are primal infeasible. Switch to phase 1 + compute_phase1_objective(lp, settings, x, objective); + phase = 1; + } else { + phase = 2; + } + + std::vector c_basic(m); + compute_dual_variables(lp, settings, objective, basic_list, nonbasic_list, ft, c_basic, y, z); + settings.log.printf("|| z || %e\n", vector_norm_inf(z)); + + const f_t init_dual_inf = dual_infeasibility(lp, vstatus, z); + settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); + const i_t iter_limit = iter + 1000; std::vector delta_y(m); std::vector delta_z(n); @@ -434,16 +496,34 @@ primal_status_t primal_phase2(i_t phase, i_t entering_index = phase2_pricing(lp, z, nonbasic_list, vstatus, direction, nonbasic_entering, dual_inf); if (entering_index == -1) { - f_t obj = compute_objective(lp, x); - f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); - settings.log.printf( - "Optimal solution found. Objective %e. Dual infeas %e. Primal " - "infeasibility %e. Iterations %d\n", - compute_user_objective(lp, obj), - dual_inf, - primal_inf, - iter); - return primal_status_t::OPTIMAL; + if (phase == 2) { + f_t obj = compute_objective(lp, x); + f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); + settings.log.printf( + "Optimal solution found. Objective %+.16e. Dual infeas %e. Primal " + "infeasibility %e. Iterations %d\n", + compute_user_objective(lp, obj), + dual_inf, + primal_inf, + iter); + return primal_status_t::OPTIMAL; + } else { + primal_inf = primal_infeasibility(lp, settings, vstatus, x); + + if (primal_inf > primal_tol) { + settings.log.printf("Primal infeasibility %e. No entering\n", primal_inf); + return primal_status_t::NUMERICAL; + } else { + // Restore the objective to the original objective + objective = lp.objective; + phase = 2; + settings.log.printf("Switching to phase 2\n"); + compute_dual_variables( + lp, settings, objective, basic_list, nonbasic_list, ft, c_basic, y, z); + iter++; + continue; + } + } } std::vector scaled_delta_xB(m); @@ -473,70 +553,97 @@ primal_status_t primal_phase2(i_t phase, i_t basic_leaving; f_t step_length; - i_t leaving_index = ratio_test(lp, vstatus, basic_list, x, delta_x, step_length, basic_leaving); - if (leaving_index == -1) { + i_t leaving_index = ratio_test( + lp, vstatus, basic_list, x, delta_x, step_length, basic_leaving, entering_index, direction); + if (leaving_index == -1 && step_length >= inf) { settings.log.printf("No leaving variable. Primal unbounded?\n"); return primal_status_t::PRIMAL_UNBOUNDED; } - assert(step_length >= 0.0); - // Update the primal variables + const bool basis_updated = (leaving_index != -1); for (i_t j = 0; j < n; ++j) { x[j] += step_length * delta_x[j]; } + if (basis_updated) { + assert(step_length >= 0.0); + basic_list[basic_leaving] = entering_index; + nonbasic_list[nonbasic_entering] = leaving_index; + vstatus[entering_index] = variable_status_t::BASIC; + if (std::abs(lp.upper[leaving_index] - lp.lower[leaving_index]) < 1e-12) { + vstatus[leaving_index] = variable_status_t::NONBASIC_FIXED; + } else if (delta_x[leaving_index] < 0) { + vstatus[leaving_index] = variable_status_t::NONBASIC_LOWER; + } else { + vstatus[leaving_index] = variable_status_t::NONBASIC_UPPER; + } - // Update the factorization - ft.update(utilde, basic_leaving); - - // Update the basis - basic_list[basic_leaving] = entering_index; - nonbasic_list[nonbasic_entering] = leaving_index; - vstatus[entering_index] = variable_status_t::BASIC; - if (std::abs(lp.upper[leaving_index] - lp.lower[leaving_index]) < 1e-12) { - vstatus[leaving_index] = variable_status_t::NONBASIC_FIXED; - } else if (direction == 1) { - vstatus[leaving_index] = variable_status_t::NONBASIC_LOWER; + bool should_refactor = ft.num_updates() > settings.refactor_frequency; + if (!should_refactor) { + i_t recommend_refactor = ft.update(utilde, basic_leaving); + should_refactor = recommend_refactor == 1; + } + if (should_refactor) { + i_t rank = factorize_basis(lp.A, + settings, + basic_list, + start_time, + L, + U, + p, + pinv, + q, + deficient, + slacks_needed, + work_estimate); + if (rank == CONCURRENT_HALT_RETURN) { return primal_status_t::CONCURRENT_LIMIT; } + if (rank == TIME_LIMIT_RETURN) { return primal_status_t::TIME_LIMIT; } + if (rank < 0) { + settings.log.printf("Failed to refactor basis. Iteration %d\n", iter); + return primal_status_t::NUMERICAL; + } + if (rank != m) { + settings.log.printf("Failed to refactor basis. rank %d m %d\n", rank, m); + return primal_status_t::NUMERICAL; + } + reorder_basic_list(q, basic_list); + ft.reset(L, U, p); + } } else { - vstatus[leaving_index] = variable_status_t::NONBASIC_UPPER; + if (direction > 0) { + vstatus[entering_index] = variable_status_t::NONBASIC_UPPER; + x[entering_index] = lp.upper[entering_index]; + } else { + vstatus[entering_index] = variable_status_t::NONBASIC_LOWER; + x[entering_index] = lp.lower[entering_index]; + } } - // Solve for y such that B'*y = c_B - for (i_t k = 0; k < m; ++k) { - const i_t j = basic_list[k]; - c_basic[k] = lp.objective[j]; - } - ft.b_transpose_solve(y, c_basic); - // zN = cN - N'*y - for (i_t k = 0; k < n - m; k++) { - const i_t j = nonbasic_list[k]; - // z_j <- c_j - z[j] = lp.objective[j]; - - // z_j <- z_j - A(:, j)'*y - const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; - f_t dot = 0.0; - for (i_t p = col_start; p < col_end; ++p) { - dot += lp.A.x[p] * y[lp.A.i[p]]; - } - z[j] -= dot; + // Check if we need to switch to phase 1 + const f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); + if (primal_inf > primal_tol) { + compute_phase1_objective(lp, settings, x, objective); + phase = 1; + } else if (phase == 1) { + objective = lp.objective; + phase = 2; } - // zB = 0 - for (i_t k = 0; k < m; ++k) { - z[basic_list[k]] = 0.0; + + if (basis_updated || primal_inf > primal_tol) { + compute_dual_variables(lp, settings, objective, basic_list, nonbasic_list, ft, c_basic, y, z); } - const f_t obj = compute_objective(lp, x); - const f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); - settings.log.printf("%3d %.10e %.2e %.2e %.2e %d %d\n", + const f_t obj = compute_objective(lp, x); + dual_inf = dual_infeasibility(lp, vstatus, z); + settings.log.printf("%3d %.10e %8.2e %8.2e %8.2e %8d %8d %d %.2f\n", iter, compute_user_objective(lp, obj), primal_inf, dual_inf, - step_length, + step_length == 0.0 ? 0.0 : step_length, entering_index, - leaving_index); - + leaving_index, + phase, + toc(start_time)); iter++; } diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 697af9e869..c13e35c525 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -288,9 +288,15 @@ lp_status_t solve_linear_program_with_advanced_basis( edge_norms, work_unit_context); } - constexpr bool primal_cleanup = false; + constexpr bool primal_cleanup = true; if (status == dual_status_t::OPTIMAL && primal_cleanup) { + settings.log.printf("Running primal cleanup\n"); primal_phase2(2, start_time, lp, settings, vstatus, solution, iter); + // TODO: We need to update ft if the basis changed + } + if (settings.inside_mip && settings.concurrent_halt != nullptr) { + settings.log.printf("Setting concurrent halt to 1 inside_mip\n"); + *settings.concurrent_halt = 1; } if (status == dual_status_t::OPTIMAL) { std::vector unscaled_x(lp.num_cols); From 8b1e60bcf2d3522113ce7b09e088addf1f234922 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Tue, 21 Jul 2026 11:48:59 -0700 Subject: [PATCH 02/34] Primal simplex pivots on dual degenerate problems to reduce integer infeasibility --- cpp/src/branch_and_bound/branch_and_bound.cpp | 200 ++++++++++++++++++ cpp/src/branch_and_bound/branch_and_bound.hpp | 9 + cpp/src/dual_simplex/primal.cpp | 162 +++++++------- cpp/src/dual_simplex/primal.hpp | 13 ++ cpp/src/dual_simplex/solve.cpp | 2 +- 5 files changed, 311 insertions(+), 75 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e4ce4dfd7b..0a3629e7f8 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +89,7 @@ i_t fractional_variables(const simplex_solver_settings_t& settings, { const i_t n = x.size(); assert(x.size() == var_types.size()); + fractional.clear(); for (i_t j = 0; j < n; ++j) { if (is_fractional(x[j], var_types[j], settings.integer_tol)) { fractional.push_back(j); } } @@ -763,6 +765,9 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& exploration_stats_.lexical_reduction_fixings_applied.load(), exploration_stats_.lexical_reduction_pruned_nodes.load()); } + if (integer_pivots_.load() > 0) { + settings_.log.print_format("Number of integer pivots: {}\n", integer_pivots_.load()); + } if (gap <= settings_.absolute_mip_gap_tol || gap_rel <= settings_.relative_mip_gap_tol) { solver_status_ = mip_status_t::OPTIMAL; @@ -1545,6 +1550,20 @@ dual_status_t branch_and_bound_t::solve_node_lp( stats.total_lp_solve_time += toc(lp_start_time); stats.total_lp_iters += node_iter; + + if (lp_status == dual_status_t::OPTIMAL) { + std::vector fractional; + i_t num_fractional = + fractional_variables(settings_, worker->leaf_solution.x, var_types_, fractional); + pivot_out_integer_variables(worker->leaf_problem, + worker->basic_list, + worker->nonbasic_list, + worker->leaf_vstatus, + worker->leaf_solution, + worker->basis_factors, + num_fractional, + fractional); + } } } @@ -2390,6 +2409,19 @@ auto branch_and_bound_t::do_cut_pass( } root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + // Refresh fractional info after re-solving with cuts; the pre-cut count is stale. + num_fractional = + fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); + + pivot_out_integer_variables(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->root_lp_with_cuts = compute_user_objective(original_lp_, root_objective_); @@ -2455,6 +2487,165 @@ auto branch_and_bound_t::do_cut_pass( return {cut_pass_action_t::CONTINUE, mip_status_t::UNSET}; } +template +void branch_and_bound_t::pivot_out_integer_variables( + const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional) +{ + std::vector zero_reduced_costs_vars; + std::vector zero_reduced_costs_vars_nonbasic_index; + for (i_t k = 0; k < static_cast(nonbasic_list.size()); k++) { + const i_t j = nonbasic_list[k]; + if (std::abs(solution.z[j]) <= 1e-10) { + zero_reduced_costs_vars.push_back(j); + zero_reduced_costs_vars_nonbasic_index.push_back(k); + } + } + if (zero_reduced_costs_vars.empty()) { return; } + + lp_solution_t soln_copy = solution; + std::vector basic_list_copy = basic_list; + std::vector nonbasic_list_copy = nonbasic_list; + std::vector vstatus_copy = vstatus; + simplex::basis_update_mpf_t basis_update_copy = basis_update; + + const i_t start_num_fractional = num_fractional; + + for (i_t k = 0; k < static_cast(zero_reduced_costs_vars.size()); k++) { + const i_t j = zero_reduced_costs_vars[k]; + if (var_types_[j] == variable_type_t::INTEGER) { continue; } + if (vstatus_copy[j] == variable_status_t::BASIC) { continue; } + + const i_t direction = + (vstatus_copy[j] == variable_status_t::NONBASIC_LOWER || + vstatus_copy[j] == variable_status_t::NONBASIC_FIXED) + ? 1 + : -1; + const i_t entering_index = j; + const i_t nonbasic_entering = zero_reduced_costs_vars_nonbasic_index[k]; + if (nonbasic_list_copy[nonbasic_entering] != j) { continue; } + + // Solve B * dxB = A(:, j) so utilde is valid for the MPF update. + // Apply direction when forming delta_x (same convention as primal_phase2). + sparse_vector_t rhs(lp.A, j); + sparse_vector_t delta_xB; + sparse_vector_t utilde_sparse; + basis_update_copy.b_solve(rhs, delta_xB, utilde_sparse); + + std::vector delta_xB_dense; + delta_xB.to_dense(delta_xB_dense); + std::vector delta_x(lp.num_cols, 0.0); + for (i_t h = 0; h < static_cast(basic_list_copy.size()); h++) { + delta_x[basic_list_copy[h]] = -direction * delta_xB_dense[h]; + } + delta_x[j] = direction; + + f_t step_length; + i_t basic_leaving; + const i_t leaving_index = simplex::primal_ratio_test(lp, + settings_, + vstatus_copy, + basic_list_copy, + soln_copy.x, + delta_x, + step_length, + basic_leaving, + entering_index, + direction); + bool binding_integer = + leaving_index != -1 && + is_fractional(soln_copy.x[leaving_index], var_types_[leaving_index], settings_.integer_tol); + if (!binding_integer) { continue; } + + std::vector test_x = soln_copy.x; + i_t integer_destroyed = 0; + for (i_t h = 0; h < lp.num_cols; ++h) { + test_x[h] += step_length * delta_x[h]; + if (var_types_[h] != variable_type_t::INTEGER) { continue; } + const bool was_fractional = + is_fractional(soln_copy.x[h], var_types_[h], settings_.integer_tol); + const bool now_fractional = + is_fractional(test_x[h], var_types_[h], settings_.integer_tol); + if (now_fractional && !was_fractional) { + integer_destroyed++; + } else if (!now_fractional && was_fractional) { + integer_destroyed--; + } + } + // Require a strict net decrease in fractional integers. + if (integer_destroyed >= 0) { continue; } + + soln_copy.x = test_x; + basic_list_copy[basic_leaving] = entering_index; + nonbasic_list_copy[nonbasic_entering] = leaving_index; + vstatus_copy[entering_index] = variable_status_t::BASIC; + if (std::abs(lp.upper[leaving_index] - lp.lower[leaving_index]) < 1e-12) { + vstatus_copy[leaving_index] = variable_status_t::NONBASIC_FIXED; + } else if (delta_x[leaving_index] < 0) { + vstatus_copy[leaving_index] = variable_status_t::NONBASIC_LOWER; + } else { + vstatus_copy[leaving_index] = variable_status_t::NONBASIC_UPPER; + } + + const i_t m = lp.num_rows; + sparse_vector_t es_sparse(m, 1); + es_sparse.i[0] = basic_leaving; + es_sparse.x[0] = 1.0; + sparse_vector_t UTsol_sparse(m, 1); + sparse_vector_t solution_sparse(m, 1); + basis_update_copy.b_transpose_solve(es_sparse, solution_sparse, UTsol_sparse); + const i_t recommend_refactor = + basis_update_copy.update(utilde_sparse, UTsol_sparse, basic_leaving); + if (recommend_refactor == 1) { + csc_matrix_t L(m, m, 1); + csc_matrix_t U(m, m, 1); + std::vector pinv(m); + std::vector p(m); + std::vector q(m); + std::vector deficient; + std::vector slacks_needed; + f_t factorize_work_estimate = 0.0; + const i_t rank = factorize_basis(lp.A, + settings_, + basic_list_copy, + exploration_stats_.start_time, + L, + U, + p, + pinv, + q, + deficient, + slacks_needed, + factorize_work_estimate); + if (rank == CONCURRENT_HALT_RETURN || rank == TIME_LIMIT_RETURN) { return; } + if (rank < 0 || rank != lp.num_rows) { return; } + simplex::reorder_basic_list(q, basic_list_copy); + basis_update_copy.reset(L, U, p); + } + } + + std::vector new_fractional; + const i_t num_new_fractional = + fractional_variables(settings_, soln_copy.x, var_types_, new_fractional); + if (num_new_fractional < start_num_fractional) { + i_t num_integer_increased = start_num_fractional - num_new_fractional; + integer_pivots_.fetch_add(num_integer_increased, std::memory_order_release); + num_fractional = num_new_fractional; + fractional = new_fractional; + basic_list = basic_list_copy; + nonbasic_list = nonbasic_list_copy; + vstatus = vstatus_copy; + basis_update = basis_update_copy; + solution = soln_copy; + } +} + template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -2656,6 +2847,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut is_running_ = true; lower_bound_numerical_ = inf; + pivot_out_integer_variables(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } cut_pool_t cut_pool(original_lp_.num_cols, settings_); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 12c93fcd91..1fe2b2b897 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -309,6 +309,15 @@ class branch_and_bound_t { const std::vector& leaf_solution, i_t leaf_depth, search_strategy_t thread_type); + omp_atomic_t integer_pivots_{0}; + void pivot_out_integer_variables(const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional); // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index ec7a9bc25c..aca2e785d2 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -150,78 +150,6 @@ i_t phase2_pricing(const lp_problem_t& lp, return entering_index; } -template -i_t ratio_test(const lp_problem_t& lp, - const std::vector& vstatus, - const std::vector& basic_list, - std::vector& x, - std::vector& delta_x, - f_t& step_length, - i_t& basic_leaving, - i_t entering_index, - i_t direction) -{ - const i_t m = lp.num_rows; - const i_t n = lp.num_cols; - basic_leaving = -1; - i_t leaving_index = -1; - f_t min_val = inf; - constexpr f_t pivot_tol = 1e-8; - - // Entering variable can hit its opposite bound: limit step by that - if (direction > 0 && lp.upper[entering_index] < inf) { - const f_t limit = lp.upper[entering_index] - x[entering_index]; - if (limit >= 0 && limit < min_val) { - min_val = limit; - leaving_index = -1; // no basic leaves; will be handled by caller - basic_leaving = -1; - } - } else if (direction < 0 && lp.lower[entering_index] > -inf) { - const f_t limit = x[entering_index] - lp.lower[entering_index]; - if (limit >= 0 && limit < min_val) { - min_val = limit; - leaving_index = -1; - basic_leaving = -1; - } - } - - for (i_t k = 0; k < m; ++k) { - const i_t j = basic_list[k]; - if (delta_x[j] == 0.0) { continue; } - if (lp.lower[j] > -inf && delta_x[j] < -pivot_tol) { - // xj + step * delta_x[j] >= lp.lower[j] - // step * delta_x[j] >= lp.lower[j] - x[j] - // step <= (lp.lower[j] - x[j]) / delta_x[j], delta_x[j] < 0 - const f_t neum = lp.lower[j] - x[j]; - f_t ratio = neum / delta_x[j]; - // Already below lower and moving further (delta_x < 0): cap step at 0 - if (x[j] < lp.lower[j]) { ratio = 0; } - if (ratio >= 0 && ratio < min_val) { - min_val = ratio; - basic_leaving = k; - leaving_index = j; - } - } - if (lp.upper[j] < inf && delta_x[j] > pivot_tol) { - // xj + step * delta_x[j] <= lp.upper[j] - // step * delta_x[j] <= lp.upper[j] - x[j] - // step <= (lp.upper[j] - x[j]) / delta_x[j], delta_x[j] > 0 - const f_t neum = lp.upper[j] - x[j]; - f_t ratio = neum / delta_x[j]; - // If x[j] > upper (slightly infeasible), ratio < 0; skip so we don't use it. - // But if we're already above upper and would move further (delta_x > 0), cap step at 0. - if (x[j] > lp.upper[j]) { ratio = 0; } - if (ratio >= 0 && ratio < min_val) { - min_val = ratio; - basic_leaving = k; - leaving_index = j; - } - } - } - step_length = min_val; - return leaving_index; -} - template f_t primal_infeasibility(const lp_problem_t& lp, const simplex_solver_settings_t& settings, @@ -323,6 +251,80 @@ void compute_dual_variables(const lp_problem_t& lp, } // namespace + +template +i_t primal_ratio_test(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& vstatus, + const std::vector& basic_list, + std::vector& x, + std::vector& delta_x, + f_t& step_length, + i_t& basic_leaving, + i_t entering_index, + i_t direction) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + basic_leaving = -1; + i_t leaving_index = -1; + f_t min_val = inf; + constexpr f_t pivot_tol = 1e-8; + + // Entering variable can hit its opposite bound: limit step by that + if (direction > 0 && lp.upper[entering_index] < inf) { + const f_t limit = lp.upper[entering_index] - x[entering_index]; + if (limit >= 0 && limit < min_val) { + min_val = limit; + leaving_index = -1; // no basic leaves; will be handled by caller + basic_leaving = -1; + } + } else if (direction < 0 && lp.lower[entering_index] > -inf) { + const f_t limit = x[entering_index] - lp.lower[entering_index]; + if (limit >= 0 && limit < min_val) { + min_val = limit; + leaving_index = -1; + basic_leaving = -1; + } + } + + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + if (delta_x[j] == 0.0) { continue; } + if (lp.lower[j] > -inf && delta_x[j] < -pivot_tol) { + // xj + step * delta_x[j] >= lp.lower[j] + // step * delta_x[j] >= lp.lower[j] - x[j] + // step <= (lp.lower[j] - x[j]) / delta_x[j], delta_x[j] < 0 + const f_t neum = lp.lower[j] - x[j]; + f_t ratio = neum / delta_x[j]; + // Already below lower and moving further (delta_x < 0): cap step at 0 + if (x[j] < lp.lower[j]) { ratio = 0; } + if (ratio >= 0 && ratio < min_val) { + min_val = ratio; + basic_leaving = k; + leaving_index = j; + } + } + if (lp.upper[j] < inf && delta_x[j] > pivot_tol) { + // xj + step * delta_x[j] <= lp.upper[j] + // step * delta_x[j] <= lp.upper[j] - x[j] + // step <= (lp.upper[j] - x[j]) / delta_x[j], delta_x[j] > 0 + const f_t neum = lp.upper[j] - x[j]; + f_t ratio = neum / delta_x[j]; + // If x[j] > upper (slightly infeasible), ratio < 0; skip so we don't use it. + // But if we're already above upper and would move further (delta_x > 0), cap step at 0. + if (x[j] > lp.upper[j]) { ratio = 0; } + if (ratio >= 0 && ratio < min_val) { + min_val = ratio; + basic_leaving = k; + leaving_index = j; + } + } + } + step_length = min_val; + return leaving_index; +} + // Note this implementation of primal simplex is experimental // It is meant only to serve as a method to remove the perturbation to the objective // after dual simplex has found a primal feasible solution @@ -553,8 +555,8 @@ primal_status_t primal_phase2(i_t phase, i_t basic_leaving; f_t step_length; - i_t leaving_index = ratio_test( - lp, vstatus, basic_list, x, delta_x, step_length, basic_leaving, entering_index, direction); + i_t leaving_index = primal_ratio_test( + lp, settings, vstatus, basic_list, x, delta_x, step_length, basic_leaving, entering_index, direction); if (leaving_index == -1 && step_length >= inf) { settings.log.printf("No leaving variable. Primal unbounded?\n"); return primal_status_t::PRIMAL_UNBOUNDED; @@ -654,6 +656,18 @@ primal_status_t primal_phase2(i_t phase, #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE +template +int primal_ratio_test(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& vstatus, + const std::vector& basic_list, + std::vector& x, + std::vector& delta_x, + double& step_length, + int& basic_leaving, + int entering_index, + int direction); + template primal_status_t primal_phase2( int phase, double start_time, diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index 930958a802..34ffbd8ba5 100644 --- a/cpp/src/dual_simplex/primal.hpp +++ b/cpp/src/dual_simplex/primal.hpp @@ -27,6 +27,19 @@ enum class primal_status_t { CONCURRENT_LIMIT = 6 }; + +template +i_t primal_ratio_test(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& vstatus, + const std::vector& basic_list, + std::vector& x, + std::vector& delta_x, + f_t& step_length, + i_t& basic_leaving, + i_t entering_index, + i_t direction); + template primal_status_t primal_phase2(i_t phase, f_t start_time, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index c13e35c525..da6834f60f 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -288,7 +288,7 @@ lp_status_t solve_linear_program_with_advanced_basis( edge_norms, work_unit_context); } - constexpr bool primal_cleanup = true; + constexpr bool primal_cleanup = false; if (status == dual_status_t::OPTIMAL && primal_cleanup) { settings.log.printf("Running primal cleanup\n"); primal_phase2(2, start_time, lp, settings, vstatus, solution, iter); From 17b2725eb5c43f285737c9810c1603da92b8ed52 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Tue, 21 Jul 2026 15:26:47 -0700 Subject: [PATCH 03/34] First stab at using the feasibility pump on a reduced problem on the optimal face --- cpp/src/branch_and_bound/branch_and_bound.cpp | 233 +++++++++++++++++- cpp/src/branch_and_bound/branch_and_bound.hpp | 16 ++ 2 files changed, 240 insertions(+), 9 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 0a3629e7f8..064367b60e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -2487,28 +2488,232 @@ auto branch_and_bound_t::do_cut_pass( return {cut_pass_action_t::CONTINUE, mip_status_t::UNSET}; } + template -void branch_and_bound_t::pivot_out_integer_variables( - const simplex::lp_problem_t& lp, +bool branch_and_bound_t::check_for_dual_degeneracy( + const simplex::lp_solution_t& solution, + const std::vector& nonbasic_list, + std::vector& zero_reduced_costs_vars, + std::vector& zero_reduced_costs_vars_nonbasic_index) +{ + const i_t num_nonbasics = nonbasic_list.size(); + for (i_t k = 0; k < num_nonbasics; k++) { + const i_t j = nonbasic_list[k]; + if (std::abs(solution.z[j]) <= 1e-10) { + zero_reduced_costs_vars.push_back(j); + zero_reduced_costs_vars_nonbasic_index.push_back(k); + } + } + return !zero_reduced_costs_vars.empty(); +} + +template +void branch_and_bound_t::dual_degenerate_feasibility_pump(const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, - simplex::lp_solution_t& solution, + simplex::lp_solution_t& soln, simplex::basis_update_mpf_t& basis_update, i_t& num_fractional, std::vector& fractional) { std::vector zero_reduced_costs_vars; std::vector zero_reduced_costs_vars_nonbasic_index; - for (i_t k = 0; k < static_cast(nonbasic_list.size()); k++) { - const i_t j = nonbasic_list[k]; - if (std::abs(solution.z[j]) <= 1e-10) { - zero_reduced_costs_vars.push_back(j); - zero_reduced_costs_vars_nonbasic_index.push_back(k); + bool dual_degenerate = check_for_dual_degeneracy(soln, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); + if (!dual_degenerate) { return; } + + // Construct a new LP problem + // minimize p^T x + // subject to B x_B + N_z x_z = b - N x_N + // l_B <= x_B <= u_B + // l_z <= x_z <= u_z + // + // where B is the basic matrix, N is the nonbasic matrix, b is the right-hand side, + + const i_t m = lp.num_rows; + const i_t n = lp.num_rows + zero_reduced_costs_vars.size(); + + i_t nnz = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + nnz += lp.A.col_start[j + 1] - lp.A.col_start[j]; + } + } + simplex::lp_problem_t lp_reduced(lp.handle_ptr, m, n, nnz); + csc_matrix_t& A_reduced = lp_reduced.A; + i_t nz = 0; + i_t reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + A_reduced.col_start[reduced_col] = nz; + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + for (i_t p = col_start; p < col_end; p++) { + const i_t i = lp.A.i[p]; + const f_t value = lp.A.x[p]; + A_reduced.i[nz] = i; + A_reduced.x[nz] = value; + nz++; + } + lp_reduced.lower[reduced_col] = lp.lower[j]; + lp_reduced.upper[reduced_col] = lp.upper[j]; + reduced_col++; + } + } + A_reduced.col_start[reduced_col] = nz; + + std::vector b_reduced = lp.rhs; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + // PASS + } else { + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + for (i_t p = col_start; p < col_end; p++) { + const i_t i = lp.A.i[p]; + const f_t value = lp.A.x[p]; + b_reduced[i] -= value * soln.x[j]; + } + } + } + lp_reduced.rhs = b_reduced; + lp_reduced.obj_scale = 1.0; + + + + settings_.log.printf("Constructed dual degenerate feasibility pump LP with %d rows and %d columns\n", m, n); + + std::vector reduced_basic_list(m); + std::vector reduced_nonbasic_list(zero_reduced_costs_vars.size()); + std::vector reduced_vstatus(n); + i_t num_basic = 0; + i_t num_nonbasic = 0; + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC){ + reduced_basic_list[num_basic++] = reduced_col; + reduced_vstatus[reduced_col++] = variable_status_t::BASIC; + } else if (std::abs(soln.z[j]) <= 1e-10) { + reduced_nonbasic_list[num_nonbasic++] = reduced_col; + reduced_vstatus[reduced_col++] = vstatus[j]; } } - if (zero_reduced_costs_vars.empty()) { return; } + simplex::lp_solution_t reduced_solution(m, n); + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + reduced_solution.x[reduced_col++] = soln.x[j]; + } + } + + std::vector reduced_edge_norms(n); + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + reduced_edge_norms[reduced_col++] = edge_norms_[j]; + } + } + + simplex::basis_update_mpf_t reduced_basis_update = basis_update; + i_t iter = 0; + + i_t max_pump_iter = 100; + simplex::random_t rng(settings_.random_seed); + + i_t best_num_fractional = num_fractional; + bool stalled = false; + for (i_t pump_iter = 0; pump_iter < max_pump_iter; pump_iter++) { + + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + lp_reduced.objective[reduced_col] = 0; + if (var_types_[j] == variable_type_t::INTEGER) { + if (is_fractional(reduced_solution.x[reduced_col], var_types_[j], settings_.integer_tol)) { + // Default to the exact nearest-integer rounding. Only perturb the + // rounding direction when the previous pass made no progress (a + // zero-pivot solve), to break out of the stall. + const f_t random_value = stalled ? 0.25 * (2.0 * rng.random() - 1.0) : 0.0; // [-0.25, 0.25] + if (reduced_solution.x[reduced_col] + random_value < std::floor(reduced_solution.x[reduced_col]) + 0.5) { + lp_reduced.objective[reduced_col] = 1; + } else { + lp_reduced.objective[reduced_col] = -1; + } + } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_LOWER) { + lp_reduced.objective[reduced_col] = 1; + } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_UPPER) { + lp_reduced.objective[reduced_col] = -1; + } + } + reduced_col++; + } + } + + + bool recompute_basis = false; + const i_t iter_before = iter; + simplex::primal_status_t lp_status = simplex::primal_phase2(2, + exploration_stats_.start_time, + lp_reduced, + settings_, + reduced_vstatus, + reduced_solution, + iter); + // Detect a stall: the solve made no pivots, so the incumbent vertex was + // already optimal for this objective and x did not move. Perturb next pass. + stalled = (iter == iter_before); + + if (lp_status == simplex::primal_status_t::OPTIMAL) { + std::vector adjusted_solution(lp.num_cols, 0.0); + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + adjusted_solution[j] = reduced_solution.x[reduced_col++]; + } else { + adjusted_solution[j] = soln.x[j]; + } + } + + // Verify the solution is primal feasible + std::vector residual = lp.rhs; + matrix_vector_multiply(lp.A, 1.0, adjusted_solution, -1.0, residual); + + settings_.log.printf("Reduced LP residual|| A*x - b ||_inf = %.16e\n", vector_norm_inf(residual)); + + std::vector tmp_fractional; + i_t num_fractional_reduced = fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); + settings_.log.printf("Reduced LP fractional variables %d/%d\n", num_fractional_reduced, num_fractional); + // Also treat a pass that fails to improve the best as a stall, so we perturb + // the next pass even when the solve pivoted (moved) without reducing the count. + stalled = stalled || (num_fractional_reduced >= best_num_fractional); + if (num_fractional_reduced < best_num_fractional) { + best_num_fractional = num_fractional_reduced; + } + } + +} + settings_.log.printf("Best number of fractional variables %d/%d\n", best_num_fractional, num_fractional); + +} + +template +void branch_and_bound_t::pivot_out_integer_variables( + const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional) +{ + + std::vector zero_reduced_costs_vars; + std::vector zero_reduced_costs_vars_nonbasic_index; + bool dual_degenerate = check_for_dual_degeneracy(solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); + if (!dual_degenerate) { return; } + lp_solution_t soln_copy = solution; std::vector basic_list_copy = basic_list; std::vector nonbasic_list_copy = nonbasic_list; @@ -2636,6 +2841,7 @@ void branch_and_bound_t::pivot_out_integer_variables( if (num_new_fractional < start_num_fractional) { i_t num_integer_increased = start_num_fractional - num_new_fractional; integer_pivots_.fetch_add(num_integer_increased, std::memory_order_release); + settings_.log.printf("Pivoted out %d integer variables: %d -> %d\n", num_integer_increased, start_num_fractional, num_new_fractional); num_fractional = num_new_fractional; fractional = new_fractional; basic_list = basic_list_copy; @@ -2856,6 +3062,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut num_fractional, fractional); + dual_degenerate_feasibility_pump(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } cut_pool_t cut_pool(original_lp_.num_cols, settings_); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 1fe2b2b897..9e3a23b440 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -309,7 +309,14 @@ class branch_and_bound_t { const std::vector& leaf_solution, i_t leaf_depth, search_strategy_t thread_type); + + omp_atomic_t integer_pivots_{0}; + bool check_for_dual_degeneracy(const simplex::lp_solution_t& solution, + const std::vector& nonbasic_list, + std::vector& zero_reduced_costs_vars, + std::vector& zero_reduced_costs_vars_nonbasic_index); + void pivot_out_integer_variables(const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, @@ -319,6 +326,15 @@ class branch_and_bound_t { i_t& num_fractional, std::vector& fractional); + void dual_degenerate_feasibility_pump(const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional); + // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); From ff6f460ec5e05ea94f0e697115073d45a1c7d3fd Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 24 Jul 2026 06:08:43 -0700 Subject: [PATCH 04/34] Add check for fast pivot using slacks --- cpp/src/branch_and_bound/branch_and_bound.cpp | 101 +++++++++++++++++- 1 file changed, 100 insertions(+), 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 064367b60e..be3af49b99 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2722,7 +2722,106 @@ void branch_and_bound_t::pivot_out_integer_variables( const i_t start_num_fractional = num_fractional; - for (i_t k = 0; k < static_cast(zero_reduced_costs_vars.size()); k++) { + const i_t num_zero_reduced_costs_vars = zero_reduced_costs_vars.size(); + + std::vector row_to_slack(lp.num_rows, -1); + for (i_t j : new_slacks_) { + if (lp.lower[j] != 0 || lp.upper[j] != inf) { continue; } + const i_t p = lp.A.col_start[j]; + row_to_slack[lp.A.i[p]] = j; + } + + std::vector fast_candidates; + std::vector fast_rows; + for (i_t j : fractional) { + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + const i_t num_rows = col_end - col_start; + i_t num_basic_slacks = 0; + i_t num_nonbasic_slacks_with_reduced_cost_zero = 0; + i_t nonbasic_slack = -1; + i_t slack_row = -1; + for (i_t p = col_start; p < col_end; p++) { + const i_t i = lp.A.i[p]; + const i_t slack = row_to_slack[i]; + if (slack >= 0) { + if (vstatus_copy[slack] == variable_status_t::BASIC) { + num_basic_slacks++; + } else if (std::abs(solution.z[slack]) <= 1e-10) { + num_nonbasic_slacks_with_reduced_cost_zero++; + nonbasic_slack = slack; + slack_row = i; + } + } + } + if (num_basic_slacks == num_rows - 1 && num_nonbasic_slacks_with_reduced_cost_zero == 1) { + fast_candidates.push_back(j); + fast_rows.push_back(slack_row); + } + } + + if (fast_candidates.size() > 0) { + settings_.log.printf("Found %ld fast candidates for pivot out integer variables\n", fast_candidates.size()); + } + + const i_t num_candidates = fast_candidates.size(); + for (i_t k = 0; k < num_candidates; k++) { + const i_t j = fast_candidates[k]; + const i_t row = fast_rows[k]; + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + const i_t num_rows = col_end - col_start; + f_t a_ij = 0.0; + for (i_t p = col_start; p < col_end; p++) { + const i_t i = lp.A.i[p]; + if (i == row) { + a_ij = lp.A.x[p]; + break; + } + } + if (a_ij == 0.0) { continue; } + + f_t bound = a_ij > 0 ? lp.lower[j] : lp.upper[j]; + if (std::abs(bound) == inf) { continue; } + + sparse_vector_t delta_x; + delta_x.n = lp.num_cols; + delta_x.i.reserve(num_rows + 1); + delta_x.x.reserve(num_rows + 1); + const f_t delta_xj = bound - solution.x[j]; + delta_x.i.push_back(j); + delta_x.x.push_back(delta_xj); + for (i_t p = col_start; p < col_end; p++) { + const i_t r = lp.A.i[p]; + const f_t a_rj = lp.A.x[p]; + const f_t delta_slack_r = -delta_xj * a_rj; + delta_x.i.push_back(row_to_slack[r]); + delta_x.x.push_back(delta_slack_r); + } + + bool ok = true; + const i_t ndx = delta_x.i.size(); + for (i_t h = 0; h < ndx; h++) { + const i_t jj = delta_x.i[h]; + if (jj == j) continue; + const f_t val = delta_x.x[h]; + const f_t slack_value = solution.x[jj]; + if (val < -slack_value) { + ok = false; + break; + } + } + + if (ok) { + std::vector delta_x_dense(lp.num_cols, 0.0); + delta_x.to_dense(delta_x_dense); + std::vector residual(lp.num_rows); + matrix_vector_multiply(lp.A, 1.0, delta_x_dense, 0.0, residual); + settings_.log.printf("Fast candidate ok || A*delta_x ||_inf = %e\n", vector_norm_inf(residual)); + } + } + + for (i_t k = 0; k < num_zero_reduced_costs_vars; k++) { const i_t j = zero_reduced_costs_vars[k]; if (var_types_[j] == variable_type_t::INTEGER) { continue; } if (vstatus_copy[j] == variable_status_t::BASIC) { continue; } From 900805a940d4f7166538c9fa6c037b186547e6ff Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Mon, 27 Jul 2026 14:06:19 -0700 Subject: [PATCH 05/34] Enable primal simplex. Solves 85/93 NETLIB LPs in under 1 minute --- .../mathematical_optimization/constants.h | 3 +- .../pdlp/solver_settings.hpp | 3 + cpp/src/branch_and_bound/branch_and_bound.cpp | 215 ++++-- cpp/src/dual_simplex/primal.cpp | 689 ++++++++++++++---- cpp/src/dual_simplex/primal.hpp | 16 +- cpp/src/dual_simplex/solve.cpp | 157 +++- cpp/src/dual_simplex/solve.hpp | 6 + cpp/src/math_optimization/solver_settings.cu | 2 +- cpp/src/pdlp/solve.cu | 67 +- 9 files changed, 955 insertions(+), 203 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index f6be07aaa9..4ed3723aa2 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -192,7 +192,8 @@ #define CUOPT_METHOD_PDLP 1 #define CUOPT_METHOD_DUAL_SIMPLEX 2 #define CUOPT_METHOD_BARRIER 3 -#define CUOPT_METHOD_UNSET 4 +#define CUOPT_METHOD_PRIMAL 4 +#define CUOPT_METHOD_UNSET 5 /* @brief PDLP precision mode constants */ #define CUOPT_PDLP_DEFAULT_PRECISION -1 diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 96f548ec32..3bf3b6ab01 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -57,6 +57,7 @@ enum pdlp_solver_mode_t : int { * PDLP: Use the PDLP method. * DualSimplex: Use the dual simplex method. * Barrier: Use the barrier method + * Primal: Use the (experimental) primal simplex method. * Unset: The value was not set. * * @note Default method is Concurrent. @@ -66,6 +67,7 @@ enum method_t : int { PDLP = CUOPT_METHOD_PDLP, DualSimplex = CUOPT_METHOD_DUAL_SIMPLEX, Barrier = CUOPT_METHOD_BARRIER, + Primal = CUOPT_METHOD_PRIMAL, Unset = CUOPT_METHOD_UNSET }; @@ -77,6 +79,7 @@ inline std::string method_to_string(method_t method) case method_t::PDLP: return "PDLP"; case method_t::Barrier: return "Barrier"; case method_t::Concurrent: return "Concurrent"; + case method_t::Primal: return "Primal Simplex"; default: return "Unset"; } } diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 6a47deb079..1da2f0f249 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3171,6 +3171,15 @@ auto branch_and_bound_t::do_cut_pass( basis_update, num_fractional, fractional); + + dual_degenerate_feasibility_pump(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->root_lp_with_cuts = @@ -3290,10 +3299,12 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple } simplex::lp_problem_t lp_reduced(lp.handle_ptr, m, n, nnz); csc_matrix_t& A_reduced = lp_reduced.A; + std::vector original_col_to_reduced_col(lp.num_cols, -1); i_t nz = 0; i_t reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + original_col_to_reduced_col[j] = reduced_col; A_reduced.col_start[reduced_col] = nz; const i_t col_start = lp.A.col_start[j]; const i_t col_end = lp.A.col_start[j + 1]; @@ -3340,11 +3351,10 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { if (vstatus[j] == variable_status_t::BASIC){ - reduced_basic_list[num_basic++] = reduced_col; reduced_vstatus[reduced_col++] = variable_status_t::BASIC; } else if (std::abs(soln.z[j]) <= 1e-10) { - reduced_nonbasic_list[num_nonbasic++] = reduced_col; - reduced_vstatus[reduced_col++] = vstatus[j]; + reduced_nonbasic_list[num_nonbasic++] = reduced_col; // Does ordering of nonbasic variables matter? + reduced_vstatus[reduced_col++] = vstatus[j]; } } @@ -3365,85 +3375,170 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple } simplex::basis_update_mpf_t reduced_basis_update = basis_update; - i_t iter = 0; + for (i_t k = 0; k < m; k++) { + reduced_basic_list[k] = original_col_to_reduced_col[basic_list[k]]; + } - i_t max_pump_iter = 100; + i_t iter = 0; + i_t max_pump_iter = 10; simplex::random_t rng(settings_.random_seed); - i_t best_num_fractional = num_fractional; - bool stalled = false; + std::vector best_reduced_vstatus(n); + bool stalled = false; for (i_t pump_iter = 0; pump_iter < max_pump_iter; pump_iter++) { - reduced_col = 0; - for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { - lp_reduced.objective[reduced_col] = 0; - if (var_types_[j] == variable_type_t::INTEGER) { - if (is_fractional(reduced_solution.x[reduced_col], var_types_[j], settings_.integer_tol)) { - // Default to the exact nearest-integer rounding. Only perturb the - // rounding direction when the previous pass made no progress (a - // zero-pivot solve), to break out of the stall. - const f_t random_value = stalled ? 0.25 * (2.0 * rng.random() - 1.0) : 0.0; // [-0.25, 0.25] - if (reduced_solution.x[reduced_col] + random_value < std::floor(reduced_solution.x[reduced_col]) + 0.5) { + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + lp_reduced.objective[reduced_col] = 0; + if (var_types_[j] == variable_type_t::INTEGER) { + if (is_fractional( + reduced_solution.x[reduced_col], var_types_[j], settings_.integer_tol)) { + // Default to the exact nearest-integer rounding. Only perturb the + // rounding direction when the previous pass made no progress (a + // zero-pivot solve), to break out of the stall. + const f_t random_value = + stalled ? 0.25 * (2.0 * rng.random() - 1.0) : 0.0; // [-0.25, 0.25] + if (reduced_solution.x[reduced_col] + random_value < + std::floor(reduced_solution.x[reduced_col]) + 0.5) { + lp_reduced.objective[reduced_col] = 1; + } else { + lp_reduced.objective[reduced_col] = -1; + } + } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_LOWER) { lp_reduced.objective[reduced_col] = 1; - } else { + } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_UPPER) { lp_reduced.objective[reduced_col] = -1; } - } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_LOWER) { - lp_reduced.objective[reduced_col] = 1; - } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_UPPER) { - lp_reduced.objective[reduced_col] = -1; } + reduced_col++; } - reduced_col++; - } + } + + bool recompute_basis = false; + const i_t iter_before = iter; + f_t primal_work_estimate = 0; + simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, + exploration_stats_.start_time, + lp_reduced, + settings_, + reduced_vstatus, + reduced_basis_update, + reduced_basic_list, + reduced_nonbasic_list, + reduced_solution, + iter, + primal_work_estimate); + // Detect a stall: the solve made no pivots, so the incumbent vertex was + // already optimal for this objective and x did not move. Perturb next pass. + stalled = (iter == iter_before); + + if (lp_status == simplex::primal_status_t::OPTIMAL) { + std::vector adjusted_solution(lp.num_cols, 0.0); + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + adjusted_solution[j] = reduced_solution.x[reduced_col++]; + } else { + adjusted_solution[j] = soln.x[j]; + } + } + + // Verify the solution is primal feasible + std::vector residual = lp.rhs; + matrix_vector_multiply(lp.A, 1.0, adjusted_solution, -1.0, residual); + + settings_.log.printf("Reduced LP residual|| A*x - b ||_inf = %.16e\n", + vector_norm_inf(residual)); + + std::vector tmp_fractional; + i_t num_fractional_reduced = + fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); + settings_.log.printf( + "Reduced LP fractional variables %d/%d\n", num_fractional_reduced, num_fractional); + // Also treat a pass that fails to improve the best as a stall, so we perturb + // the next pass even when the solve pivoted (moved) without reducing the count. + stalled = stalled || (num_fractional_reduced >= best_num_fractional); + if (num_fractional_reduced < best_num_fractional) { + best_num_fractional = num_fractional_reduced; + best_reduced_vstatus = reduced_vstatus; + } + } } - - bool recompute_basis = false; - const i_t iter_before = iter; - simplex::primal_status_t lp_status = simplex::primal_phase2(2, - exploration_stats_.start_time, - lp_reduced, - settings_, - reduced_vstatus, - reduced_solution, - iter); - // Detect a stall: the solve made no pivots, so the incumbent vertex was - // already optimal for this objective and x did not move. Perturb next pass. - stalled = (iter == iter_before); - - if (lp_status == simplex::primal_status_t::OPTIMAL) { - std::vector adjusted_solution(lp.num_cols, 0.0); - reduced_col = 0; + settings_.log.printf("Best number of fractional variables %d/%d\n", best_num_fractional, num_fractional); + if (best_num_fractional < num_fractional) { + // Translate the vstatus from the reduced problem to the vstatus for the original problem + i_t reduced_cols = 0; for (i_t j = 0; j < lp.num_cols; j++) { if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { - adjusted_solution[j] = reduced_solution.x[reduced_col++]; + vstatus[j] = best_reduced_vstatus[reduced_cols++]; + } + } + + std::vector superbasic_list; + nonbasic_list.clear(); + simplex::get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); + assert(superbasic_list.empty()); + const i_t refactor_status = basis_update.refactor_basis(lp.A, + settings_, + lp.lower, + lp.upper, + exploration_stats_.start_time, + basic_list, + nonbasic_list, + vstatus); + if (refactor_status == CONCURRENT_HALT_RETURN || refactor_status == TIME_LIMIT_RETURN) { + // TODO: On failure vstatus, basic_list, and nonbasic_list are in a bad state. + // We should save copies before the failure and restore them after the failure. + return; + } + if (refactor_status != 0) { + settings_.log.printf("Failed to refactor basis after dual degenerate feasibility pump. " + "%d deficient columns.\n", + refactor_status); + return; + } + + // Update the solution + // First set the nonbasic variables on their bounds + for (i_t k = 0; k < lp.num_cols - lp.num_rows; k++) { + const i_t j = nonbasic_list[k]; + if (vstatus[j] == variable_status_t::NONBASIC_LOWER || vstatus[j] == variable_status_t::NONBASIC_FIXED) { + soln.x[j] = lp.lower[j]; + } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER) { + soln.x[j] = lp.upper[j]; } else { - adjusted_solution[j] = soln.x[j]; + soln.x[j] = 0; } } + // Then compute the effective rhs + std::vector rhs = lp.rhs; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC) { continue; } + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; - // Verify the solution is primal feasible - std::vector residual = lp.rhs; - matrix_vector_multiply(lp.A, 1.0, adjusted_solution, -1.0, residual); + const f_t x_j = soln.x[j]; + for (i_t p = col_start; p < col_end; p++) { + const i_t i = lp.A.i[p]; + const f_t aij = lp.A.x[p]; + rhs[i] -= aij * x_j; + } + } - settings_.log.printf("Reduced LP residual|| A*x - b ||_inf = %.16e\n", vector_norm_inf(residual)); + // Then solve B xB = rhs + std::vector xB(lp.num_rows); + basis_update.b_solve(rhs, xB); - std::vector tmp_fractional; - i_t num_fractional_reduced = fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); - settings_.log.printf("Reduced LP fractional variables %d/%d\n", num_fractional_reduced, num_fractional); - // Also treat a pass that fails to improve the best as a stall, so we perturb - // the next pass even when the solve pivoted (moved) without reducing the count. - stalled = stalled || (num_fractional_reduced >= best_num_fractional); - if (num_fractional_reduced < best_num_fractional) { - best_num_fractional = num_fractional_reduced; + // Then update the basic variables + for (i_t k = 0; k < lp.num_rows; k++) { + soln.x[basic_list[k]] = xB[k]; } - } + + fractional.clear(); + num_fractional = fractional_variables(settings_, soln.x, var_types_, fractional); -} - settings_.log.printf("Best number of fractional variables %d/%d\n", best_num_fractional, num_fractional); - + } } template diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index aca2e785d2..d12f24e98f 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -14,6 +14,8 @@ #include #include +#include + namespace cuopt::mathematical_optimization::simplex { namespace { @@ -57,13 +59,13 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, template f_t dual_infeasibility(const lp_problem_t& lp, const std::vector& vstatus, - const std::vector& z) + const std::vector& z, + f_t tight_tol, + i_t& num_infeasible) { const i_t n = lp.num_cols; - const i_t m = lp.num_rows; - i_t num_infeasible = 0; + num_infeasible = 0; f_t sum_infeasible = 0.0; - constexpr f_t tight_tol = 0; i_t lower_bound_inf = 0; i_t upper_bound_inf = 0; i_t free_inf = 0; @@ -110,6 +112,7 @@ i_t phase2_pricing(const lp_problem_t& lp, const std::vector& z, const std::vector& nonbasic_list, const std::vector& vstatus, + f_t dual_tol, i_t& direction, i_t& basic_entering, f_t& dual_inf) @@ -120,8 +123,7 @@ i_t phase2_pricing(const lp_problem_t& lp, f_t max_infeas = 0.0; dual_inf = 0.0; for (i_t k = 0; k < n - m; ++k) { - const i_t j = nonbasic_list[k]; - constexpr f_t dual_tol = 1e-6; + const i_t j = nonbasic_list[k]; if (vstatus[j] == variable_status_t::NONBASIC_FIXED) { continue; } if ((vstatus[j] == variable_status_t::NONBASIC_LOWER || vstatus[j] == variable_status_t::NONBASIC_FREE) && @@ -154,15 +156,20 @@ template f_t primal_infeasibility(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& vstatus, - const std::vector& x) + const std::vector& x, + i_t& num_infeasible) { const i_t n = lp.num_cols; f_t primal_inf = 0; + num_infeasible = 0; for (i_t j = 0; j < n; ++j) { + // Nonbasics are pinned to a bound; only basics can be (legitimately) infeasible. + if (vstatus[j] != variable_status_t::BASIC) { continue; } if (x[j] < lp.lower[j] - settings.primal_tol) { // x_j < l_j => -x_j > -l_j => -x_j + l_j > 0 const f_t infeas = -x[j] + lp.lower[j]; primal_inf += infeas; + num_infeasible++; if (infeas > 1e-6) { settings.log.debug("x %d infeas %e lo %e val %e up %e vstatus %hhd\n", j, @@ -177,6 +184,7 @@ f_t primal_infeasibility(const lp_problem_t& lp, // x_j > u_j => x_j - u_j > 0 const f_t infeas = x[j] - lp.upper[j]; primal_inf += infeas; + num_infeasible++; if (infeas > 1e-6) { settings.log.debug("x %d infeas %e lo %e val %e up %e vstatus %hhd\n", j, @@ -191,15 +199,28 @@ f_t primal_infeasibility(const lp_problem_t& lp, return primal_inf; } +template +f_t primal_infeasibility(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& vstatus, + const std::vector& x) +{ + i_t num_infeasible = 0; + return primal_infeasibility(lp, settings, vstatus, x, num_infeasible); +} + template void compute_phase1_objective(const lp_problem_t& lp, const simplex_solver_settings_t& settings, + const std::vector& vstatus, const std::vector& x, std::vector& objective) { const i_t n = lp.num_cols; for (i_t j = 0; j < n; ++j) { - if (x[j] < lp.lower[j] - settings.primal_tol) { + if (vstatus[j] != variable_status_t::BASIC) { + objective[j] = 0.0; + } else if (x[j] < lp.lower[j] - settings.primal_tol) { objective[j] = -1.0; } else if (x[j] > lp.upper[j] + settings.primal_tol) { objective[j] = 1.0; @@ -209,13 +230,79 @@ void compute_phase1_objective(const lp_problem_t& lp, } } +template +void compute_delta_y(const basis_update_mpf_t& basis_update, + i_t basic_leaving, + sparse_vector_t& delta_y, + sparse_vector_t& etilde) +{ + const i_t m = delta_y.n; + sparse_vector_t ei(m, 1); + ei.i[0] = basic_leaving; + ei.x[0] = 1.0; + delta_y.clear(); + etilde.clear(); + basis_update.b_transpose_solve(ei, delta_y, etilde); +} + +template +void compute_delta_z(const csr_matrix_t& Arow, + const std::vector& vstatus, + const sparse_vector_t& delta_y, + std::vector& delta_z) +{ + // A^T delta_y + delta_z = 0 + // delta_z = -A^T delta_y = - sum_i A(i, :) * delta_y_i + std::fill(delta_z.begin(), delta_z.end(), 0.0); + for (i_t k = 0; k < static_cast(delta_y.i.size()); ++k) { + const i_t i = delta_y.i[k]; + const f_t delta_y_i = delta_y.x[k]; + const i_t row_start = Arow.row_start[i]; + const i_t row_end = Arow.row_start[i + 1]; + for (i_t p = row_start; p < row_end; ++p) { + const i_t j = Arow.j[p]; + if (vstatus[j] != variable_status_t::BASIC) { delta_z[j] -= Arow.x[p] * delta_y_i; } + } + } +} + +template +f_t compute_dual_step_length(f_t entering_reduced_cost, f_t pivot) +{ + assert(pivot != 0.0); + return entering_reduced_cost / pivot; +} + +template +void update_y(f_t dual_step_length, const sparse_vector_t& delta_y, std::vector& y) +{ + for (i_t k = 0; k < static_cast(delta_y.i.size()); ++k) { + const i_t i = delta_y.i[k]; + y[i] += dual_step_length * delta_y.x[k]; + } +} + +template +void update_z(f_t dual_step_length, + const std::vector& nonbasic_list, + i_t entering_index, + const std::vector& delta_z, + std::vector& z) +{ + for (i_t k = 0; k < static_cast(nonbasic_list.size()); ++k) { + const i_t j = nonbasic_list[k]; + z[j] += dual_step_length * delta_z[j]; + } + z[entering_index] = 0.0; +} + template void compute_dual_variables(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& objective, const std::vector& basic_list, const std::vector& nonbasic_list, - basis_update_t& ft, + basis_update_mpf_t& ft, std::vector& c_basic, std::vector& y, std::vector& z) @@ -249,6 +336,40 @@ void compute_dual_variables(const lp_problem_t& lp, } } +template +void compute_basic_primal_variables(const lp_problem_t& lp, + const basis_update_mpf_t& basis_update, + const std::vector& basic_list, + const std::vector& nonbasic_list, + std::vector& x) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + std::vector rhs = lp.rhs; + for (i_t k = 0; k < n - m; ++k) { + const i_t j = nonbasic_list[k]; + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + const f_t xj = x[j]; + for (i_t p = col_start; p < col_end; ++p) { + rhs[lp.A.i[p]] -= xj * lp.A.x[p]; + } + } + std::vector xB(m); + basis_update.b_solve(rhs, xB); + for (i_t k = 0; k < m; ++k) { + x[basic_list[k]] = xB[k]; + } +} + +template +f_t primal_constraint_residual(const lp_problem_t& lp, const std::vector& x) +{ + std::vector residual = lp.rhs; + matrix_vector_multiply(lp.A, 1.0, x, -1.0, residual); + return vector_norm_inf(residual); +} + } // namespace @@ -269,6 +390,7 @@ i_t primal_ratio_test(const lp_problem_t& lp, basic_leaving = -1; i_t leaving_index = -1; f_t min_val = inf; + f_t current_dx = 0.0; constexpr f_t pivot_tol = 1e-8; // Entering variable can hit its opposite bound: limit step by that @@ -291,33 +413,72 @@ i_t primal_ratio_test(const lp_problem_t& lp, for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; if (delta_x[j] == 0.0) { continue; } + + // Already below lower and moving back up: stop when we reach the lower bound. + // Without this, phase I can take an unbounded step (false unbounded) or skip the + // breakpoint of the piecewise phase-I objective and stall still infeasible. + if (x[j] < lp.lower[j] && delta_x[j] > pivot_tol && lp.lower[j] > -inf) { + const f_t ratio = (lp.lower[j] - x[j]) / delta_x[j]; + if (ratio >= 0 && ratio < min_val) { + min_val = ratio; + basic_leaving = k; + leaving_index = j; + current_dx = delta_x[j]; + } + } + // Already above upper and moving back down: stop when we reach the upper bound. + if (x[j] > lp.upper[j] && delta_x[j] < -pivot_tol && lp.upper[j] < inf) { + const f_t ratio = (lp.upper[j] - x[j]) / delta_x[j]; + if (ratio >= 0 && ratio < min_val) { + min_val = ratio; + basic_leaving = k; + leaving_index = j; + current_dx = -delta_x[j]; + } + } + if (lp.lower[j] > -inf && delta_x[j] < -pivot_tol) { // xj + step * delta_x[j] >= lp.lower[j] // step * delta_x[j] >= lp.lower[j] - x[j] // step <= (lp.lower[j] - x[j]) / delta_x[j], delta_x[j] < 0 - const f_t neum = lp.lower[j] - x[j]; - f_t ratio = neum / delta_x[j]; - // Already below lower and moving further (delta_x < 0): cap step at 0 - if (x[j] < lp.lower[j]) { ratio = 0; } + f_t neum = lp.lower[j] - x[j]; + // A basic sitting a hair below its bound (within the primal tolerance) is on + // the bound numerically, but gives a tiny negative ratio. Dropping it lets + // the step run straight through the bound, so treat it as a zero-length + // block. A genuine violation is left to the branches above, which stop at + // the bound when the variable moves back toward it. + if (neum > 0 && neum <= settings.primal_tol) { neum = 0.0; } + f_t ratio = neum / delta_x[j]; if (ratio >= 0 && ratio < min_val) { min_val = ratio; basic_leaving = k; leaving_index = j; + current_dx = -delta_x[j]; + } else if (ratio >= 0 && ratio < min_val + 1e-9 && -delta_x[j] > current_dx) { + min_val = ratio; + basic_leaving = k; + leaving_index = j; + current_dx = -delta_x[j]; } } if (lp.upper[j] < inf && delta_x[j] > pivot_tol) { // xj + step * delta_x[j] <= lp.upper[j] // step * delta_x[j] <= lp.upper[j] - x[j] // step <= (lp.upper[j] - x[j]) / delta_x[j], delta_x[j] > 0 - const f_t neum = lp.upper[j] - x[j]; - f_t ratio = neum / delta_x[j]; - // If x[j] > upper (slightly infeasible), ratio < 0; skip so we don't use it. - // But if we're already above upper and would move further (delta_x > 0), cap step at 0. - if (x[j] > lp.upper[j]) { ratio = 0; } + f_t neum = lp.upper[j] - x[j]; + // Mirror of the lower bound case: a hair above the bound is on the bound. + if (neum < 0 && -neum <= settings.primal_tol) { neum = 0.0; } + f_t ratio = neum / delta_x[j]; if (ratio >= 0 && ratio < min_val) { min_val = ratio; basic_leaving = k; leaving_index = j; + current_dx = delta_x[j]; + } else if (ratio >= 0 && ratio < min_val + 1e-9 && delta_x[j] > current_dx) { + min_val = ratio; + basic_leaving = k; + leaving_index = j; + current_dx = delta_x[j]; } } } @@ -325,9 +486,7 @@ i_t primal_ratio_test(const lp_problem_t& lp, return leaving_index; } -// Note this implementation of primal simplex is experimental -// It is meant only to serve as a method to remove the perturbation to the objective -// after dual simplex has found a primal feasible solution + template primal_status_t primal_phase2(i_t phase, f_t start_time, @@ -339,32 +498,11 @@ primal_status_t primal_phase2(i_t phase, { const i_t m = lp.num_rows; const i_t n = lp.num_cols; - assert(m <= n); - assert(vstatus.size() == n); - assert(lp.A.m == m); - assert(lp.A.n == n); - assert(lp.objective.size() == n); - assert(lp.lower.size() == n); - assert(lp.upper.size() == n); - assert(lp.rhs.size() == m); + f_t work_estimate = 0; std::vector basic_list(m); std::vector nonbasic_list; std::vector superbasic_list; - std::vector bound_info(n - m); - - std::vector& x = sol.x; - std::vector& y = sol.y; - std::vector& z = sol.z; - - std::vector incoming_x = x; - std::vector incoming_vstatus = vstatus; - - settings.log.printf("Primal Simplex Phase %d\n", phase); - settings.log.printf("Solving a problem with %d constraints %d variables %d nonzeros\n", - lp.num_rows, - lp.num_cols, - lp.A.col_start[lp.num_cols]); get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); assert(superbasic_list.size() == 0); @@ -436,7 +574,58 @@ primal_status_t primal_phase2(i_t phase, } } reorder_basic_list(q, basic_list); - basis_update_t ft(L, U, p); + basis_update_mpf_t ft(L, U, p, settings.refactor_frequency); + + return primal_phase2_with_advanced_basis(phase, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + work_estimate); +} +// Note this implementation of primal simplex is experimental +// It is meant only to serve as a method to remove the perturbation to the objective +// after dual simplex has found a primal feasible solution +template +primal_status_t primal_phase2_with_advanced_basis( + i_t phase, + f_t start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + i_t& iter, + f_t& work_estimate) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + assert(m <= n); + assert(vstatus.size() == n); + assert(lp.A.m == m); + assert(lp.A.n == n); + assert(lp.objective.size() == n); + assert(lp.lower.size() == n); + assert(lp.upper.size() == n); + assert(lp.rhs.size() == m); + + std::vector& x = sol.x; + std::vector& y = sol.y; + std::vector& z = sol.z; + + std::vector incoming_x = x; + std::vector incoming_vstatus = vstatus; + settings.log.printf("Primal Simplex\n"); + // Nonbasics must be on their bounds before forming B x_B = b - A_N x_N. + // Setting them after the solve leaves ||A*x - b|| large whenever x_N != 0. + set_primal_variables_on_bounds(lp, settings, vstatus, x); std::vector rhs = lp.rhs; // rhs = b - sum_{j : x_j = l_j} A(:, j) l(j) - sum_{j : x_j = u_j} A(:, j) * @@ -452,91 +641,237 @@ primal_status_t primal_phase2(i_t phase, } std::vector xB(m); - ft.b_solve(rhs, xB); + basis_update.b_solve(rhs, xB); for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; x[j] = xB[k]; } - set_primal_variables_on_bounds(lp, settings, vstatus, x); - settings.log.printf("|| x || %e\n", vector_norm2(x)); + constexpr bool print_norms = false; + if constexpr (print_norms) { + settings.log.printf("|| x || %e\n", vector_norm2(x)); + } std::vector residual = lp.rhs; matrix_vector_multiply(lp.A, 1.0, x, -1.0, residual); f_t primal_residual = vector_norm_inf(residual); - if (primal_residual > 1e-6) { settings.log.printf("|| A*x - b || %e\n", primal_residual); } - f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); - settings.log.printf("Initial primal infeasibility %e\n", primal_inf); - + if (primal_residual > settings.primal_tol) { + settings.log.printf("|| A*x - b || %e\n", primal_residual); + } + + std::vector objective = lp.objective; const f_t primal_tol = settings.primal_tol; + f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); if (primal_inf > primal_tol) { // We are primal infeasible. Switch to phase 1 - compute_phase1_objective(lp, settings, x, objective); + compute_phase1_objective(lp, settings, vstatus, x, objective); + settings.log.printf("Phase 1\n"); + settings.log.printf("Initial primal infeasibility %e\n", primal_inf); phase = 1; } else { + settings.log.printf("Phase 2\n"); phase = 2; } std::vector c_basic(m); - compute_dual_variables(lp, settings, objective, basic_list, nonbasic_list, ft, c_basic, y, z); - settings.log.printf("|| z || %e\n", vector_norm_inf(z)); + compute_dual_variables( + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + if constexpr (print_norms) { + settings.log.printf("|| z || %e\n", vector_norm_inf(z)); + } + + i_t num_dual_inf = 0; + i_t num_primal_inf = 0; + const f_t init_dual_inf = + dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf); + if (num_dual_inf > 0) { + settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); + } - const f_t init_dual_inf = dual_infeasibility(lp, vstatus, z); - settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); + csr_matrix_t Arow(m, n, lp.A.nnz()); + lp.A.to_compressed_row(Arow); - const i_t iter_limit = iter + 1000; - std::vector delta_y(m); + const i_t iter_limit = settings.iteration_limit; + const i_t start_iter = iter; + sparse_vector_t delta_y(m, 0); + sparse_vector_t etilde(m, 0); std::vector delta_z(n); std::vector delta_x(n); - settings.log.printf("Iter Objective Primal inf Dual Inf. Step Entering Leaving\n"); + f_t dual_inf = init_dual_inf; + f_t obj = compute_objective(lp, x); + f_t pricing_dual_tol = settings.dual_tol; + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); + settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); + settings.log.printf("%5d %+.16e %7d %.8e %.2f\n", + iter, + compute_user_objective(lp, obj), + phase == 1 ? num_primal_inf : num_dual_inf, + phase == 1 ? primal_inf : dual_inf, + toc(start_time)); + bool switched_phase = false; while (iter < iter_limit) { i_t nonbasic_entering = -1; - f_t dual_inf; i_t direction; - i_t entering_index = - phase2_pricing(lp, z, nonbasic_list, vstatus, direction, nonbasic_entering, dual_inf); + i_t entering_index = phase2_pricing( + lp, z, nonbasic_list, vstatus, pricing_dual_tol, direction, nonbasic_entering, dual_inf); if (entering_index == -1) { if (phase == 2) { - f_t obj = compute_objective(lp, x); - f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); - settings.log.printf( - "Optimal solution found. Objective %+.16e. Dual infeas %e. Primal " - "infeasibility %e. Iterations %d\n", - compute_user_objective(lp, obj), - dual_inf, - primal_inf, - iter); + // Verify optimality with a consistent basic solution: refactor, put + // nonbasics exactly on their status bounds, rebuild x_B so Ax = b, and + // refresh duals. If that point is not primal/dual feasible, continue. + if (basis_update.num_updates() > 0) { + i_t rank = basis_update.refactor_basis( + lp.A, settings, lp.lower, lp.upper, start_time, basic_list, nonbasic_list, vstatus); + if (rank == CONCURRENT_HALT_RETURN) { return primal_status_t::CONCURRENT_LIMIT; } + if (rank == TIME_LIMIT_RETURN) { return primal_status_t::TIME_LIMIT; } + if (rank != 0) { + settings.log.printf("Failed to refactor basis at optimality check. Iteration %d\n", + iter); + return primal_status_t::NUMERICAL; + } + work_estimate = basis_update.work_estimate(); + } + set_primal_variables_on_bounds(lp, settings, vstatus, x); + compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); + compute_dual_variables( + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); + dual_inf = + dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf); + if (primal_inf > primal_tol) { + compute_phase1_objective(lp, settings, vstatus, x, objective); + phase = 1; + pricing_dual_tol = settings.dual_tol; + compute_dual_variables( + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + settings.log.printf( + "Switching to Primal Simplex Phase 1 after optimality refresh. " + "Primal infeasibility %e\n", + primal_inf); + settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); + switched_phase = true; + continue; + } + if (num_dual_inf > 0) { + // The refreshed reduced costs contain a candidate visible at the active + // pricing tolerance. + continue; + } + + i_t num_tight_dual_inf = 0; + const f_t tight_dual_inf = + dual_infeasibility(lp, vstatus, z, f_t(0.0), num_tight_dual_inf); + if (tight_dual_inf > settings.dual_tol) { + // No candidate is visible at the active pricing tolerance, but the + // zero-tolerance residual is still material. Try tighter pricing before + // accepting optimality. This is needed for problems such as cycle, + // where many small reduced-cost violations lead to improving pivots. + f_t retry_dual_tol = pricing_dual_tol; + f_t retry_dual_inf = 0.0; + i_t retry_entering = -1; + while (retry_entering == -1 && retry_dual_tol > f_t(1e-10)) { + retry_dual_tol *= f_t(0.1); + retry_entering = phase2_pricing(lp, + z, + nonbasic_list, + vstatus, + retry_dual_tol, + direction, + nonbasic_entering, + retry_dual_inf); + } + if (retry_entering != -1) { + pricing_dual_tol = retry_dual_tol; + continue; + } + } + // Report the unfiltered residual at the accepted solution. + dual_inf = tight_dual_inf; + num_dual_inf = num_tight_dual_inf; + obj = compute_objective(lp, x); + sol.objective = obj; + sol.user_objective = compute_user_objective(lp, obj); + if (!settings.inside_mip) { + settings.log.printf("\n"); + settings.log.printf( + "Optimal solution found in %d iterations and %.2fs\n", iter, toc(start_time)); + settings.log.printf("Objective %+.8e\n", sol.user_objective); + settings.log.printf("\n"); + settings.log.printf("Primal infeasibility (abs): %.2e\n", primal_inf); + settings.log.printf("Dual infeasibility (abs): %.2e\n", dual_inf); + settings.log.printf("Primal residual ||Ax-b||: %.2e\n", + primal_constraint_residual(lp, x)); + } return primal_status_t::OPTIMAL; } else { - primal_inf = primal_infeasibility(lp, settings, vstatus, x); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); if (primal_inf > primal_tol) { - settings.log.printf("Primal infeasibility %e. No entering\n", primal_inf); - return primal_status_t::NUMERICAL; + // Incremental duals may be stale relative to the current phase-I + // objective. Refresh objective and duals, then retry pricing with + // successively tighter dual tolerances. + settings.log.printf("Refreshing phase-I objective and duals. Num updates %d. Iter %d\n", + basis_update.num_updates(), iter); + compute_phase1_objective(lp, settings, vstatus, x, objective); + compute_dual_variables( + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + f_t retry_dual_tol = pricing_dual_tol; + while (entering_index == -1 && retry_dual_tol > f_t(1e-10)) { + retry_dual_tol *= f_t(0.1); + settings.log.printf("Retrying phase-I pricing with dual_tol %e\n", retry_dual_tol); + entering_index = phase2_pricing(lp, + z, + nonbasic_list, + vstatus, + retry_dual_tol, + direction, + nonbasic_entering, + dual_inf); + } + if (entering_index == -1) { + settings.log.printf( + "Numerical issues encountered. No entering variable found with large " + "infeasibility %e (%d).\n", + primal_inf, + num_primal_inf); + return primal_status_t::NUMERICAL; + } + pricing_dual_tol = retry_dual_tol; } else { // Restore the objective to the original objective - objective = lp.objective; - phase = 2; - settings.log.printf("Switching to phase 2\n"); + objective = lp.objective; + phase = 2; + pricing_dual_tol = settings.dual_tol; + settings.log.printf( + "Primal phase I complete. Iterations %d. Time %.2f\n", iter, toc(start_time)); + settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, ft, c_basic, y, z); + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + obj = compute_objective(lp, x); + dual_inf = + dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf); iter++; + // Print here: continue may hit dual-optimal Phase 2 and return before + // the end-of-loop log checks switched_phase. + settings.log.printf("%5d %+.16e %7d %.8e %.2f\n", + iter, + compute_user_objective(lp, obj), + num_dual_inf, + dual_inf, + toc(start_time)); continue; } } } + sparse_vector_t rhs_sparse(lp.A, entering_index); + sparse_vector_t scaled_delta_xB_sparse(m, 0); + sparse_vector_t utilde_sparse(m, 0); + basis_update.b_solve(rhs_sparse, scaled_delta_xB_sparse, utilde_sparse); std::vector scaled_delta_xB(m); - std::vector rhs(m); - const i_t col_start = lp.A.col_start[entering_index]; - const i_t col_end = lp.A.col_start[entering_index + 1]; - for (i_t p = col_start; p < col_end; ++p) { - rhs[lp.A.i[p]] = lp.A.x[p]; - } - std::vector utilde(m); - ft.b_solve(rhs, scaled_delta_xB, utilde); + scaled_delta_xB_sparse.to_dense(scaled_delta_xB); for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; @@ -548,67 +883,116 @@ primal_status_t primal_phase2(i_t phase, } delta_x[entering_index] = direction; - std::vector residual(m); - matrix_vector_multiply(lp.A, 1.0, delta_x, 1.0, residual); +#ifdef CHECK_NULLSPACE + std::vector residual(m, 0.0); + matrix_vector_multiply(lp.A, 1.0, delta_x, 0.0, residual); f_t primal_step_err = vector_norm_inf(residual); - if (primal_step_err > 1e-3) { printf("|| A * dx || %e\n", primal_step_err); } + if (primal_step_err > 1e-3) { + settings.log.printf("|| A * dx || %e at iter %d (updates %d)\n", + primal_step_err, + iter, + basis_update.num_updates()); + } +#endif i_t basic_leaving; f_t step_length; - i_t leaving_index = primal_ratio_test( - lp, settings, vstatus, basic_list, x, delta_x, step_length, basic_leaving, entering_index, direction); + i_t leaving_index = primal_ratio_test(lp, + settings, + vstatus, + basic_list, + x, + delta_x, + step_length, + basic_leaving, + entering_index, + direction); if (leaving_index == -1 && step_length >= inf) { settings.log.printf("No leaving variable. Primal unbounded?\n"); return primal_status_t::PRIMAL_UNBOUNDED; } const bool basis_updated = (leaving_index != -1); + bool recompute_duals = false; for (i_t j = 0; j < n; ++j) { x[j] += step_length * delta_x[j]; } + +#ifdef COMPUTE_RESIDUAL + f_t debug_primal_residual = primal_constraint_residual(lp, x); + if (debug_primal_residual > 1e-6) { + settings.log.printf("|| A * x - b || %e at iteration %d (updates %d)\n", debug_primal_residual, iter, basis_update.num_updates()); + } +#endif + + if (basis_updated) { assert(step_length >= 0.0); + + bool should_refactor = basis_update.num_updates() > settings.refactor_frequency; + f_t dual_step_length = 0.0; + if (!should_refactor) { + compute_delta_y(basis_update, basic_leaving, delta_y, etilde); + const f_t pivot = scaled_delta_xB[basic_leaving]; + dual_step_length = compute_dual_step_length(z[entering_index], pivot); + } + basic_list[basic_leaving] = entering_index; nonbasic_list[nonbasic_entering] = leaving_index; vstatus[entering_index] = variable_status_t::BASIC; + // Place the leaver on its leaving bound. If that bound is far from the + // current value (typical after a zero-step leave of an already-infeasible + // basic), rebuild x_B after the factor matches the new basis so Ax = b; + // phase handling below may then (re)enter Phase I if basics are infeasible. + bool rebuild_x_after_bound_snap = false; + f_t leave_bound = 0.0; if (std::abs(lp.upper[leaving_index] - lp.lower[leaving_index]) < 1e-12) { vstatus[leaving_index] = variable_status_t::NONBASIC_FIXED; - } else if (delta_x[leaving_index] < 0) { - vstatus[leaving_index] = variable_status_t::NONBASIC_LOWER; + leave_bound = lp.lower[leaving_index]; } else { - vstatus[leaving_index] = variable_status_t::NONBASIC_UPPER; + // Classify by which bound was hit. Using sign(delta_x) is wrong when the + // variable approached the bound from the infeasible side (phase I). + const f_t x_leave = x[leaving_index]; + const f_t dist_to_lower = std::abs(x_leave - lp.lower[leaving_index]); + const f_t dist_to_upper = std::abs(x_leave - lp.upper[leaving_index]); + if (lp.lower[leaving_index] > -inf && + (lp.upper[leaving_index] >= inf || dist_to_lower <= dist_to_upper)) { + vstatus[leaving_index] = variable_status_t::NONBASIC_LOWER; + leave_bound = lp.lower[leaving_index]; + } else { + vstatus[leaving_index] = variable_status_t::NONBASIC_UPPER; + leave_bound = lp.upper[leaving_index]; + } + } + if (std::abs(x[leaving_index] - leave_bound) > settings.primal_tol) { + rebuild_x_after_bound_snap = true; } + x[leaving_index] = leave_bound; - bool should_refactor = ft.num_updates() > settings.refactor_frequency; if (!should_refactor) { - i_t recommend_refactor = ft.update(utilde, basic_leaving); - should_refactor = recommend_refactor == 1; + compute_delta_z(Arow, vstatus, delta_y, delta_z); + update_y(dual_step_length, delta_y, y); + update_z(dual_step_length, nonbasic_list, entering_index, delta_z, z); + should_refactor = basis_update.update(utilde_sparse, etilde, basic_leaving) == 1; } if (should_refactor) { - i_t rank = factorize_basis(lp.A, - settings, - basic_list, - start_time, - L, - U, - p, - pinv, - q, - deficient, - slacks_needed, - work_estimate); + i_t rank = basis_update.refactor_basis( + lp.A, settings, lp.lower, lp.upper, start_time, basic_list, nonbasic_list, vstatus); if (rank == CONCURRENT_HALT_RETURN) { return primal_status_t::CONCURRENT_LIMIT; } if (rank == TIME_LIMIT_RETURN) { return primal_status_t::TIME_LIMIT; } - if (rank < 0) { + if (rank != 0) { settings.log.printf("Failed to refactor basis. Iteration %d\n", iter); return primal_status_t::NUMERICAL; } - if (rank != m) { - settings.log.printf("Failed to refactor basis. rank %d m %d\n", rank, m); - return primal_status_t::NUMERICAL; - } - reorder_basic_list(q, basic_list); - ft.reset(L, U, p); + work_estimate = basis_update.work_estimate(); + recompute_duals = true; + // Factor matches basic_list: rebuild x_B so Ax = b exactly. + set_primal_variables_on_bounds(lp, settings, vstatus, x); + compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); + } else if (rebuild_x_after_bound_snap) { + // FT update already matches the new basis; recompute x_B with the leaver + // snapped onto its bound. + compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); } } else { if (direction > 0) { @@ -620,33 +1004,53 @@ primal_status_t primal_phase2(i_t phase, } } - // Check if we need to switch to phase 1 - const f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); if (primal_inf > primal_tol) { - compute_phase1_objective(lp, settings, x, objective); - phase = 1; + if (phase != 1) { + settings.log.printf( + "Switching to Primal Simplex Phase 1. Iteration %d. Primal infeasibility %e\n", + iter, + primal_inf); + settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); + switched_phase = true; + } + compute_phase1_objective(lp, settings, vstatus, x, objective); + phase = 1; + recompute_duals = true; } else if (phase == 1) { - objective = lp.objective; - phase = 2; + objective = lp.objective; + phase = 2; + pricing_dual_tol = settings.dual_tol; + recompute_duals = true; + settings.log.printf( + "Primal phase I complete. Iterations %d. Time %.2f\n", iter, toc(start_time)); + settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); + switched_phase = true; } - if (basis_updated || primal_inf > primal_tol) { - compute_dual_variables(lp, settings, objective, basic_list, nonbasic_list, ft, c_basic, y, z); + if (recompute_duals) { + compute_dual_variables( + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); } - const f_t obj = compute_objective(lp, x); - dual_inf = dual_infeasibility(lp, vstatus, z); - settings.log.printf("%3d %.10e %8.2e %8.2e %8.2e %8d %8d %d %.2f\n", - iter, - compute_user_objective(lp, obj), - primal_inf, - dual_inf, - step_length == 0.0 ? 0.0 : step_length, - entering_index, - leaving_index, - phase, - toc(start_time)); + obj = compute_objective(lp, x); + dual_inf = + dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf); + iter++; + + f_t now = toc(start_time); + if (0|| (iter - start_iter) < settings.first_iteration_log || + (iter % settings.iteration_log_frequency) == 0 || switched_phase) { + const f_t user_obj = compute_user_objective(lp, obj); + settings.log.printf("%5d %+.16e %7d %.8e %.2f\n", + iter, + user_obj, + phase == 1 ? num_primal_inf : num_dual_inf, + phase == 1 ? primal_inf : dual_inf, + now); + switched_phase = false; + } } if (iter == iter_limit) { return primal_status_t::ITERATION_LIMIT; } @@ -677,6 +1081,19 @@ template primal_status_t primal_phase2( lp_solution_t& sol, int& iter); +template primal_status_t primal_phase2_with_advanced_basis( + int phase, + double start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + int& iter, + double& work_estimate); + #endif } // namespace cuopt::mathematical_optimization::simplex diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index 34ffbd8ba5..79008829d5 100644 --- a/cpp/src/dual_simplex/primal.hpp +++ b/cpp/src/dual_simplex/primal.hpp @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -27,7 +28,6 @@ enum class primal_status_t { CONCURRENT_LIMIT = 6 }; - template i_t primal_ratio_test(const lp_problem_t& lp, const simplex_solver_settings_t& settings, @@ -40,6 +40,20 @@ i_t primal_ratio_test(const lp_problem_t& lp, i_t entering_index, i_t direction); +template +primal_status_t primal_phase2_with_advanced_basis( + i_t phase, + f_t start_time, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + std::vector& vstatus, + basis_update_mpf_t& basis_update, + std::vector& basic_list, + std::vector& nonbasic_list, + lp_solution_t& sol, + i_t& iter, + f_t& work_estimate); + template primal_status_t primal_phase2(i_t phase, f_t start_time, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index da6834f60f..adad745109 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -61,6 +61,52 @@ void write_matlab(const std::string& filename, const simplex::lp_problem_t +void initialize_slack_basis_vstatus(const lp_problem_t& lp, + std::vector& vstatus) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + vstatus.resize(n); + for (i_t j = 0; j < n; ++j) { + if (lp.lower[j] == -inf && lp.upper[j] == inf) { + vstatus[j] = variable_status_t::NONBASIC_FREE; + } else if (std::abs(lp.upper[j] - lp.lower[j]) < 1e-12) { + vstatus[j] = variable_status_t::NONBASIC_FIXED; + } else if (lp.lower[j] > -inf) { + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } else { + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } + } + i_t num_basic = 0; + for (i_t j = n - 1; j >= 0; --j) { + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + const i_t nz = col_end - col_start; + if (nz == 1 && std::abs(lp.A.x[col_start]) == 1.0) { + vstatus[j] = variable_status_t::BASIC; + num_basic++; + } + if (num_basic == m) { break; } + } + assert(num_basic == m); +} + } // namespace template @@ -288,7 +334,7 @@ lp_status_t solve_linear_program_with_advanced_basis( edge_norms, work_unit_context); } - constexpr bool primal_cleanup = false; + constexpr bool primal_cleanup = true; if (status == dual_status_t::OPTIMAL && primal_cleanup) { settings.log.printf("Running primal cleanup\n"); primal_phase2(2, start_time, lp, settings, vstatus, solution, iter); @@ -684,6 +730,109 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us return solve_linear_program_with_barrier(user_problem, settings, start_time, solution); } +template +lp_status_t solve_linear_program_with_primal(const user_problem_t& user_problem, + const simplex_solver_settings_t& settings, + f_t start_time, + lp_solution_t& solution) +{ + raft::common::nvtx::range scope("PrimalSimplex::solve_lp"); + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + std::vector new_slacks; + dualize_info_t dualize_info; + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + solution.resize(user_problem.num_rows, user_problem.num_cols); + lp_solution_t original_solution(original_lp.num_rows, original_lp.num_cols); + + // Presolve adds/retains artificial variables so a full slack basis exists. + lp_problem_t presolved_lp(original_lp.handle_ptr, 1, 1, 1); + presolve_info_t presolve_info; + const i_t ok = presolve(original_lp, settings, presolved_lp, presolve_info); + if (ok == CONCURRENT_HALT_RETURN) { return lp_status_t::CONCURRENT_LIMIT; } + if (ok == TIME_LIMIT_RETURN) { return lp_status_t::TIME_LIMIT; } + if (ok == -1) { return lp_status_t::INFEASIBLE; } + + lp_problem_t lp(original_lp.handle_ptr, + presolved_lp.num_rows, + presolved_lp.num_cols, + presolved_lp.A.col_start[presolved_lp.num_cols]); + std::vector column_scales; + std::vector row_scales; + scaling(presolved_lp, settings, lp, column_scales, row_scales); + + std::vector vstatus; + initialize_slack_basis_vstatus(lp, vstatus); + + lp_solution_t lp_solution(lp.num_rows, lp.num_cols); + i_t iter = 0; + const primal_status_t primal_status = + primal_phase2(2, start_time, lp, settings, vstatus, lp_solution, iter); + lp_solution.iterations = iter; + original_solution.iterations = iter; + + if (primal_status == primal_status_t::CONCURRENT_LIMIT) { + solution.iterations = iter; + return lp_status_t::CONCURRENT_LIMIT; + } + + if (primal_status == primal_status_t::OPTIMAL) { + lp_solution.objective = compute_objective(lp, lp_solution.x); + lp_solution.user_objective = compute_user_objective(lp, lp_solution.objective); + + std::vector residual = lp.rhs; + matrix_vector_multiply(lp.A, 1.0, lp_solution.x, -1.0, residual); + lp_solution.l2_primal_residual = vector_norm2(residual); + + std::vector dual_residual = lp_solution.z; + for (i_t j = 0; j < lp.num_cols; ++j) { + dual_residual[j] -= lp.objective[j]; + } + matrix_transpose_vector_multiply(lp.A, 1.0, lp_solution.y, 1.0, dual_residual); + lp_solution.l2_dual_residual = vector_norm2(dual_residual); + + std::vector unscaled_x(lp.num_cols); + std::vector unscaled_y(lp.num_rows); + std::vector unscaled_z(lp.num_cols); + unscale_solution(column_scales, + row_scales, + lp_solution.x, + lp_solution.y, + lp_solution.z, + unscaled_x, + unscaled_y, + unscaled_z); + uncrush_solution(presolve_info, + settings, + original_lp, + unscaled_x, + unscaled_y, + unscaled_z, + original_solution.x, + original_solution.y, + original_solution.z); + original_solution.objective = lp_solution.objective; + original_solution.user_objective = lp_solution.user_objective; + original_solution.l2_primal_residual = lp_solution.l2_primal_residual; + original_solution.l2_dual_residual = lp_solution.l2_dual_residual; + } + + uncrush_primal_solution(user_problem, original_lp, original_solution.x, solution.x); + uncrush_dual_solution(user_problem, + original_lp, + original_solution.y, + original_solution.z, + solution.y, + solution.z); + solution.objective = original_solution.objective; + solution.user_objective = original_solution.user_objective; + solution.iterations = original_solution.iterations; + solution.l2_primal_residual = original_solution.l2_primal_residual; + solution.l2_dual_residual = original_solution.l2_dual_residual; + return map_primal_status_to_lp_status(primal_status); +} + + template lp_status_t solve_linear_program(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, @@ -831,6 +980,12 @@ template lp_status_t solve_linear_program_with_barrier( double start_time, lp_solution_t& solution); +template lp_status_t solve_linear_program_with_primal( + const user_problem_t& user_problem, + const simplex_solver_settings_t& settings, + double start_time, + lp_solution_t& solution); + template lp_status_t solve_linear_program(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, lp_solution_t& solution); diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 7cc9a9f5cf..f4807306e0 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -98,6 +98,12 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us f_t start_time, lp_solution_t& solution); +template +lp_status_t solve_linear_program_with_primal(const user_problem_t& user_problem, + const simplex_solver_settings_t& settings, + f_t start_time, + lp_solution_t& solution); + template lp_status_t solve_linear_program(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 9193112d71..ab129b9c89 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -131,7 +131,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_ITERATION_LIMIT, &pdlp_settings.iteration_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, {CUOPT_NODE_LIMIT, &mip_settings.node_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, {CUOPT_PDLP_SOLVER_MODE, reinterpret_cast(&pdlp_settings.pdlp_solver_mode), CUOPT_PDLP_SOLVER_MODE_STABLE1, CUOPT_PDLP_SOLVER_MODE_STABLE3, CUOPT_PDLP_SOLVER_MODE_STABLE3}, - {CUOPT_METHOD, reinterpret_cast(&pdlp_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_BARRIER, CUOPT_METHOD_CONCURRENT}, + {CUOPT_METHOD, reinterpret_cast(&pdlp_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_PRIMAL, CUOPT_METHOD_CONCURRENT}, {CUOPT_NUM_CPU_THREADS, &mip_settings.num_cpu_threads, -1, std::numeric_limits::max(), -1}, {CUOPT_AUGMENTED, &pdlp_settings.augmented, -1, 1, -1}, {CUOPT_FOLDING, &pdlp_settings.folding, -1, 1, -1}, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 25b427fa9a..173619c5d1 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -431,7 +431,7 @@ optimization_problem_solution_t convert_dual_simplex_sol( termination_status != pdlp_termination_status_t::TimeLimit && termination_status != pdlp_termination_status_t::ConcurrentLimit) { CUOPT_LOG_INFO("%s Solve status %s", - method == method_t::DualSimplex ? "Dual Simplex" : "Barrier", + method_to_string(method).c_str(), sol.get_termination_status_string().c_str()); } @@ -630,6 +630,59 @@ optimization_problem_solution_t run_dual_simplex( method_t::DualSimplex); } +template +std::tuple, simplex::lp_status_t, f_t, f_t, f_t> run_primal( + simplex::user_problem_t& user_problem, + pdlp_solver_settings_t const& settings, + const timer_t& timer) +{ + f_t norm_user_objective = vector_norm2(user_problem.objective); + f_t norm_rhs = vector_norm2(user_problem.rhs); + + simplex::simplex_solver_settings_t primal_settings; + primal_settings.time_limit = settings.time_limit; + primal_settings.iteration_limit = settings.iteration_limit; + primal_settings.concurrent_halt = settings.concurrent_halt; + if (primal_settings.concurrent_halt != nullptr) { + // Don't show the primal simplex log in concurrent mode. Show the PDLP log instead + primal_settings.log.log = false; + } + + simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); + auto status = simplex::solve_linear_program_with_primal( + user_problem, primal_settings, timer.get_tic_start(), solution); + + CUOPT_LOG_CONDITIONAL_INFO( + !settings.inside_mip, "Primal simplex finished in %.2f seconds", timer.elapsed_time()); + + if (settings.concurrent_halt != nullptr && + (status == simplex::lp_status_t::OPTIMAL || status == simplex::lp_status_t::UNBOUNDED || + status == simplex::lp_status_t::INFEASIBLE || + status == simplex::lp_status_t::UNBOUNDED_OR_INFEASIBLE)) { + // We finished. Tell PDLP to stop if it is still running. + *settings.concurrent_halt = 1; + } + + return {std::move(solution), status, timer.elapsed_time(), norm_user_objective, norm_rhs}; +} + +template +optimization_problem_solution_t run_primal(mip::problem_t& problem, + pdlp_solver_settings_t const& settings, + const timer_t& timer) +{ + simplex::user_problem_t primal_problem = + cuopt_problem_to_user_problem(problem.handle_ptr, problem); + auto sol_primal = run_primal(primal_problem, settings, timer); + return convert_dual_simplex_sol(problem, + std::get<0>(sol_primal), + std::get<1>(sol_primal), + std::get<2>(sol_primal), + std::get<3>(sol_primal), + std::get<4>(sol_primal), + method_t::Primal); +} + #if PDLP_INSTANTIATE_FLOAT || CUOPT_INSTANTIATE_FLOAT template @@ -1754,19 +1807,27 @@ optimization_problem_solution_t solve_lp_with_method( if constexpr (std::is_same_v) { if (settings.method == method_t::DualSimplex) { return run_dual_simplex(problem, settings, timer); + } else if (settings.method == method_t::Primal) { + return run_primal(problem, settings, timer); } else if (settings.method == method_t::Barrier) { return run_barrier(problem, settings, timer); } else if (settings.method == method_t::Concurrent) { return run_concurrent(problem, settings, timer, is_batch_mode); + } else if (settings.method == method_t::PDLP) { + return run_pdlp(problem, settings, timer, is_batch_mode); } else { + cuopt_expects(false, + error_type_t::ValidationError, + "Invalid LP method. Valid values: Concurrent(0), PDLP(1), DualSimplex(2), " + "Barrier(3), Primal(4)."); return run_pdlp(problem, settings, timer, is_batch_mode); } } else { // Float precision only supports PDLP without presolve/crossover cuopt_expects(settings.method == method_t::PDLP, error_type_t::ValidationError, - "Float precision only supports PDLP method. DualSimplex, Barrier, and Concurrent " - "require double precision."); + "Float precision only supports PDLP method. DualSimplex, Primal, Barrier, and " + "Concurrent require double precision."); return run_pdlp(problem, settings, timer, is_batch_mode); } } From efc9fb021fba49bbef3506f4c217b44bcb6f17dd Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Tue, 28 Jul 2026 10:47:40 -0700 Subject: [PATCH 06/34] Use primal simplex to remove a perturbation from dual simplex --- cpp/src/dual_simplex/phase2.cpp | 59 ++++++++++++++++++++++++++++++--- cpp/src/dual_simplex/primal.cpp | 14 ++++---- cpp/src/dual_simplex/primal.hpp | 5 ++- cpp/src/dual_simplex/solve.cpp | 2 +- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index c15f7f554c..d3867086f3 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -2331,13 +2332,15 @@ void prepare_optimality(i_t info, const simplex_solver_settings_t& settings, basis_update_mpf_t& ft, const std::vector& objective, - const std::vector& basic_list, - const std::vector& nonbasic_list, - const std::vector& vstatus, + // Primal cleanup below pivots, so the basis, the statuses + // and the iteration count are updated in place. + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, int phase, f_t start_time, f_t max_val, - i_t iter, + i_t& iter, const std::vector& x, std::vector& y, std::vector& z, @@ -2370,6 +2373,54 @@ void prepare_optimality(i_t info, settings.log.printf("Unperturbed dual infeasibility: %.2e\n", dual_infeas); settings.log.printf("Objective: %+.16e\n", sol.user_objective); settings.log.printf("Num updates: %d\n", ft.num_updates()); + + // Primal pivots in place, so keep the perturbed solution to fall back on. + // The factor is snapshot rather than refactorized on failure: the copy is + // exact, keeps ft consistent with the restored basis, and cannot itself + // fail the way a refactorization can. + const basis_update_mpf_t saved_ft = ft; + const std::vector saved_x = sol.x; + const std::vector saved_y = sol.y; + const std::vector saved_z = sol.z; + const std::vector saved_vstatus = vstatus; + const std::vector saved_basic_list = basic_list; + const std::vector saved_nonbasic_list = nonbasic_list; + + // Reoptimize the unperturbed objective from this basis. The point is + // primal feasible, so primal simplex stays in phase 2 and pivots only to + // restore dual feasibility. It writes through sol, so x, y and z here see + // the cleaned up solution. It prints no summary; the one below reports the + // final result. + primal_status_t primal_status = primal_phase2_with_advanced_basis(2, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + work_estimate, + false); + if (primal_status == primal_status_t::OPTIMAL) { + // z now prices the original objective, so no perturbation remains. + settings.log.printf("Primal cleanup successful.\n"); + perturbation = 0.0; + sol.objective = compute_objective(lp, sol.x); + sol.user_objective = compute_user_objective(lp, sol.objective); + } else { + // Restore the perturbed optimum; a partially pivoted basis is worse than + // the dual feasible point we started from. + settings.log.printf("Primal cleanup failed. Reporting the perturbed solution.\n"); + ft = saved_ft; + sol.x = saved_x; + sol.y = saved_y; + sol.z = saved_z; + vstatus = saved_vstatus; + basic_list = saved_basic_list; + nonbasic_list = saved_nonbasic_list; + } } } } diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index d12f24e98f..7633f9a1b7 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -442,7 +442,7 @@ i_t primal_ratio_test(const lp_problem_t& lp, // step * delta_x[j] >= lp.lower[j] - x[j] // step <= (lp.lower[j] - x[j]) / delta_x[j], delta_x[j] < 0 f_t neum = lp.lower[j] - x[j]; - // A basic sitting a hair below its bound (within the primal tolerance) is on + // A basic sitting below its bound (within the primal tolerance) is on // the bound numerically, but gives a tiny negative ratio. Dropping it lets // the step run straight through the bound, so treat it as a zero-length // block. A genuine violation is left to the branches above, which stop at @@ -466,7 +466,7 @@ i_t primal_ratio_test(const lp_problem_t& lp, // step * delta_x[j] <= lp.upper[j] - x[j] // step <= (lp.upper[j] - x[j]) / delta_x[j], delta_x[j] > 0 f_t neum = lp.upper[j] - x[j]; - // Mirror of the lower bound case: a hair above the bound is on the bound. + // Mirror of the lower bound case: slightly above the bound is considered on the bound. if (neum < 0 && -neum <= settings.primal_tol) { neum = 0.0; } f_t ratio = neum / delta_x[j]; if (ratio >= 0 && ratio < min_val) { @@ -603,7 +603,8 @@ primal_status_t primal_phase2_with_advanced_basis( std::vector& nonbasic_list, lp_solution_t& sol, i_t& iter, - f_t& work_estimate) + f_t& work_estimate, + bool print_summary) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -793,7 +794,7 @@ primal_status_t primal_phase2_with_advanced_basis( obj = compute_objective(lp, x); sol.objective = obj; sol.user_objective = compute_user_objective(lp, obj); - if (!settings.inside_mip) { + if (!settings.inside_mip && print_summary) { settings.log.printf("\n"); settings.log.printf( "Optimal solution found in %d iterations and %.2fs\n", iter, toc(start_time)); @@ -990,7 +991,7 @@ primal_status_t primal_phase2_with_advanced_basis( set_primal_variables_on_bounds(lp, settings, vstatus, x); compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); } else if (rebuild_x_after_bound_snap) { - // FT update already matches the new basis; recompute x_B with the leaver + // FT update already matches the new basis; recompute x_B with the leaving variable // snapped onto its bound. compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); } @@ -1092,7 +1093,8 @@ template primal_status_t primal_phase2_with_advanced_basis( std::vector& nonbasic_list, lp_solution_t& sol, int& iter, - double& work_estimate); + double& work_estimate, + bool print_summary); #endif diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index 79008829d5..63f4761c0e 100644 --- a/cpp/src/dual_simplex/primal.hpp +++ b/cpp/src/dual_simplex/primal.hpp @@ -52,7 +52,10 @@ primal_status_t primal_phase2_with_advanced_basis( std::vector& nonbasic_list, lp_solution_t& sol, i_t& iter, - f_t& work_estimate); + f_t& work_estimate, + // Callers that print their own summary (dual simplex perturbation cleanup) + // suppress this one, so optimality is not reported twice. + bool print_summary = true); template primal_status_t primal_phase2(i_t phase, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index adad745109..a24d12fc55 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -334,7 +334,7 @@ lp_status_t solve_linear_program_with_advanced_basis( edge_norms, work_unit_context); } - constexpr bool primal_cleanup = true; + constexpr bool primal_cleanup = false; if (status == dual_status_t::OPTIMAL && primal_cleanup) { settings.log.printf("Running primal cleanup\n"); primal_phase2(2, start_time, lp, settings, vstatus, solution, iter); From cf3d4d4821f52292653a9d2a81720696a7672d13 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 29 Jul 2026 14:04:05 -0700 Subject: [PATCH 07/34] Clean up logging of degenerate feasibility pump --- cpp/src/branch_and_bound/branch_and_bound.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 1da2f0f249..5e5e8540df 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3417,10 +3417,12 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple bool recompute_basis = false; const i_t iter_before = iter; f_t primal_work_estimate = 0; + simplex_solver_settings_t primal_settings = settings_; + primal_settings.log.log = false; simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, exploration_stats_.start_time, lp_reduced, - settings_, + primal_settings, reduced_vstatus, reduced_basis_update, reduced_basic_list, @@ -3446,15 +3448,17 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple // Verify the solution is primal feasible std::vector residual = lp.rhs; matrix_vector_multiply(lp.A, 1.0, adjusted_solution, -1.0, residual); + const f_t primal_residual = vector_norm_inf(residual); - settings_.log.printf("Reduced LP residual|| A*x - b ||_inf = %.16e\n", - vector_norm_inf(residual)); + if (primal_residual > 1e-6) { + settings_.log.printf("Reduced LP residual|| A*x - b ||_inf = %.4e\n", primal_residual); + } std::vector tmp_fractional; i_t num_fractional_reduced = fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); settings_.log.printf( - "Reduced LP fractional variables %d/%d\n", num_fractional_reduced, num_fractional); + "Degenerate feasibility pump (%d/%d): fractional variables %d/%d\n", pump_iter, max_pump_iter, num_fractional_reduced, num_fractional); // Also treat a pass that fails to improve the best as a stall, so we perturb // the next pass even when the solve pivoted (moved) without reducing the count. stalled = stalled || (num_fractional_reduced >= best_num_fractional); @@ -3465,7 +3469,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple } } - settings_.log.printf("Best number of fractional variables %d/%d\n", best_num_fractional, num_fractional); + settings_.log.printf("Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables %d/%d\n", iter, best_num_fractional, num_fractional); if (best_num_fractional < num_fractional) { // Translate the vstatus from the reduced problem to the vstatus for the original problem i_t reduced_cols = 0; From a7cfd191962139fae9f34148543a97ea03a5627b Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 29 Jul 2026 15:03:22 -0700 Subject: [PATCH 08/34] Add work estimates to primal simplex --- cpp/src/branch_and_bound/branch_and_bound.cpp | 4 +- cpp/src/dual_simplex/primal.cpp | 184 +++++++++++++----- cpp/src/dual_simplex/primal.hpp | 3 +- 3 files changed, 136 insertions(+), 55 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 5e5e8540df..57a93eaf47 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3700,6 +3700,7 @@ void branch_and_bound_t::pivot_out_integer_variables( f_t step_length; i_t basic_leaving; + f_t work_estimate = 0.0; const i_t leaving_index = simplex::primal_ratio_test(lp, settings_, vstatus_copy, @@ -3709,7 +3710,8 @@ void branch_and_bound_t::pivot_out_integer_variables( step_length, basic_leaving, entering_index, - direction); + direction, + work_estimate); bool binding_integer = leaving_index != -1 && is_fractional(soln_copy.x[leaving_index], var_types_[leaving_index], settings_.integer_tol); diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 7633f9a1b7..f5a756a78a 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -24,9 +24,12 @@ template void set_primal_variables_on_bounds(const lp_problem_t& lp, const simplex_solver_settings_t& settings, std::vector& vstatus, - std::vector& x) + std::vector& x, + f_t& work_estimate) { - const i_t n = lp.num_cols; + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + constexpr f_t diff_tol = 1e-6; for (i_t j = 0; j < n; ++j) { if (vstatus[j] == variable_status_t::BASIC) { continue; } @@ -54,6 +57,7 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, assert(1 == 0); } } + work_estimate += n + 3.0*(n - m); } template @@ -61,7 +65,8 @@ f_t dual_infeasibility(const lp_problem_t& lp, const std::vector& vstatus, const std::vector& z, f_t tight_tol, - i_t& num_infeasible) + i_t& num_infeasible, + f_t& work_estimate) { const i_t n = lp.num_cols; num_infeasible = 0; @@ -103,6 +108,7 @@ f_t dual_infeasibility(const lp_problem_t& lp, non_basic_upper_inf++; } } + work_estimate += 8 * n; return sum_infeasible; } @@ -115,7 +121,8 @@ i_t phase2_pricing(const lp_problem_t& lp, f_t dual_tol, i_t& direction, i_t& basic_entering, - f_t& dual_inf) + f_t& dual_inf, + f_t& work_estimate) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -149,6 +156,7 @@ i_t phase2_pricing(const lp_problem_t& lp, } } } + work_estimate += 4 * (n - m); return entering_index; } @@ -157,8 +165,10 @@ f_t primal_infeasibility(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& vstatus, const std::vector& x, - i_t& num_infeasible) + i_t& num_infeasible, + f_t& work_estimate) { + const i_t m = lp.num_rows; const i_t n = lp.num_cols; f_t primal_inf = 0; num_infeasible = 0; @@ -196,6 +206,7 @@ f_t primal_infeasibility(const lp_problem_t& lp, } } } + work_estimate += n + 4*m; return primal_inf; } @@ -203,19 +214,23 @@ template f_t primal_infeasibility(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& vstatus, - const std::vector& x) + const std::vector& x, + f_t& work_estimate) { i_t num_infeasible = 0; - return primal_infeasibility(lp, settings, vstatus, x, num_infeasible); + return primal_infeasibility(lp, settings, vstatus, x, num_infeasible, work_estimate); } +// work estimate: n-m + 4 * m template void compute_phase1_objective(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& vstatus, const std::vector& x, - std::vector& objective) + std::vector& objective, + f_t& work_estimate) { + const i_t m = lp.num_rows; const i_t n = lp.num_cols; for (i_t j = 0; j < n; ++j) { if (vstatus[j] != variable_status_t::BASIC) { @@ -228,6 +243,7 @@ void compute_phase1_objective(const lp_problem_t& lp, objective[j] = 0.0; } } + work_estimate += n-m + 4 * m; } template @@ -249,11 +265,13 @@ template void compute_delta_z(const csr_matrix_t& Arow, const std::vector& vstatus, const sparse_vector_t& delta_y, - std::vector& delta_z) + std::vector& delta_z, + f_t& work_estimate) { // A^T delta_y + delta_z = 0 // delta_z = -A^T delta_y = - sum_i A(i, :) * delta_y_i std::fill(delta_z.begin(), delta_z.end(), 0.0); + work_estimate += delta_z.size(); for (i_t k = 0; k < static_cast(delta_y.i.size()); ++k) { const i_t i = delta_y.i[k]; const f_t delta_y_i = delta_y.x[k]; @@ -263,7 +281,9 @@ void compute_delta_z(const csr_matrix_t& Arow, const i_t j = Arow.j[p]; if (vstatus[j] != variable_status_t::BASIC) { delta_z[j] -= Arow.x[p] * delta_y_i; } } + work_estimate += 4*(row_end - row_start); } + work_estimate += 4 * delta_y.i.size(); } template @@ -274,12 +294,16 @@ f_t compute_dual_step_length(f_t entering_reduced_cost, f_t pivot) } template -void update_y(f_t dual_step_length, const sparse_vector_t& delta_y, std::vector& y) +void update_y(f_t dual_step_length, + const sparse_vector_t& delta_y, + std::vector& y, + f_t& work_estimate) { for (i_t k = 0; k < static_cast(delta_y.i.size()); ++k) { const i_t i = delta_y.i[k]; y[i] += dual_step_length * delta_y.x[k]; } + work_estimate += 3 * delta_y.i.size(); } template @@ -287,12 +311,14 @@ void update_z(f_t dual_step_length, const std::vector& nonbasic_list, i_t entering_index, const std::vector& delta_z, - std::vector& z) + std::vector& z, + f_t& work_estimate) { for (i_t k = 0; k < static_cast(nonbasic_list.size()); ++k) { const i_t j = nonbasic_list[k]; z[j] += dual_step_length * delta_z[j]; } + work_estimate += 3 * nonbasic_list.size(); z[entering_index] = 0.0; } @@ -305,7 +331,8 @@ void compute_dual_variables(const lp_problem_t& lp, basis_update_mpf_t& ft, std::vector& c_basic, std::vector& y, - std::vector& z) + std::vector& z, + f_t& work_estimate) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -314,6 +341,7 @@ void compute_dual_variables(const lp_problem_t& lp, const i_t j = basic_list[k]; c_basic[k] = objective[j]; } + work_estimate += 3 * m; ft.b_transpose_solve(c_basic, y); // zN = cN - N'*y for (i_t k = 0; k < n - m; k++) { @@ -328,12 +356,15 @@ void compute_dual_variables(const lp_problem_t& lp, for (i_t p = col_start; p < col_end; ++p) { dot += lp.A.x[p] * y[lp.A.i[p]]; } + work_estimate += 3.0*(col_end - col_start); z[j] -= dot; } + work_estimate += 6 * (n - m); // zB = 0 for (i_t k = 0; k < m; ++k) { z[basic_list[k]] = 0.0; } + work_estimate += 2*m; } template @@ -341,7 +372,8 @@ void compute_basic_primal_variables(const lp_problem_t& lp, const basis_update_mpf_t& basis_update, const std::vector& basic_list, const std::vector& nonbasic_list, - std::vector& x) + std::vector& x, + f_t& work_estimate) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -354,12 +386,16 @@ void compute_basic_primal_variables(const lp_problem_t& lp, for (i_t p = col_start; p < col_end; ++p) { rhs[lp.A.i[p]] -= xj * lp.A.x[p]; } + work_estimate += 3.0*(col_end - col_start); } + work_estimate += 4 * (n - m); std::vector xB(m); + work_estimate += m; basis_update.b_solve(rhs, xB); for (i_t k = 0; k < m; ++k) { x[basic_list[k]] = xB[k]; } + work_estimate += 3 * m; } template @@ -383,7 +419,8 @@ i_t primal_ratio_test(const lp_problem_t& lp, f_t& step_length, i_t& basic_leaving, i_t entering_index, - i_t direction) + i_t direction, + f_t& work_estimate) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -482,6 +519,7 @@ i_t primal_ratio_test(const lp_problem_t& lp, } } } + work_estimate += 10*m; step_length = min_val; return leaving_index; } @@ -505,6 +543,7 @@ primal_status_t primal_phase2(i_t phase, std::vector superbasic_list; get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); + work_estimate += 2*n; assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); @@ -623,12 +662,14 @@ primal_status_t primal_phase2_with_advanced_basis( std::vector incoming_x = x; std::vector incoming_vstatus = vstatus; + work_estimate += 2.0 * n; settings.log.printf("Primal Simplex\n"); // Nonbasics must be on their bounds before forming B x_B = b - A_N x_N. // Setting them after the solve leaves ||A*x - b|| large whenever x_N != 0. - set_primal_variables_on_bounds(lp, settings, vstatus, x); + set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); std::vector rhs = lp.rhs; + work_estimate += m; // rhs = b - sum_{j : x_j = l_j} A(:, j) l(j) - sum_{j : x_j = u_j} A(:, j) * // u(j) for (i_t k = 0; k < n - m; ++k) { @@ -639,34 +680,45 @@ primal_status_t primal_phase2_with_advanced_basis( for (i_t p = col_start; p < col_end; ++p) { rhs[lp.A.i[p]] -= xj * lp.A.x[p]; } + work_estimate += 3.0*(col_end - col_start); } + work_estimate += 4 * (n - m); + std::vector xB(m); + work_estimate += m; + basis_update.b_solve(rhs, xB); for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; x[j] = xB[k]; } + work_estimate += 3 * m; + constexpr bool print_norms = false; if constexpr (print_norms) { settings.log.printf("|| x || %e\n", vector_norm2(x)); } std::vector residual = lp.rhs; + work_estimate += m; matrix_vector_multiply(lp.A, 1.0, x, -1.0, residual); + work_estimate += m + 2*n + 4.0*lp.A.col_start[lp.A.n]; f_t primal_residual = vector_norm_inf(residual); + work_estimate += m; if (primal_residual > settings.primal_tol) { settings.log.printf("|| A*x - b || %e\n", primal_residual); } std::vector objective = lp.objective; + work_estimate += 2*n; const f_t primal_tol = settings.primal_tol; - f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x); + f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x, work_estimate); if (primal_inf > primal_tol) { // We are primal infeasible. Switch to phase 1 - compute_phase1_objective(lp, settings, vstatus, x, objective); + compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); settings.log.printf("Phase 1\n"); settings.log.printf("Initial primal infeasibility %e\n", primal_inf); phase = 1; @@ -676,8 +728,9 @@ primal_status_t primal_phase2_with_advanced_basis( } std::vector c_basic(m); + work_estimate += m; compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); if constexpr (print_norms) { settings.log.printf("|| z || %e\n", vector_norm_inf(z)); } @@ -685,13 +738,15 @@ primal_status_t primal_phase2_with_advanced_basis( i_t num_dual_inf = 0; i_t num_primal_inf = 0; const f_t init_dual_inf = - dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf); + dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf, work_estimate); if (num_dual_inf > 0) { settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); } csr_matrix_t Arow(m, n, lp.A.nnz()); + work_estimate += n + 2*lp.A.nnz(); lp.A.to_compressed_row(Arow); + work_estimate += m + 6*lp.A.nnz(); const i_t iter_limit = settings.iteration_limit; const i_t start_iter = iter; @@ -699,11 +754,13 @@ primal_status_t primal_phase2_with_advanced_basis( sparse_vector_t etilde(m, 0); std::vector delta_z(n); std::vector delta_x(n); + work_estimate += 2*m + 2*n; f_t dual_inf = init_dual_inf; f_t obj = compute_objective(lp, x); + work_estimate += 2*n; f_t pricing_dual_tol = settings.dual_tol; - primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); settings.log.printf("%5d %+.16e %7d %.8e %.2f\n", iter, @@ -712,11 +769,15 @@ primal_status_t primal_phase2_with_advanced_basis( phase == 1 ? primal_inf : dual_inf, toc(start_time)); bool switched_phase = false; + + work_estimate += basis_update.work_estimate(); + basis_update.clear_work_estimate(); + while (iter < iter_limit) { i_t nonbasic_entering = -1; i_t direction; i_t entering_index = phase2_pricing( - lp, z, nonbasic_list, vstatus, pricing_dual_tol, direction, nonbasic_entering, dual_inf); + lp, z, nonbasic_list, vstatus, pricing_dual_tol, direction, nonbasic_entering, dual_inf, work_estimate); if (entering_index == -1) { if (phase == 2) { // Verify optimality with a consistent basic solution: refactor, put @@ -732,23 +793,24 @@ primal_status_t primal_phase2_with_advanced_basis( iter); return primal_status_t::NUMERICAL; } - work_estimate = basis_update.work_estimate(); + work_estimate += basis_update.work_estimate(); + basis_update.clear_work_estimate(); } - set_primal_variables_on_bounds(lp, settings, vstatus, x); - compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); + set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); + compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x, work_estimate); compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); - primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); dual_inf = - dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf); + dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf, work_estimate); if (primal_inf > primal_tol) { - compute_phase1_objective(lp, settings, vstatus, x, objective); + compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); phase = 1; pricing_dual_tol = settings.dual_tol; compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); settings.log.printf( - "Switching to Primal Simplex Phase 1 after optimality refresh. " + "Switching to Primal Simplex Phase 1 after near optimality. " "Primal infeasibility %e\n", primal_inf); settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); @@ -763,7 +825,7 @@ primal_status_t primal_phase2_with_advanced_basis( i_t num_tight_dual_inf = 0; const f_t tight_dual_inf = - dual_infeasibility(lp, vstatus, z, f_t(0.0), num_tight_dual_inf); + dual_infeasibility(lp, vstatus, z, f_t(0.0), num_tight_dual_inf, work_estimate); if (tight_dual_inf > settings.dual_tol) { // No candidate is visible at the active pricing tolerance, but the // zero-tolerance residual is still material. Try tighter pricing before @@ -781,7 +843,8 @@ primal_status_t primal_phase2_with_advanced_basis( retry_dual_tol, direction, nonbasic_entering, - retry_dual_inf); + retry_dual_inf, + work_estimate); } if (retry_entering != -1) { pricing_dual_tol = retry_dual_tol; @@ -792,6 +855,7 @@ primal_status_t primal_phase2_with_advanced_basis( dual_inf = tight_dual_inf; num_dual_inf = num_tight_dual_inf; obj = compute_objective(lp, x); + work_estimate += 2*n; sol.objective = obj; sol.user_objective = compute_user_objective(lp, obj); if (!settings.inside_mip && print_summary) { @@ -807,7 +871,7 @@ primal_status_t primal_phase2_with_advanced_basis( } return primal_status_t::OPTIMAL; } else { - primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); if (primal_inf > primal_tol) { // Incremental duals may be stale relative to the current phase-I @@ -815,9 +879,9 @@ primal_status_t primal_phase2_with_advanced_basis( // successively tighter dual tolerances. settings.log.printf("Refreshing phase-I objective and duals. Num updates %d. Iter %d\n", basis_update.num_updates(), iter); - compute_phase1_objective(lp, settings, vstatus, x, objective); + compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); f_t retry_dual_tol = pricing_dual_tol; while (entering_index == -1 && retry_dual_tol > f_t(1e-10)) { retry_dual_tol *= f_t(0.1); @@ -829,7 +893,8 @@ primal_status_t primal_phase2_with_advanced_basis( retry_dual_tol, direction, nonbasic_entering, - dual_inf); + dual_inf, + work_estimate); } if (entering_index == -1) { settings.log.printf( @@ -849,10 +914,11 @@ primal_status_t primal_phase2_with_advanced_basis( "Primal phase I complete. Iterations %d. Time %.2f\n", iter, toc(start_time)); settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); obj = compute_objective(lp, x); + work_estimate += 2*n; dual_inf = - dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf); + dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf, work_estimate); iter++; // Print here: continue may hit dual-optimal Phase 2 and return before // the end-of-loop log checks switched_phase. @@ -868,20 +934,24 @@ primal_status_t primal_phase2_with_advanced_basis( } sparse_vector_t rhs_sparse(lp.A, entering_index); + work_estimate += 3*rhs_sparse.i.size(); sparse_vector_t scaled_delta_xB_sparse(m, 0); sparse_vector_t utilde_sparse(m, 0); basis_update.b_solve(rhs_sparse, scaled_delta_xB_sparse, utilde_sparse); std::vector scaled_delta_xB(m); scaled_delta_xB_sparse.to_dense(scaled_delta_xB); - + work_estimate += m + scaled_delta_xB_sparse.i.size(); + for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; delta_x[j] = -direction * scaled_delta_xB[k]; } + work_estimate += 3*m; for (i_t k = 0; k < n - m; ++k) { const i_t j = nonbasic_list[k]; delta_x[j] = 0.0; } + work_estimate += 2*(n - m); delta_x[entering_index] = direction; #ifdef CHECK_NULLSPACE @@ -907,7 +977,8 @@ primal_status_t primal_phase2_with_advanced_basis( step_length, basic_leaving, entering_index, - direction); + direction, + work_estimate); if (leaving_index == -1 && step_length >= inf) { settings.log.printf("No leaving variable. Primal unbounded?\n"); return primal_status_t::PRIMAL_UNBOUNDED; @@ -918,6 +989,7 @@ primal_status_t primal_phase2_with_advanced_basis( for (i_t j = 0; j < n; ++j) { x[j] += step_length * delta_x[j]; } + work_estimate += 2*n; #ifdef COMPUTE_RESIDUAL f_t debug_primal_residual = primal_constraint_residual(lp, x); @@ -971,9 +1043,9 @@ primal_status_t primal_phase2_with_advanced_basis( x[leaving_index] = leave_bound; if (!should_refactor) { - compute_delta_z(Arow, vstatus, delta_y, delta_z); - update_y(dual_step_length, delta_y, y); - update_z(dual_step_length, nonbasic_list, entering_index, delta_z, z); + compute_delta_z(Arow, vstatus, delta_y, delta_z, work_estimate); + update_y(dual_step_length, delta_y, y, work_estimate); + update_z(dual_step_length, nonbasic_list, entering_index, delta_z, z, work_estimate); should_refactor = basis_update.update(utilde_sparse, etilde, basic_leaving) == 1; } if (should_refactor) { @@ -985,15 +1057,16 @@ primal_status_t primal_phase2_with_advanced_basis( settings.log.printf("Failed to refactor basis. Iteration %d\n", iter); return primal_status_t::NUMERICAL; } - work_estimate = basis_update.work_estimate(); + work_estimate += basis_update.work_estimate(); + basis_update.clear_work_estimate(); recompute_duals = true; // Factor matches basic_list: rebuild x_B so Ax = b exactly. - set_primal_variables_on_bounds(lp, settings, vstatus, x); - compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); + set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); + compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x, work_estimate); } else if (rebuild_x_after_bound_snap) { // FT update already matches the new basis; recompute x_B with the leaving variable // snapped onto its bound. - compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x); + compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x, work_estimate); } } else { if (direction > 0) { @@ -1005,7 +1078,7 @@ primal_status_t primal_phase2_with_advanced_basis( } } - primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); if (primal_inf > primal_tol) { if (phase != 1) { settings.log.printf( @@ -1015,7 +1088,7 @@ primal_status_t primal_phase2_with_advanced_basis( settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); switched_phase = true; } - compute_phase1_objective(lp, settings, vstatus, x, objective); + compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); phase = 1; recompute_duals = true; } else if (phase == 1) { @@ -1031,17 +1104,18 @@ primal_status_t primal_phase2_with_advanced_basis( if (recompute_duals) { compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z); + lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); } obj = compute_objective(lp, x); + work_estimate += 2*n; dual_inf = - dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf); + dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf, work_estimate); iter++; f_t now = toc(start_time); - if (0|| (iter - start_iter) < settings.first_iteration_log || + if ((iter - start_iter) < settings.first_iteration_log || (iter % settings.iteration_log_frequency) == 0 || switched_phase) { const f_t user_obj = compute_user_objective(lp, obj); settings.log.printf("%5d %+.16e %7d %.8e %.2f\n", @@ -1052,6 +1126,9 @@ primal_status_t primal_phase2_with_advanced_basis( now); switched_phase = false; } + + work_estimate += basis_update.work_estimate(); + basis_update.clear_work_estimate(); } if (iter == iter_limit) { return primal_status_t::ITERATION_LIMIT; } @@ -1071,7 +1148,8 @@ int primal_ratio_test(const lp_problem_t& lp, double& step_length, int& basic_leaving, int entering_index, - int direction); + int direction, + double& work_estimate); template primal_status_t primal_phase2( int phase, diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index 63f4761c0e..df2c998db5 100644 --- a/cpp/src/dual_simplex/primal.hpp +++ b/cpp/src/dual_simplex/primal.hpp @@ -38,7 +38,8 @@ i_t primal_ratio_test(const lp_problem_t& lp, f_t& step_length, i_t& basic_leaving, i_t entering_index, - i_t direction); + i_t direction, + f_t& work_estimate); template primal_status_t primal_phase2_with_advanced_basis( From 6dfebbfcbf77e0a92ed784724df356bae0b424be Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 29 Jul 2026 15:10:31 -0700 Subject: [PATCH 09/34] Display work estimate and simplex iterations --- cpp/src/branch_and_bound/branch_and_bound.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 57a93eaf47..c7338f74aa 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3379,6 +3379,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple reduced_basic_list[k] = original_col_to_reduced_col[basic_list[k]]; } + f_t primal_work_estimate = 0.0; i_t iter = 0; i_t max_pump_iter = 10; simplex::random_t rng(settings_.random_seed); @@ -3416,7 +3417,6 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple bool recompute_basis = false; const i_t iter_before = iter; - f_t primal_work_estimate = 0; simplex_solver_settings_t primal_settings = settings_; primal_settings.log.log = false; simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, @@ -3458,7 +3458,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple i_t num_fractional_reduced = fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); settings_.log.printf( - "Degenerate feasibility pump (%d/%d): fractional variables %d/%d\n", pump_iter, max_pump_iter, num_fractional_reduced, num_fractional); + "Degenerate feasibility pump (%d/%d): primal work estimate %.2e, iter %d, fractional variables %d/%d\n", pump_iter, max_pump_iter, primal_work_estimate, iter, num_fractional_reduced, num_fractional); // Also treat a pass that fails to improve the best as a stall, so we perturb // the next pass even when the solve pivoted (moved) without reducing the count. stalled = stalled || (num_fractional_reduced >= best_num_fractional); From 99d7eade28398413fbe80b92a514706f1495466a Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Mon, 3 Aug 2026 17:15:38 -0700 Subject: [PATCH 10/34] Primal in crossover. Crossover tolerance mismatch fix. Pipe work estimates for root relaxation. Add initial perturbation parameter --- .../mathematical_optimization/constants.h | 1 + .../pdlp/solver_settings.hpp | 1 + cpp/src/branch_and_bound/branch_and_bound.cpp | 424 ++++++++++++------ cpp/src/branch_and_bound/branch_and_bound.hpp | 27 +- cpp/src/branch_and_bound/pseudo_costs.cpp | 6 +- cpp/src/dual_simplex/basis_updates.cpp | 29 ++ cpp/src/dual_simplex/basis_updates.hpp | 8 + cpp/src/dual_simplex/crossover.cpp | 65 ++- cpp/src/dual_simplex/phase2.cpp | 25 +- cpp/src/dual_simplex/phase2.hpp | 2 + .../dual_simplex/simplex_solver_settings.hpp | 1 + cpp/src/dual_simplex/solve.cpp | 17 +- cpp/src/dual_simplex/solve.hpp | 2 + cpp/src/math_optimization/solver_settings.cu | 1 + cpp/src/pdlp/solve.cu | 1 + 15 files changed, 448 insertions(+), 162 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 4ed3723aa2..9752b41937 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -52,6 +52,7 @@ #define CUOPT_ELIMINATE_DENSE_COLUMNS "eliminate_dense_columns" #define CUOPT_CUDSS_DETERMINISTIC "cudss_deterministic" #define CUOPT_PRESOLVE "presolve" +#define CUOPT_INITIAL_PERTURBATION "initial_perturbation" #define CUOPT_MIP_PROBING "mip_probing" #define CUOPT_DUAL_POSTSOLVE "dual_postsolve" #define CUOPT_MIP_DETERMINISM_MODE "mip_determinism_mode" diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 3bf3b6ab01..521b234b52 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -282,6 +282,7 @@ class pdlp_solver_settings_t { i_t augmented{-1}; i_t dualize{-1}; i_t ordering{-1}; + i_t initial_perturbation{-1}; i_t barrier_dual_initial_point{-1}; bool eliminate_dense_columns{true}; pdlp_precision_t pdlp_precision{pdlp_precision_t::DefaultPrecision}; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index c7338f74aa..9303159465 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -708,9 +708,18 @@ bool branch_and_bound_t::repair_solution(const std::vector& edge_ lp_settings.set_log(false); lp_settings.inside_mip = 2; std::vector leaf_edge_norms = edge_norms; + f_t repair_work_estimate = 0.0; // should probably set the cut off here lp_settings.cut_off - dual_status_t lp_status = simplex::dual_phase2( - 2, 0, lp_start_time, repair_lp, lp_settings, vstatus, lp_solution, iter, leaf_edge_norms); + dual_status_t lp_status = simplex::dual_phase2(2, + 0, + lp_start_time, + repair_lp, + lp_settings, + vstatus, + lp_solution, + iter, + repair_work_estimate, + leaf_edge_norms); repaired_solution = lp_solution.x; if (lp_status == dual_status_t::OPTIMAL) { @@ -1608,8 +1617,9 @@ dual_status_t branch_and_bound_t::solve_node_lp( feasible = apply_symmetry_reductions(node_ptr, worker, stats); if (feasible) { - i_t node_iter = 0; - f_t lp_start_time = tic(); + i_t node_iter = 0; + f_t lp_start_time = tic(); + f_t node_work_estimate = 0.0; lp_status = dual_phase2_with_advanced_basis(2, 0, @@ -1623,6 +1633,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( worker->nonbasic_list, worker->leaf_solution, node_iter, + node_work_estimate, worker->leaf_edge_norms); if (lp_status == dual_status_t::NUMERICAL) { @@ -1636,7 +1647,8 @@ dual_status_t branch_and_bound_t::solve_node_lp( worker->basic_list, worker->nonbasic_list, worker->leaf_vstatus, - worker->leaf_edge_norms); + worker->leaf_edge_norms, + node_work_estimate); lp_status = convert_lp_status_to_dual_status(second_status); } @@ -2796,7 +2808,8 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( basis_update_mpf_t& basis_update, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& edge_norms) + std::vector& edge_norms, + f_t& work_estimate) { lp_status_t root_status; @@ -2812,6 +2825,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( nonbasic_list, root_vstatus_, edge_norms_, + work_estimate, nullptr); } @@ -3108,6 +3122,7 @@ auto branch_and_bound_t::do_cut_pass( bool initialize_basis = false; lp_settings.concurrent_halt = NULL; f_t dual_phase2_start_time = tic(); + f_t cut_work_estimate = 0.0; dual_status_t cut_status = dual_phase2_with_advanced_basis(2, 0, initialize_basis, @@ -3120,6 +3135,7 @@ auto branch_and_bound_t::do_cut_pass( nonbasic_list, root_relax_soln_, iter, + cut_work_estimate, edge_norms_); exploration_stats_.total_simplex_iters += iter; f_t dual_phase2_time = toc(dual_phase2_start_time); @@ -3143,7 +3159,8 @@ auto branch_and_bound_t::do_cut_pass( basic_list, nonbasic_list, root_vstatus_, - edge_norms_); + edge_norms_, + cut_work_estimate); if (scratch_status == lp_status_t::OPTIMAL) { // We recovered cut_status = convert_lp_status_to_dual_status(scratch_status); @@ -3171,7 +3188,7 @@ auto branch_and_bound_t::do_cut_pass( basis_update, num_fractional, fractional); - + dual_degenerate_feasibility_pump(original_lp_, basic_list, nonbasic_list, @@ -3275,13 +3292,14 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple i_t& num_fractional, std::vector& fractional) { + f_t dual_degenerate_feasibility_pump_start_time = tic(); std::vector zero_reduced_costs_vars; std::vector zero_reduced_costs_vars_nonbasic_index; bool dual_degenerate = check_for_dual_degeneracy(soln, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); if (!dual_degenerate) { return; } - // Construct a new LP problem - // minimize p^T x + // Construct a new LP problem + // minimize p^T x // subject to B x_B + N_z x_z = b - N x_N // l_B <= x_B <= u_B // l_z <= x_z <= u_z @@ -3339,7 +3357,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple lp_reduced.rhs = b_reduced; lp_reduced.obj_scale = 1.0; - + settings_.log.printf("Constructed dual degenerate feasibility pump LP with %d rows and %d columns\n", m, n); @@ -3354,7 +3372,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple reduced_vstatus[reduced_col++] = variable_status_t::BASIC; } else if (std::abs(soln.z[j]) <= 1e-10) { reduced_nonbasic_list[num_nonbasic++] = reduced_col; // Does ordering of nonbasic variables matter? - reduced_vstatus[reduced_col++] = vstatus[j]; + reduced_vstatus[reduced_col++] = vstatus[j]; } } @@ -3458,7 +3476,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple i_t num_fractional_reduced = fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); settings_.log.printf( - "Degenerate feasibility pump (%d/%d): primal work estimate %.2e, iter %d, fractional variables %d/%d\n", pump_iter, max_pump_iter, primal_work_estimate, iter, num_fractional_reduced, num_fractional); + "Degenerate feasibility pump (%d/%d): primal work estimate %.2e, iter %d, fractional variables %d/%d. Time %.2f\n", pump_iter, max_pump_iter, primal_work_estimate, iter, num_fractional_reduced, num_fractional, toc(dual_degenerate_feasibility_pump_start_time)); // Also treat a pass that fails to improve the best as a stall, so we perturb // the next pass even when the solve pivoted (moved) without reducing the count. stalled = stalled || (num_fractional_reduced >= best_num_fractional); @@ -3469,7 +3487,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple } } - settings_.log.printf("Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables %d/%d\n", iter, best_num_fractional, num_fractional); + settings_.log.printf("Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables %d/%d. Time %.2f\n", iter, best_num_fractional, num_fractional, toc(dual_degenerate_feasibility_pump_start_time)); if (best_num_fractional < num_fractional) { // Translate the vstatus from the reduced problem to the vstatus for the original problem i_t reduced_cols = 0; @@ -3538,13 +3556,120 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple for (i_t k = 0; k < lp.num_rows; k++) { soln.x[basic_list[k]] = xB[k]; } - + fractional.clear(); num_fractional = fractional_variables(settings_, soln.x, var_types_, fractional); } } + +template +void branch_and_bound_t::apply_delta_x_for_integer_pivot( + const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + i_t entering_index, + i_t nonbasic_entering, + i_t direction, + std::vector& delta_x, + const sparse_vector_t& utilde_sparse, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate) +{ + f_t step_length; + i_t basic_leaving; + const i_t leaving_index = simplex::primal_ratio_test(lp, + settings_, + vstatus, + basic_list, + solution.x, + delta_x, + step_length, + basic_leaving, + entering_index, + direction, + work_estimate); + bool binding_integer = + leaving_index != -1 && + is_fractional(solution.x[leaving_index], var_types_[leaving_index], settings_.integer_tol); + if (!binding_integer) { return; } + + std::vector test_x = solution.x; + i_t integer_destroyed = 0; + for (i_t h = 0; h < lp.num_cols; ++h) { + test_x[h] += step_length * delta_x[h]; + if (var_types_[h] != variable_type_t::INTEGER) { continue; } + const bool was_fractional = + is_fractional(solution.x[h], var_types_[h], settings_.integer_tol); + const bool now_fractional = + is_fractional(test_x[h], var_types_[h], settings_.integer_tol); + if (now_fractional && !was_fractional) { + integer_destroyed++; + } else if (!now_fractional && was_fractional) { + integer_destroyed--; + } + } + // Require a strict net decrease in fractional integers. + if (integer_destroyed >= 0) { return; } + + solution.x = test_x; + basic_list[basic_leaving] = entering_index; + nonbasic_list[nonbasic_entering] = leaving_index; + vstatus[entering_index] = variable_status_t::BASIC; + if (std::abs(lp.upper[leaving_index] - lp.lower[leaving_index]) < 1e-12) { + vstatus[leaving_index] = variable_status_t::NONBASIC_FIXED; + } else if (delta_x[leaving_index] < 0) { + vstatus[leaving_index] = variable_status_t::NONBASIC_LOWER; + } else { + vstatus[leaving_index] = variable_status_t::NONBASIC_UPPER; + } + + // Keep nonbasic_index consistent with nonbasic_list: entering_index is now basic, + // and leaving_index has taken its slot in nonbasic_list. + nonbasic_index[entering_index] = -1; + nonbasic_index[leaving_index] = nonbasic_entering; + + const i_t m = lp.num_rows; + sparse_vector_t es_sparse(m, 1); + es_sparse.i[0] = basic_leaving; + es_sparse.x[0] = 1.0; + sparse_vector_t UTsol_sparse(m, 1); + sparse_vector_t solution_sparse(m, 1); + basis_update.b_transpose_solve(es_sparse, solution_sparse, UTsol_sparse); + const i_t recommend_refactor = + basis_update.update(utilde_sparse, UTsol_sparse, basic_leaving); + if (recommend_refactor == 1) { + csc_matrix_t L(m, m, 1); + csc_matrix_t U(m, m, 1); + std::vector pinv(m); + std::vector p(m); + std::vector q(m); + std::vector deficient; + std::vector slacks_needed; + f_t factorize_work_estimate = 0.0; + const i_t rank = factorize_basis(lp.A, + settings_, + basic_list, + exploration_stats_.start_time, + L, + U, + p, + pinv, + q, + deficient, + slacks_needed, + factorize_work_estimate); + if (rank == CONCURRENT_HALT_RETURN || rank == TIME_LIMIT_RETURN) { return; } + if (rank < 0 || rank != lp.num_rows) { return; } + simplex::reorder_basic_list(q, basic_list); + basis_update.reset(L, U, p); + } +} + template void branch_and_bound_t::pivot_out_integer_variables( const simplex::lp_problem_t& lp, @@ -3556,12 +3681,12 @@ void branch_and_bound_t::pivot_out_integer_variables( i_t& num_fractional, std::vector& fractional) { - + f_t pivot_out_integer_variables_start_time = tic(); std::vector zero_reduced_costs_vars; std::vector zero_reduced_costs_vars_nonbasic_index; bool dual_degenerate = check_for_dual_degeneracy(solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); if (!dual_degenerate) { return; } - + lp_solution_t soln_copy = solution; std::vector basic_list_copy = basic_list; std::vector nonbasic_list_copy = nonbasic_list; @@ -3571,7 +3696,7 @@ void branch_and_bound_t::pivot_out_integer_variables( const i_t start_num_fractional = num_fractional; const i_t num_zero_reduced_costs_vars = zero_reduced_costs_vars.size(); - + std::vector row_to_slack(lp.num_rows, -1); for (i_t j : new_slacks_) { if (lp.lower[j] != 0 || lp.upper[j] != inf) { continue; } @@ -3579,18 +3704,21 @@ void branch_and_bound_t::pivot_out_integer_variables( row_to_slack[lp.A.i[p]] = j; } + f_t work_estimate = 0.0; + std::vector fast_candidates; std::vector fast_rows; + std::vector fast_nonbasic_slacks; for (i_t j : fractional) { - const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; - const i_t num_rows = col_end - col_start; - i_t num_basic_slacks = 0; + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + const i_t num_rows = col_end - col_start; + i_t num_basic_slacks = 0; i_t num_nonbasic_slacks_with_reduced_cost_zero = 0; - i_t nonbasic_slack = -1; - i_t slack_row = -1; + i_t nonbasic_slack = -1; + i_t slack_row = -1; for (i_t p = col_start; p < col_end; p++) { - const i_t i = lp.A.i[p]; + const i_t i = lp.A.i[p]; const i_t slack = row_to_slack[i]; if (slack >= 0) { if (vstatus_copy[slack] == variable_status_t::BASIC) { @@ -3598,28 +3726,43 @@ void branch_and_bound_t::pivot_out_integer_variables( } else if (std::abs(solution.z[slack]) <= 1e-10) { num_nonbasic_slacks_with_reduced_cost_zero++; nonbasic_slack = slack; - slack_row = i; + slack_row = i; } } } if (num_basic_slacks == num_rows - 1 && num_nonbasic_slacks_with_reduced_cost_zero == 1) { fast_candidates.push_back(j); fast_rows.push_back(slack_row); + fast_nonbasic_slacks.push_back(nonbasic_slack); } } if (fast_candidates.size() > 0) { - settings_.log.printf("Found %ld fast candidates for pivot out integer variables\n", fast_candidates.size()); + settings_.log.printf("Found %ld fast candidates for pivot out integer variables\n", + fast_candidates.size()); + } + + // Build a reverse index nonbasic_index[v] = position of v in nonbasic_list_copy, or -1 if not + // present. Used to locate the entering variable's slot in the fast-candidate path. + // apply_delta_x_for_integer_pivot keeps this index consistent by applying an O(1) fix-up + // on each successful pivot; the two variables whose (non)basic status changes are the only + // entries that need to be updated. + std::vector nonbasic_index(lp.num_cols, -1); + for (i_t p = 0; p < static_cast(nonbasic_list_copy.size()); ++p) { + nonbasic_index[nonbasic_list_copy[p]] = p; } const i_t num_candidates = fast_candidates.size(); for (i_t k = 0; k < num_candidates; k++) { - const i_t j = fast_candidates[k]; - const i_t row = fast_rows[k]; + const i_t j = fast_candidates[k]; + const i_t row = fast_rows[k]; + const i_t nonbasic_slack = fast_nonbasic_slacks[k]; + // Skip if state changed by a prior successful pivot. + if (vstatus_copy[j] != variable_status_t::BASIC) { continue; } + if (vstatus_copy[nonbasic_slack] == variable_status_t::BASIC) { continue; } const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; - const i_t num_rows = col_end - col_start; - f_t a_ij = 0.0; + const i_t col_end = lp.A.col_start[j + 1]; + f_t a_ij = 0.0; for (i_t p = col_start; p < col_end; p++) { const i_t i = lp.A.i[p]; if (i == row) { @@ -3632,40 +3775,90 @@ void branch_and_bound_t::pivot_out_integer_variables( f_t bound = a_ij > 0 ? lp.lower[j] : lp.upper[j]; if (std::abs(bound) == inf) { continue; } - sparse_vector_t delta_x; - delta_x.n = lp.num_cols; - delta_x.i.reserve(num_rows + 1); - delta_x.x.reserve(num_rows + 1); - const f_t delta_xj = bound - solution.x[j]; - delta_x.i.push_back(j); - delta_x.x.push_back(delta_xj); + // Build delta_x describing "move x[j] to its bound, let the basic slacks compensate to + // keep A*x = b". This is a feasible direction (A*delta_x = 0). The nonzero pattern lives + // on the entries of column A(:, j) plus j itself. In the nonbasic_slack slot, + // delta_x[nonbasic_slack] = -delta_xj * a_ij > 0, i.e. the entering slack moves up from + // its lower bound 0. We build the sparse version to feed the feasibility scan, then + // scatter into a dense vector and normalize so that delta_x[nonbasic_slack] == 1 (the + // convention primal_ratio_test expects for entering variables). + sparse_vector_t delta_x_sparse; + delta_x_sparse.n = lp.num_cols; + delta_x_sparse.i.reserve(col_end - col_start + 1); + delta_x_sparse.x.reserve(col_end - col_start + 1); + const f_t delta_xj = bound - soln_copy.x[j]; + delta_x_sparse.i.push_back(j); + delta_x_sparse.x.push_back(delta_xj); for (i_t p = col_start; p < col_end; p++) { - const i_t r = lp.A.i[p]; - const f_t a_rj = lp.A.x[p]; + const i_t r = lp.A.i[p]; + const f_t a_rj = lp.A.x[p]; const f_t delta_slack_r = -delta_xj * a_rj; - delta_x.i.push_back(row_to_slack[r]); - delta_x.x.push_back(delta_slack_r); + delta_x_sparse.i.push_back(row_to_slack[r]); + delta_x_sparse.x.push_back(delta_slack_r); } - bool ok = true; - const i_t ndx = delta_x.i.size(); + // Reject if the full unit step would drive any basic slack below zero. + bool ok = true; + const i_t ndx = delta_x_sparse.i.size(); for (i_t h = 0; h < ndx; h++) { - const i_t jj = delta_x.i[h]; + const i_t jj = delta_x_sparse.i[h]; if (jj == j) continue; - const f_t val = delta_x.x[h]; - const f_t slack_value = solution.x[jj]; + const f_t val = delta_x_sparse.x[h]; + const f_t slack_value = soln_copy.x[jj]; if (val < -slack_value) { ok = false; break; } } + if (!ok) { continue; } - if (ok) { - std::vector delta_x_dense(lp.num_cols, 0.0); - delta_x.to_dense(delta_x_dense); - std::vector residual(lp.num_rows); - matrix_vector_multiply(lp.A, 1.0, delta_x_dense, 0.0, residual); - settings_.log.printf("Fast candidate ok || A*delta_x ||_inf = %e\n", vector_norm_inf(residual)); + std::vector delta_x(lp.num_cols, 0.0); + delta_x_sparse.to_dense(delta_x); + + // Normalize so that delta_x[nonbasic_slack] == 1 (the standard entering-direction + // convention). Also confirms A*delta_x = 0 at debug log time. + const f_t scale = delta_x[nonbasic_slack]; + if (!(std::abs(scale) > 1e-12)) { continue; } + for (i_t h = 0; h < lp.num_cols; ++h) { delta_x[h] /= scale; } + + // Entering variable is the nonbasic slack, moving up from its lower bound 0. + const i_t entering_index = nonbasic_slack; + const i_t nonbasic_entering = nonbasic_index[nonbasic_slack]; + if (nonbasic_entering < 0) { continue; } + const i_t direction = 1; + + // Recover B^{-1} * abar from the full-vector delta_x. In our sign convention, + // delta_x[basic_list[h]] = -direction * (B^{-1} abar)[h], so + // (B^{-1} abar)[h] = -direction * delta_x[basic_list[h]]. + // Then utilde = L^{-1} P abar = U * (B^{-1} abar). In MPF, U == U0 (rank-1 updates all + // live in L), so u_multiply is a single sparse matvec against U0. + std::vector b_inv_abar(lp.num_rows); + for (i_t h = 0; h < lp.num_rows; ++h) { + b_inv_abar[h] = -direction * delta_x[basic_list_copy[h]]; + } + std::vector utilde_dense; + basis_update_copy.u_multiply(b_inv_abar, utilde_dense); + sparse_vector_t utilde_sparse; + utilde_sparse.from_dense(utilde_dense); + + apply_delta_x_for_integer_pivot(lp, + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + entering_index, + nonbasic_entering, + direction, + delta_x, + utilde_sparse, + soln_copy, + basis_update_copy, + work_estimate); + // apply_delta_x_for_integer_pivot only mutates vstatus when the pivot actually fires, + // so entering_index transitioning to BASIC is a reliable success signal. + if (vstatus_copy[entering_index] == variable_status_t::BASIC) { + settings_.log.printf( + "Fast candidate pivot succeeded: j=%d entering slack=%d row=%d\n", j, entering_index, row); } } @@ -3681,7 +3874,11 @@ void branch_and_bound_t::pivot_out_integer_variables( : -1; const i_t entering_index = j; const i_t nonbasic_entering = zero_reduced_costs_vars_nonbasic_index[k]; - if (nonbasic_list_copy[nonbasic_entering] != j) { continue; } + if (nonbasic_entering < 0 || + nonbasic_entering >= static_cast(nonbasic_list_copy.size()) || + nonbasic_list_copy[nonbasic_entering] != j) { + continue; + } // Solve B * dxB = A(:, j) so utilde is valid for the MPF update. // Apply direction when forming delta_x (same convention as primal_phase2). @@ -3698,90 +3895,19 @@ void branch_and_bound_t::pivot_out_integer_variables( } delta_x[j] = direction; - f_t step_length; - i_t basic_leaving; - f_t work_estimate = 0.0; - const i_t leaving_index = simplex::primal_ratio_test(lp, - settings_, - vstatus_copy, - basic_list_copy, - soln_copy.x, - delta_x, - step_length, - basic_leaving, - entering_index, - direction, - work_estimate); - bool binding_integer = - leaving_index != -1 && - is_fractional(soln_copy.x[leaving_index], var_types_[leaving_index], settings_.integer_tol); - if (!binding_integer) { continue; } - - std::vector test_x = soln_copy.x; - i_t integer_destroyed = 0; - for (i_t h = 0; h < lp.num_cols; ++h) { - test_x[h] += step_length * delta_x[h]; - if (var_types_[h] != variable_type_t::INTEGER) { continue; } - const bool was_fractional = - is_fractional(soln_copy.x[h], var_types_[h], settings_.integer_tol); - const bool now_fractional = - is_fractional(test_x[h], var_types_[h], settings_.integer_tol); - if (now_fractional && !was_fractional) { - integer_destroyed++; - } else if (!now_fractional && was_fractional) { - integer_destroyed--; - } - } - // Require a strict net decrease in fractional integers. - if (integer_destroyed >= 0) { continue; } - - soln_copy.x = test_x; - basic_list_copy[basic_leaving] = entering_index; - nonbasic_list_copy[nonbasic_entering] = leaving_index; - vstatus_copy[entering_index] = variable_status_t::BASIC; - if (std::abs(lp.upper[leaving_index] - lp.lower[leaving_index]) < 1e-12) { - vstatus_copy[leaving_index] = variable_status_t::NONBASIC_FIXED; - } else if (delta_x[leaving_index] < 0) { - vstatus_copy[leaving_index] = variable_status_t::NONBASIC_LOWER; - } else { - vstatus_copy[leaving_index] = variable_status_t::NONBASIC_UPPER; - } - - const i_t m = lp.num_rows; - sparse_vector_t es_sparse(m, 1); - es_sparse.i[0] = basic_leaving; - es_sparse.x[0] = 1.0; - sparse_vector_t UTsol_sparse(m, 1); - sparse_vector_t solution_sparse(m, 1); - basis_update_copy.b_transpose_solve(es_sparse, solution_sparse, UTsol_sparse); - const i_t recommend_refactor = - basis_update_copy.update(utilde_sparse, UTsol_sparse, basic_leaving); - if (recommend_refactor == 1) { - csc_matrix_t L(m, m, 1); - csc_matrix_t U(m, m, 1); - std::vector pinv(m); - std::vector p(m); - std::vector q(m); - std::vector deficient; - std::vector slacks_needed; - f_t factorize_work_estimate = 0.0; - const i_t rank = factorize_basis(lp.A, - settings_, - basic_list_copy, - exploration_stats_.start_time, - L, - U, - p, - pinv, - q, - deficient, - slacks_needed, - factorize_work_estimate); - if (rank == CONCURRENT_HALT_RETURN || rank == TIME_LIMIT_RETURN) { return; } - if (rank < 0 || rank != lp.num_rows) { return; } - simplex::reorder_basic_list(q, basic_list_copy); - basis_update_copy.reset(L, U, p); - } + apply_delta_x_for_integer_pivot(lp, + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + entering_index, + nonbasic_entering, + direction, + delta_x, + utilde_sparse, + soln_copy, + basis_update_copy, + work_estimate); } std::vector new_fractional; @@ -3790,7 +3916,7 @@ void branch_and_bound_t::pivot_out_integer_variables( if (num_new_fractional < start_num_fractional) { i_t num_integer_increased = start_num_fractional - num_new_fractional; integer_pivots_.fetch_add(num_integer_increased, std::memory_order_release); - settings_.log.printf("Pivoted out %d integer variables: %d -> %d\n", num_integer_increased, start_num_fractional, num_new_fractional); + settings_.log.printf("Pivoted out %d integer variables: %d -> %d in %.2f\n", num_integer_increased, start_num_fractional, num_new_fractional, toc(pivot_out_integer_variables_start_time)); num_fractional = num_new_fractional; fractional = new_fractional; basic_list = basic_list_copy; @@ -3883,7 +4009,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solving_root_relaxation_ = true; f_t root_relax_start_time = tic(); - + f_t root_relax_work_estimate = 0.0; if (!enable_concurrent_lp_root_solve()) { // RINS/SUBMIP path settings_.log.printf("\n"); @@ -3896,7 +4022,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basic_list, nonbasic_list, root_vstatus_, - edge_norms_); + edge_norms_, + root_relax_work_estimate); root_relax_solved_by = DualSimplex; exploration_stats_.total_simplex_iters = root_relax_soln_.iterations; @@ -3909,12 +4036,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, basic_list, nonbasic_list, - edge_norms_); + edge_norms_, + root_relax_work_estimate); } solving_root_relaxation_ = false; f_t root_relax_elapsed_time = toc(root_relax_start_time); exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; + i_t root_iterations = exploration_stats_.total_simplex_iters; + if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("\nThe root LP relaxation is infeasible\n", @@ -3968,6 +4098,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.iterations, root_relax_elapsed_time, method_to_string(root_relax_solved_by)); + settings_.log.printf("Dual simplex iteration %d work estimate %.2e work per second %.2e\n", root_iterations, root_relax_work_estimate, root_relax_work_estimate / root_relax_elapsed_time); settings_.log.printf("Root relaxation objective %+.8e\n\n", root_relax_soln_.user_objective); assert(root_vstatus_.size() == original_lp_.num_cols); @@ -4917,7 +5048,7 @@ node_status_t branch_and_bound_t::solve_node_deterministic( i_t node_iter = 0; f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; - + f_t dual_work_estimate = 0.0; dual_status_t lp_status = dual_phase2_with_advanced_basis(2, 0, worker.recompute_bounds_and_basis, @@ -4930,6 +5061,7 @@ node_status_t branch_and_bound_t::solve_node_deterministic( worker.nonbasic_list, worker.leaf_solution, node_iter, + dual_work_estimate, leaf_edge_norms, &worker.work_context); @@ -4945,6 +5077,7 @@ node_status_t branch_and_bound_t::solve_node_deterministic( worker.nonbasic_list, worker.leaf_vstatus, leaf_edge_norms, + dual_work_estimate, &worker.work_context); lp_status = convert_lp_status_to_dual_status(second_status); } @@ -5530,6 +5663,7 @@ void branch_and_bound_t::deterministic_dive( worker.leaf_solution.resize(worker.leaf_problem.num_rows, worker.leaf_problem.num_cols); i_t node_iter = 0; f_t lp_start_time = tic(); + f_t dual_work_estimate = 0.0; std::vector leaf_edge_norms = edge_norms_; decompress_vstatus(node_ptr->packed_vstatus, worker.leaf_problem.num_cols, worker.leaf_vstatus); @@ -5545,6 +5679,7 @@ void branch_and_bound_t::deterministic_dive( worker.nonbasic_list, worker.leaf_solution, node_iter, + dual_work_estimate, leaf_edge_norms, &worker.work_context); @@ -5558,6 +5693,7 @@ void branch_and_bound_t::deterministic_dive( worker.nonbasic_list, worker.leaf_vstatus, leaf_edge_norms, + dual_work_estimate, &worker.work_context); lp_status = convert_lp_status_to_dual_status(second_status); } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index ed4af6d6bc..c0ff1761a2 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -173,7 +173,8 @@ class branch_and_bound_t { simplex::basis_update_mpf_t& basis_update, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& edge_norms); + std::vector& edge_norms, + f_t& work_estimate); i_t find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, @@ -347,6 +348,7 @@ class branch_and_bound_t { std::vector& zero_reduced_costs_vars, std::vector& zero_reduced_costs_vars_nonbasic_index); + void pivot_out_integer_variables(const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, @@ -356,6 +358,29 @@ class branch_and_bound_t { i_t& num_fractional, std::vector& fractional); + // Try to pivot the nonbasic variable `entering_index` (currently at position + // `nonbasic_entering` in `nonbasic_list`) into the basis along the direction + // `delta_x`, with `utilde` = U * B^{-1} A(:, entering_index) satisfying + // L * utilde = P * A(:, entering_index). Applies the pivot only if it yields a + // strict net decrease in the number of fractional integer variables. On success, + // mutates basic_list/nonbasic_list/vstatus/solution/basis_update in place and applies + // an O(1) fix-up to `nonbasic_index` for the two variables whose (non)basic status + // changed. On skip, leaves all outputs untouched. + void apply_delta_x_for_integer_pivot( + const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + i_t entering_index, + i_t nonbasic_entering, + i_t direction, + std::vector& delta_x, + const sparse_vector_t& utilde_sparse, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate); + void dual_degenerate_feasibility_pump(const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, diff --git a/cpp/src/branch_and_bound/pseudo_costs.cpp b/cpp/src/branch_and_bound/pseudo_costs.cpp index cdba90f219..eaa60cf475 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.cpp +++ b/cpp/src/branch_and_bound/pseudo_costs.cpp @@ -370,6 +370,7 @@ void strong_branch_helper(i_t start, i_t iter = 0; std::vector vstatus = root_vstatus; std::vector child_edge_norms = edge_norms; + f_t child_work_estimate = 0.0; dual_status_t status = simplex::dual_phase2(2, 0, lp_start_time, @@ -378,6 +379,7 @@ void strong_branch_helper(i_t start, vstatus, solution, iter, + child_work_estimate, child_edge_norms); f_t obj = std::numeric_limits::quiet_NaN(); @@ -506,7 +508,8 @@ std::pair trial_branching(const lp_problem_t& orig // Only refactor the basis if we encounter numerical issues. child_basis_factors.set_refactor_frequency(iter_limit); - dual_status_t status = simplex::dual_phase2_with_advanced_basis(2, + f_t child_work_estimate = 0.0; + dual_status_t status = simplex::dual_phase2_with_advanced_basis(2, 0, initialize_basis, start_time, @@ -518,6 +521,7 @@ std::pair trial_branching(const lp_problem_t& orig child_nonbasic_list, solution, iter, + child_work_estimate, child_edge_norms); settings.log.debug("Trial branching on variable %d. Lo: %e Up: %e. Iter %d. Status %s. Obj %e\n", diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index f81962d054..1081cc4773 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2009,6 +2009,35 @@ i_t basis_update_mpf_t::u_solve(sparse_vector_t& rhs) const return 0; } + +// Compute y = U*x. In the MPF factorization, the rank-1 update factors are absorbed into L, so +// U == U0 and U*x reduces to a sparse matvec against U0. +template +void basis_update_mpf_t::u_multiply(const std::vector& x, + std::vector& y) const +{ + const i_t m = L0_.m; + y.assign(m, 0.0); + matrix_vector_multiply(U0_, f_t(1.0), x, f_t(0.0), y); + work_estimate_ += 2 * U0_.col_start[U0_.n]; +} + +// Sparse-in/sparse-out overload of u_multiply. Same semantics as the dense version. +template +void basis_update_mpf_t::u_multiply(const sparse_vector_t& x, + sparse_vector_t& y) const +{ + const i_t m = L0_.m; + // Scatter x into a dense workspace, compute U0 * x, gather back to sparse. + std::vector x_dense; + x.to_dense(x_dense); + std::vector y_dense(m, 0.0); + matrix_vector_multiply(U0_, f_t(1.0), x_dense, f_t(0.0), y_dense); + work_estimate_ += 2 * U0_.col_start[U0_.n]; + y.from_dense(y_dense); + work_estimate_ += m; +} + // Solve for x such that L*x = y template i_t basis_update_mpf_t::l_solve(std::vector& rhs) const diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index d1c623db55..bdedcc4a18 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -353,6 +353,14 @@ class basis_update_mpf_t { // Solve for x such that U'*x = y i_t u_transpose_solve(sparse_vector_t& rhs) const; + // Compute y = U*x. In the MPF factorization the rank-1 update factors are absorbed into L, so + // U is unchanged from the initial factorization (U == U0), and U*x is just a sparse matvec + // against U0. + void u_multiply(const std::vector& x, std::vector& y) const; + + // Sparse-in/sparse-out overload of u_multiply. + void u_multiply(const sparse_vector_t& x, sparse_vector_t& y) const; + // Replace the column B(:, leaving_index) with the vector abar. Pass in utilde such that L*utilde // = abar i_t update(const std::vector& utilde, const std::vector& etilde, i_t leaving_index); diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index e1ba272adf..5b0dd451e3 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -168,9 +168,10 @@ f_t primal_infeasibility(const lp_problem_t& lp, f_t primal_inf = 0; constexpr bool verbose = false; constexpr f_t infeas_tol = 1e-3; + const f_t primal_tol = settings.primal_tol; for (i_t j = 0; j < n; ++j) { - if (x[j] < lp.lower[j]) { - // x_j < l_j => -x_j > -l_j => -x_j + l_j > 0 + if (x[j] < lp.lower[j] - primal_tol) { + // x_j < l_j - tol => violation exceeds per-variable threshold const f_t infeas = -x[j] + lp.lower[j]; primal_inf += infeas; if (verbose && infeas > infeas_tol) { @@ -183,8 +184,8 @@ f_t primal_infeasibility(const lp_problem_t& lp, vstatus[j]); } } - if (x[j] > lp.upper[j]) { - // x_j > u_j => x_j - u_j > 0 + if (x[j] > lp.upper[j] + primal_tol) { + // x_j > u_j + tol => violation exceeds per-variable threshold const f_t infeas = x[j] - lp.upper[j]; primal_inf += infeas; if (verbose && infeas > infeas_tol) { @@ -1423,8 +1424,11 @@ crossover_status_t crossover(const lp_problem_t& lp, } else if (dual_feasible && !primal_feasible) { i_t dual_iter = 0; std::vector edge_norms; + f_t work_estimate = 0.0; + simplex_solver_settings_t dual_settings = settings; + dual_settings.iteration_limit = std::numeric_limits::max(); dual_status_t status = - dual_phase2(2, 0, start_time, lp, settings, vstatus, solution, dual_iter, edge_norms); + dual_phase2(2, 0, start_time, lp, dual_settings, vstatus, solution, dual_iter, work_estimate, edge_norms); if (toc(start_time) > settings.time_limit) { settings.log.printf("Time limit exceeded\n"); return crossover_status_t::TIME_LIMIT; @@ -1443,7 +1447,32 @@ crossover_status_t crossover(const lp_problem_t& lp, solution.iterations += dual_iter; primal_feasible = primal_infeas <= primal_tol && primal_res <= primal_tol; dual_feasible = dual_infeas <= dual_tol && dual_res <= dual_tol; + } else if (primal_feasible && !dual_feasible) { + i_t primal_iter = 0; + simplex_solver_settings_t primal_settings = settings; + primal_settings.iteration_limit = std::numeric_limits::max(); + primal_status_t primal_status = primal_phase2(2, start_time, lp, primal_settings, vstatus, solution, primal_iter); + if (toc(start_time) > settings.time_limit) { + settings.log.printf("Time limit exceeded\n"); + return crossover_status_t::TIME_LIMIT; + } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { + if (!settings.inside_mip) { settings.log.printf("Concurrent halt\n"); } + return crossover_status_t::CONCURRENT_LIMIT; + } + primal_infeas = primal_infeasibility(lp, settings, vstatus, solution.x); + dual_infeas = dual_infeasibility(lp, settings, vstatus, solution.z); + primal_res = primal_residual(lp, solution); + dual_res = dual_residual(lp, solution); + if (primal_status != primal_status_t::OPTIMAL) { + print_crossover_info(lp, settings, vstatus, solution, "Primal phase 2 complete"); + } + solution.iterations += primal_iter; + primal_feasible = primal_infeas <= primal_tol && primal_res <= primal_tol; + dual_feasible = dual_infeas <= dual_tol && dual_res <= dual_tol; } else { + simplex_solver_settings_t dual_settings = settings; + dual_settings.iteration_limit = std::numeric_limits::max(); lp_problem_t phase1_problem(lp.handle_ptr, 1, 1, 1); create_phase1_problem(lp, phase1_problem); std::vector phase1_vstatus(n); @@ -1469,8 +1498,17 @@ crossover_status_t crossover(const lp_problem_t& lp, i_t iter = 0; lp_solution_t phase1_solution(phase1_problem.num_rows, phase1_problem.num_cols); std::vector junk; - dual_status_t phase1_status = dual_phase2( - 1, 1, start_time, phase1_problem, settings, phase1_vstatus, phase1_solution, iter, junk); + f_t phase1_work_estimate = 0.0; + dual_status_t phase1_status = dual_phase2(1, + 1, + start_time, + phase1_problem, + dual_settings, + phase1_vstatus, + phase1_solution, + iter, + phase1_work_estimate, + junk); if (phase1_status == dual_status_t::NUMERICAL || phase1_status == dual_status_t::DUAL_UNBOUNDED) { settings.log.printf("Failed in Phase 1\n"); @@ -1585,8 +1623,17 @@ crossover_status_t crossover(const lp_problem_t& lp, dual_status_t status = dual_status_t::NUMERICAL; if (dual_infeas <= settings.dual_tol) { std::vector edge_norms; - status = dual_phase2( - 2, iter == 0 ? 1 : 0, start_time, lp, settings, vstatus, solution, iter, edge_norms); + f_t phase2_work_estimate = 0.0; + status = dual_phase2(2, + iter == 0 ? 1 : 0, + start_time, + lp, + dual_settings, + vstatus, + solution, + iter, + phase2_work_estimate, + edge_norms); if (toc(start_time) > settings.time_limit) { settings.log.printf("Time limit exceeded\n"); return crossover_status_t::TIME_LIMIT; diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index d3867086f3..d803dab930 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -470,7 +470,7 @@ void initial_perturbation(const lp_problem_t& lp, f_t sum_perturb = 0.0; i_t num_perturb = 0; - random_t random(settings.seed); + random_t random(settings.random_seed); for (i_t j = 0; j < n; ++j) { f_t obj = objective[j] = lp.objective[j]; @@ -2340,6 +2340,7 @@ void prepare_optimality(i_t info, int phase, f_t start_time, f_t max_val, + f_t& work_estimate, i_t& iter, const std::vector& x, std::vector& y, @@ -2348,7 +2349,6 @@ void prepare_optimality(i_t info, { const i_t m = lp.num_rows; const i_t n = lp.num_cols; - f_t work_estimate = 0; // Work in this function is not captured sol.objective = compute_objective(lp, sol.x); sol.user_objective = compute_user_objective(lp, sol.objective); @@ -2373,6 +2373,9 @@ void prepare_optimality(i_t info, settings.log.printf("Unperturbed dual infeasibility: %.2e\n", dual_infeas); settings.log.printf("Objective: %+.16e\n", sol.user_objective); settings.log.printf("Num updates: %d\n", ft.num_updates()); + settings.log.printf("Iterations: %d\n", iter); + + i_t dual_iter = iter; // Primal pivots in place, so keep the perturbed solution to fall back on. // The factor is snapshot rather than refactorized on failure: the copy is @@ -2405,7 +2408,7 @@ void prepare_optimality(i_t info, false); if (primal_status == primal_status_t::OPTIMAL) { // z now prices the original objective, so no perturbation remains. - settings.log.printf("Primal cleanup successful.\n"); + settings.log.printf("Primal cleanup successful. Iterations %d\n", iter - dual_iter); perturbation = 0.0; sol.objective = compute_objective(lp, sol.x); sol.user_objective = compute_user_objective(lp, sol.objective); @@ -2433,6 +2436,9 @@ void prepare_optimality(i_t info, settings.log.printf("Dual phase I complete. Iterations %d. Time %.2f\n", iter, toc(start_time)); } if (phase == 2) { + if (settings.inside_mip == 0 || settings.inside_mip == 1) { + settings.log.printf("Work estimate: %.2e\n", work_estimate); + } if (!settings.inside_mip) { settings.log.printf("\n"); settings.log.printf( @@ -2557,6 +2563,7 @@ dual_status_t dual_phase2(i_t phase, std::vector& vstatus, lp_solution_t& sol, i_t& iter, + f_t& work_estimate, std::vector& delta_y_steepest_edge, work_limit_context_t* work_unit_context) { @@ -2579,6 +2586,7 @@ dual_status_t dual_phase2(i_t phase, nonbasic_list, sol, iter, + work_estimate, delta_y_steepest_edge, work_unit_context); } @@ -2596,6 +2604,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, std::vector& nonbasic_list, lp_solution_t& sol, i_t& iter, + f_t& phase2_work_estimate, std::vector& delta_y_steepest_edge, work_limit_context_t* work_unit_context) { @@ -2610,7 +2619,6 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, assert(lp.lower.size() == n); assert(lp.upper.size() == n); assert(lp.rhs.size() == m); - f_t phase2_work_estimate = 0.0; ft.clear_work_estimate(); std::vector& x = sol.x; @@ -2663,6 +2671,10 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, if (toc(start_time) > settings.time_limit) { return dual_status_t::TIME_LIMIT; } } + if (settings.initial_perturbation == 1 && phase == 2) { + phase2::initial_perturbation(lp, settings, vstatus, objective); + } + // Populate c_basic after basis is initialized for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; @@ -3035,6 +3047,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase, start_time, max_val, + phase2_work_estimate, iter, x, y, @@ -3255,6 +3268,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase, start_time, max_val, + phase2_work_estimate, iter, x, y, @@ -3311,6 +3325,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase, start_time, max_val, + phase2_work_estimate, iter, x, y, @@ -3806,6 +3821,7 @@ template dual_status_t dual_phase2( std::vector& vstatus, lp_solution_t& sol, int& iter, + double& work_estimate, std::vector& steepest_edge_norms, work_limit_context_t* work_unit_context); @@ -3822,6 +3838,7 @@ template dual_status_t dual_phase2_with_advanced_basis( std::vector& nonbasic_list, lp_solution_t& sol, int& iter, + double& work_estimate, std::vector& steepest_edge_norms, work_limit_context_t* work_unit_context); diff --git a/cpp/src/dual_simplex/phase2.hpp b/cpp/src/dual_simplex/phase2.hpp index daa946e019..e5a4bacf62 100644 --- a/cpp/src/dual_simplex/phase2.hpp +++ b/cpp/src/dual_simplex/phase2.hpp @@ -60,6 +60,7 @@ dual_status_t dual_phase2(i_t phase, std::vector& vstatus, lp_solution_t& sol, i_t& iter, + f_t& work_estimate, std::vector& steepest_edge_norms, work_limit_context_t* work_unit_context = nullptr); @@ -76,6 +77,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, std::vector& nonbasic_list, lp_solution_t& sol, i_t& iter, + f_t& work_estimate, std::vector& delta_y_steepest_edge, work_limit_context_t* work_unit_context = nullptr); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 6a69cdfcd2..a5f137e2e5 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -167,6 +167,7 @@ struct simplex_solver_settings_t { i_t augmented; // -1 automatic, 0 to solve with ADAT, 1 to solve with augmented system i_t dualize; // -1 automatic, 0 to not dualize, 1 to dualize i_t ordering; // -1 automatic, 0 to use nested dissection, 1 to use AMD + i_t initial_perturbation; // -1 automatic, 0 to not perturb, 1 to perturb i_t barrier_dual_initial_point; // -1 automatic, 0 to use Lustig, Marsten, and Shanno initial // point, 1 to use initial point form dual least squares problem bool check_Q; // true to check if Q is positive semidefinite diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index a24d12fc55..db064dabf8 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -157,6 +157,7 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original lp_solution_t& original_solution, std::vector& vstatus, std::vector& edge_norms, + f_t& work_estimate, work_limit_context_t* work_unit_context) { raft::common::nvtx::range scope("DualSimplex::solve_lp"); @@ -175,6 +176,7 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original nonbasic_list, vstatus, edge_norms, + work_estimate, work_unit_context); return result; } @@ -190,6 +192,7 @@ lp_status_t solve_linear_program_with_advanced_basis( std::vector& nonbasic_list, std::vector& vstatus, std::vector& edge_norms, + f_t& work_estimate, work_limit_context_t* work_unit_context) { lp_status_t lp_status = lp_status_t::UNSET; @@ -257,6 +260,7 @@ lp_status_t solve_linear_program_with_advanced_basis( phase1_vstatus, phase1_solution, iter, + work_estimate, edge_norms, work_unit_context); } @@ -295,6 +299,7 @@ lp_status_t solve_linear_program_with_advanced_basis( nonbasic_list, solution, iter, + work_estimate, edge_norms, work_unit_context); if (status == dual_status_t::NUMERICAL) { @@ -315,6 +320,7 @@ lp_status_t solve_linear_program_with_advanced_basis( nonbasic_list, phase1_solution, iter, + work_estimate, edge_norms, work_unit_context); vstatus = phase1_vstatus; @@ -331,6 +337,7 @@ lp_status_t solve_linear_program_with_advanced_basis( nonbasic_list, solution, iter, + work_estimate, edge_norms, work_unit_context); } @@ -341,7 +348,7 @@ lp_status_t solve_linear_program_with_advanced_basis( // TODO: We need to update ft if the basis changed } if (settings.inside_mip && settings.concurrent_halt != nullptr) { - settings.log.printf("Setting concurrent halt to 1 inside_mip\n"); + settings.log.debug("Setting concurrent halt to 1 inside_mip\n"); *settings.concurrent_halt = 1; } if (status == dual_status_t::OPTIMAL) { @@ -847,8 +854,9 @@ lp_status_t solve_linear_program(const user_problem_t& user_problem, lp_solution_t lp_solution(original_lp.num_rows, original_lp.num_cols); std::vector vstatus; std::vector edge_norms; + f_t work_estimate = 0.0; lp_status_t status = solve_linear_program_advanced( - original_lp, start_time, settings, lp_solution, vstatus, edge_norms); + original_lp, start_time, settings, lp_solution, vstatus, edge_norms, work_estimate); if (status == lp_status_t::CONCURRENT_LIMIT) { solution.iterations = lp_solution.iterations; return lp_status_t::CONCURRENT_LIMIT; @@ -900,8 +908,9 @@ i_t solve(const user_problem_t& problem, lp_solution_t solution(original_lp.num_rows, original_lp.num_cols); std::vector vstatus; std::vector edge_norms; + f_t work_estimate = 0.0; lp_status_t lp_status = solve_linear_program_advanced( - original_lp, start_time, settings, solution, vstatus, edge_norms); + original_lp, start_time, settings, solution, vstatus, edge_norms, work_estimate); primal_solution = solution.x; if (lp_status == lp_status_t::OPTIMAL) { status = 0; @@ -955,6 +964,7 @@ template lp_status_t solve_linear_program_advanced( lp_solution_t& original_solution, std::vector& vstatus, std::vector& edge_norms, + double& work_estimate, work_limit_context_t* work_unit_context); template lp_status_t solve_linear_program_with_advanced_basis( @@ -967,6 +977,7 @@ template lp_status_t solve_linear_program_with_advanced_basis( std::vector& nonbasic_list, std::vector& vstatus, std::vector& edge_norms, + double& work_estimate, work_limit_context_t* work_unit_context); template lp_status_t solve_linear_program_with_barrier( diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index f4807306e0..f295792369 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -70,6 +70,7 @@ lp_status_t solve_linear_program_advanced(const lp_problem_t& original lp_solution_t& original_solution, std::vector& vstatus, std::vector& edge_norms, + f_t& work_estimate, work_limit_context_t* work_unit_context = nullptr); // Solve the LP using dual simplex and keep the `basis_update_mpf_t` @@ -85,6 +86,7 @@ lp_status_t solve_linear_program_with_advanced_basis( std::vector& nonbasic_list, std::vector& vstatus, std::vector& edge_norms, + f_t& work_estimate, work_limit_context_t* work_unit_context = nullptr); template diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index ab129b9c89..a4b3d550a7 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -137,6 +137,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_FOLDING, &pdlp_settings.folding, -1, 1, -1}, {CUOPT_DUALIZE, &pdlp_settings.dualize, -1, 1, -1}, {CUOPT_ORDERING, &pdlp_settings.ordering, -1, 1, -1}, + {CUOPT_INITIAL_PERTURBATION, &pdlp_settings.initial_perturbation, -1, 1, -1}, {CUOPT_BARRIER_DUAL_INITIAL_POINT, &pdlp_settings.barrier_dual_initial_point, -1, 1, -1}, {CUOPT_MIP_CUT_PASSES, &mip_settings.max_cut_passes, -1, std::numeric_limits::max(), 10}, {CUOPT_MIP_MIXED_INTEGER_ROUNDING_CUTS, &mip_settings.mir_cuts, -1, 1, -1}, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 173619c5d1..d09ea65052 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -588,6 +588,7 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t dual_simplex_settings.time_limit = settings.time_limit; dual_simplex_settings.iteration_limit = settings.iteration_limit; dual_simplex_settings.concurrent_halt = settings.concurrent_halt; + dual_simplex_settings.initial_perturbation = settings.initial_perturbation; if (dual_simplex_settings.concurrent_halt != nullptr) { // Don't show the dual simplex log in concurrent mode. Show the PDLP log instead dual_simplex_settings.log.log = false; From 0cfc746f9593db4d0d8de5e50a53f893bcf50b2c Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 14:27:32 -0700 Subject: [PATCH 11/34] Address coderabbit review comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 4 ++++ cpp/src/dual_simplex/primal.cpp | 24 ++++++++++--------- cpp/src/dual_simplex/primal.hpp | 15 ++++++------ cpp/src/dual_simplex/solve.cpp | 1 + cpp/src/dual_simplex/solve.hpp | 2 +- .../solver_settings/solver_settings.pyx | 1 + 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 706ce51c07..980f9bffb0 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3439,6 +3439,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple const i_t iter_before = iter; simplex_solver_settings_t primal_settings = settings_; primal_settings.log.log = false; + primal_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, exploration_stats_.start_time, lp_reduced, @@ -3486,6 +3487,8 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple best_num_fractional = num_fractional_reduced; best_reduced_vstatus = reduced_vstatus; } + } else { + break; } } @@ -3683,6 +3686,7 @@ void branch_and_bound_t::pivot_out_integer_variables( i_t& num_fractional, std::vector& fractional) { + if (num_fractional == 0) { return; } f_t pivot_out_integer_variables_start_time = tic(); std::vector zero_reduced_costs_vars; std::vector zero_reduced_costs_vars_nonbasic_index; diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index f5a756a78a..1778299c79 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -29,7 +29,7 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, { const i_t m = lp.num_rows; const i_t n = lp.num_cols; - + constexpr f_t diff_tol = 1e-6; for (i_t j = 0; j < n; ++j) { if (vstatus[j] == variable_status_t::BASIC) { continue; } @@ -221,7 +221,7 @@ f_t primal_infeasibility(const lp_problem_t& lp, return primal_infeasibility(lp, settings, vstatus, x, num_infeasible, work_estimate); } -// work estimate: n-m + 4 * m +// work estimate: n-m + 4 * m template void compute_phase1_objective(const lp_problem_t& lp, const simplex_solver_settings_t& settings, @@ -294,7 +294,7 @@ f_t compute_dual_step_length(f_t entering_reduced_cost, f_t pivot) } template -void update_y(f_t dual_step_length, +void update_y(f_t dual_step_length, const sparse_vector_t& delta_y, std::vector& y, f_t& work_estimate) @@ -710,8 +710,8 @@ primal_status_t primal_phase2_with_advanced_basis( if (primal_residual > settings.primal_tol) { settings.log.printf("|| A*x - b || %e\n", primal_residual); } - - + + std::vector objective = lp.objective; work_estimate += 2*n; const f_t primal_tol = settings.primal_tol; @@ -877,7 +877,7 @@ primal_status_t primal_phase2_with_advanced_basis( // Incremental duals may be stale relative to the current phase-I // objective. Refresh objective and duals, then retry pricing with // successively tighter dual tolerances. - settings.log.printf("Refreshing phase-I objective and duals. Num updates %d. Iter %d\n", + settings.log.printf("Refreshing phase-I objective and duals. Num updates %d. Iter %d\n", basis_update.num_updates(), iter); compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); compute_dual_variables( @@ -898,11 +898,11 @@ primal_status_t primal_phase2_with_advanced_basis( } if (entering_index == -1) { settings.log.printf( - "Numerical issues encountered. No entering variable found with large " + "No entering variable found with large " "infeasibility %e (%d).\n", primal_inf, num_primal_inf); - return primal_status_t::NUMERICAL; + return primal_status_t::PRIMAL_INFEASIBLE; } pricing_dual_tol = retry_dual_tol; } else { @@ -941,7 +941,7 @@ primal_status_t primal_phase2_with_advanced_basis( std::vector scaled_delta_xB(m); scaled_delta_xB_sparse.to_dense(scaled_delta_xB); work_estimate += m + scaled_delta_xB_sparse.i.size(); - + for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; delta_x[j] = -direction * scaled_delta_xB[k]; @@ -996,7 +996,7 @@ primal_status_t primal_phase2_with_advanced_basis( if (debug_primal_residual > 1e-6) { settings.log.printf("|| A * x - b || %e at iteration %d (updates %d)\n", debug_primal_residual, iter, basis_update.num_updates()); } -#endif +#endif if (basis_updated) { @@ -1129,9 +1129,11 @@ primal_status_t primal_phase2_with_advanced_basis( work_estimate += basis_update.work_estimate(); basis_update.clear_work_estimate(); + + if (now > settings.time_limit) { return primal_status_t::TIME_LIMIT; } } - if (iter == iter_limit) { return primal_status_t::ITERATION_LIMIT; } + if (iter >= iter_limit) { return primal_status_t::ITERATION_LIMIT; } return primal_status_t::NUMERICAL; } diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index df2c998db5..7e4d280655 100644 --- a/cpp/src/dual_simplex/primal.hpp +++ b/cpp/src/dual_simplex/primal.hpp @@ -19,13 +19,14 @@ namespace cuopt::mathematical_optimization::simplex { enum class primal_status_t { - OPTIMAL = 0, - PRIMAL_UNBOUNDED = 1, - NUMERICAL = 2, - NOT_LOADED = 3, - TIME_LIMIT = 4, - ITERATION_LIMIT = 5, - CONCURRENT_LIMIT = 6 + OPTIMAL = 0, + PRIMAL_UNBOUNDED = 1, + PRIMAL_INFEASIBLE = 2, + NUMERICAL = 3, + TIME_LIMIT = 5, + ITERATION_LIMIT = 6, + CONCURRENT_LIMIT = 7, + NOT_LOADED = 8 }; template diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 8266b545df..dce731d4d1 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -66,6 +66,7 @@ lp_status_t map_primal_status_to_lp_status(primal_status_t status) switch (status) { case primal_status_t::OPTIMAL: return lp_status_t::OPTIMAL; case primal_status_t::PRIMAL_UNBOUNDED: return lp_status_t::UNBOUNDED; + case primal_status_t::PRIMAL_INFEASIBLE: return lp_status_t::INFEASIBLE; case primal_status_t::TIME_LIMIT: return lp_status_t::TIME_LIMIT; case primal_status_t::ITERATION_LIMIT: return lp_status_t::ITERATION_LIMIT; case primal_status_t::CONCURRENT_LIMIT: return lp_status_t::CONCURRENT_LIMIT; diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 8ac0c0194d..291675a67b 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -105,7 +105,7 @@ lp_status_t solve_linear_program_with_primal(const user_problem_t& use const simplex_solver_settings_t& settings, f_t start_time, lp_solution_t& solution); - +template lp_status_t solve_linear_program_with_barrier(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, f_t start_time, diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx index a5dcc78d18..ce3ef6fef3 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx @@ -62,6 +62,7 @@ class SolverMethod(IntEnum): PDLP = auto() DualSimplex = auto() Barrier = auto() + Primal = auto() Unset = auto() def __str__(self): From 1c2c01a46036b71136d22d28a9bb58aca15e82e5 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 14:36:18 -0700 Subject: [PATCH 12/34] Address coderabbit review comments --- cpp/src/dual_simplex/simplex_solver_settings.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 120a4bcb99..c4338810bc 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -77,6 +77,7 @@ struct simplex_solver_settings_t { augmented(0), dualize(-1), ordering(-1), + initial_perturbation(-1), barrier_dual_initial_point(-1), postsolve_info(-1), qcqp_ruiz_equilibration(-1), From b2de00d19f684a6ff4a233b69a9359249feedaba Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 14:48:23 -0700 Subject: [PATCH 13/34] Use tight tol for reduced costs zero check --- cpp/src/branch_and_bound/branch_and_bound.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 980f9bffb0..e346f33c5e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3276,7 +3276,7 @@ bool branch_and_bound_t::check_for_dual_degeneracy( const i_t num_nonbasics = nonbasic_list.size(); for (i_t k = 0; k < num_nonbasics; k++) { const i_t j = nonbasic_list[k]; - if (std::abs(solution.z[j]) <= 1e-10) { + if (std::abs(solution.z[j]) <= settings_.tight_tol) { zero_reduced_costs_vars.push_back(j); zero_reduced_costs_vars_nonbasic_index.push_back(k); } @@ -3313,7 +3313,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple i_t nnz = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { nnz += lp.A.col_start[j + 1] - lp.A.col_start[j]; } } @@ -3323,7 +3323,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple i_t nz = 0; i_t reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { original_col_to_reduced_col[j] = reduced_col; A_reduced.col_start[reduced_col] = nz; const i_t col_start = lp.A.col_start[j]; @@ -3344,7 +3344,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple std::vector b_reduced = lp.rhs; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { // PASS } else { const i_t col_start = lp.A.col_start[j]; @@ -3381,7 +3381,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple simplex::lp_solution_t reduced_solution(m, n); reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { reduced_solution.x[reduced_col++] = soln.x[j]; } } @@ -3389,7 +3389,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple std::vector reduced_edge_norms(n); reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { reduced_edge_norms[reduced_col++] = edge_norms_[j]; } } @@ -3409,7 +3409,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple for (i_t pump_iter = 0; pump_iter < max_pump_iter; pump_iter++) { reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { lp_reduced.objective[reduced_col] = 0; if (var_types_[j] == variable_type_t::INTEGER) { if (is_fractional( @@ -3459,7 +3459,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple std::vector adjusted_solution(lp.num_cols, 0.0); reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { adjusted_solution[j] = reduced_solution.x[reduced_col++]; } else { adjusted_solution[j] = soln.x[j]; @@ -3497,7 +3497,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple // Translate the vstatus from the reduced problem to the vstatus for the original problem i_t reduced_cols = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= 1e-10) { + if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { vstatus[j] = best_reduced_vstatus[reduced_cols++]; } } From a92825c96d341f90b17e67dd2b883197bdccd7e3 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 15:04:17 -0700 Subject: [PATCH 14/34] Try to clean up normalization --- cpp/src/branch_and_bound/branch_and_bound.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e346f33c5e..dadf7c2e72 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3776,23 +3776,24 @@ void branch_and_bound_t::pivot_out_integer_variables( break; } } - if (a_ij == 0.0) { continue; } - f_t bound = a_ij > 0 ? lp.lower[j] : lp.upper[j]; if (std::abs(bound) == inf) { continue; } + const f_t delta_xj = bound - soln_copy.x[j]; + const f_t scale = -delta_xj * a_ij; + if (std::abs(scale) <= 1e-12) { continue; } + // Build delta_x describing "move x[j] to its bound, let the basic slacks compensate to // keep A*x = b". This is a feasible direction (A*delta_x = 0). The nonzero pattern lives // on the entries of column A(:, j) plus j itself. In the nonbasic_slack slot, // delta_x[nonbasic_slack] = -delta_xj * a_ij > 0, i.e. the entering slack moves up from // its lower bound 0. We build the sparse version to feed the feasibility scan, then - // scatter into a dense vector and normalize so that delta_x[nonbasic_slack] == 1 (the - // convention primal_ratio_test expects for entering variables). + // normalize so that delta_x[nonbasic_slack] == 1 (the convention primal_ratio_test expects + // for entering variables) and scatter into a dense vector. sparse_vector_t delta_x_sparse; delta_x_sparse.n = lp.num_cols; delta_x_sparse.i.reserve(col_end - col_start + 1); delta_x_sparse.x.reserve(col_end - col_start + 1); - const f_t delta_xj = bound - soln_copy.x[j]; delta_x_sparse.i.push_back(j); delta_x_sparse.x.push_back(delta_xj); for (i_t p = col_start; p < col_end; p++) { @@ -3818,15 +3819,14 @@ void branch_and_bound_t::pivot_out_integer_variables( } if (!ok) { continue; } + // Normalize so that delta_x[nonbasic_slack] == 1 (the standard entering-direction + // convention). Done on the sparse vector, after the feasibility scan above, which reads + // the unnormalized values. + for (f_t& val : delta_x_sparse.x) { val /= scale; } + std::vector delta_x(lp.num_cols, 0.0); delta_x_sparse.to_dense(delta_x); - // Normalize so that delta_x[nonbasic_slack] == 1 (the standard entering-direction - // convention). Also confirms A*delta_x = 0 at debug log time. - const f_t scale = delta_x[nonbasic_slack]; - if (!(std::abs(scale) > 1e-12)) { continue; } - for (i_t h = 0; h < lp.num_cols; ++h) { delta_x[h] /= scale; } - // Entering variable is the nonbasic slack, moving up from its lower bound 0. const i_t entering_index = nonbasic_slack; const i_t nonbasic_entering = nonbasic_index[nonbasic_slack]; From 31436ba4f93b32226811b41dc34aa76ab97333c4 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 15:06:21 -0700 Subject: [PATCH 15/34] Remove AI slop --- cpp/src/branch_and_bound/branch_and_bound.hpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index c0ff1761a2..7bc5d905fc 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -358,14 +358,6 @@ class branch_and_bound_t { i_t& num_fractional, std::vector& fractional); - // Try to pivot the nonbasic variable `entering_index` (currently at position - // `nonbasic_entering` in `nonbasic_list`) into the basis along the direction - // `delta_x`, with `utilde` = U * B^{-1} A(:, entering_index) satisfying - // L * utilde = P * A(:, entering_index). Applies the pivot only if it yields a - // strict net decrease in the number of fractional integer variables. On success, - // mutates basic_list/nonbasic_list/vstatus/solution/basis_update in place and applies - // an O(1) fix-up to `nonbasic_index` for the two variables whose (non)basic status - // changed. On skip, leaves all outputs untouched. void apply_delta_x_for_integer_pivot( const simplex::lp_problem_t& lp, std::vector& basic_list, From ef752075a332ea08f1ae54d496231924dc8e9e44 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 15:07:41 -0700 Subject: [PATCH 16/34] Style fixes --- cpp/src/branch_and_bound/branch_and_bound.cpp | 193 +++++++++-------- cpp/src/branch_and_bound/branch_and_bound.hpp | 29 ++- cpp/src/dual_simplex/basis_updates.cpp | 3 +- cpp/src/dual_simplex/crossover.cpp | 17 +- cpp/src/dual_simplex/phase2.cpp | 4 +- cpp/src/dual_simplex/primal.cpp | 199 +++++++++++------- .../dual_simplex/simplex_solver_settings.hpp | 2 +- cpp/src/dual_simplex/solve.cpp | 11 +- cpp/src/pdlp/solve.cu | 13 +- .../solver_settings/solver_settings.pyx | 2 +- 10 files changed, 263 insertions(+), 210 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index dadf7c2e72..99e4c2416d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -24,9 +24,9 @@ #include #include #include -#include #include #include +#include #include #include #include @@ -710,7 +710,7 @@ bool branch_and_bound_t::repair_solution(const std::vector& edge_ std::vector leaf_edge_norms = edge_norms; f_t repair_work_estimate = 0.0; // should probably set the cut off here lp_settings.cut_off - dual_status_t lp_status = simplex::dual_phase2(2, + dual_status_t lp_status = simplex::dual_phase2(2, 0, lp_start_time, repair_lp, @@ -720,7 +720,7 @@ bool branch_and_bound_t::repair_solution(const std::vector& edge_ iter, repair_work_estimate, leaf_edge_norms); - repaired_solution = lp_solution.x; + repaired_solution = lp_solution.x; if (lp_status == dual_status_t::OPTIMAL) { f_t primal_error; @@ -3179,8 +3179,7 @@ auto branch_and_bound_t::do_cut_pass( root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); // Refresh fractional info after re-solving with cuts; the pre-cut count is stale. - num_fractional = - fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); + num_fractional = fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); pivot_out_integer_variables(original_lp_, basic_list, @@ -3265,7 +3264,6 @@ auto branch_and_bound_t::do_cut_pass( return {cut_pass_action_t::CONTINUE, mip_status_t::UNSET}; } - template bool branch_and_bound_t::check_for_dual_degeneracy( const simplex::lp_solution_t& solution, @@ -3285,7 +3283,8 @@ bool branch_and_bound_t::check_for_dual_degeneracy( } template -void branch_and_bound_t::dual_degenerate_feasibility_pump(const simplex::lp_problem_t& lp, +void branch_and_bound_t::dual_degenerate_feasibility_pump( + const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, @@ -3297,7 +3296,8 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple f_t dual_degenerate_feasibility_pump_start_time = tic(); std::vector zero_reduced_costs_vars; std::vector zero_reduced_costs_vars_nonbasic_index; - bool dual_degenerate = check_for_dual_degeneracy(soln, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); + bool dual_degenerate = check_for_dual_degeneracy( + soln, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); if (!dual_degenerate) { return; } // Construct a new LP problem @@ -3320,16 +3320,16 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple simplex::lp_problem_t lp_reduced(lp.handle_ptr, m, n, nnz); csc_matrix_t& A_reduced = lp_reduced.A; std::vector original_col_to_reduced_col(lp.num_cols, -1); - i_t nz = 0; + i_t nz = 0; i_t reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { if (vstatus[j] == variable_status_t::BASIC || std::abs(soln.z[j]) <= settings_.tight_tol) { - original_col_to_reduced_col[j] = reduced_col; + original_col_to_reduced_col[j] = reduced_col; A_reduced.col_start[reduced_col] = nz; - const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; for (i_t p = col_start; p < col_end; p++) { - const i_t i = lp.A.i[p]; + const i_t i = lp.A.i[p]; const f_t value = lp.A.x[p]; A_reduced.i[nz] = i; A_reduced.x[nz] = value; @@ -3348,32 +3348,32 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple // PASS } else { const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; + const i_t col_end = lp.A.col_start[j + 1]; for (i_t p = col_start; p < col_end; p++) { - const i_t i = lp.A.i[p]; + const i_t i = lp.A.i[p]; const f_t value = lp.A.x[p]; b_reduced[i] -= value * soln.x[j]; } } } - lp_reduced.rhs = b_reduced; + lp_reduced.rhs = b_reduced; lp_reduced.obj_scale = 1.0; - - - settings_.log.printf("Constructed dual degenerate feasibility pump LP with %d rows and %d columns\n", m, n); + settings_.log.printf( + "Constructed dual degenerate feasibility pump LP with %d rows and %d columns\n", m, n); std::vector reduced_basic_list(m); std::vector reduced_nonbasic_list(zero_reduced_costs_vars.size()); std::vector reduced_vstatus(n); - i_t num_basic = 0; + i_t num_basic = 0; i_t num_nonbasic = 0; - reduced_col = 0; + reduced_col = 0; for (i_t j = 0; j < lp.num_cols; j++) { - if (vstatus[j] == variable_status_t::BASIC){ + if (vstatus[j] == variable_status_t::BASIC) { reduced_vstatus[reduced_col++] = variable_status_t::BASIC; } else if (std::abs(soln.z[j]) <= 1e-10) { - reduced_nonbasic_list[num_nonbasic++] = reduced_col; // Does ordering of nonbasic variables matter? + reduced_nonbasic_list[num_nonbasic++] = + reduced_col; // Does ordering of nonbasic variables matter? reduced_vstatus[reduced_col++] = vstatus[j]; } } @@ -3400,8 +3400,8 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple } f_t primal_work_estimate = 0.0; - i_t iter = 0; - i_t max_pump_iter = 10; + i_t iter = 0; + i_t max_pump_iter = 10; simplex::random_t rng(settings_.random_seed); i_t best_num_fractional = num_fractional; std::vector best_reduced_vstatus(n); @@ -3435,22 +3435,23 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple } } - bool recompute_basis = false; - const i_t iter_before = iter; + bool recompute_basis = false; + const i_t iter_before = iter; simplex_solver_settings_t primal_settings = settings_; - primal_settings.log.log = false; + primal_settings.log.log = false; primal_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); - simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, - exploration_stats_.start_time, - lp_reduced, - primal_settings, - reduced_vstatus, - reduced_basis_update, - reduced_basic_list, - reduced_nonbasic_list, - reduced_solution, - iter, - primal_work_estimate); + simplex::primal_status_t lp_status = + simplex::primal_phase2_with_advanced_basis(2, + exploration_stats_.start_time, + lp_reduced, + primal_settings, + reduced_vstatus, + reduced_basis_update, + reduced_basic_list, + reduced_nonbasic_list, + reduced_solution, + iter, + primal_work_estimate); // Detect a stall: the solve made no pivots, so the incumbent vertex was // already optimal for this objective and x did not move. Perturb next pass. stalled = (iter == iter_before); @@ -3479,7 +3480,15 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple i_t num_fractional_reduced = fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); settings_.log.printf( - "Degenerate feasibility pump (%d/%d): primal work estimate %.2e, iter %d, fractional variables %d/%d. Time %.2f\n", pump_iter, max_pump_iter, primal_work_estimate, iter, num_fractional_reduced, num_fractional, toc(dual_degenerate_feasibility_pump_start_time)); + "Degenerate feasibility pump (%d/%d): primal work estimate %.2e, iter %d, fractional " + "variables %d/%d. Time %.2f\n", + pump_iter, + max_pump_iter, + primal_work_estimate, + iter, + num_fractional_reduced, + num_fractional, + toc(dual_degenerate_feasibility_pump_start_time)); // Also treat a pass that fails to improve the best as a stall, so we perturb // the next pass even when the solve pivoted (moved) without reducing the count. stalled = stalled || (num_fractional_reduced >= best_num_fractional); @@ -3492,7 +3501,13 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple } } - settings_.log.printf("Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables %d/%d. Time %.2f\n", iter, best_num_fractional, num_fractional, toc(dual_degenerate_feasibility_pump_start_time)); + settings_.log.printf( + "Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables " + "%d/%d. Time %.2f\n", + iter, + best_num_fractional, + num_fractional, + toc(dual_degenerate_feasibility_pump_start_time)); if (best_num_fractional < num_fractional) { // Translate the vstatus from the reduced problem to the vstatus for the original problem i_t reduced_cols = 0; @@ -3520,9 +3535,10 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple return; } if (refactor_status != 0) { - settings_.log.printf("Failed to refactor basis after dual degenerate feasibility pump. " - "%d deficient columns.\n", - refactor_status); + settings_.log.printf( + "Failed to refactor basis after dual degenerate feasibility pump. " + "%d deficient columns.\n", + refactor_status); return; } @@ -3530,7 +3546,8 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple // First set the nonbasic variables on their bounds for (i_t k = 0; k < lp.num_cols - lp.num_rows; k++) { const i_t j = nonbasic_list[k]; - if (vstatus[j] == variable_status_t::NONBASIC_LOWER || vstatus[j] == variable_status_t::NONBASIC_FIXED) { + if (vstatus[j] == variable_status_t::NONBASIC_LOWER || + vstatus[j] == variable_status_t::NONBASIC_FIXED) { soln.x[j] = lp.lower[j]; } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER) { soln.x[j] = lp.upper[j]; @@ -3543,11 +3560,11 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple for (i_t j = 0; j < lp.num_cols; j++) { if (vstatus[j] == variable_status_t::BASIC) { continue; } const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; + const i_t col_end = lp.A.col_start[j + 1]; const f_t x_j = soln.x[j]; for (i_t p = col_start; p < col_end; p++) { - const i_t i = lp.A.i[p]; + const i_t i = lp.A.i[p]; const f_t aij = lp.A.x[p]; rhs[i] -= aij * x_j; } @@ -3564,11 +3581,9 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump(const simple fractional.clear(); num_fractional = fractional_variables(settings_, soln.x, var_types_, fractional); - } } - template void branch_and_bound_t::apply_delta_x_for_integer_pivot( const simplex::lp_problem_t& lp, @@ -3608,10 +3623,8 @@ void branch_and_bound_t::apply_delta_x_for_integer_pivot( for (i_t h = 0; h < lp.num_cols; ++h) { test_x[h] += step_length * delta_x[h]; if (var_types_[h] != variable_type_t::INTEGER) { continue; } - const bool was_fractional = - is_fractional(solution.x[h], var_types_[h], settings_.integer_tol); - const bool now_fractional = - is_fractional(test_x[h], var_types_[h], settings_.integer_tol); + const bool was_fractional = is_fractional(solution.x[h], var_types_[h], settings_.integer_tol); + const bool now_fractional = is_fractional(test_x[h], var_types_[h], settings_.integer_tol); if (now_fractional && !was_fractional) { integer_destroyed++; } else if (!now_fractional && was_fractional) { @@ -3645,8 +3658,7 @@ void branch_and_bound_t::apply_delta_x_for_integer_pivot( sparse_vector_t UTsol_sparse(m, 1); sparse_vector_t solution_sparse(m, 1); basis_update.b_transpose_solve(es_sparse, solution_sparse, UTsol_sparse); - const i_t recommend_refactor = - basis_update.update(utilde_sparse, UTsol_sparse, basic_leaving); + const i_t recommend_refactor = basis_update.update(utilde_sparse, UTsol_sparse, basic_leaving); if (recommend_refactor == 1) { csc_matrix_t L(m, m, 1); csc_matrix_t U(m, m, 1); @@ -3657,17 +3669,17 @@ void branch_and_bound_t::apply_delta_x_for_integer_pivot( std::vector slacks_needed; f_t factorize_work_estimate = 0.0; const i_t rank = factorize_basis(lp.A, - settings_, - basic_list, - exploration_stats_.start_time, - L, - U, - p, - pinv, - q, - deficient, - slacks_needed, - factorize_work_estimate); + settings_, + basic_list, + exploration_stats_.start_time, + L, + U, + p, + pinv, + q, + deficient, + slacks_needed, + factorize_work_estimate); if (rank == CONCURRENT_HALT_RETURN || rank == TIME_LIMIT_RETURN) { return; } if (rank < 0 || rank != lp.num_rows) { return; } simplex::reorder_basic_list(q, basic_list); @@ -3690,7 +3702,8 @@ void branch_and_bound_t::pivot_out_integer_variables( f_t pivot_out_integer_variables_start_time = tic(); std::vector zero_reduced_costs_vars; std::vector zero_reduced_costs_vars_nonbasic_index; - bool dual_degenerate = check_for_dual_degeneracy(solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); + bool dual_degenerate = check_for_dual_degeneracy( + solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); if (!dual_degenerate) { return; } lp_solution_t soln_copy = solution; @@ -3706,7 +3719,7 @@ void branch_and_bound_t::pivot_out_integer_variables( std::vector row_to_slack(lp.num_rows, -1); for (i_t j : new_slacks_) { if (lp.lower[j] != 0 || lp.upper[j] != inf) { continue; } - const i_t p = lp.A.col_start[j]; + const i_t p = lp.A.col_start[j]; row_to_slack[lp.A.i[p]] = j; } @@ -3745,7 +3758,7 @@ void branch_and_bound_t::pivot_out_integer_variables( if (fast_candidates.size() > 0) { settings_.log.printf("Found %ld fast candidates for pivot out integer variables\n", - fast_candidates.size()); + fast_candidates.size()); } // Build a reverse index nonbasic_index[v] = position of v in nonbasic_list_copy, or -1 if not @@ -3780,7 +3793,7 @@ void branch_and_bound_t::pivot_out_integer_variables( if (std::abs(bound) == inf) { continue; } const f_t delta_xj = bound - soln_copy.x[j]; - const f_t scale = -delta_xj * a_ij; + const f_t scale = -delta_xj * a_ij; if (std::abs(scale) <= 1e-12) { continue; } // Build delta_x describing "move x[j] to its bound, let the basic slacks compensate to @@ -3797,8 +3810,8 @@ void branch_and_bound_t::pivot_out_integer_variables( delta_x_sparse.i.push_back(j); delta_x_sparse.x.push_back(delta_xj); for (i_t p = col_start; p < col_end; p++) { - const i_t r = lp.A.i[p]; - const f_t a_rj = lp.A.x[p]; + const i_t r = lp.A.i[p]; + const f_t a_rj = lp.A.x[p]; const f_t delta_slack_r = -delta_xj * a_rj; delta_x_sparse.i.push_back(row_to_slack[r]); delta_x_sparse.x.push_back(delta_slack_r); @@ -3822,7 +3835,9 @@ void branch_and_bound_t::pivot_out_integer_variables( // Normalize so that delta_x[nonbasic_slack] == 1 (the standard entering-direction // convention). Done on the sparse vector, after the feasibility scan above, which reads // the unnormalized values. - for (f_t& val : delta_x_sparse.x) { val /= scale; } + for (f_t& val : delta_x_sparse.x) { + val /= scale; + } std::vector delta_x(lp.num_cols, 0.0); delta_x_sparse.to_dense(delta_x); @@ -3873,15 +3888,13 @@ void branch_and_bound_t::pivot_out_integer_variables( if (var_types_[j] == variable_type_t::INTEGER) { continue; } if (vstatus_copy[j] == variable_status_t::BASIC) { continue; } - const i_t direction = - (vstatus_copy[j] == variable_status_t::NONBASIC_LOWER || - vstatus_copy[j] == variable_status_t::NONBASIC_FIXED) - ? 1 - : -1; + const i_t direction = (vstatus_copy[j] == variable_status_t::NONBASIC_LOWER || + vstatus_copy[j] == variable_status_t::NONBASIC_FIXED) + ? 1 + : -1; const i_t entering_index = j; const i_t nonbasic_entering = zero_reduced_costs_vars_nonbasic_index[k]; - if (nonbasic_entering < 0 || - nonbasic_entering >= static_cast(nonbasic_list_copy.size()) || + if (nonbasic_entering < 0 || nonbasic_entering >= static_cast(nonbasic_list_copy.size()) || nonbasic_list_copy[nonbasic_entering] != j) { continue; } @@ -3922,7 +3935,11 @@ void branch_and_bound_t::pivot_out_integer_variables( if (num_new_fractional < start_num_fractional) { i_t num_integer_increased = start_num_fractional - num_new_fractional; integer_pivots_.fetch_add(num_integer_increased, std::memory_order_release); - settings_.log.printf("Pivoted out %d integer variables: %d -> %d in %.2f\n", num_integer_increased, start_num_fractional, num_new_fractional, toc(pivot_out_integer_variables_start_time)); + settings_.log.printf("Pivoted out %d integer variables: %d -> %d in %.2f\n", + num_integer_increased, + start_num_fractional, + num_new_fractional, + toc(pivot_out_integer_variables_start_time)); num_fractional = num_new_fractional; fractional = new_fractional; basic_list = basic_list_copy; @@ -4014,7 +4031,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lp_status_t root_status = lp_status_t::UNSET; solving_root_relaxation_ = true; - f_t root_relax_start_time = tic(); + f_t root_relax_start_time = tic(); f_t root_relax_work_estimate = 0.0; if (!enable_concurrent_lp_root_solve()) { // RINS/SUBMIP path @@ -4049,8 +4066,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solving_root_relaxation_ = false; f_t root_relax_elapsed_time = toc(root_relax_start_time); exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; - i_t root_iterations = exploration_stats_.total_simplex_iters; - + i_t root_iterations = exploration_stats_.total_simplex_iters; if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("\nThe root LP relaxation is infeasible\n", @@ -4104,7 +4120,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.iterations, root_relax_elapsed_time, method_to_string(root_relax_solved_by)); - settings_.log.printf("Dual simplex iteration %d work estimate %.2e work per second %.2e\n", root_iterations, root_relax_work_estimate, root_relax_work_estimate / root_relax_elapsed_time); + settings_.log.printf("Dual simplex iteration %d work estimate %.2e work per second %.2e\n", + root_iterations, + root_relax_work_estimate, + root_relax_work_estimate / root_relax_elapsed_time); settings_.log.printf("Root relaxation objective %+.8e\n\n", root_relax_soln_.user_objective); assert(root_vstatus_.size() == original_lp_.num_cols); @@ -5054,8 +5073,8 @@ node_status_t branch_and_bound_t::solve_node_deterministic( i_t node_iter = 0; f_t lp_start_time = tic(); std::vector leaf_edge_norms = edge_norms_; - f_t dual_work_estimate = 0.0; - dual_status_t lp_status = dual_phase2_with_advanced_basis(2, + f_t dual_work_estimate = 0.0; + dual_status_t lp_status = dual_phase2_with_advanced_basis(2, 0, worker.recompute_bounds_and_basis, lp_start_time, diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 7bc5d905fc..17f6f7a3a3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -341,14 +341,12 @@ class branch_and_bound_t { i_t leaf_depth, search_strategy_t thread_type); - omp_atomic_t integer_pivots_{0}; bool check_for_dual_degeneracy(const simplex::lp_solution_t& solution, const std::vector& nonbasic_list, std::vector& zero_reduced_costs_vars, std::vector& zero_reduced_costs_vars_nonbasic_index); - void pivot_out_integer_variables(const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, @@ -358,20 +356,19 @@ class branch_and_bound_t { i_t& num_fractional, std::vector& fractional); - void apply_delta_x_for_integer_pivot( - const simplex::lp_problem_t& lp, - std::vector& basic_list, - std::vector& nonbasic_list, - std::vector& nonbasic_index, - std::vector& vstatus, - i_t entering_index, - i_t nonbasic_entering, - i_t direction, - std::vector& delta_x, - const sparse_vector_t& utilde_sparse, - simplex::lp_solution_t& solution, - simplex::basis_update_mpf_t& basis_update, - f_t& work_estimate); + void apply_delta_x_for_integer_pivot(const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + i_t entering_index, + i_t nonbasic_entering, + i_t direction, + std::vector& delta_x, + const sparse_vector_t& utilde_sparse, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate); void dual_degenerate_feasibility_pump(const simplex::lp_problem_t& lp, std::vector& basic_list, diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 1081cc4773..a3d3787183 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2013,8 +2013,7 @@ i_t basis_update_mpf_t::u_solve(sparse_vector_t& rhs) const // Compute y = U*x. In the MPF factorization, the rank-1 update factors are absorbed into L, so // U == U0 and U*x reduces to a sparse matvec against U0. template -void basis_update_mpf_t::u_multiply(const std::vector& x, - std::vector& y) const +void basis_update_mpf_t::u_multiply(const std::vector& x, std::vector& y) const { const i_t m = L0_.m; y.assign(m, 0.0); diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 5b0dd451e3..977f5e5511 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -1424,11 +1424,11 @@ crossover_status_t crossover(const lp_problem_t& lp, } else if (dual_feasible && !primal_feasible) { i_t dual_iter = 0; std::vector edge_norms; - f_t work_estimate = 0.0; + f_t work_estimate = 0.0; simplex_solver_settings_t dual_settings = settings; - dual_settings.iteration_limit = std::numeric_limits::max(); - dual_status_t status = - dual_phase2(2, 0, start_time, lp, dual_settings, vstatus, solution, dual_iter, work_estimate, edge_norms); + dual_settings.iteration_limit = std::numeric_limits::max(); + dual_status_t status = dual_phase2( + 2, 0, start_time, lp, dual_settings, vstatus, solution, dual_iter, work_estimate, edge_norms); if (toc(start_time) > settings.time_limit) { settings.log.printf("Time limit exceeded\n"); return crossover_status_t::TIME_LIMIT; @@ -1448,10 +1448,11 @@ crossover_status_t crossover(const lp_problem_t& lp, primal_feasible = primal_infeas <= primal_tol && primal_res <= primal_tol; dual_feasible = dual_infeas <= dual_tol && dual_res <= dual_tol; } else if (primal_feasible && !dual_feasible) { - i_t primal_iter = 0; + i_t primal_iter = 0; simplex_solver_settings_t primal_settings = settings; - primal_settings.iteration_limit = std::numeric_limits::max(); - primal_status_t primal_status = primal_phase2(2, start_time, lp, primal_settings, vstatus, solution, primal_iter); + primal_settings.iteration_limit = std::numeric_limits::max(); + primal_status_t primal_status = + primal_phase2(2, start_time, lp, primal_settings, vstatus, solution, primal_iter); if (toc(start_time) > settings.time_limit) { settings.log.printf("Time limit exceeded\n"); return crossover_status_t::TIME_LIMIT; @@ -1472,7 +1473,7 @@ crossover_status_t crossover(const lp_problem_t& lp, dual_feasible = dual_infeas <= dual_tol && dual_res <= dual_tol; } else { simplex_solver_settings_t dual_settings = settings; - dual_settings.iteration_limit = std::numeric_limits::max(); + dual_settings.iteration_limit = std::numeric_limits::max(); lp_problem_t phase1_problem(lp.handle_ptr, 1, 1, 1); create_phase1_problem(lp, phase1_problem); std::vector phase1_vstatus(n); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 233bc1ea6a..f86aeb0333 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2347,8 +2347,8 @@ void prepare_optimality(i_t info, std::vector& z, lp_solution_t& sol) { - const i_t m = lp.num_rows; - const i_t n = lp.num_cols; + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; sol.objective = compute_objective(lp, sol.x); sol.user_objective = compute_user_objective(lp, sol.objective); diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 1778299c79..27351a3685 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -57,7 +57,7 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, assert(1 == 0); } } - work_estimate += n + 3.0*(n - m); + work_estimate += n + 3.0 * (n - m); } template @@ -168,7 +168,7 @@ f_t primal_infeasibility(const lp_problem_t& lp, i_t& num_infeasible, f_t& work_estimate) { - const i_t m = lp.num_rows; + const i_t m = lp.num_rows; const i_t n = lp.num_cols; f_t primal_inf = 0; num_infeasible = 0; @@ -206,7 +206,7 @@ f_t primal_infeasibility(const lp_problem_t& lp, } } } - work_estimate += n + 4*m; + work_estimate += n + 4 * m; return primal_inf; } @@ -243,7 +243,7 @@ void compute_phase1_objective(const lp_problem_t& lp, objective[j] = 0.0; } } - work_estimate += n-m + 4 * m; + work_estimate += n - m + 4 * m; } template @@ -281,7 +281,7 @@ void compute_delta_z(const csr_matrix_t& Arow, const i_t j = Arow.j[p]; if (vstatus[j] != variable_status_t::BASIC) { delta_z[j] -= Arow.x[p] * delta_y_i; } } - work_estimate += 4*(row_end - row_start); + work_estimate += 4 * (row_end - row_start); } work_estimate += 4 * delta_y.i.size(); } @@ -356,7 +356,7 @@ void compute_dual_variables(const lp_problem_t& lp, for (i_t p = col_start; p < col_end; ++p) { dot += lp.A.x[p] * y[lp.A.i[p]]; } - work_estimate += 3.0*(col_end - col_start); + work_estimate += 3.0 * (col_end - col_start); z[j] -= dot; } work_estimate += 6 * (n - m); @@ -364,7 +364,7 @@ void compute_dual_variables(const lp_problem_t& lp, for (i_t k = 0; k < m; ++k) { z[basic_list[k]] = 0.0; } - work_estimate += 2*m; + work_estimate += 2 * m; } template @@ -386,7 +386,7 @@ void compute_basic_primal_variables(const lp_problem_t& lp, for (i_t p = col_start; p < col_end; ++p) { rhs[lp.A.i[p]] -= xj * lp.A.x[p]; } - work_estimate += 3.0*(col_end - col_start); + work_estimate += 3.0 * (col_end - col_start); } work_estimate += 4 * (n - m); std::vector xB(m); @@ -408,7 +408,6 @@ f_t primal_constraint_residual(const lp_problem_t& lp, const std::vect } // namespace - template i_t primal_ratio_test(const lp_problem_t& lp, const simplex_solver_settings_t& settings, @@ -519,12 +518,11 @@ i_t primal_ratio_test(const lp_problem_t& lp, } } } - work_estimate += 10*m; + work_estimate += 10 * m; step_length = min_val; return leaving_index; } - template primal_status_t primal_phase2(i_t phase, f_t start_time, @@ -543,7 +541,7 @@ primal_status_t primal_phase2(i_t phase, std::vector superbasic_list; get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); - work_estimate += 2*n; + work_estimate += 2 * n; assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); @@ -680,11 +678,10 @@ primal_status_t primal_phase2_with_advanced_basis( for (i_t p = col_start; p < col_end; ++p) { rhs[lp.A.i[p]] -= xj * lp.A.x[p]; } - work_estimate += 3.0*(col_end - col_start); + work_estimate += 3.0 * (col_end - col_start); } work_estimate += 4 * (n - m); - std::vector xB(m); work_estimate += m; @@ -697,25 +694,22 @@ primal_status_t primal_phase2_with_advanced_basis( work_estimate += 3 * m; constexpr bool print_norms = false; - if constexpr (print_norms) { - settings.log.printf("|| x || %e\n", vector_norm2(x)); - } + if constexpr (print_norms) { settings.log.printf("|| x || %e\n", vector_norm2(x)); } std::vector residual = lp.rhs; work_estimate += m; matrix_vector_multiply(lp.A, 1.0, x, -1.0, residual); - work_estimate += m + 2*n + 4.0*lp.A.col_start[lp.A.n]; + work_estimate += m + 2 * n + 4.0 * lp.A.col_start[lp.A.n]; f_t primal_residual = vector_norm_inf(residual); work_estimate += m; if (primal_residual > settings.primal_tol) { settings.log.printf("|| A*x - b || %e\n", primal_residual); } - std::vector objective = lp.objective; - work_estimate += 2*n; - const f_t primal_tol = settings.primal_tol; - f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x, work_estimate); + work_estimate += 2 * n; + const f_t primal_tol = settings.primal_tol; + f_t primal_inf = primal_infeasibility(lp, settings, vstatus, x, work_estimate); if (primal_inf > primal_tol) { // We are primal infeasible. Switch to phase 1 compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); @@ -731,22 +725,18 @@ primal_status_t primal_phase2_with_advanced_basis( work_estimate += m; compute_dual_variables( lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); - if constexpr (print_norms) { - settings.log.printf("|| z || %e\n", vector_norm_inf(z)); - } + if constexpr (print_norms) { settings.log.printf("|| z || %e\n", vector_norm_inf(z)); } - i_t num_dual_inf = 0; - i_t num_primal_inf = 0; + i_t num_dual_inf = 0; + i_t num_primal_inf = 0; const f_t init_dual_inf = dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf, work_estimate); - if (num_dual_inf > 0) { - settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); - } + if (num_dual_inf > 0) { settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); } csr_matrix_t Arow(m, n, lp.A.nnz()); - work_estimate += n + 2*lp.A.nnz(); + work_estimate += n + 2 * lp.A.nnz(); lp.A.to_compressed_row(Arow); - work_estimate += m + 6*lp.A.nnz(); + work_estimate += m + 6 * lp.A.nnz(); const i_t iter_limit = settings.iteration_limit; const i_t start_iter = iter; @@ -754,13 +744,13 @@ primal_status_t primal_phase2_with_advanced_basis( sparse_vector_t etilde(m, 0); std::vector delta_z(n); std::vector delta_x(n); - work_estimate += 2*m + 2*n; + work_estimate += 2 * m + 2 * n; f_t dual_inf = init_dual_inf; f_t obj = compute_objective(lp, x); - work_estimate += 2*n; + work_estimate += 2 * n; f_t pricing_dual_tol = settings.dual_tol; - primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); settings.log.printf("%5d %+.16e %7d %.8e %.2f\n", iter, @@ -776,8 +766,15 @@ primal_status_t primal_phase2_with_advanced_basis( while (iter < iter_limit) { i_t nonbasic_entering = -1; i_t direction; - i_t entering_index = phase2_pricing( - lp, z, nonbasic_list, vstatus, pricing_dual_tol, direction, nonbasic_entering, dual_inf, work_estimate); + i_t entering_index = phase2_pricing(lp, + z, + nonbasic_list, + vstatus, + pricing_dual_tol, + direction, + nonbasic_entering, + dual_inf, + work_estimate); if (entering_index == -1) { if (phase == 2) { // Verify optimality with a consistent basic solution: refactor, put @@ -797,9 +794,18 @@ primal_status_t primal_phase2_with_advanced_basis( basis_update.clear_work_estimate(); } set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); - compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x, work_estimate); - compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); + compute_basic_primal_variables( + lp, basis_update, basic_list, nonbasic_list, x, work_estimate); + compute_dual_variables(lp, + settings, + objective, + basic_list, + nonbasic_list, + basis_update, + c_basic, + y, + z, + work_estimate); primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); dual_inf = dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf, work_estimate); @@ -807,8 +813,16 @@ primal_status_t primal_phase2_with_advanced_basis( compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); phase = 1; pricing_dual_tol = settings.dual_tol; - compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); + compute_dual_variables(lp, + settings, + objective, + basic_list, + nonbasic_list, + basis_update, + c_basic, + y, + z, + work_estimate); settings.log.printf( "Switching to Primal Simplex Phase 1 after near optimality. " "Primal infeasibility %e\n", @@ -852,10 +866,10 @@ primal_status_t primal_phase2_with_advanced_basis( } } // Report the unfiltered residual at the accepted solution. - dual_inf = tight_dual_inf; - num_dual_inf = num_tight_dual_inf; - obj = compute_objective(lp, x); - work_estimate += 2*n; + dual_inf = tight_dual_inf; + num_dual_inf = num_tight_dual_inf; + obj = compute_objective(lp, x); + work_estimate += 2 * n; sol.objective = obj; sol.user_objective = compute_user_objective(lp, obj); if (!settings.inside_mip && print_summary) { @@ -878,10 +892,19 @@ primal_status_t primal_phase2_with_advanced_basis( // objective. Refresh objective and duals, then retry pricing with // successively tighter dual tolerances. settings.log.printf("Refreshing phase-I objective and duals. Num updates %d. Iter %d\n", - basis_update.num_updates(), iter); + basis_update.num_updates(), + iter); compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); - compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); + compute_dual_variables(lp, + settings, + objective, + basic_list, + nonbasic_list, + basis_update, + c_basic, + y, + z, + work_estimate); f_t retry_dual_tol = pricing_dual_tol; while (entering_index == -1 && retry_dual_tol > f_t(1e-10)) { retry_dual_tol *= f_t(0.1); @@ -913,10 +936,18 @@ primal_status_t primal_phase2_with_advanced_basis( settings.log.printf( "Primal phase I complete. Iterations %d. Time %.2f\n", iter, toc(start_time)); settings.log.printf(" Iter Objective Num Inf. Sum Inf. Time\n"); - compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); - obj = compute_objective(lp, x); - work_estimate += 2*n; + compute_dual_variables(lp, + settings, + objective, + basic_list, + nonbasic_list, + basis_update, + c_basic, + y, + z, + work_estimate); + obj = compute_objective(lp, x); + work_estimate += 2 * n; dual_inf = dual_infeasibility(lp, vstatus, z, settings.dual_tol, num_dual_inf, work_estimate); iter++; @@ -934,7 +965,7 @@ primal_status_t primal_phase2_with_advanced_basis( } sparse_vector_t rhs_sparse(lp.A, entering_index); - work_estimate += 3*rhs_sparse.i.size(); + work_estimate += 3 * rhs_sparse.i.size(); sparse_vector_t scaled_delta_xB_sparse(m, 0); sparse_vector_t utilde_sparse(m, 0); basis_update.b_solve(rhs_sparse, scaled_delta_xB_sparse, utilde_sparse); @@ -946,12 +977,12 @@ primal_status_t primal_phase2_with_advanced_basis( const i_t j = basic_list[k]; delta_x[j] = -direction * scaled_delta_xB[k]; } - work_estimate += 3*m; + work_estimate += 3 * m; for (i_t k = 0; k < n - m; ++k) { const i_t j = nonbasic_list[k]; delta_x[j] = 0.0; } - work_estimate += 2*(n - m); + work_estimate += 2 * (n - m); delta_x[entering_index] = direction; #ifdef CHECK_NULLSPACE @@ -989,16 +1020,18 @@ primal_status_t primal_phase2_with_advanced_basis( for (i_t j = 0; j < n; ++j) { x[j] += step_length * delta_x[j]; } - work_estimate += 2*n; + work_estimate += 2 * n; #ifdef COMPUTE_RESIDUAL f_t debug_primal_residual = primal_constraint_residual(lp, x); if (debug_primal_residual > 1e-6) { - settings.log.printf("|| A * x - b || %e at iteration %d (updates %d)\n", debug_primal_residual, iter, basis_update.num_updates()); + settings.log.printf("|| A * x - b || %e at iteration %d (updates %d)\n", + debug_primal_residual, + iter, + basis_update.num_updates()); } #endif - if (basis_updated) { assert(step_length >= 0.0); @@ -1062,11 +1095,13 @@ primal_status_t primal_phase2_with_advanced_basis( recompute_duals = true; // Factor matches basic_list: rebuild x_B so Ax = b exactly. set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); - compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x, work_estimate); + compute_basic_primal_variables( + lp, basis_update, basic_list, nonbasic_list, x, work_estimate); } else if (rebuild_x_after_bound_snap) { // FT update already matches the new basis; recompute x_B with the leaving variable // snapped onto its bound. - compute_basic_primal_variables(lp, basis_update, basic_list, nonbasic_list, x, work_estimate); + compute_basic_primal_variables( + lp, basis_update, basic_list, nonbasic_list, x, work_estimate); } } else { if (direction > 0) { @@ -1103,14 +1138,21 @@ primal_status_t primal_phase2_with_advanced_basis( } if (recompute_duals) { - compute_dual_variables( - lp, settings, objective, basic_list, nonbasic_list, basis_update, c_basic, y, z, work_estimate); + compute_dual_variables(lp, + settings, + objective, + basic_list, + nonbasic_list, + basis_update, + c_basic, + y, + z, + work_estimate); } - obj = compute_objective(lp, x); - work_estimate += 2*n; - dual_inf = - dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf, work_estimate); + obj = compute_objective(lp, x); + work_estimate += 2 * n; + dual_inf = dual_infeasibility(lp, vstatus, z, pricing_dual_tol, num_dual_inf, work_estimate); iter++; @@ -1140,18 +1182,17 @@ primal_status_t primal_phase2_with_advanced_basis( #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE -template -int primal_ratio_test(const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - const std::vector& vstatus, - const std::vector& basic_list, - std::vector& x, - std::vector& delta_x, - double& step_length, - int& basic_leaving, - int entering_index, - int direction, - double& work_estimate); +template int primal_ratio_test(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& vstatus, + const std::vector& basic_list, + std::vector& x, + std::vector& delta_x, + double& step_length, + int& basic_leaving, + int entering_index, + int direction, + double& work_estimate); template primal_status_t primal_phase2( int phase, diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index c4338810bc..50e8ca15a7 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -172,7 +172,7 @@ struct simplex_solver_settings_t { i_t augmented; // -1 automatic, 0 to solve with ADAT, 1 to solve with augmented system i_t dualize; // -1 automatic, 0 to not dualize, 1 to dualize i_t ordering; // -1 automatic, 0 to use nested dissection, 1 to use AMD - i_t initial_perturbation; // -1 automatic, 0 to not perturb, 1 to perturb + i_t initial_perturbation; // -1 automatic, 0 to not perturb, 1 to perturb i_t barrier_dual_initial_point; // -1 automatic, 0 to use Lustig, Marsten, and Shanno initial // point, 1 to use initial point form dual least squares problem i_t postsolve_info; // -1 automatic (disabled), 0 disabled, 1 enabled diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index dce731d4d1..fedb9de356 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -796,7 +796,7 @@ lp_status_t solve_linear_program_with_primal(const user_problem_t& use i_t iter = 0; const primal_status_t primal_status = primal_phase2(2, start_time, lp, settings, vstatus, lp_solution, iter); - lp_solution.iterations = iter; + lp_solution.iterations = iter; original_solution.iterations = iter; if (primal_status == primal_status_t::CONCURRENT_LIMIT) { @@ -846,12 +846,8 @@ lp_status_t solve_linear_program_with_primal(const user_problem_t& use } uncrush_primal_solution(user_problem, original_lp, original_solution.x, solution.x); - uncrush_dual_solution(user_problem, - original_lp, - original_solution.y, - original_solution.z, - solution.y, - solution.z); + uncrush_dual_solution( + user_problem, original_lp, original_solution.y, original_solution.z, solution.y, solution.z); solution.objective = original_solution.objective; solution.user_objective = original_solution.user_objective; solution.iterations = original_solution.iterations; @@ -860,7 +856,6 @@ lp_status_t solve_linear_program_with_primal(const user_problem_t& use return map_primal_status_to_lp_status(primal_status); } - template lp_status_t solve_linear_program(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 4a7a6bec31..beb53e8a17 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -594,9 +594,9 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t f_t norm_rhs = vector_norm2(user_problem.rhs); simplex::simplex_solver_settings_t dual_simplex_settings; - dual_simplex_settings.time_limit = settings.time_limit; - dual_simplex_settings.iteration_limit = settings.iteration_limit; - dual_simplex_settings.concurrent_halt = settings.concurrent_halt; + dual_simplex_settings.time_limit = settings.time_limit; + dual_simplex_settings.iteration_limit = settings.iteration_limit; + dual_simplex_settings.concurrent_halt = settings.concurrent_halt; dual_simplex_settings.initial_perturbation = settings.initial_perturbation; if (dual_simplex_settings.concurrent_halt != nullptr) { // Don't show the dual simplex log in concurrent mode. Show the PDLP log instead @@ -677,9 +677,10 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t } template -optimization_problem_solution_t run_primal(mip::problem_t& problem, - pdlp_solver_settings_t const& settings, - const timer_t& timer) +optimization_problem_solution_t run_primal( + mip::problem_t& problem, + pdlp_solver_settings_t const& settings, + const timer_t& timer) { simplex::user_problem_t primal_problem = cuopt_problem_to_user_problem(problem.handle_ptr, problem); diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx index ce3ef6fef3..73a2ccccf9 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cython: profile=False From 117b2b4348bc75a620d36120773fe05492360581 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 16:49:40 -0700 Subject: [PATCH 17/34] Fix work estimate bug in BFRT. And improve work estimates --- cpp/src/dual_simplex/basis_updates.cpp | 2 +- .../bound_flipping_ratio_test.cpp | 5 +- .../bound_flipping_ratio_test.hpp | 2 +- cpp/src/dual_simplex/phase2.cpp | 183 +++++++++++------- 4 files changed, 119 insertions(+), 73 deletions(-) diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index a3d3787183..84468ba097 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2230,7 +2230,7 @@ i_t basis_update_mpf_t::update(const sparse_vector_t& utilde // Ensure the workspace is sorted. Otherwise, the sparse dot will be incorrect. std::sort(xi_workspace_.begin() + m, xi_workspace_.begin() + m + nz, std::less()); - work_estimate_ += (m + nz) * std::log2(m + nz); + work_estimate_ += nz > 1 ? nz * std::log2(nz) : 0; // Gather the workspace into a column of S i_t S_start; diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index cb0964dc05..3fbfbd1f82 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -229,14 +229,14 @@ void bound_flipping_ratio_test_t::heap_passes(const std::vector& }; std::make_heap(bare_idx.begin(), bare_idx.end(), compare); - work_estimate_ += 3 * bare_idx.size(); + work_estimate_ += 10 * bare_idx.size(); while (bare_idx.size() > 0 && slope > 0) { // Remove minimum ratio from the heap and rebalance i_t heap_index = bare_idx.front(); std::pop_heap(bare_idx.begin(), bare_idx.end(), compare); - work_estimate_ += 2 * std::log2(bare_idx.size()); bare_idx.pop_back(); + work_estimate_ += 7 * std::log2(bare_idx.size() + 1); nonbasic_entering = current_indicies[heap_index]; const i_t j = entering_index = nonbasic_list_[nonbasic_entering]; @@ -264,6 +264,7 @@ void bound_flipping_ratio_test_t::heap_passes(const std::vector& // The variable is not bounded. Stop the search. break; } + work_estimate_ += 10; if (toc(start_time_) > settings_.time_limit) { entering_index = RATIO_TEST_TIME_LIMIT; diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp index 2e73d05eff..2f73069451 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp @@ -100,7 +100,7 @@ class bound_flipping_ratio_test_t { i_t n_; i_t m_; - f_t work_estimate_; + f_t work_estimate_{0.0}; }; } // namespace cuopt::mathematical_optimization::simplex diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index f86aeb0333..6e8ef4bbdd 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -161,7 +161,7 @@ void compute_delta_z(const csr_matrix_t& Arow, } } work_estimate += 4 * nz_delta_y; - work_estimate += 4 * nnz_processed; + work_estimate += 5 * nnz_processed; work_estimate += 2 * delta_z_indices.size(); // delta_zB = sigma*ei @@ -905,7 +905,7 @@ bool update_primal_infeasibilities(const lp_problem_t& lp, primal_inf); if (old_val != 0.0 && squared_infeasibilities[j] == 0.0) { became_feasible = true; } } - work_estimate += 8 * nz; + work_estimate += 9 * nz; return became_feasible; } @@ -1257,6 +1257,7 @@ i_t flip_bounds(const lp_problem_t& lp, num_flipped++; } } + work_estimate += 4 * delta_z_indices.size(); return num_flipped; } @@ -2473,6 +2474,21 @@ void prepare_optimality(i_t info, #endif } +template +struct work_timer_t { + work_timer_t(f_t t) : time(t) {} + f_t time{0.0}; + f_t work{0.0}; +}; + +template +work_timer_t& operator+=(work_timer_t& lhs, const work_timer_t& rhs) +{ + lhs.time += rhs.time; + lhs.work += rhs.work; + return lhs; +} + template class phase2_timers_t { public: @@ -2495,60 +2511,89 @@ class phase2_timers_t { { } - void start_timer() + void start_timer(f_t work) { if (!record_time) { return; } start_time = tic(); + start_work = work; + } + + work_timer_t stop_timer(f_t stop_work) + { + if (!record_time) { return work_timer_t(0.0); } + work_timer_t result(toc(start_time)); + result.work = stop_work - start_work; + return result; } - f_t stop_timer() + void print_one(const simplex_solver_settings_t& settings, + const char* name, + const work_timer_t& t, + f_t total_time, + f_t total_work) const { - if (!record_time) { return 0.0; } - return toc(start_time); + const f_t work_per_sec = t.time > 0.0 ? t.work / t.time : f_t(0); + settings.log.printf("%-15s %.2fs %4.1f%% (%.2e work %4.1f%% %.2e/s)\n", + name, + t.time, + total_time > 0.0 ? 100.0 * t.time / total_time : 0.0, + t.work, + total_work > 0.0 ? 100.0 * t.work / total_work : 0.0, + work_per_sec); } void print_timers(const simplex_solver_settings_t& settings) const { if (!record_time) { return; } - const f_t total_time = bfrt_time + pricing_time + btran_time + ftran_time + flip_time + - delta_z_time + lu_update_time + lu_factorization_time + se_norms_time + - se_entering_time + perturb_time + vector_time + objective_time + - update_infeasibility_time; + const f_t total_time = bfrt_time.time + pricing_time.time + btran_time.time + ftran_time.time + + flip_time.time + delta_z_time.time + lu_update_time.time + + lu_factorization_time.time + se_norms_time.time + se_entering_time.time + + perturb_time.time + vector_time.time + objective_time.time + + update_infeasibility_time.time; + const f_t total_work = bfrt_time.work + pricing_time.work + btran_time.work + ftran_time.work + + flip_time.work + delta_z_time.work + lu_update_time.work + + lu_factorization_time.work + se_norms_time.work + se_entering_time.work + + perturb_time.work + vector_time.work + objective_time.work + + update_infeasibility_time.work; // clang-format off - settings.log.printf("BFRT time %.2fs %4.1f%\n", bfrt_time, 100.0 * bfrt_time / total_time); - settings.log.printf("Pricing time %.2fs %4.1f%\n", pricing_time, 100.0 * pricing_time / total_time); - settings.log.printf("BTran time %.2fs %4.1f%\n", btran_time, 100.0 * btran_time / total_time); - settings.log.printf("FTran time %.2fs %4.1f%\n", ftran_time, 100.0 * ftran_time / total_time); - settings.log.printf("Flip time %.2fs %4.1f%\n", flip_time, 100.0 * flip_time / total_time); - settings.log.printf("Delta_z time %.2fs %4.1f%\n", delta_z_time, 100.0 * delta_z_time / total_time); - settings.log.printf("LU update time %.2fs %4.1f%\n", lu_update_time, 100.0 * lu_update_time / total_time); - settings.log.printf("LU factor time %.2fs %4.1f%\n", lu_factorization_time, 100.0 * lu_factorization_time / total_time); - settings.log.printf("SE norms time %.2fs %4.1f%\n", se_norms_time, 100.0 * se_norms_time / total_time); - settings.log.printf("SE enter time %.2fs %4.1f%\n", se_entering_time, 100.0 * se_entering_time / total_time); - settings.log.printf("Perturb time %.2fs %4.1f%\n", perturb_time, 100.0 * perturb_time / total_time); - settings.log.printf("Vector time %.2fs %4.1f%\n", vector_time, 100.0 * vector_time / total_time); - settings.log.printf("Objective time %.2fs %4.1f%\n", objective_time, 100.0 * objective_time / total_time); - settings.log.printf("Inf update time %.2fs %4.1f%\n", update_infeasibility_time, 100.0 * update_infeasibility_time / total_time); - settings.log.printf("Sum %.2fs\n", total_time); + print_one(settings, "BFRT time", bfrt_time, total_time, total_work); + print_one(settings, "Pricing time", pricing_time, total_time, total_work); + print_one(settings, "BTran time", btran_time, total_time, total_work); + print_one(settings, "FTran time", ftran_time, total_time, total_work); + print_one(settings, "Flip time", flip_time, total_time, total_work); + print_one(settings, "Delta_z time", delta_z_time, total_time, total_work); + print_one(settings, "LU update time", lu_update_time, total_time, total_work); + print_one(settings, "LU factor time", lu_factorization_time, total_time, total_work); + print_one(settings, "SE norms time", se_norms_time, total_time, total_work); + print_one(settings, "SE enter time", se_entering_time, total_time, total_work); + print_one(settings, "Perturb time", perturb_time, total_time, total_work); + print_one(settings, "Vector time", vector_time, total_time, total_work); + print_one(settings, "Objective time", objective_time, total_time, total_work); + print_one(settings, "Inf update time", update_infeasibility_time, total_time, total_work); + settings.log.printf("Sum %.2fs (%.2e work %.2e/s)\n", + total_time, + total_work, + total_time > 0.0 ? total_work / total_time : f_t(0)); // clang-format on } - f_t bfrt_time; - f_t pricing_time; - f_t btran_time; - f_t ftran_time; - f_t flip_time; - f_t delta_z_time; - f_t se_norms_time; - f_t se_entering_time; - f_t lu_update_time; - f_t lu_factorization_time; - f_t perturb_time; - f_t vector_time; - f_t objective_time; - f_t update_infeasibility_time; + work_timer_t bfrt_time; + work_timer_t pricing_time; + work_timer_t btran_time; + work_timer_t ftran_time; + work_timer_t flip_time; + work_timer_t delta_z_time; + work_timer_t se_norms_time; + work_timer_t se_entering_time; + work_timer_t lu_update_time; + work_timer_t lu_factorization_time; + work_timer_t perturb_time; + work_timer_t vector_time; + work_timer_t objective_time; + work_timer_t update_infeasibility_time; private: f_t start_time; + f_t start_work; bool record_time; }; @@ -2901,7 +2946,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, i_t basic_leaving_index = -1; i_t leaving_index = -1; f_t max_val; - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); { PHASE2_NVTX_RANGE("DualSimplex::pricing"); if (settings.use_steepest_edge_pricing) { @@ -2922,7 +2967,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, lp, settings, x, basic_list, direction, basic_leaving_index, primal_infeasibility); } } - timers.pricing_time += timers.stop_timer(); + timers.pricing_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); if (leaving_index == -1) { #ifdef CHECK_BASIS_UPDATE for (i_t k = 0; k < basic_list.size(); k++) { @@ -3065,7 +3110,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, // BTran // BT*delta_y = -delta_zB = -sigma*ei - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); delta_y_sparse.clear(); UTsol_sparse.clear(); f_t btran_start_work = ft.work_estimate(); @@ -3073,7 +3118,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, PHASE2_NVTX_RANGE("DualSimplex::btran"); phase2::compute_delta_y(ft, basic_leaving_index, direction, delta_y_sparse, UTsol_sparse); } - timers.btran_time += timers.stop_timer(); + timers.btran_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); solve_work += (ft.work_estimate() - btran_start_work); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { @@ -3097,7 +3142,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, continue; } - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); i_t delta_y_nz0 = 0; const i_t nz_delta_y = delta_y_sparse.i.size(); for (i_t k = 0; k < nz_delta_y; k++) { @@ -3136,7 +3181,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate); } } - timers.delta_z_time += timers.stop_timer(); + timers.delta_z_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return dual_status_t::CONCURRENT_LIMIT; } @@ -3172,7 +3217,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, step_length, nonbasic_entering_index); } else if (bound_flip_ratio) { - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); f_t slope = direction == 1 ? (lp.lower[leaving_index] - x[leaving_index]) : (x[leaving_index] - lp.upper[leaving_index]); bound_flipping_ratio_test_t bfrt(settings, @@ -3195,7 +3240,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, settings.log.printf("Numerical issues encountered in ratio test.\n"); return dual_status_t::NUMERICAL; } - timers.bfrt_time += timers.stop_timer(); + timers.bfrt_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); } else { entering_index = phase2::phase2_ratio_test( lp, settings, vstatus, nonbasic_list, z, delta_z, step_length, nonbasic_entering_index); @@ -3386,7 +3431,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, return dual_status_t::DUAL_UNBOUNDED; } - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); // Update dual variables // y <- y + steplength * delta_y // z <- z + steplength * delta_z @@ -3402,7 +3447,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, settings.log.printf("Numerical issues encountered in update_dual_variables.\n"); return dual_status_t::NUMERICAL; } - timers.vector_time += timers.stop_timer(); + timers.vector_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); #ifdef COMPUTE_DUAL_RESIDUAL std::vector dual_res1; @@ -3413,7 +3458,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } #endif - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); // Update primal variable const i_t num_flipped = phase2::flip_bounds(lp, settings, @@ -3430,12 +3475,12 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, atilde_index, phase2_work_estimate); - timers.flip_time += timers.stop_timer(); + timers.flip_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); total_bound_flips += num_flipped; delta_xB_0_sparse.clear(); if (num_flipped > 0) { - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); phase2::adjust_for_flips(ft, basic_list, delta_z_indices, @@ -3447,10 +3492,10 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, delta_x_flip, x, phase2_work_estimate); - timers.ftran_time += timers.stop_timer(); + timers.ftran_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); } - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); utilde_sparse.clear(); scaled_delta_xB_sparse.clear(); rhs_sparse.from_csc_column(lp.A, entering_index); @@ -3477,7 +3522,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } } solve_work += (ft.work_estimate() - ftran_start_work); - timers.ftran_time += timers.stop_timer(); + timers.ftran_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return dual_status_t::CONCURRENT_LIMIT; } @@ -3489,7 +3534,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, if (primal_step_err > 1e-4) { settings.log.printf("|| A * dx || %e\n", primal_step_err); } #endif - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); f_t se_norms_start_work = ft.work_estimate(); const i_t steepest_edge_status = phase2::update_steepest_edge_norms(settings, basic_list, @@ -3511,18 +3556,18 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } #endif assert(steepest_edge_status == 0); - timers.se_norms_time += timers.stop_timer(); + timers.se_norms_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); solve_work += (ft.work_estimate() - se_norms_start_work); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return dual_status_t::CONCURRENT_LIMIT; } - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); // x <- x + delta_x phase2::update_primal_variables( scaled_delta_xB_sparse, basic_list, delta_x, entering_index, x, phase2_work_estimate); - timers.vector_time += timers.stop_timer(); + timers.vector_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); #ifdef COMPUTE_PRIMAL_RESIDUAL residual = lp.rhs; @@ -3533,7 +3578,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } #endif - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); // TODO(CMM): Do I also need to update the objective due to the bound flips? // TODO(CMM): I'm using the unperturbed objective here, should this be the perturbed objective? phase2::update_objective(basic_list, @@ -3543,9 +3588,9 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, entering_index, obj, phase2_work_estimate); - timers.objective_time += timers.stop_timer(); + timers.objective_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); // Update primal infeasibilities due to changes in basic variables // from flipping bounds #ifdef CHECK_BASIC_INFEASIBILITIES @@ -3598,17 +3643,17 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2::check_primal_infeasibilities( lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); #endif - timers.update_infeasibility_time += timers.stop_timer(); + timers.update_infeasibility_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); // Clear delta_x phase2::clear_delta_x( basic_list, entering_index, scaled_delta_xB_sparse, delta_x, phase2_work_estimate); - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); f_t sum_perturb = 0.0; phase2::compute_perturbation( lp, settings, delta_z_indices, z, objective, sum_perturb, phase2_work_estimate); - timers.perturb_time += timers.stop_timer(); + timers.perturb_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); // Update basis information vstatus[entering_index] = variable_status_t::BASIC; @@ -3631,7 +3676,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 5); #endif - timers.start_timer(); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); // Refactor or update the basis factorization { PHASE2_NVTX_RANGE("DualSimplex::basis_update"); @@ -3647,8 +3692,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2::check_update(lp, settings, ft, basic_list, basic_leaving_index); #endif should_refactor = recommend_refactor == 1; - timers.lu_update_time += timers.stop_timer(); - timers.start_timer(); + timers.lu_update_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); + timers.start_timer(phase2_work_estimate + ft.work_estimate()); } #ifdef CHECK_BASIC_INFEASIBILITIES @@ -3726,7 +3771,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2::check_basic_infeasibilities(basic_list, basic_mark, infeasibility_indices, 7); #endif } - timers.lu_factorization_time += timers.stop_timer(); + timers.lu_factorization_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); #ifdef STEEPEST_EDGE_DEBUG if (iter < 100 || iter % 100 == 0)) From 99207b22c0da28146cab92ac09d97e4960a5842b Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 6 Aug 2026 19:16:22 -0700 Subject: [PATCH 18/34] Harris ratio test; timers in primal; limit feasibility pump to do less work than root relaxation --- cpp/src/branch_and_bound/branch_and_bound.cpp | 14 +- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + cpp/src/dual_simplex/primal.cpp | 269 +++++++++++++++--- cpp/src/dual_simplex/primal.hpp | 3 +- 4 files changed, 237 insertions(+), 50 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 99e4c2416d..6471071fbd 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3440,6 +3440,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( simplex_solver_settings_t primal_settings = settings_; primal_settings.log.log = false; primal_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); + primal_settings.work_limit = root_relax_work_estimate_; simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, exploration_stats_.start_time, @@ -3503,10 +3504,11 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( settings_.log.printf( "Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables " - "%d/%d. Time %.2f\n", + "%d/%d. Work estimate %.2e, Time %.2f\n", iter, best_num_fractional, num_fractional, + primal_work_estimate, toc(dual_degenerate_feasibility_pump_start_time)); if (best_num_fractional < num_fractional) { // Translate the vstatus from the reduced problem to the vstatus for the original problem @@ -4032,7 +4034,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solving_root_relaxation_ = true; f_t root_relax_start_time = tic(); - f_t root_relax_work_estimate = 0.0; + root_relax_work_estimate_ = 0.0; if (!enable_concurrent_lp_root_solve()) { // RINS/SUBMIP path settings_.log.printf("\n"); @@ -4046,7 +4048,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut nonbasic_list, root_vstatus_, edge_norms_, - root_relax_work_estimate); + root_relax_work_estimate_); root_relax_solved_by = DualSimplex; exploration_stats_.total_simplex_iters = root_relax_soln_.iterations; @@ -4060,7 +4062,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basic_list, nonbasic_list, edge_norms_, - root_relax_work_estimate); + root_relax_work_estimate_); } solving_root_relaxation_ = false; @@ -4122,8 +4124,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut method_to_string(root_relax_solved_by)); settings_.log.printf("Dual simplex iteration %d work estimate %.2e work per second %.2e\n", root_iterations, - root_relax_work_estimate, - root_relax_work_estimate / root_relax_elapsed_time); + root_relax_work_estimate_, + root_relax_work_estimate_ / root_relax_elapsed_time); settings_.log.printf("Root relaxation objective %+.8e\n\n", root_relax_soln_.user_objective); assert(root_vstatus_.size() == original_lp_.num_cols); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 17f6f7a3a3..eaf622b1e3 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -251,6 +251,7 @@ class branch_and_bound_t { simplex::lp_solution_t root_relax_soln_; simplex::lp_solution_t root_crossover_soln_; method_t root_relax_solved_by{Unset}; + f_t root_relax_work_estimate_; std::vector edge_norms_; std::atomic root_crossover_solution_set_{false}; omp_atomic_t root_lp_current_lower_bound_; diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 27351a3685..8867c8c9b4 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -18,6 +18,112 @@ namespace cuopt::mathematical_optimization::simplex { +template +struct primal_work_timer_t { + primal_work_timer_t(f_t t) : time(t) {} + f_t time{0.0}; + f_t work{0.0}; +}; + +template +primal_work_timer_t& operator+=(primal_work_timer_t& lhs, + const primal_work_timer_t& rhs) +{ + lhs.time += rhs.time; + lhs.work += rhs.work; + return lhs; +} + +template +class primal_timers_t { + public: + primal_timers_t(bool should_time) + : record_time(should_time), + pricing_time(0), + ftran_time(0), + ratio_test_time(0), + btran_time(0), + delta_z_time(0), + update_duals_time(0), + lu_update_time(0), + lu_factorization_time(0), + update_x_time(0) + { + } + + void start_timer(f_t work) + { + if (!record_time) { return; } + start_time_ = tic(); + start_work_ = work; + } + + primal_work_timer_t stop_timer(f_t stop_work) + { + if (!record_time) { return primal_work_timer_t(0.0); } + primal_work_timer_t result(toc(start_time_)); + result.work = stop_work - start_work_; + return result; + } + + void print_one(const simplex_solver_settings_t& settings, + const char* name, + const primal_work_timer_t& t, + f_t total_time, + f_t total_work) const + { + const f_t work_per_sec = t.time > 0.0 ? t.work / t.time : f_t(0); + settings.log.printf("%-15s %.2fs %4.1f%% (%.2e work %4.1f%% %.2e/s)\n", + name, + t.time, + total_time > 0.0 ? 100.0 * t.time / total_time : 0.0, + t.work, + total_work > 0.0 ? 100.0 * t.work / total_work : 0.0, + work_per_sec); + } + + void print_timers(const simplex_solver_settings_t& settings) const + { + if (!record_time) { return; } + const f_t total_time = pricing_time.time + ftran_time.time + ratio_test_time.time + + btran_time.time + delta_z_time.time + update_duals_time.time + + lu_update_time.time + lu_factorization_time.time + update_x_time.time; + const f_t total_work = pricing_time.work + ftran_time.work + ratio_test_time.work + + btran_time.work + delta_z_time.work + update_duals_time.work + + lu_update_time.work + lu_factorization_time.work + update_x_time.work; + // clang-format off + print_one(settings, "Pricing time", pricing_time, total_time, total_work); + print_one(settings, "FTran time", ftran_time, total_time, total_work); + print_one(settings, "Ratio test", ratio_test_time, total_time, total_work); + print_one(settings, "BTran time", btran_time, total_time, total_work); + print_one(settings, "Delta_z time", delta_z_time, total_time, total_work); + print_one(settings, "Update duals", update_duals_time, total_time, total_work); + print_one(settings, "LU update time", lu_update_time, total_time, total_work); + print_one(settings, "LU factor time", lu_factorization_time, total_time, total_work); + print_one(settings, "Update x time", update_x_time, total_time, total_work); + settings.log.printf("Sum %.2fs (%.2e work %.2e/s)\n", + total_time, + total_work, + total_time > 0.0 ? total_work / total_time : f_t(0)); + // clang-format on + } + + primal_work_timer_t pricing_time; + primal_work_timer_t ftran_time; + primal_work_timer_t ratio_test_time; + primal_work_timer_t btran_time; + primal_work_timer_t delta_z_time; + primal_work_timer_t update_duals_time; + primal_work_timer_t lu_update_time; + primal_work_timer_t lu_factorization_time; + primal_work_timer_t update_x_time; + + private: + f_t start_time_; + f_t start_work_; + bool record_time; +}; + namespace { template @@ -156,7 +262,7 @@ i_t phase2_pricing(const lp_problem_t& lp, } } } - work_estimate += 4 * (n - m); + work_estimate += 5 * (n - m); return entering_index; } @@ -281,7 +387,7 @@ void compute_delta_z(const csr_matrix_t& Arow, const i_t j = Arow.j[p]; if (vstatus[j] != variable_status_t::BASIC) { delta_z[j] -= Arow.x[p] * delta_y_i; } } - work_estimate += 4 * (row_end - row_start); + work_estimate += 5 * (row_end - row_start); } work_estimate += 4 * delta_y.i.size(); } @@ -422,104 +528,147 @@ i_t primal_ratio_test(const lp_problem_t& lp, f_t& work_estimate) { const i_t m = lp.num_rows; - const i_t n = lp.num_cols; basic_leaving = -1; i_t leaving_index = -1; - f_t min_val = inf; - f_t current_dx = 0.0; constexpr f_t pivot_tol = 1e-8; + constexpr f_t harris_tol = 1e-8; + + // Harris ratio test: two passes. + // Pass 1: find the maximum step length alpha_1 such that no variable + // moves more than harris_tol past its bound. + // Pass 2: among all candidates with ratio <= alpha_1, pick the one + // with the largest pivot (|delta_x[j]|). + + f_t alpha_1 = inf; // Entering variable can hit its opposite bound: limit step by that if (direction > 0 && lp.upper[entering_index] < inf) { const f_t limit = lp.upper[entering_index] - x[entering_index]; - if (limit >= 0 && limit < min_val) { - min_val = limit; - leaving_index = -1; // no basic leaves; will be handled by caller + if (limit >= 0 && limit < alpha_1) { alpha_1 = limit; } + } else if (direction < 0 && lp.lower[entering_index] > -inf) { + const f_t limit = x[entering_index] - lp.lower[entering_index]; + if (limit >= 0 && limit < alpha_1) { alpha_1 = limit; } + } + + // Pass 1: compute alpha_1 (Harris step) + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + if (std::abs(delta_x[j]) <= pivot_tol) { continue; } + + // Already below lower and moving back up: stop exactly at the bound. + // No harris tolerance here — these variables are already infeasible + // and must not overshoot their bound (needed for Phase I correctness). + if (x[j] < lp.lower[j] && delta_x[j] > pivot_tol && lp.lower[j] > -inf) { + const f_t ratio = (lp.lower[j] - x[j]) / delta_x[j]; + if (ratio >= 0 && ratio < alpha_1) { alpha_1 = ratio; } + } + // Already above upper and moving back down: stop exactly at the bound. + if (x[j] > lp.upper[j] && delta_x[j] < -pivot_tol && lp.upper[j] < inf) { + const f_t ratio = (lp.upper[j] - x[j]) / delta_x[j]; + if (ratio >= 0 && ratio < alpha_1) { alpha_1 = ratio; } + } + + if (lp.lower[j] > -inf && delta_x[j] < -pivot_tol) { + // xj + step * delta_x[j] >= lp.lower[j] - harris_tol + f_t neum = lp.lower[j] - x[j] - harris_tol; + if (neum > 0 && neum <= settings.primal_tol) { neum = 0.0; } + f_t ratio = neum / delta_x[j]; + if (ratio >= 0 && ratio < alpha_1) { alpha_1 = ratio; } + } + if (lp.upper[j] < inf && delta_x[j] > pivot_tol) { + // xj + step * delta_x[j] <= lp.upper[j] + harris_tol + f_t neum = lp.upper[j] - x[j] + harris_tol; + if (neum < 0 && -neum <= settings.primal_tol) { neum = 0.0; } + f_t ratio = neum / delta_x[j]; + if (ratio >= 0 && ratio < alpha_1) { alpha_1 = ratio; } + } + } + + // Pass 2: among candidates with exact ratio <= alpha_1, pick largest pivot + f_t best_pivot = 0.0; + step_length = alpha_1; + + // Check entering variable bound (no pivot selection needed — it's fixed at direction) + if (direction > 0 && lp.upper[entering_index] < inf) { + const f_t limit = lp.upper[entering_index] - x[entering_index]; + if (limit >= 0 && limit <= alpha_1) { + // Entering hits its own bound — this is always pivot = 1.0 effectively + step_length = limit; + leaving_index = -1; basic_leaving = -1; + best_pivot = inf; // Always prefer this if it's within alpha_1 } } else if (direction < 0 && lp.lower[entering_index] > -inf) { const f_t limit = x[entering_index] - lp.lower[entering_index]; - if (limit >= 0 && limit < min_val) { - min_val = limit; + if (limit >= 0 && limit <= alpha_1) { + step_length = limit; leaving_index = -1; basic_leaving = -1; + best_pivot = inf; } } for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; - if (delta_x[j] == 0.0) { continue; } + if (std::abs(delta_x[j]) <= pivot_tol) { continue; } + + const f_t abs_dx = std::abs(delta_x[j]); // Already below lower and moving back up: stop when we reach the lower bound. // Without this, phase I can take an unbounded step (false unbounded) or skip the // breakpoint of the piecewise phase-I objective and stall still infeasible. if (x[j] < lp.lower[j] && delta_x[j] > pivot_tol && lp.lower[j] > -inf) { const f_t ratio = (lp.lower[j] - x[j]) / delta_x[j]; - if (ratio >= 0 && ratio < min_val) { - min_val = ratio; + if (ratio >= 0 && ratio <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = ratio; basic_leaving = k; leaving_index = j; - current_dx = delta_x[j]; } } - // Already above upper and moving back down: stop when we reach the upper bound. + // Already above upper and moving back down if (x[j] > lp.upper[j] && delta_x[j] < -pivot_tol && lp.upper[j] < inf) { const f_t ratio = (lp.upper[j] - x[j]) / delta_x[j]; - if (ratio >= 0 && ratio < min_val) { - min_val = ratio; + if (ratio >= 0 && ratio <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = ratio; basic_leaving = k; leaving_index = j; - current_dx = -delta_x[j]; } } if (lp.lower[j] > -inf && delta_x[j] < -pivot_tol) { // xj + step * delta_x[j] >= lp.lower[j] - // step * delta_x[j] >= lp.lower[j] - x[j] // step <= (lp.lower[j] - x[j]) / delta_x[j], delta_x[j] < 0 f_t neum = lp.lower[j] - x[j]; // A basic sitting below its bound (within the primal tolerance) is on - // the bound numerically, but gives a tiny negative ratio. Dropping it lets - // the step run straight through the bound, so treat it as a zero-length - // block. A genuine violation is left to the branches above, which stop at - // the bound when the variable moves back toward it. + // the bound numerically. Treat it as a zero-length block. if (neum > 0 && neum <= settings.primal_tol) { neum = 0.0; } f_t ratio = neum / delta_x[j]; - if (ratio >= 0 && ratio < min_val) { - min_val = ratio; + if (ratio >= 0 && ratio <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = ratio; basic_leaving = k; leaving_index = j; - current_dx = -delta_x[j]; - } else if (ratio >= 0 && ratio < min_val + 1e-9 && -delta_x[j] > current_dx) { - min_val = ratio; - basic_leaving = k; - leaving_index = j; - current_dx = -delta_x[j]; } } if (lp.upper[j] < inf && delta_x[j] > pivot_tol) { // xj + step * delta_x[j] <= lp.upper[j] - // step * delta_x[j] <= lp.upper[j] - x[j] // step <= (lp.upper[j] - x[j]) / delta_x[j], delta_x[j] > 0 f_t neum = lp.upper[j] - x[j]; - // Mirror of the lower bound case: slightly above the bound is considered on the bound. + // Mirror of the lower bound case: slightly above the bound is on the bound. if (neum < 0 && -neum <= settings.primal_tol) { neum = 0.0; } f_t ratio = neum / delta_x[j]; - if (ratio >= 0 && ratio < min_val) { - min_val = ratio; - basic_leaving = k; - leaving_index = j; - current_dx = delta_x[j]; - } else if (ratio >= 0 && ratio < min_val + 1e-9 && delta_x[j] > current_dx) { - min_val = ratio; + if (ratio >= 0 && ratio <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = ratio; basic_leaving = k; leaving_index = j; - current_dx = delta_x[j]; } } } + work_estimate += 10 * m; - step_length = min_val; return leaving_index; } @@ -763,7 +912,14 @@ primal_status_t primal_phase2_with_advanced_basis( work_estimate += basis_update.work_estimate(); basis_update.clear_work_estimate(); + if (work_estimate > settings.work_limit) { + return primal_status_t::WORK_LIMIT; + } + + primal_timers_t timers(false); + while (iter < iter_limit) { + timers.start_timer(work_estimate + basis_update.work_estimate()); i_t nonbasic_entering = -1; i_t direction; i_t entering_index = phase2_pricing(lp, @@ -775,6 +931,7 @@ primal_status_t primal_phase2_with_advanced_basis( nonbasic_entering, dual_inf, work_estimate); + timers.pricing_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); if (entering_index == -1) { if (phase == 2) { // Verify optimality with a consistent basic solution: refactor, put @@ -883,6 +1040,7 @@ primal_status_t primal_phase2_with_advanced_basis( settings.log.printf("Primal residual ||Ax-b||: %.2e\n", primal_constraint_residual(lp, x)); } + timers.print_timers(settings); return primal_status_t::OPTIMAL; } else { primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); @@ -968,6 +1126,7 @@ primal_status_t primal_phase2_with_advanced_basis( work_estimate += 3 * rhs_sparse.i.size(); sparse_vector_t scaled_delta_xB_sparse(m, 0); sparse_vector_t utilde_sparse(m, 0); + timers.start_timer(work_estimate + basis_update.work_estimate()); basis_update.b_solve(rhs_sparse, scaled_delta_xB_sparse, utilde_sparse); std::vector scaled_delta_xB(m); scaled_delta_xB_sparse.to_dense(scaled_delta_xB); @@ -984,6 +1143,7 @@ primal_status_t primal_phase2_with_advanced_basis( } work_estimate += 2 * (n - m); delta_x[entering_index] = direction; + timers.ftran_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); #ifdef CHECK_NULLSPACE std::vector residual(m, 0.0); @@ -997,6 +1157,7 @@ primal_status_t primal_phase2_with_advanced_basis( } #endif + timers.start_timer(work_estimate + basis_update.work_estimate()); i_t basic_leaving; f_t step_length; i_t leaving_index = primal_ratio_test(lp, @@ -1010,6 +1171,7 @@ primal_status_t primal_phase2_with_advanced_basis( entering_index, direction, work_estimate); + timers.ratio_test_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); if (leaving_index == -1 && step_length >= inf) { settings.log.printf("No leaving variable. Primal unbounded?\n"); return primal_status_t::PRIMAL_UNBOUNDED; @@ -1017,10 +1179,12 @@ primal_status_t primal_phase2_with_advanced_basis( const bool basis_updated = (leaving_index != -1); bool recompute_duals = false; + timers.start_timer(work_estimate + basis_update.work_estimate()); for (i_t j = 0; j < n; ++j) { x[j] += step_length * delta_x[j]; } work_estimate += 2 * n; + timers.update_x_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); #ifdef COMPUTE_RESIDUAL f_t debug_primal_residual = primal_constraint_residual(lp, x); @@ -1038,7 +1202,9 @@ primal_status_t primal_phase2_with_advanced_basis( bool should_refactor = basis_update.num_updates() > settings.refactor_frequency; f_t dual_step_length = 0.0; if (!should_refactor) { + timers.start_timer(work_estimate + basis_update.work_estimate()); compute_delta_y(basis_update, basic_leaving, delta_y, etilde); + timers.btran_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); const f_t pivot = scaled_delta_xB[basic_leaving]; dual_step_length = compute_dual_step_length(z[entering_index], pivot); } @@ -1076,12 +1242,19 @@ primal_status_t primal_phase2_with_advanced_basis( x[leaving_index] = leave_bound; if (!should_refactor) { + timers.start_timer(work_estimate + basis_update.work_estimate()); compute_delta_z(Arow, vstatus, delta_y, delta_z, work_estimate); + timers.delta_z_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); + timers.start_timer(work_estimate + basis_update.work_estimate()); update_y(dual_step_length, delta_y, y, work_estimate); update_z(dual_step_length, nonbasic_list, entering_index, delta_z, z, work_estimate); + timers.update_duals_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); + timers.start_timer(work_estimate + basis_update.work_estimate()); should_refactor = basis_update.update(utilde_sparse, etilde, basic_leaving) == 1; + timers.lu_update_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); } if (should_refactor) { + timers.start_timer(work_estimate + basis_update.work_estimate()); i_t rank = basis_update.refactor_basis( lp.A, settings, lp.lower, lp.upper, start_time, basic_list, nonbasic_list, vstatus); if (rank == CONCURRENT_HALT_RETURN) { return primal_status_t::CONCURRENT_LIMIT; } @@ -1097,6 +1270,8 @@ primal_status_t primal_phase2_with_advanced_basis( set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); compute_basic_primal_variables( lp, basis_update, basic_list, nonbasic_list, x, work_estimate); + timers.lu_factorization_time += + timers.stop_timer(work_estimate + basis_update.work_estimate()); } else if (rebuild_x_after_bound_snap) { // FT update already matches the new basis; recompute x_B with the leaving variable // snapped onto its bound. @@ -1172,9 +1347,17 @@ primal_status_t primal_phase2_with_advanced_basis( work_estimate += basis_update.work_estimate(); basis_update.clear_work_estimate(); - if (now > settings.time_limit) { return primal_status_t::TIME_LIMIT; } + if (now > settings.time_limit) { + timers.print_timers(settings); + return primal_status_t::TIME_LIMIT; + } + if (work_estimate > settings.work_limit) { + timers.print_timers(settings); + return primal_status_t::WORK_LIMIT; + } } + timers.print_timers(settings); if (iter >= iter_limit) { return primal_status_t::ITERATION_LIMIT; } return primal_status_t::NUMERICAL; diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index 7e4d280655..fc47d90368 100644 --- a/cpp/src/dual_simplex/primal.hpp +++ b/cpp/src/dual_simplex/primal.hpp @@ -26,7 +26,8 @@ enum class primal_status_t { TIME_LIMIT = 5, ITERATION_LIMIT = 6, CONCURRENT_LIMIT = 7, - NOT_LOADED = 8 + WORK_LIMIT = 8, + NOT_LOADED = 9 }; template From eb47c20cf8c6ddf69bfca7056869e2e17e90669d Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 7 Aug 2026 19:53:26 -0700 Subject: [PATCH 19/34] Add reduced cost bounds table. Change objective in feasibility pump. Fix work limit in feasibility pump. Check for reduced cost violation before calling primal simplex. Check if we reduced number of integer infeasibilites when we hit a work limit --- cpp/src/branch_and_bound/branch_and_bound.cpp | 268 +++++++++++++++--- cpp/src/branch_and_bound/branch_and_bound.hpp | 154 +++++++++- 2 files changed, 386 insertions(+), 36 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 6471071fbd..a41bc1bd95 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -445,6 +445,69 @@ void branch_and_bound_t::report( settings_.log.printf("%s\n", log_line.c_str()); } + +template +void branch_and_bound_t::update_reduced_cost_bounds( + f_t relaxation_objective, + const std::vector& reduced_costs, + const std::vector& var_status, + reduced_cost_bounds_t& reduced_cost_bounds) +{ + const i_t n = reduced_cost_bounds.num_cols(); + const f_t threshold = 100.0 * settings_.integer_tol; + const f_t tol = 1e-2; + for (i_t j = 0; j < n; ++j) { + if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold && + var_status[j] != variable_status_t::BASIC) { + const f_t lower_j = original_lp_.lower[j]; + const f_t upper_j = original_lp_.upper[j]; + + // x_j <= l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] + // Let u_tilde_j = u_j - epsilon, so that floor(u_tilde_j) = u_j - 1 + // We want to solve for want the incumbent objective needs to be to make + // x_j <= u_tilde_j + // This means l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] <= u_tilde_j + // Or equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * (u_tilde_j - l_j) when reduced_costs[j] > 0 + if (lower_j > -inf && reduced_costs[j] > 0) { + const f_t u_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? upper_j - tol + : std::max(upper_j - 1.0, lower_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::floor(u_tilde_j) : u_tilde_j; + const f_t diff = u_tilde_j - lower_j; + const f_t objective_j = relaxation_objective + diff * reduced_costs[j]; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == upper_j - 1.0) || + var_types_[j] != variable_type_t::INTEGER) && std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_upper_bound(j, objective_j, bound_j); + //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. Info %d\n", objective_j, bound_j, j, info); + } + } + + // x_j >= u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] when reduced_costs[j] < 0 + // Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 + // We want to solve for want the incumbent objective needs to be to make + // x_j >= l_tilde_j + // This means u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j + // Or equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * (l_tilde_j - u_j) when reduced_costs[j] < 0 + if (upper_j < inf && reduced_costs[j] < 0) { + const f_t l_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? lower_j + tol + : std::min(lower_j + 1.0, upper_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::ceil(l_tilde_j) : l_tilde_j; + const f_t diff = l_tilde_j - upper_j; + const f_t objective_j = relaxation_objective + diff * reduced_costs[j]; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == lower_j + 1.0) || + var_types_[j] != variable_type_t::INTEGER) && std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_lower_bound(j, objective_j, bound_j); + //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. Info %d\n", objective_j, bound_j, j, info); + } + } + } + } +} + + template i_t branch_and_bound_t::find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, @@ -2946,7 +3009,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( } template -auto branch_and_bound_t::do_cut_pass( +typename branch_and_bound_t::cut_pass_result_t branch_and_bound_t::do_cut_pass( [[maybe_unused]] i_t cut_pass, mip_solution_t& solution, i_t& num_fractional, @@ -2963,8 +3026,9 @@ auto branch_and_bound_t::do_cut_pass( f_t& last_upper_bound, f_t& last_objective, f_t root_relax_objective, + reduced_cost_bounds_t& reduced_cost_bounds, i_t& cut_pool_size, - [[maybe_unused]] const std::vector& saved_solution) -> cut_pass_result_t + [[maybe_unused]] const std::vector& saved_solution) { #ifdef PRINT_FRACTIONAL_INFO settings_.log.printf("Found %d fractional variables on cut pass %d\n", num_fractional, cut_pass); @@ -3006,6 +3070,28 @@ auto branch_and_bound_t::do_cut_pass( if (cut_generation_time > 1.0) { settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); } + + num_fractional = fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); + + f_t pivot_out_integer_variables_start_time = tic(); + i_t num_integer_increased = pivot_out_integer_variables(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", num_integer_increased, toc(pivot_out_integer_variables_start_time)); + dual_degenerate_feasibility_pump(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + // Score the cuts f_t score_start_time = tic(); cut_pool.score_cuts(root_relax_soln_.x); @@ -3073,14 +3159,20 @@ auto branch_and_bound_t::do_cut_pass( if (settings_.reduced_cost_strengthening >= 1 && upper_bound_.load() < last_upper_bound) { mutex_upper_.lock(); last_upper_bound = upper_bound_.load(); - std::vector lower_bounds; - std::vector upper_bounds; - find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); + std::vector lower_bounds = original_lp_.lower; + std::vector upper_bounds = original_lp_.upper; + f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); + i_t new_bounds = reduced_cost_bounds.update_bounds_from_new_incumbent( + upper_bound_.load(), var_types_, lower_bounds, upper_bounds); mutex_upper_.unlock(); mutex_original_lp_.lock(); original_lp_.lower = lower_bounds; original_lp_.upper = upper_bounds; mutex_original_lp_.unlock(); + if (1 || new_bounds > 0) { + settings_.log.printf( + "Updated %d bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", new_bounds, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); + } } // Try to do bound strengthening @@ -3178,27 +3270,13 @@ auto branch_and_bound_t::do_cut_pass( } root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + if (settings_.reduced_cost_strengthening >= 1) { + update_reduced_cost_bounds(root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + } + // Refresh fractional info after re-solving with cuts; the pre-cut count is stale. num_fractional = fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); - pivot_out_integer_variables(original_lp_, - basic_list, - nonbasic_list, - root_vstatus_, - root_relax_soln_, - basis_update, - num_fractional, - fractional); - - dual_degenerate_feasibility_pump(original_lp_, - basic_list, - nonbasic_list, - root_vstatus_, - root_relax_soln_, - basis_update, - num_fractional, - fractional); - if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->root_lp_with_cuts = compute_user_objective(original_lp_, root_objective_); @@ -3395,6 +3473,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( } simplex::basis_update_mpf_t reduced_basis_update = basis_update; + reduced_basis_update.clear_work_estimate(); for (i_t k = 0; k < m; k++) { reduced_basic_list[k] = original_col_to_reduced_col[basic_list[k]]; } @@ -3426,15 +3505,76 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( lp_reduced.objective[reduced_col] = -1; } } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_LOWER) { - lp_reduced.objective[reduced_col] = 1; + lp_reduced.objective[reduced_col] = 0.1; } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_UPPER) { - lp_reduced.objective[reduced_col] = -1; + lp_reduced.objective[reduced_col] = -0.1; } } reduced_col++; } } + // Check reduced costs before calling primal simplex. + // Compute y = B^{-T} * c_B (BTRAN with the pump objective on basic variables) + std::vector c_basic_pump(m, 0.0); + for (i_t k = 0; k < m; k++) { + c_basic_pump[k] = lp_reduced.objective[reduced_basic_list[k]]; + } + std::vector y_pump(m); + reduced_basis_update.b_transpose_solve(c_basic_pump, y_pump); + + // Check if any nonbasic has a violated reduced cost + i_t num_violated = 0; + f_t max_violation = 0.0; + const i_t num_nonbasics_reduced = reduced_nonbasic_list.size(); + for (i_t k = 0; k < num_nonbasics_reduced; k++) { + const i_t j = reduced_nonbasic_list[k]; + // z[j] = c[j] - y^T * A(:,j) + f_t zj = lp_reduced.objective[j]; + const i_t col_start = A_reduced.col_start[j]; + const i_t col_end = A_reduced.col_start[j + 1]; + for (i_t p = col_start; p < col_end; p++) { + zj -= y_pump[A_reduced.i[p]] * A_reduced.x[p]; + } + // Check pricing condition + bool violated = false; + if (reduced_vstatus[j] == variable_status_t::NONBASIC_LOWER || + reduced_vstatus[j] == variable_status_t::NONBASIC_FIXED) { + if (zj < -settings_.dual_tol) { violated = true; } + } else if (reduced_vstatus[j] == variable_status_t::NONBASIC_UPPER) { + if (zj > settings_.dual_tol) { violated = true; } + } + if (violated) { + num_violated++; + max_violation = std::max(max_violation, std::abs(zj)); + } + } + + if (num_violated == 0) { + settings_.log.printf( + "Degenerate feasibility pump (%d/%d): skipping primal simplex, no violated reduced costs " + "(%d nonbasics checked)\n", + pump_iter, + max_pump_iter, + num_nonbasics_reduced); + primal_work_estimate += reduced_basis_update.work_estimate(); + reduced_basis_update.clear_work_estimate(); + // Don't count this as a pump iteration, but break if we've skipped twice + // in a row (perturbation isn't helping) + if (stalled) { break; } + stalled = true; + pump_iter--; + continue; + } + settings_.log.printf( + "Degenerate feasibility pump (%d/%d): %d violated reduced costs (max %.2e) out of %d " + "nonbasics\n", + pump_iter, + max_pump_iter, + num_violated, + max_violation, + num_nonbasics_reduced); + bool recompute_basis = false; const i_t iter_before = iter; simplex_solver_settings_t primal_settings = settings_; @@ -3498,18 +3638,59 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( best_reduced_vstatus = reduced_vstatus; } } else { + settings_.log.printf( + "Degenerate feasibility pump: primal simplex returned non-optimal status %d at pump_iter " + "%d. Work estimate %.2e\n", + static_cast(lp_status), + pump_iter, + primal_work_estimate); + // Even if we hit work/time limit, the solution may have improved. + // Check fractional count before breaking. + if (lp_status == simplex::primal_status_t::WORK_LIMIT || + lp_status == simplex::primal_status_t::TIME_LIMIT) { + std::vector adjusted_solution(lp.num_cols, 0.0); + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || + std::abs(soln.z[j]) <= settings_.tight_tol) { + adjusted_solution[j] = reduced_solution.x[reduced_col++]; + } else { + adjusted_solution[j] = soln.x[j]; + } + } + std::vector residual = lp.rhs; + matrix_vector_multiply(lp.A, 1.0, adjusted_solution, -1.0, residual); + const f_t primal_residual = vector_norm_inf(residual); + if (primal_residual <= 1e-6) { + std::vector tmp_fractional; + i_t num_fractional_reduced = + fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); + settings_.log.printf( + "Degenerate feasibility pump (%d/%d): after work/time limit, fractional " + "variables %d/%d\n", + pump_iter, + max_pump_iter, + num_fractional_reduced, + num_fractional); + if (num_fractional_reduced < best_num_fractional) { + best_num_fractional = num_fractional_reduced; + best_reduced_vstatus = reduced_vstatus; + } + } + } break; } } settings_.log.printf( "Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables " - "%d/%d. Work estimate %.2e, Time %.2f\n", + "%d/%d. Work estimate %.2e, Time %.2f, Basis updates %d\n", iter, best_num_fractional, num_fractional, primal_work_estimate, - toc(dual_degenerate_feasibility_pump_start_time)); + toc(dual_degenerate_feasibility_pump_start_time), + reduced_basis_update.num_updates()); if (best_num_fractional < num_fractional) { // Translate the vstatus from the reduced problem to the vstatus for the original problem i_t reduced_cols = 0; @@ -3690,7 +3871,7 @@ void branch_and_bound_t::apply_delta_x_for_integer_pivot( } template -void branch_and_bound_t::pivot_out_integer_variables( +i_t branch_and_bound_t::pivot_out_integer_variables( const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, @@ -3700,13 +3881,13 @@ void branch_and_bound_t::pivot_out_integer_variables( i_t& num_fractional, std::vector& fractional) { - if (num_fractional == 0) { return; } + if (num_fractional == 0) { return 0; } f_t pivot_out_integer_variables_start_time = tic(); std::vector zero_reduced_costs_vars; std::vector zero_reduced_costs_vars_nonbasic_index; bool dual_degenerate = check_for_dual_degeneracy( solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); - if (!dual_degenerate) { return; } + if (!dual_degenerate) { return 0; } lp_solution_t soln_copy = solution; std::vector basic_list_copy = basic_list; @@ -3937,11 +4118,13 @@ void branch_and_bound_t::pivot_out_integer_variables( if (num_new_fractional < start_num_fractional) { i_t num_integer_increased = start_num_fractional - num_new_fractional; integer_pivots_.fetch_add(num_integer_increased, std::memory_order_release); +#if 0 settings_.log.printf("Pivoted out %d integer variables: %d -> %d in %.2f\n", num_integer_increased, start_num_fractional, num_new_fractional, toc(pivot_out_integer_variables_start_time)); +#endif num_fractional = num_new_fractional; fractional = new_fractional; basic_list = basic_list_copy; @@ -3949,7 +4132,9 @@ void branch_and_bound_t::pivot_out_integer_variables( vstatus = vstatus_copy; basis_update = basis_update_copy; solution = soln_copy; + return num_integer_increased; } + return 0; } template @@ -4168,7 +4353,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut is_running_ = true; lower_bound_numerical_ = inf; - pivot_out_integer_variables(original_lp_, + reduced_cost_bounds_t reduced_cost_bounds(original_lp_.num_cols); + update_reduced_cost_bounds(root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + + f_t pivot_out_integer_variables_start_time = tic(); + i_t num_integer_increased = pivot_out_integer_variables(original_lp_, basic_list, nonbasic_list, root_vstatus_, @@ -4176,6 +4365,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, num_fractional, fractional); + settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", num_integer_increased, toc(pivot_out_integer_variables_start_time)); dual_degenerate_feasibility_pump(original_lp_, basic_list, @@ -4274,6 +4464,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut last_upper_bound, last_objective, root_relax_objective, + reduced_cost_bounds, cut_pool_size, saved_solution); root_fj_cpu_worker.stop(); @@ -4390,10 +4581,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { - std::vector lower_bounds; - std::vector upper_bounds; - i_t num_fixed = find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); - if (num_fixed > 0) { + std::vector lower_bounds = original_lp_.lower; + std::vector upper_bounds = original_lp_.upper; + f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); + i_t num_changed = reduced_cost_bounds.update_bounds_from_new_incumbent( + upper_bound_.load(), var_types_, lower_bounds, upper_bounds); + settings_.log.printf("Updated %d bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", num_changed, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); + mutex_original_lp_.lock(); + original_lp_.lower = lower_bounds; + original_lp_.upper = upper_bounds; + mutex_original_lp_.unlock(); + if (num_changed > 0) { std::vector bounds_changed(original_lp_.num_cols, true); std::vector row_sense; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index eaf622b1e3..185e3ce79e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include @@ -90,6 +91,152 @@ struct deterministic_bfs_policy_t; template struct deterministic_diving_policy_t; +template +struct objective_bound_pair_t { + objective_bound_pair_t() + : objective(std::numeric_limits::quiet_NaN()), + bound(std::numeric_limits::quiet_NaN()) + { + } + objective_bound_pair_t(f_t objective_in, f_t bound_in) + : objective(objective_in), bound(bound_in) + { + } + bool is_valid() { return objective == objective && bound == bound; } + f_t objective; + f_t bound; +}; + +template +class reduced_cost_bounds_t { + public: + reduced_cost_bounds_t(i_t original_cols) + : max_objective_(-std::numeric_limits::infinity()), lower_bounds_(original_cols), upper_bounds_(original_cols) + { + } + + i_t add_lower_bound(i_t col, f_t objective, f_t bound) + { + if (col < static_cast(lower_bounds_.size())) { + if (!lower_bounds_[col].is_valid()) { + lower_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { + max_objective_ = objective; + } + return 1; + } else { + if (bound > lower_bounds_[col].bound) { + lower_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { + max_objective_ = objective; + } + return 2; + } else if (bound == lower_bounds_[col].bound && objective > lower_bounds_[col].objective) { + lower_bounds_[col].objective = objective; + if (objective > max_objective_) { + max_objective_ = objective; + } + return 1; + } else { + return -2; + } + } + } else { + return -1; + } + } + + i_t add_upper_bound(i_t col, f_t objective, f_t bound) + { + if (col < static_cast(upper_bounds_.size())) { + if (!upper_bounds_[col].is_valid()) { + upper_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { + max_objective_ = objective; + } + return 1; + } else { + if (bound < upper_bounds_[col].bound) { + upper_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { + max_objective_ = objective; + } + return 2; + } else if (bound == upper_bounds_[col].bound && objective > upper_bounds_[col].objective) { + upper_bounds_[col].objective = objective; + if (objective > max_objective_) { + max_objective_ = objective; + } + return 1; + } else { + return -2; + } + } + } else { + return -1; + } + } + + i_t update_bounds_from_new_incumbent(f_t incumbent_objective, + const std::vector& var_types, + std::vector& lower_bounds, + std::vector& upper_bounds) + { + const i_t n = static_cast(lower_bounds_.size()); + f_t max_objective = -std::numeric_limits::infinity(); + i_t bounds_updated = 0; + for (i_t j = 0; j < n; ++j) { + if (lower_bounds_[j].is_valid()) { + if (incumbent_objective <= lower_bounds_[j].objective && + lower_bounds_[j].bound > lower_bounds[j]) { + //printf("RCF Variable %d (%d): lower %e -> %e\n", j, static_cast(var_types[j]), lower_bounds[j], lower_bounds_[j].bound); + lower_bounds[j] = lower_bounds_[j].bound; + bounds_updated++; + lower_bounds_[j].bound = lower_bounds_[j].objective = std::numeric_limits::quiet_NaN(); + } + if (lower_bounds_[j].objective > max_objective) { + max_objective = lower_bounds_[j].objective; + } + } + if (upper_bounds_[j].is_valid()) { + if (incumbent_objective <= upper_bounds_[j].objective && + upper_bounds_[j].bound < upper_bounds[j]) { + //printf("RCF Variable %d (%d): upper %e -> %e\n", j, static_cast(var_types[j]), upper_bounds[j], upper_bounds_[j].bound); + upper_bounds[j] = upper_bounds_[j].bound; + bounds_updated++; + upper_bounds_[j].bound = upper_bounds_[j].objective = std::numeric_limits::quiet_NaN(); + } + if (upper_bounds_[j].objective > max_objective) { + max_objective = upper_bounds_[j].objective; + } + } + } + max_objective_ = max_objective; + return bounds_updated; + } + + f_t get_current_lower_bound(i_t col) + { + if (col < static_cast(lower_bounds_.size())) { return lower_bounds_[col].bound; } + return std::numeric_limits::quiet_NaN(); + } + + f_t get_current_upper_bound(i_t col) + { + if (col < static_cast(upper_bounds_.size())) { return upper_bounds_[col].bound; } + return std::numeric_limits::quiet_NaN(); + } + + i_t num_cols() { return static_cast(lower_bounds_.size()); } + + f_t get_max_objective() { return max_objective_; } + + private: + f_t max_objective_; + std::vector> lower_bounds_; + std::vector> upper_bounds_; +}; + template class branch_and_bound_t { public: @@ -176,6 +323,10 @@ class branch_and_bound_t { std::vector& edge_norms, f_t& work_estimate); + void update_reduced_cost_bounds(f_t relaxation_objective, + const std::vector& reduced_costs, + const std::vector& var_status, + reduced_cost_bounds_t& reduced_cost_bounds); i_t find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, std::vector& upper_bounds); @@ -324,6 +475,7 @@ class branch_and_bound_t { f_t& last_upper_bound, f_t& last_objective, f_t root_relax_objective, + reduced_cost_bounds_t& reduced_cost_bounds, i_t& cut_pool_size, const std::vector& saved_solution); @@ -348,7 +500,7 @@ class branch_and_bound_t { std::vector& zero_reduced_costs_vars, std::vector& zero_reduced_costs_vars_nonbasic_index); - void pivot_out_integer_variables(const simplex::lp_problem_t& lp, + i_t pivot_out_integer_variables(const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, From 7641ad3523d25c2ef498cc6a589845fe472ac243 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 12 Aug 2026 16:04:48 -0700 Subject: [PATCH 20/34] V3 of pump and pivots, better work estimates, also exploit primal degeneracy --- cpp/src/branch_and_bound/branch_and_bound.cpp | 727 +++++++++++++++--- cpp/src/branch_and_bound/branch_and_bound.hpp | 50 +- cpp/src/dual_simplex/basis_updates.cpp | 6 +- cpp/src/dual_simplex/phase2.cpp | 30 +- cpp/src/dual_simplex/right_looking_lu.cpp | 21 +- 5 files changed, 706 insertions(+), 128 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index a41bc1bd95..b842ddfb63 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3171,7 +3171,7 @@ typename branch_and_bound_t::cut_pass_result_t branch_and_bound_t 0) { settings_.log.printf( - "Updated %d bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", new_bounds, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); + "Updated %d integer bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", new_bounds, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); } } @@ -3272,6 +3272,16 @@ typename branch_and_bound_t::cut_pass_result_t branch_and_bound_t= 1) { update_reduced_cost_bounds(root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + settings_.log.printf("New reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); + pivot_to_improve_reduced_cost_strengthening(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional, root_objective_, reduced_cost_bounds); + settings_.log.printf("After pivoting: new reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); } // Refresh fractional info after re-solving with cuts; the pre-cut count is stale. @@ -3577,10 +3587,17 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( bool recompute_basis = false; const i_t iter_before = iter; + f_t primal_work_before = primal_work_estimate; + f_t pump_call_start_time = tic(); simplex_solver_settings_t primal_settings = settings_; primal_settings.log.log = false; - primal_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time); - primal_settings.work_limit = root_relax_work_estimate_; + primal_settings.time_limit = settings_.time_limit; + primal_settings.work_limit = root_relax_work_estimate_ / 10; + settings_.log.printf( + "Degenerate feasibility pump: calling primal simplex with %d rows, %d cols, %d nnz, " + "%d basis updates, work_limit %.2e, primal_work_estimate %.2e\n", + m, n, A_reduced.col_start[n], reduced_basis_update.num_updates(), + primal_settings.work_limit, primal_work_estimate); simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, exploration_stats_.start_time, @@ -3593,6 +3610,16 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( reduced_solution, iter, primal_work_estimate); + f_t pump_call_time = toc(pump_call_start_time); + f_t pump_call_work = primal_work_estimate - primal_work_before; + i_t pump_call_iters = iter - iter_before; + settings_.log.printf( + "Degenerate feasibility pump: primal simplex returned status %d, %d iters, " + "work %.2e (%.2e/iter), time %.2f (%.2e work/s)\n", + static_cast(lp_status), pump_call_iters, pump_call_work, + pump_call_iters > 0 ? pump_call_work / pump_call_iters : 0.0, + pump_call_time, + pump_call_time > 0 ? pump_call_work / pump_call_time : 0.0); // Detect a stall: the solve made no pivots, so the incumbent vertex was // already optimal for this objective and x did not move. Perturb next pass. stalled = (iter == iter_before); @@ -3768,7 +3795,7 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( } template -void branch_and_bound_t::apply_delta_x_for_integer_pivot( +i_t branch_and_bound_t::apply_delta_x_for_integer_pivot( const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, @@ -3799,7 +3826,15 @@ void branch_and_bound_t::apply_delta_x_for_integer_pivot( bool binding_integer = leaving_index != -1 && is_fractional(solution.x[leaving_index], var_types_[leaving_index], settings_.integer_tol); - if (!binding_integer) { return; } + if (!binding_integer) { + if (leaving_index == -1) { + return -4; // unbounded or entering hit its own bound + } else if (var_types_[leaving_index] != variable_type_t::INTEGER) { + return -5; // continuous variable won ratio test + } else { + return -6; // integer variable won but it's not fractional (already at integer value) + } + } std::vector test_x = solution.x; i_t integer_destroyed = 0; @@ -3815,7 +3850,7 @@ void branch_and_bound_t::apply_delta_x_for_integer_pivot( } } // Require a strict net decrease in fractional integers. - if (integer_destroyed >= 0) { return; } + if (integer_destroyed >= 0) { return -2; } solution.x = test_x; basic_list[basic_leaving] = entering_index; @@ -3863,51 +3898,29 @@ void branch_and_bound_t::apply_delta_x_for_integer_pivot( deficient, slacks_needed, factorize_work_estimate); - if (rank == CONCURRENT_HALT_RETURN || rank == TIME_LIMIT_RETURN) { return; } - if (rank < 0 || rank != lp.num_rows) { return; } + if (rank == CONCURRENT_HALT_RETURN || rank == TIME_LIMIT_RETURN) { return -3; } + if (rank < 0 || rank != lp.num_rows) { return -3; } simplex::reorder_basic_list(q, basic_list); basis_update.reset(L, U, p); } + + return 0; } template -i_t branch_and_bound_t::pivot_out_integer_variables( +void branch_and_bound_t::fast_slack_integer_pivot( const simplex::lp_problem_t& lp, + const std::vector& fractional, + const std::vector& row_to_slack, + const simplex::lp_solution_t& solution, std::vector& basic_list, std::vector& nonbasic_list, + std::vector& nonbasic_index, std::vector& vstatus, - simplex::lp_solution_t& solution, + simplex::lp_solution_t& soln, simplex::basis_update_mpf_t& basis_update, - i_t& num_fractional, - std::vector& fractional) + f_t& work_estimate) { - if (num_fractional == 0) { return 0; } - f_t pivot_out_integer_variables_start_time = tic(); - std::vector zero_reduced_costs_vars; - std::vector zero_reduced_costs_vars_nonbasic_index; - bool dual_degenerate = check_for_dual_degeneracy( - solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); - if (!dual_degenerate) { return 0; } - - lp_solution_t soln_copy = solution; - std::vector basic_list_copy = basic_list; - std::vector nonbasic_list_copy = nonbasic_list; - std::vector vstatus_copy = vstatus; - simplex::basis_update_mpf_t basis_update_copy = basis_update; - - const i_t start_num_fractional = num_fractional; - - const i_t num_zero_reduced_costs_vars = zero_reduced_costs_vars.size(); - - std::vector row_to_slack(lp.num_rows, -1); - for (i_t j : new_slacks_) { - if (lp.lower[j] != 0 || lp.upper[j] != inf) { continue; } - const i_t p = lp.A.col_start[j]; - row_to_slack[lp.A.i[p]] = j; - } - - f_t work_estimate = 0.0; - std::vector fast_candidates; std::vector fast_rows; std::vector fast_nonbasic_slacks; @@ -3923,7 +3936,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( const i_t i = lp.A.i[p]; const i_t slack = row_to_slack[i]; if (slack >= 0) { - if (vstatus_copy[slack] == variable_status_t::BASIC) { + if (vstatus[slack] == variable_status_t::BASIC) { num_basic_slacks++; } else if (std::abs(solution.z[slack]) <= 1e-10) { num_nonbasic_slacks_with_reduced_cost_zero++; @@ -3944,24 +3957,26 @@ i_t branch_and_bound_t::pivot_out_integer_variables( fast_candidates.size()); } - // Build a reverse index nonbasic_index[v] = position of v in nonbasic_list_copy, or -1 if not + // Build a reverse index nonbasic_index[v] = position of v in nonbasic_list, or -1 if not // present. Used to locate the entering variable's slot in the fast-candidate path. // apply_delta_x_for_integer_pivot keeps this index consistent by applying an O(1) fix-up // on each successful pivot; the two variables whose (non)basic status changes are the only // entries that need to be updated. - std::vector nonbasic_index(lp.num_cols, -1); - for (i_t p = 0; p < static_cast(nonbasic_list_copy.size()); ++p) { - nonbasic_index[nonbasic_list_copy[p]] = p; + nonbasic_index.assign(lp.num_cols, -1); + for (i_t p = 0; p < static_cast(nonbasic_list.size()); ++p) { + nonbasic_index[nonbasic_list[p]] = p; } const i_t num_candidates = fast_candidates.size(); + f_t last_log = tic(); + f_t loop_start = tic(); for (i_t k = 0; k < num_candidates; k++) { const i_t j = fast_candidates[k]; const i_t row = fast_rows[k]; const i_t nonbasic_slack = fast_nonbasic_slacks[k]; // Skip if state changed by a prior successful pivot. - if (vstatus_copy[j] != variable_status_t::BASIC) { continue; } - if (vstatus_copy[nonbasic_slack] == variable_status_t::BASIC) { continue; } + if (vstatus[j] != variable_status_t::BASIC) { continue; } + if (vstatus[nonbasic_slack] == variable_status_t::BASIC) { continue; } const i_t col_start = lp.A.col_start[j]; const i_t col_end = lp.A.col_start[j + 1]; f_t a_ij = 0.0; @@ -3975,7 +3990,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( f_t bound = a_ij > 0 ? lp.lower[j] : lp.upper[j]; if (std::abs(bound) == inf) { continue; } - const f_t delta_xj = bound - soln_copy.x[j]; + const f_t delta_xj = bound - soln.x[j]; const f_t scale = -delta_xj * a_ij; if (std::abs(scale) <= 1e-12) { continue; } @@ -4007,7 +4022,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( const i_t jj = delta_x_sparse.i[h]; if (jj == j) continue; const f_t val = delta_x_sparse.x[h]; - const f_t slack_value = soln_copy.x[jj]; + const f_t slack_value = soln.x[jj]; if (val < -slack_value) { ok = false; break; @@ -4038,80 +4053,377 @@ i_t branch_and_bound_t::pivot_out_integer_variables( // live in L), so u_multiply is a single sparse matvec against U0. std::vector b_inv_abar(lp.num_rows); for (i_t h = 0; h < lp.num_rows; ++h) { - b_inv_abar[h] = -direction * delta_x[basic_list_copy[h]]; + b_inv_abar[h] = -direction * delta_x[basic_list[h]]; } std::vector utilde_dense; - basis_update_copy.u_multiply(b_inv_abar, utilde_dense); + basis_update.u_multiply(b_inv_abar, utilde_dense); sparse_vector_t utilde_sparse; utilde_sparse.from_dense(utilde_dense); - apply_delta_x_for_integer_pivot(lp, - basic_list_copy, - nonbasic_list_copy, + i_t error = apply_delta_x_for_integer_pivot(lp, + basic_list, + nonbasic_list, nonbasic_index, - vstatus_copy, + vstatus, entering_index, nonbasic_entering, direction, delta_x, utilde_sparse, - soln_copy, - basis_update_copy, + soln, + basis_update, work_estimate); // apply_delta_x_for_integer_pivot only mutates vstatus when the pivot actually fires, // so entering_index transitioning to BASIC is a reliable success signal. - if (vstatus_copy[entering_index] == variable_status_t::BASIC) { + if (!error) { settings_.log.printf( "Fast candidate pivot succeeded: j=%d entering slack=%d row=%d\n", j, entering_index, row); } + + if (toc(last_log) > 1.0) { + settings_.log.printf("Fast candidates %d/%d processed in %.2f seconds\n", k + 1, num_candidates, toc(loop_start)); + last_log = tic(); + } } + settings_.log.printf("Fast candidates: %d/%d processed in %.2f seconds\n", num_candidates, num_candidates, toc(loop_start)); +} - for (i_t k = 0; k < num_zero_reduced_costs_vars; k++) { - const i_t j = zero_reduced_costs_vars[k]; - if (var_types_[j] == variable_type_t::INTEGER) { continue; } - if (vstatus_copy[j] == variable_status_t::BASIC) { continue; } +template +i_t branch_and_bound_t::pivot_out_integer_variables( + const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional) +{ + if (num_fractional == 0) { return 0; } + f_t pivot_out_integer_variables_start_time = tic(); + std::vector zero_reduced_costs_vars; + std::vector zero_reduced_costs_vars_nonbasic_index; + bool dual_degenerate = check_for_dual_degeneracy( + solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); + if (!dual_degenerate) { return 0; } - const i_t direction = (vstatus_copy[j] == variable_status_t::NONBASIC_LOWER || - vstatus_copy[j] == variable_status_t::NONBASIC_FIXED) - ? 1 - : -1; - const i_t entering_index = j; - const i_t nonbasic_entering = zero_reduced_costs_vars_nonbasic_index[k]; - if (nonbasic_entering < 0 || nonbasic_entering >= static_cast(nonbasic_list_copy.size()) || - nonbasic_list_copy[nonbasic_entering] != j) { - continue; + lp_solution_t soln_copy = solution; + std::vector basic_list_copy = basic_list; + std::vector nonbasic_list_copy = nonbasic_list; + std::vector vstatus_copy = vstatus; + simplex::basis_update_mpf_t basis_update_copy = basis_update; + + const i_t start_num_fractional = num_fractional; + + const i_t num_zero_reduced_costs_vars = zero_reduced_costs_vars.size(); + + std::vector row_to_slack(lp.num_rows, -1); + for (i_t j : new_slacks_) { + if (lp.lower[j] != 0 || lp.upper[j] != inf) { continue; } + const i_t p = lp.A.col_start[j]; + row_to_slack[lp.A.i[p]] = j; + } + + f_t work_estimate = 0.0; + + // Count primal degenerate basic variables + i_t num_degenerate = 0; + i_t num_degenerate_continuous = 0; + i_t num_degenerate_integer = 0; + for (i_t k = 0; k < lp.num_rows; k++) { + const i_t j = basic_list_copy[k]; + const f_t slack_to_lower = soln_copy.x[j] - lp.lower[j]; + const f_t slack_to_upper = lp.upper[j] - soln_copy.x[j]; + if (slack_to_lower <= settings_.primal_tol || slack_to_upper <= settings_.primal_tol) { + num_degenerate++; + if (var_types_[j] == variable_type_t::INTEGER) { + num_degenerate_integer++; + } else { + num_degenerate_continuous++; + } } + } + const f_t degeneracy_fraction = static_cast(num_degenerate) / lp.num_rows; + settings_.log.printf( + "Primal degeneracy: %d/%d basic variables are degenerate (%.1f%%), " + "continuous=%d, integer=%d\n", + num_degenerate, lp.num_rows, + 100.0 * degeneracy_fraction, + num_degenerate_continuous, num_degenerate_integer); + + // Skip pivot_out entirely if primal degeneracy is too high — the ratio test + // will almost always be won by a degenerate variable, making pivots hopeless. + if (degeneracy_fraction > 0.5) { + settings_.log.printf("Skipping pivot_out_integer_variables: degeneracy %.1f%% > 50%%\n", + 100.0 * degeneracy_fraction); + return 0; + } + + std::vector nonbasic_index; + fast_slack_integer_pivot(lp, + fractional, + row_to_slack, + solution, + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + soln_copy, + basis_update_copy, + work_estimate); + + + std::vector work_list = fractional; + std::vector to_basic_position(lp.num_cols, -1); - // Solve B * dxB = A(:, j) so utilde is valid for the MPF update. - // Apply direction when forming delta_x (same convention as primal_phase2). - sparse_vector_t rhs(lp.A, j); - sparse_vector_t delta_xB; - sparse_vector_t utilde_sparse; - basis_update_copy.b_solve(rhs, delta_xB, utilde_sparse); + for (i_t k = 0; k < lp.num_rows; k++) { + to_basic_position[basic_list_copy[k]] = k; + } + + sparse_vector_t ep; + ep.n = lp.num_rows; + ep.i.resize(1); + ep.x.resize(1); + ep.x[0] = 1.0; + + std::vector delta_y_dense(lp.num_rows, 0.0); + + // Track which entering variables are actually tried (to detect duplication) + std::vector entering_tried_count(lp.num_cols, 0); + + i_t worklist_total_processed = 0; + i_t worklist_skipped = 0; + i_t worklist_btran_done = 0; + i_t worklist_ftran_done = 0; + i_t worklist_pivots_succeeded = 0; + i_t worklist_readded = 0; + f_t worklist_btran_time = 0.0; + f_t worklist_dot_time = 0.0; + f_t worklist_ftran_time = 0.0; + i_t worklist_no_candidates = 0; // target had no nonzero dot_q + i_t worklist_ratio_test_fail = 0; // ratio test didn't pick a fractional integer (error -1) + i_t worklist_net_increase_fail = 0; // pivot would net-increase fractionals (error -2) + i_t worklist_unbounded = 0; // entering hit its own bound or unbounded (error -4) + i_t worklist_continuous_won = 0; // continuous variable won ratio test (error -5) + i_t worklist_nonfrac_int_won = 0; // non-fractional integer won ratio test (error -6) + + f_t worklist_loop_start = tic(); + f_t worklist_last_log = tic(); + + while (!work_list.empty()) { + const i_t j = work_list.back(); + const i_t p = to_basic_position[j]; + work_list.pop_back(); + worklist_total_processed++; + + // Skip if j is no longer basic and fractional (may have been fixed by a prior pivot) + if (p < 0) { worklist_skipped++; continue; } + if (vstatus_copy[j] != variable_status_t::BASIC) { worklist_skipped++; continue; } + if (!is_fractional(soln_copy.x[j], var_types_[j], settings_.integer_tol)) { worklist_skipped++; continue; } + + // We want to pivot variable j out of the basis. + // We solve B^T * delta_y = e_p, where p is the position of j in the basis. + // Or delta_y = B^{-T} e_p, or delta_y^T = e_p^T B^{-T} + + ep.i[0] = p; + sparse_vector_t delta_y_sparse; + sparse_vector_t UTsol_sparse; + f_t btran_start = tic(); + basis_update_copy.b_transpose_solve(ep, delta_y_sparse, UTsol_sparse); + worklist_btran_time += toc(btran_start); + worklist_btran_done++; + + // Scatter delta_y_sparse into dense workspace for dot product computation + const i_t delta_y_nz = delta_y_sparse.i.size(); + for (i_t h = 0; h < delta_y_nz; h++) { + delta_y_dense[delta_y_sparse.i[h]] = delta_y_sparse.x[h]; + } + + // We also have that + // B*delta_xB + N*delta_xN = 0 + // So delta_xB = -B^{-1} N * delta_xN + // And delta_xB[p] = e_p^T * delta_xB = -e_p^T B^{-1} N * delta_xN + // = -delta_y^T N * delta_xN + // Recall that delta_xN = e_q where q is the entering variables + // So delta_xB[p] = -delta_y^T A(:, q) + // + // For p to be the leaving variable, we need it to be the binding + // member in the ratio test + // x_B + alpha * delta_xB >= l_B + // x_B + alpha * delta_xB <= u_B + // + // Or alpha <= (l_B[p] - x_B[p]) / delta_xB[p] when delta_xB[p] < 0 + // Or alpha <= (u_B[p] - x_B[p]) / delta_xB[p] when delta_xB[p] > 0 + // + // Thus, if we want to push x_B[p] up to u_B[p], we want + // alpha = (u_B[p] - x_B[p]) / delta_xB[p] to be small + // And if we want to push x_B[p] down to l_B[p], we want + // alpha = (l_B[p] - x_B[p]) / delta_xB[p] to be small + // + // Or equivalently, we want delta_xB[p] to be large + + // Find top 3 candidates by merit = |dot_q| / nnz(A(:,q)) + // Large |dot_q| means the target moves a lot (small step to hit bound). + // Small nnz means the FTRAN result is likely sparse, so fewer competing + // basic variables will have nonzero delta_xB components to block the target. + // Skip entering variables that have already been tried (and failed) by prior targets. + f_t values[3] = {0.0, 0.0, 0.0}; + i_t indices[3] = {-1, -1, -1}; + f_t dot_start = tic(); + for (i_t q : zero_reduced_costs_vars) { + if (var_types_[q] == variable_type_t::INTEGER) { continue; } + if (nonbasic_index[q] < 0) { continue; } + if (entering_tried_count[q] > 0) { continue; } + // Compute dot_q = delta_y^T * A(:, q) using dense delta_y + const i_t col_start = lp.A.col_start[q]; + const i_t col_end = lp.A.col_start[q + 1]; + const i_t col_nnz = col_end - col_start; + f_t dot_q = 0.0; + for (i_t pp = col_start; pp < col_end; pp++) { + dot_q += delta_y_dense[lp.A.i[pp]] * lp.A.x[pp]; + } + const f_t abs_dot_q = std::abs(dot_q); + if (abs_dot_q <= 1e-12) { continue; } + const f_t merit = abs_dot_q / static_cast(col_nnz); + + if (merit > values[0]) { + indices[2] = indices[1]; values[2] = values[1]; + indices[1] = indices[0]; values[1] = values[0]; + indices[0] = q; values[0] = merit; + } else if (merit > values[1]) { + indices[2] = indices[1]; values[2] = values[1]; + indices[1] = q; values[1] = merit; + } else if (merit > values[2]) { + indices[2] = q; values[2] = merit; + } + } + worklist_dot_time += toc(dot_start); + + if (indices[0] == -1) { worklist_no_candidates++; } + + // Try the top 3 candidates + for (i_t h = 0; h < 3; h++) { + if (indices[h] == -1) break; + + const i_t q = indices[h]; + const i_t entering_index = q; + const i_t nonbasic_entering = nonbasic_index[q]; + if (nonbasic_entering < 0) { continue; } + entering_tried_count[q]++; + + // Determine direction based on entering variable's status + const i_t direction = (vstatus_copy[q] == variable_status_t::NONBASIC_LOWER || + vstatus_copy[q] == variable_status_t::NONBASIC_FIXED) + ? 1 + : -1; + + // Solve B * delta_xB = A(:, q) so utilde is valid for the MPF update. + sparse_vector_t rhs(lp.A, q); + sparse_vector_t delta_xB; + sparse_vector_t utilde_sparse; + f_t ftran_start = tic(); + basis_update_copy.b_solve(rhs, delta_xB, utilde_sparse); + worklist_ftran_time += toc(ftran_start); + worklist_ftran_done++; + + std::vector delta_xB_dense; + delta_xB.to_dense(delta_xB_dense); + std::vector delta_x(lp.num_cols, 0.0); + for (i_t i = 0; i < lp.num_rows; i++) { + delta_x[basic_list_copy[i]] = -direction * delta_xB_dense[i]; + } + delta_x[q] = direction; + + i_t error = apply_delta_x_for_integer_pivot(lp, + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + entering_index, + nonbasic_entering, + direction, + delta_x, + utilde_sparse, + soln_copy, + basis_update_copy, + work_estimate); + + if (error == -2) { worklist_net_increase_fail++; } + if (error == -4) { worklist_unbounded++; worklist_ratio_test_fail++; } + if (error == -5) { worklist_continuous_won++; worklist_ratio_test_fail++; } + if (error == -6) { worklist_nonfrac_int_won++; worklist_ratio_test_fail++; } + + if (!error) { + worklist_pivots_succeeded++; + // Update to_basic_position for the variables that changed status + // entering_index is now basic, leaving_index is now nonbasic + // Find the leaving variable: it's the one that took entering_index's slot in nonbasic_list + const i_t leaving_index = nonbasic_list_copy[nonbasic_entering]; + to_basic_position[entering_index] = to_basic_position[leaving_index]; + to_basic_position[leaving_index] = -1; + + // We did a successful pivot; add fractional variables whose values changed to work list + for (i_t k : fractional) { + if (vstatus_copy[k] != variable_status_t::BASIC) { continue; } + if (std::abs(delta_x[k]) > settings_.zero_tol) { + //work_list.push_back(k); + //worklist_readded++; + } + } + break; + } + } - std::vector delta_xB_dense; - delta_xB.to_dense(delta_xB_dense); - std::vector delta_x(lp.num_cols, 0.0); - for (i_t h = 0; h < static_cast(basic_list_copy.size()); h++) { - delta_x[basic_list_copy[h]] = -direction * delta_xB_dense[h]; + // Clear dense workspace for next target + for (i_t h = 0; h < delta_y_nz; h++) { + delta_y_dense[delta_y_sparse.i[h]] = 0.0; } - delta_x[j] = direction; - apply_delta_x_for_integer_pivot(lp, - basic_list_copy, - nonbasic_list_copy, - nonbasic_index, - vstatus_copy, - entering_index, - nonbasic_entering, - direction, - delta_x, - utilde_sparse, - soln_copy, - basis_update_copy, - work_estimate); + if (toc(worklist_last_log) > 1.0) { + settings_.log.printf( + "Worklist progress: %d/%d processed, %d pivots, %d ratio_fail (unb=%d cont=%d nfint=%d), %d net_inc_fail, %d no_cand, %.2f seconds\n", + worklist_total_processed, static_cast(fractional.size()), + worklist_pivots_succeeded, worklist_ratio_test_fail, + worklist_unbounded, worklist_continuous_won, worklist_nonfrac_int_won, + worklist_net_increase_fail, + worklist_no_candidates, toc(worklist_loop_start)); + worklist_last_log = tic(); + } } + // Count unique entering variables and duplication + i_t unique_entering = 0; + i_t max_entering_count = 0; + i_t entering_tried_once = 0; + i_t entering_tried_multiple = 0; + for (i_t q = 0; q < lp.num_cols; q++) { + if (entering_tried_count[q] > 0) { + unique_entering++; + max_entering_count = std::max(max_entering_count, entering_tried_count[q]); + if (entering_tried_count[q] == 1) { entering_tried_once++; } + else { entering_tried_multiple++; } + } + } + settings_.log.printf( + "Worklist entering stats: unique=%d, tried_once=%d, tried_multiple=%d, " + "max_count=%d, total_ftran=%d, duplication_ratio=%.1fx\n", + unique_entering, entering_tried_once, entering_tried_multiple, + max_entering_count, worklist_ftran_done, + worklist_ftran_done / std::max(1.0, static_cast(unique_entering))); + + settings_.log.printf( + "Worklist stats: processed=%d skipped=%d btran=%d ftran=%d pivots=%d readded=%d " + "btran_time=%.2f dot_time=%.2f ftran_time=%.2f zero_rc_vars=%d " + "no_candidates=%d ratio_test_fail=%d (unbounded=%d continuous_won=%d nonfrac_int_won=%d) " + "net_increase_fail=%d\n", + worklist_total_processed, worklist_skipped, worklist_btran_done, worklist_ftran_done, + worklist_pivots_succeeded, worklist_readded, + worklist_btran_time, worklist_dot_time, worklist_ftran_time, + num_zero_reduced_costs_vars, + worklist_no_candidates, worklist_ratio_test_fail, + worklist_unbounded, worklist_continuous_won, worklist_nonfrac_int_won, + worklist_net_increase_fail); + std::vector new_fractional; const i_t num_new_fractional = fractional_variables(settings_, soln_copy.x, var_types_, new_fractional); @@ -4137,6 +4449,218 @@ i_t branch_and_bound_t::pivot_out_integer_variables( return 0; } +template +void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( + const simplex::lp_problem_t& lp, + const std::vector& basic_list, + const std::vector& nonbasic_list, + const std::vector& vstatus, + const simplex::lp_solution_t& soln, + const simplex::basis_update_mpf_t& basis_update, + const i_t num_fractional, + const std::vector& fractional, + const f_t relaxation_objective, + reduced_cost_bounds_t& reduced_cost_bounds) +{ + // Count primal degenerate basic variables + i_t num_degenerate = 0; + i_t num_degenerate_continuous = 0; + i_t num_degenerate_integer = 0; + std::vector degenerate_integer_list; + degenerate_integer_list.reserve(lp.num_rows); + for (i_t k = 0; k < lp.num_rows; k++) { + const i_t j = basic_list[k]; + const f_t slack_to_lower = soln.x[j] - lp.lower[j]; + const f_t slack_to_upper = lp.upper[j] - soln.x[j]; + if (slack_to_lower <= settings_.primal_tol || slack_to_upper <= settings_.primal_tol) { + num_degenerate++; + if (var_types_[j] == variable_type_t::INTEGER) { + num_degenerate_integer++; + degenerate_integer_list.push_back(j); + } else { + num_degenerate_continuous++; + } + } + } + + if (num_degenerate_integer == 0) return; + + std::vector variable_to_basic_position(lp.num_cols, -1); + for (i_t k = 0; k < lp.num_rows; k++) { + variable_to_basic_position[basic_list[k]] = k; + } + std::vector delta_y(lp.num_rows, 0); + std::vector delta_z(lp.num_cols, 0); + std::vector delta_z_mark(lp.num_cols, 0); + std::vector delta_z_indices; + delta_z_indices.reserve(lp.num_cols); + + f_t work_estimate = 0; + const f_t threshold = 100.0 * settings_.integer_tol; + const f_t tol = 1e-2; + const f_t pivot_tol = settings_.pivot_tol; + const f_t dual_tol = settings_.dual_tol / 10; + + i_t num_bounds_added = 0; + for (i_t j : degenerate_integer_list) { + // x_j is a degenerate integer basic variable. + // We would like a dual-feasible point where x_j is nonbasic with a nonzero + // reduced cost that may be used for reduced cost strengthening. + // We do not need to take the pivot; a dual step along either ray is enough. + // + // One BTRAN: B^T * delta_y = e_p, which matches direction == -1 in + // compute_reduced_cost_update (B^T * delta_y = -direction * e_p). + // The opposite direction is the negated (delta_y, delta_z) ray. + const i_t leaving_index = j; + const i_t p = variable_to_basic_position[j]; + if (p == -1) continue; + + sparse_vector_t ep(lp.num_rows, 1); + ep.i[0] = p; + ep.x[0] = 1.0; + sparse_vector_t delta_y_sparse; + sparse_vector_t UTsol_sparse; + basis_update.b_transpose_solve(ep, delta_y_sparse, UTsol_sparse); + + // delta_zN = -N^T * delta_y, delta_z[leaving] = -1 + delta_y_sparse.to_dense(delta_y); + simplex::compute_reduced_cost_update(lp, + basic_list, + nonbasic_list, + delta_y, + leaving_index, + /*direction=*/-1, + delta_z_mark, + delta_z_indices, + delta_z, + work_estimate); + + const f_t lower_j = lp.lower[j]; + const f_t upper_j = lp.upper[j]; + + // Try both dual rays. scale == +1 uses the computed delta_z (direction -1); + // scale == -1 uses -delta_z (direction +1). Either or both may yield an RCS bound. + for (const f_t scale : {1.0, -1.0}) { + // Maximum dual step-length alpha that keeps dual feasibility on this ray. + // zl_j + alpha * delta_zN_j >= 0 for nonbasic j on lower bound + // zu_j + alpha * delta_zN_j <= 0 for nonbasic j on upper bound + f_t alpha = inf; + for (i_t jj : delta_z_indices) { + if (vstatus[jj] == variable_status_t::NONBASIC_FIXED) { continue; } + const f_t dz = scale * delta_z[jj]; + if (vstatus[jj] == variable_status_t::NONBASIC_LOWER && dz < -pivot_tol) { + const f_t ratio = std::max((-dual_tol - soln.z[jj]) / dz, 0.0); + if (ratio < alpha) { alpha = ratio; } + } + if (vstatus[jj] == variable_status_t::NONBASIC_UPPER && dz > pivot_tol) { + const f_t ratio = std::max((dual_tol - soln.z[jj]) / dz, 0.0); + if (ratio < alpha) { alpha = ratio; } + } + } + if (alpha == 0.0 || !std::isfinite(alpha)) { continue; } + + // Verify dual feasibility of the new point z_new = z + alpha * scale * delta_z + // For NONBASIC_LOWER: z_new[jj] >= -dual_tol + // For NONBASIC_UPPER: z_new[jj] <= dual_tol + { + f_t max_dual_infeas = 0.0; + i_t num_dual_infeas = 0; + i_t worst_j = -1; + for (i_t jj : delta_z_indices) { + if (vstatus[jj] == variable_status_t::NONBASIC_FIXED) { continue; } + const f_t new_zj = soln.z[jj] + alpha * scale * delta_z[jj]; + if (vstatus[jj] == variable_status_t::NONBASIC_LOWER && new_zj < -settings_.dual_tol) { + num_dual_infeas++; + if (std::abs(new_zj) > max_dual_infeas) { + max_dual_infeas = std::abs(new_zj); + worst_j = jj; + } + } + if (vstatus[jj] == variable_status_t::NONBASIC_UPPER && new_zj > settings_.dual_tol) { + num_dual_infeas++; + if (std::abs(new_zj) > max_dual_infeas) { + max_dual_infeas = std::abs(new_zj); + worst_j = jj; + } + } + } + // Also check the leaving variable itself + const f_t new_zj_leaving = soln.z[j] + alpha * scale * delta_z[j]; + if (num_dual_infeas > 0) { + settings_.log.printf( + "WARNING pivot_to_improve_rc: dual infeasibility after step! " + "var=%d alpha=%.6e scale=%.0f num_infeas=%d max_infeas=%.6e worst_j=%d " + "new_rc_leaving=%.6e\n", + j, alpha, scale, num_dual_infeas, max_dual_infeas, worst_j, new_zj_leaving); + } + } + + // Claim: We don't actually need to take a pivot if all we want to do is add a bound + // coming from reduced cost strengthening + const f_t new_reduced_cost = soln.z[j] + alpha * scale * delta_z[j]; + + // x_j <= l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] + // Let u_tilde_j = u_j - epsilon, so that floor(u_tilde_j) = u_j - 1 + // We want to solve for want the incumbent objective needs to be to make + // x_j <= u_tilde_j + // This means l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] <= + // u_tilde_j Or equivalently, incumbent_objective <= relaxation_objective + + // reduced_costs[j] * (u_tilde_j - l_j) when reduced_costs[j] > 0 + if (lower_j > -inf && new_reduced_cost > threshold) { + const f_t u_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? upper_j - tol + : std::max(upper_j - 1.0, lower_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::floor(u_tilde_j) : u_tilde_j; + const f_t diff = u_tilde_j - lower_j; + const f_t objective_j = relaxation_objective + diff * new_reduced_cost; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == upper_j - 1.0) || + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_upper_bound(j, objective_j, bound_j); + if (info > 0) { num_bounds_added++; } + //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. Info %d\n", objective_j, bound_j, j, info); + } + } + + // x_j >= u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] when + // reduced_costs[j] < 0 Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 We want + // to solve for want the incumbent objective needs to be to make x_j >= l_tilde_j This means + // u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j Or + // equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * + // (l_tilde_j - u_j) when reduced_costs[j] < 0 + if (upper_j < inf && new_reduced_cost < -threshold) { + const f_t l_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? lower_j + tol + : std::min(lower_j + 1.0, upper_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::ceil(l_tilde_j) : l_tilde_j; + const f_t diff = l_tilde_j - upper_j; + const f_t objective_j = relaxation_objective + diff * new_reduced_cost; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == lower_j + 1.0) || + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_lower_bound(j, objective_j, bound_j); + if (info > 0) { num_bounds_added++; } + //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. Info %d\n", objective_j, bound_j, j, info); + } + } + } + + // Clear arrays for next iteration + for (i_t k : delta_z_indices) { + delta_z_mark[k] = 0; + delta_z[k] = 0.0; + } + delta_z[leaving_index] = 0.0; + delta_z_indices.clear(); + for (i_t k : delta_y_sparse.i) { + delta_y[k] = 0.0; + } + } + settings_.log.printf("Added %d bounds for reduced cost strengthening\n", num_bounds_added); +} + template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -4355,6 +4879,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut reduced_cost_bounds_t reduced_cost_bounds(original_lp_.num_cols); update_reduced_cost_bounds(root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + settings_.log.printf("New reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); + pivot_to_improve_reduced_cost_strengthening(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional, + root_objective_, reduced_cost_bounds); + settings_.log.printf("After pivoting: new reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); f_t pivot_out_integer_variables_start_time = tic(); i_t num_integer_increased = pivot_out_integer_variables(original_lp_, @@ -4586,7 +5121,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); i_t num_changed = reduced_cost_bounds.update_bounds_from_new_incumbent( upper_bound_.load(), var_types_, lower_bounds, upper_bounds); - settings_.log.printf("Updated %d bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", num_changed, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); + settings_.log.printf("Updated %d integer bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", num_changed, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); mutex_original_lp_.lock(); original_lp_.lower = lower_bounds; original_lp_.upper = upper_bounds; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 185e3ce79e..a6c1526d95 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -184,14 +184,14 @@ class reduced_cost_bounds_t { { const i_t n = static_cast(lower_bounds_.size()); f_t max_objective = -std::numeric_limits::infinity(); - i_t bounds_updated = 0; + i_t integer_bounds_updated = 0; for (i_t j = 0; j < n; ++j) { if (lower_bounds_[j].is_valid()) { if (incumbent_objective <= lower_bounds_[j].objective && lower_bounds_[j].bound > lower_bounds[j]) { //printf("RCF Variable %d (%d): lower %e -> %e\n", j, static_cast(var_types[j]), lower_bounds[j], lower_bounds_[j].bound); lower_bounds[j] = lower_bounds_[j].bound; - bounds_updated++; + if (var_types[j] == simplex::variable_type_t::INTEGER) { integer_bounds_updated++; } lower_bounds_[j].bound = lower_bounds_[j].objective = std::numeric_limits::quiet_NaN(); } if (lower_bounds_[j].objective > max_objective) { @@ -203,7 +203,7 @@ class reduced_cost_bounds_t { upper_bounds_[j].bound < upper_bounds[j]) { //printf("RCF Variable %d (%d): upper %e -> %e\n", j, static_cast(var_types[j]), upper_bounds[j], upper_bounds_[j].bound); upper_bounds[j] = upper_bounds_[j].bound; - bounds_updated++; + if (var_types[j] == simplex::variable_type_t::INTEGER) { integer_bounds_updated++; } upper_bounds_[j].bound = upper_bounds_[j].objective = std::numeric_limits::quiet_NaN(); } if (upper_bounds_[j].objective > max_objective) { @@ -212,7 +212,7 @@ class reduced_cost_bounds_t { } } max_objective_ = max_objective; - return bounds_updated; + return integer_bounds_updated; } f_t get_current_lower_bound(i_t col) @@ -500,16 +500,28 @@ class branch_and_bound_t { std::vector& zero_reduced_costs_vars, std::vector& zero_reduced_costs_vars_nonbasic_index); + void fast_slack_integer_pivot(const simplex::lp_problem_t& lp, + const std::vector& fractional, + const std::vector& row_to_slack, + const simplex::lp_solution_t& solution, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate); + i_t pivot_out_integer_variables(const simplex::lp_problem_t& lp, - std::vector& basic_list, - std::vector& nonbasic_list, - std::vector& vstatus, - simplex::lp_solution_t& soln, - simplex::basis_update_mpf_t& basis_update, - i_t& num_fractional, - std::vector& fractional); - - void apply_delta_x_for_integer_pivot(const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional); + + i_t apply_delta_x_for_integer_pivot(const simplex::lp_problem_t& lp, std::vector& basic_list, std::vector& nonbasic_list, std::vector& nonbasic_index, @@ -532,6 +544,18 @@ class branch_and_bound_t { i_t& num_fractional, std::vector& fractional); + void pivot_to_improve_reduced_cost_strengthening( + const simplex::lp_problem_t& lp, + const std::vector& basic_list, + const std::vector& nonbasic_list, + const std::vector& vstatus, + const simplex::lp_solution_t& soln, + const simplex::basis_update_mpf_t& basis_update, + i_t num_fractional, + const std::vector& fractional, + f_t relaxation_objective, + reduced_cost_bounds_t& reduced_cost_bounds); + // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 84468ba097..c2a7027548 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1507,7 +1507,7 @@ f_t basis_update_mpf_t::dot_product(i_t col, nz_mark++; } } - work_estimate_ += 2 * nz_mark + (col_end - col_start); + work_estimate_ += 2 * (col_end - col_start) + 2 * nz_mark; return dot; } @@ -1524,7 +1524,7 @@ void basis_update_mpf_t::add_sparse_column(const csc_matrix_t @@ -1549,7 +1549,7 @@ void basis_update_mpf_t::add_sparse_column(const csc_matrix_t diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 6e8ef4bbdd..8c59bd464f 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -1456,7 +1456,7 @@ i_t update_steepest_edge_norms(const simplex_solver_settings_t& settin work_estimate += 2 * v_sparse.i.size(); } v_sparse.scatter(v); - work_estimate += 2 * v_sparse.i.size(); + work_estimate += 4 * v_sparse.i.size(); const i_t leaving_index = basic_list[basic_leaving_index]; const f_t prev_dy_norm_squared = delta_y_steepest_edge[leaving_index]; @@ -1508,7 +1508,7 @@ i_t update_steepest_edge_norms(const simplex_solver_settings_t& settin delta_y_steepest_edge[j] = new_val; } } - work_estimate += 5 * scaled_delta_xB_nz; + work_estimate += 6 * scaled_delta_xB_nz; const i_t v_nz = v_sparse.i.size(); for (i_t k = 0; k < v_nz; ++k) { @@ -2904,7 +2904,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, i_t num_refactors = 0; i_t total_bound_flips = 0; f_t delta_y_nz_percentage = 0.0; - phase2::phase2_timers_t timers(false); + phase2::phase2_timers_t timers(true); // Sparse vectors for main loop (declared outside loop for instrumentation) sparse_vector_t delta_y_sparse(m, 0); @@ -2922,10 +2922,11 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += ft.work_estimate(); ft.clear_work_estimate(); + f_t last_work_reported = 0.0; if (work_unit_context) { work_unit_context->record_work_sync_on_horizon((phase2_work_estimate) / 1e8); + last_work_reported = phase2_work_estimate; } - phase2_work_estimate = 0.0; if (phase == 2) { settings.log.printf("%5d %+.16e %7d %.8e %.2e %.2f\n", @@ -3080,6 +3081,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } } + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); phase2::prepare_optimality(0, primal_infeasibility, lp, @@ -3301,6 +3304,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, obj = phase2::compute_perturbed_objective(objective, x); phase2_work_estimate += 2 * n; if (dual_infeas <= settings.dual_tol && primal_infeasibility <= settings.primal_tol) { + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); phase2::prepare_optimality(1, primal_infeasibility, lp, @@ -3358,6 +3363,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, if (primal_infeasibility <= settings.primal_tol && orig_dual_infeas <= settings.dual_tol) { + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); phase2::prepare_optimality(2, primal_infeasibility, lp, @@ -3790,16 +3797,19 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += 3 * delta_z_indices.size(); phase2::clear_delta_z(entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); + // Flush basis update work into the total work estimate every iteration + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); + f_t now = toc(start_time); // Feature logging for regression training (every FEATURE_LOG_INTERVAL iterations) if ((iter % FEATURE_LOG_INTERVAL) == 0 && work_unit_context) { [[maybe_unused]] i_t iters_elapsed = iter - last_feature_log_iter; - phase2_work_estimate += ft.work_estimate(); - ft.clear_work_estimate(); - work_unit_context->record_work_sync_on_horizon(phase2_work_estimate / 1e8); - phase2_work_estimate = 0.0; + work_unit_context->record_work_sync_on_horizon( + (phase2_work_estimate - last_work_reported) / 1e8); + last_work_reported = phase2_work_estimate; last_feature_log_iter = iter; } @@ -3839,6 +3849,10 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } if (iter >= iter_limit) { status = dual_status_t::ITERATION_LIMIT; } + // Flush any remaining work from the basis update into the total work estimate + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); + if (phase == 2) { timers.print_timers(settings); constexpr bool print_stats = false; diff --git a/cpp/src/dual_simplex/right_looking_lu.cpp b/cpp/src/dual_simplex/right_looking_lu.cpp index 6a717cd257..63f5cb7c0f 100644 --- a/cpp/src/dual_simplex/right_looking_lu.cpp +++ b/cpp/src/dual_simplex/right_looking_lu.cpp @@ -209,7 +209,8 @@ class trailing_matrix_t { const f_t max_in_col = max_in_column_[j]; const i_t c_start = col_start_[j]; const i_t c_end = col_end_[j]; - for (i_t p = c_start; p < c_end; p++) { + i_t p; + for (p = c_start; p < c_end; p++) { const i_t i = c_i_[p]; const f_t val = c_x_[p]; const i_t rdeg = row_counts_.get_count(i); @@ -224,7 +225,7 @@ class trailing_matrix_t { if (markowitz <= markowitz_lower_bound) { break; } } } - work_estimate_ += 3 * (c_end - c_start); + work_estimate_ += 3 * (p - c_start); nsearch++; if (markowitz <= markowitz_lower_bound) { break; } } @@ -241,19 +242,21 @@ class trailing_matrix_t { assert(rdeg == nz); const i_t r_start = row_start_[i]; const i_t r_end = row_end_[i]; - for (i_t p = r_start; p < r_end; p++) { + i_t p; + for (p = r_start; p < r_end; p++) { const i_t j = r_j_[p]; // Look up the value from the column copy of j f_t val = 0; const i_t c_start = col_start_[j]; const i_t c_end = col_end_[j]; - for (i_t q = c_start; q < c_end; q++) { + i_t q; + for (q = c_start; q < c_end; q++) { if (c_i_[q] == i) { val = c_x_[q]; break; } } - work_estimate_ += 2 * (c_end - c_start); + work_estimate_ += 2 * (q - c_start); const f_t max_in_col = max_in_column_[j]; const i_t cdeg = col_counts_.get_count(j); assert(cdeg >= 0); @@ -267,7 +270,7 @@ class trailing_matrix_t { if (markowitz <= markowitz_lower_bound) { break; } } } - work_estimate_ += 5 * (r_end - r_start); + work_estimate_ += 5 * (p - r_start); nsearch++; if (markowitz <= markowitz_lower_bound) { break; } } @@ -334,7 +337,7 @@ class trailing_matrix_t { } } } - work_estimate_ += 2 * (c_end - c_start) + 6 * (pivot_col_count - n_fillin); + work_estimate_ += 2 * (c_end - c_start) + 5 * (pivot_col_count - n_fillin); // Step 2b: Remove cancellations (entries that became zero). if (n_cancel > 0) { @@ -1285,12 +1288,14 @@ class symmetric_trailing_matrix_t { const i_t j = r_j_[rp]; // Look up A(pivot_p, j) from column j f_t val = 0; - for (i_t q = col_start_[j]; q < col_end_[j]; q++) { + i_t q; + for (q = col_start_[j]; q < col_end_[j]; q++) { if (c_i_[q] == pivot_p) { val = c_x_[q]; break; } } + work_estimate_ += 2 * (q - col_start_[j]); const f_t lj = val / pivot_val; pivot_col_val_[j] = lj; pivot_col_mark_[j] = 1; From 39cb576e0f2c0ad7622fcaf5225c4754dbff7c89 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 14 Aug 2026 14:30:49 -0700 Subject: [PATCH 21/34] Devex pricing in primal; try to remove perturbations in dual --- .../mathematical_optimization/constants.h | 2 + .../pdlp/solver_settings.hpp | 2 + cpp/src/dual_simplex/phase2.cpp | 82 +++++++++++-- cpp/src/dual_simplex/primal.cpp | 116 ++++++++++++++++-- .../dual_simplex/simplex_solver_settings.hpp | 6 + cpp/src/dual_simplex/solve.cpp | 8 ++ cpp/src/math_optimization/solver_settings.cu | 2 + cpp/src/pdlp/solve.cu | 3 + 8 files changed, 202 insertions(+), 19 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 5a807de308..d21b293a32 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -54,6 +54,8 @@ #define CUOPT_CUDSS_DETERMINISTIC "cudss_deterministic" #define CUOPT_PRESOLVE "presolve" #define CUOPT_INITIAL_PERTURBATION "initial_perturbation" +#define CUOPT_REMOVE_PERTURBATION "remove_perturbation" +#define CUOPT_PRIMAL_PRICING "primal_pricing" #define CUOPT_MIP_PROBING "mip_probing" #define CUOPT_DUAL_POSTSOLVE "dual_postsolve" #define CUOPT_MIP_DETERMINISM_MODE "mip_determinism_mode" diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 80197e8a85..8d267c8be5 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -298,6 +298,8 @@ class pdlp_solver_settings_t { i_t dualize{-1}; i_t ordering{-1}; i_t initial_perturbation{-1}; + i_t remove_perturbation{-1}; + i_t primal_pricing{0}; i_t barrier_dual_initial_point{-1}; i_t postsolve_info{-1}; // Ruiz equilibration for QCQP (barrier) scaling: -1 automatic (row/column diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 8c59bd464f..e2a3c17d11 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -464,7 +464,30 @@ void initial_perturbation(const lp_problem_t& lp, max_abs_obj_coeff = std::max(max_abs_obj_coeff, std::abs(lp.objective[j])); } - const f_t dual_tol = settings.dual_tol; + // Dampen large costs + if (max_abs_obj_coeff > 100.0) { + max_abs_obj_coeff = std::sqrt(std::sqrt(max_abs_obj_coeff)); + } + // Ensure a minimum perturbation even for tiny-cost problems + if (max_abs_obj_coeff < 1.0) { + max_abs_obj_coeff = 1.0; + } + + // If few boxed variables, cap max_abs_obj_coeff at 1.0 + i_t num_boxed = 0; + for (i_t j = 0; j < n; ++j) { + if (lp.lower[j] > -inf && lp.upper[j] < inf && lp.lower[j] != lp.upper[j]) { + num_boxed++; + } + } + if (static_cast(num_boxed) / n < 0.01) { + max_abs_obj_coeff = std::min(max_abs_obj_coeff, f_t(1.0)); + } + + const f_t perturbation_base = 5e-7 * max_abs_obj_coeff; + + settings.log.printf("Perturbation debug: max_abs_obj_coeff=%e (dampened), perturbation_base=%e, n=%d, num_boxed=%d\n", + max_abs_obj_coeff, perturbation_base, n, num_boxed); objective.resize(n); f_t sum_perturb = 0.0; @@ -476,22 +499,26 @@ void initial_perturbation(const lp_problem_t& lp, const f_t lower = lp.lower[j]; const f_t upper = lp.upper[j]; - if (vstatus[j] == variable_status_t::NONBASIC_FIXED || - vstatus[j] == variable_status_t::NONBASIC_FREE || lower == upper || - lower == -inf && upper == inf) { + // Skip truly fixed variables and free variables + if (lower == upper || (lower == -inf && upper == inf)) { + continue; + } + // Skip basic variables + if (vstatus[j] == variable_status_t::BASIC) { continue; } const f_t rand_val = random.random(); - const f_t perturb = - (1e-5 * std::abs(obj) + 1e-7 * max_abs_obj_coeff + 10 * dual_tol) * (1.0 + rand_val); + const f_t cost_factor = std::min(std::abs(obj) + 1.0, max_abs_obj_coeff + 1.0); + const f_t perturb = (1.0 + rand_val) * cost_factor * perturbation_base; - if (vstatus[j] == variable_status_t::NONBASIC_LOWER || lower > -inf && upper < inf && obj > 0) { + if (vstatus[j] == variable_status_t::NONBASIC_LOWER || + vstatus[j] == variable_status_t::NONBASIC_FIXED) { + // NONBASIC_FIXED from phase 1 for boxed variables — treat as at lower bound objective[j] = obj + perturb; sum_perturb += perturb; num_perturb++; - } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER || - lower > -inf && upper < inf && obj < 0) { + } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER) { objective[j] = obj - perturb; sum_perturb += perturb; num_perturb++; @@ -1543,6 +1570,40 @@ i_t check_steepest_edge_norms(const simplex_solver_settings_t& setting return 0; } +// Remove the perturbation from a variable that is leaving the basis. Since it +// is nonbasic, its cost affects only its own reduced cost. If removing the +// perturbation would violate dual feasibility, apply just enough perturbation +// to maintain feasibility (for one-sided variables) or leave it unperturbed +// (for boxed variables, which can be flipped). +template +void remove_leaving_perturbation(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + i_t leaving_index, + std::vector& z, + std::vector& objective) +{ + const f_t perturb = objective[leaving_index] - lp.objective[leaving_index]; + if (perturb == 0.0) return; + + z[leaving_index] -= perturb; + objective[leaving_index] = lp.objective[leaving_index]; + + // Restore dual feasibility if needed + const f_t lower = lp.lower[leaving_index]; + const f_t upper = lp.upper[leaving_index]; + if (upper == inf && lower > -inf && z[leaving_index] < -settings.tight_tol) { + // At lower bound, needs z >= 0 + const f_t correction = -z[leaving_index]; + z[leaving_index] = 0.0; + objective[leaving_index] += correction; + } else if (lower == -inf && upper < inf && z[leaving_index] > settings.tight_tol) { + // At upper bound, needs z <= 0 + const f_t correction = z[leaving_index]; + z[leaving_index] = 0.0; + objective[leaving_index] -= correction; + } +} + template i_t compute_perturbation(const lp_problem_t& lp, const simplex_solver_settings_t& settings, @@ -3657,6 +3718,9 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, basic_list, entering_index, scaled_delta_xB_sparse, delta_x, phase2_work_estimate); timers.start_timer(phase2_work_estimate + ft.work_estimate()); + if (settings.remove_perturbation == 1) { + phase2::remove_leaving_perturbation(lp, settings, leaving_index, z, objective); + } f_t sum_perturb = 0.0; phase2::compute_perturbation( lp, settings, delta_z_indices, z, objective, sum_perturb, phase2_work_estimate); diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 8867c8c9b4..7548e2d28f 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -266,6 +266,54 @@ i_t phase2_pricing(const lp_problem_t& lp, return entering_index; } +template +i_t devex_pricing(const lp_problem_t& lp, + const std::vector& z, + const std::vector& devex_weight, + const std::vector& nonbasic_list, + const std::vector& vstatus, + f_t dual_tol, + i_t& direction, + i_t& basic_entering, + f_t& dual_inf, + f_t& work_estimate) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + i_t entering_index = -1; + f_t max_score = 0.0; + dual_inf = 0.0; + for (i_t k = 0; k < n - m; ++k) { + const i_t j = nonbasic_list[k]; + if (vstatus[j] == variable_status_t::NONBASIC_FIXED) { continue; } + f_t infeas = 0.0; + i_t dir = 0; + if ((vstatus[j] == variable_status_t::NONBASIC_LOWER || + vstatus[j] == variable_status_t::NONBASIC_FREE) && + z[j] < -dual_tol) { + infeas = -z[j]; + dir = 1; + } else if ((vstatus[j] == variable_status_t::NONBASIC_UPPER || + vstatus[j] == variable_status_t::NONBASIC_FREE) && + z[j] > dual_tol) { + infeas = z[j]; + dir = -1; + } + if (infeas > 0.0) { + dual_inf += infeas; + const f_t score = (infeas * infeas) / devex_weight[j]; + if (score > max_score) { + max_score = score; + basic_entering = k; + entering_index = j; + direction = dir; + } + } + } + work_estimate += 7 * (n - m); + return entering_index; +} + template f_t primal_infeasibility(const lp_problem_t& lp, const simplex_solver_settings_t& settings, @@ -811,6 +859,7 @@ primal_status_t primal_phase2_with_advanced_basis( std::vector incoming_vstatus = vstatus; work_estimate += 2.0 * n; settings.log.printf("Primal Simplex\n"); + settings.log.printf("Pricing: %s\n", settings.primal_pricing == 1 ? "Devex" : "Dantzig"); // Nonbasics must be on their bounds before forming B x_B = b - A_N x_N. // Setting them after the solve leaves ||A*x - b|| large whenever x_N != 0. set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); @@ -893,7 +942,9 @@ primal_status_t primal_phase2_with_advanced_basis( sparse_vector_t etilde(m, 0); std::vector delta_z(n); std::vector delta_x(n); - work_estimate += 2 * m + 2 * n; + std::vector devex_weight(n, 1.0); + i_t num_bad_devex_weight = 0; + work_estimate += 2 * m + 3 * n; f_t dual_inf = init_dual_inf; f_t obj = compute_objective(lp, x); @@ -922,15 +973,29 @@ primal_status_t primal_phase2_with_advanced_basis( timers.start_timer(work_estimate + basis_update.work_estimate()); i_t nonbasic_entering = -1; i_t direction; - i_t entering_index = phase2_pricing(lp, - z, - nonbasic_list, - vstatus, - pricing_dual_tol, - direction, - nonbasic_entering, - dual_inf, - work_estimate); + i_t entering_index; + if (settings.primal_pricing == 1) { + entering_index = devex_pricing(lp, + z, + devex_weight, + nonbasic_list, + vstatus, + pricing_dual_tol, + direction, + nonbasic_entering, + dual_inf, + work_estimate); + } else { + entering_index = phase2_pricing(lp, + z, + nonbasic_list, + vstatus, + pricing_dual_tol, + direction, + nonbasic_entering, + dual_inf, + work_estimate); + } timers.pricing_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); if (entering_index == -1) { if (phase == 2) { @@ -1249,6 +1314,37 @@ primal_status_t primal_phase2_with_advanced_basis( update_y(dual_step_length, delta_y, y, work_estimate); update_z(dual_step_length, nonbasic_list, entering_index, delta_z, z, work_estimate); timers.update_duals_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); + + // Devex weight update (only when using Devex pricing) + if (settings.primal_pricing == 1) { + const f_t pivot = scaled_delta_xB[basic_leaving]; + const f_t pivot_sq = pivot * pivot; + const f_t w_enter = devex_weight[entering_index]; + // Exact pivot weight for entering variable is 1/pivot_sq + // Check if stored weight was a bad approximation + const f_t exact_pivot_weight = 1.0 / pivot_sq; + if (w_enter > 3.0 * exact_pivot_weight) { num_bad_devex_weight++; } + // Update weights for all nonbasic columns using the pivot row (delta_z) + // After compute_delta_z and update_z, delta_z[j] still holds the raw + // pivot row entries (update_z multiplies by dual_step_length into z, not delta_z) + for (i_t k = 0; k < n - m; ++k) { + const i_t j = nonbasic_list[k]; + const f_t a_j = delta_z[j]; + const f_t candidate = (a_j * a_j / pivot_sq) * w_enter; + if (candidate > devex_weight[j]) { devex_weight[j] = candidate; } + } + // Weight for leaving variable (now nonbasic) + devex_weight[leaving_index] = std::max(1.0 / pivot_sq, f_t(1e-4)); + // Weight for entering variable (now basic) — reset + devex_weight[entering_index] = 1.0; + work_estimate += 5 * (n - m); + // Reset framework if too many bad weights + if (num_bad_devex_weight > 3) { + std::fill(devex_weight.begin(), devex_weight.end(), f_t(1.0)); + num_bad_devex_weight = 0; + } + } + timers.start_timer(work_estimate + basis_update.work_estimate()); should_refactor = basis_update.update(utilde_sparse, etilde, basic_leaving) == 1; timers.lu_update_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 50e8ca15a7..0189cd51de 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -78,11 +78,14 @@ struct simplex_solver_settings_t { dualize(-1), ordering(-1), initial_perturbation(-1), + remove_perturbation(-1), + primal_pricing(0), barrier_dual_initial_point(-1), postsolve_info(-1), qcqp_ruiz_equilibration(-1), check_Q(false), crossover(false), + unscaled_max_abs_obj_coeff(-1.0), refactor_frequency(100), iteration_log_frequency(1000), first_iteration_log(2), @@ -173,12 +176,15 @@ struct simplex_solver_settings_t { i_t dualize; // -1 automatic, 0 to not dualize, 1 to dualize i_t ordering; // -1 automatic, 0 to use nested dissection, 1 to use AMD i_t initial_perturbation; // -1 automatic, 0 to not perturb, 1 to perturb + i_t remove_perturbation; // -1 automatic, 0 disabled, 1 enabled + i_t primal_pricing; // 0 Dantzig (default), 1 Devex i_t barrier_dual_initial_point; // -1 automatic, 0 to use Lustig, Marsten, and Shanno initial // point, 1 to use initial point form dual least squares problem i_t postsolve_info; // -1 automatic (disabled), 0 disabled, 1 enabled i_t qcqp_ruiz_equilibration; // -1 automatic (imbalance heuristic), 0 disabled, 1 enabled bool check_Q; // true to check if Q is positive semidefinite bool crossover; // true to do crossover, false to not + f_t unscaled_max_abs_obj_coeff; // max |c_j| before scaling (-1 = not set, compute from lp) i_t refactor_frequency; // number of basis updates before refactorization i_t iteration_log_frequency; // number of iterations between log updates i_t first_iteration_log; // number of iterations to log at beginning of solve diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index fedb9de356..cd866ea1d2 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -221,6 +221,14 @@ lp_status_t solve_linear_program_with_advanced_basis( presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; std::vector row_scales_simplex; + // Compute max |c_j| before scaling for perturbation calibration + if (settings.unscaled_max_abs_obj_coeff < 0.0) { + f_t max_obj = 0.0; + for (i_t j = 0; j < presolved_lp.num_cols; ++j) { + max_obj = std::max(max_obj, std::abs(presolved_lp.objective[j])); + } + const_cast&>(settings).unscaled_max_abs_obj_coeff = max_obj; + } { raft::common::nvtx::range scope_scaling("DualSimplex::scaling"); scaling(presolved_lp, settings, lp, column_scales, row_scales_simplex); diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index ff39bbda22..8b7172d2f7 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -139,6 +139,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_DUALIZE, &pdlp_settings.dualize, -1, 1, -1}, {CUOPT_ORDERING, &pdlp_settings.ordering, -1, 1, -1}, {CUOPT_INITIAL_PERTURBATION, &pdlp_settings.initial_perturbation, -1, 1, -1}, + {CUOPT_REMOVE_PERTURBATION, &pdlp_settings.remove_perturbation, -1, 1, -1}, + {CUOPT_PRIMAL_PRICING, &pdlp_settings.primal_pricing, 0, 1, 0}, {CUOPT_BARRIER_DUAL_INITIAL_POINT, &pdlp_settings.barrier_dual_initial_point, -1, 1, -1}, {CUOPT_POSTSOLVE_INFO, &pdlp_settings.postsolve_info, -1, 1, -1}, {CUOPT_MIP_CUT_PASSES, &mip_settings.max_cut_passes, -1, std::numeric_limits::max(), 10}, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index beb53e8a17..7d8ce1e2d3 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -598,6 +598,8 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t dual_simplex_settings.iteration_limit = settings.iteration_limit; dual_simplex_settings.concurrent_halt = settings.concurrent_halt; dual_simplex_settings.initial_perturbation = settings.initial_perturbation; + dual_simplex_settings.remove_perturbation = settings.remove_perturbation; + dual_simplex_settings.primal_pricing = settings.primal_pricing; if (dual_simplex_settings.concurrent_halt != nullptr) { // Don't show the dual simplex log in concurrent mode. Show the PDLP log instead dual_simplex_settings.log.log = false; @@ -653,6 +655,7 @@ std::tuple, simplex::lp_status_t, f_t, f_t, f_t primal_settings.time_limit = settings.time_limit; primal_settings.iteration_limit = settings.iteration_limit; primal_settings.concurrent_halt = settings.concurrent_halt; + primal_settings.primal_pricing = settings.primal_pricing; if (primal_settings.concurrent_halt != nullptr) { // Don't show the primal simplex log in concurrent mode. Show the PDLP log instead primal_settings.log.log = false; From f54d57fedf73ba7763d1f25729d111d9cc62a8c2 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 26 Aug 2026 13:04:26 -0700 Subject: [PATCH 22/34] 15% improvement in dual simplex; new BFRT, perturbations, initial point, etc. The bound-flipping ratio test is rewritten. Instead of a heap-based approach a coarse filter is used to increase the step-length by multiples of 10. This is followed by a bucket sort. Each bucket contains variables with the same Harris ratio. We start from the final bucket, and go backward, trying to find a variable that statisfies our pivot threshold (more than 1/10th the maximum pivot) and maximizes the step length. When Phase-I completes and there are many bounded variables with NONBASIC_FIXED status with a small reduced costs | z_j | < dual_tol. These variables can be put on either bound. So we try three different initial points: 1) Set these variables on their lower bounds 2) Use a heuristic that assumes we have a slack basis B and sets the variable to the bound that minimizes the column sum. 3) Use a heuristic that sets the variable on the bound with the smallest absolute value. We test each of these points and choose the one that improves over the default (lower bounds) with less primal infeasibilities and small sum of primal infeasibilites squared. We do pertubations differently: 1) We attempt to remove perturbations as a variable leaves the basis. 2) We add a perturbation to the cost of the entering variable when we take a degenerate step. This accumulates after many degenerate steps and when we have a refactorization results in a different y and thus different reduced costs z 3) We don't apply perturbations if we are close to optimal. 4) We call set_primal_variables_on_bound after applying perturbation. Below we show a table of the baseline cuOpt code, compared to v5 (this PR). The HiGHS times were taken from a faster machine. So HiGHS advantage is slightly exaggerated. But it still exists. Problem Baseline v5 HiGHS v5/HiGHS ------------------------------------------------------------------------- momentum1 0.69 0.73 300.00 0.00 var-smallemery-m6j6 0.66 0.74 300.00 0.00 supportcase42 0.52 0.77 36.20 0.02 neos-5114902-kasavu 87.42 23.31 300.00 0.08 neos-5049753-cuanza 7.69 3.23 29.85 0.11 supportcase12 4.68 4.30 37.11 0.12 proteindesign121hz512p9 0.91 0.48 2.04 0.24 proteindesign122trx11p8 0.64 0.31 1.26 0.25 roi5alpha10n8 1.26 3.29 11.90 0.28 supportcase22 2.04 1.11 3.97 0.28 supportcase18 0.06 0.04 0.12 0.33 neos-787933 0.07 0.07 0.18 0.39 supportcase7 1.29 1.46 3.52 0.41 rocII-5-11 0.09 0.09 0.21 0.43 30n20b8 0.08 0.05 0.11 0.45 neos-860300 0.10 0.07 0.15 0.47 neos-5093327-huahum 0.23 0.23 0.48 0.48 cryptanalysiskb128n5obj14 29.78 6.14 12.54 0.49 co-100 0.68 0.64 1.28 0.50 fhnw-binpack4-48 0.07 0.03 0.06 0.50 mzzv11 40.19 8.46 16.71 0.51 rd-rplusc-21 0.23 0.25 0.49 0.51 lectsched-5-obj 0.13 0.10 0.18 0.56 roi2alpha3n4 0.31 0.67 1.16 0.58 dws008-01 0.04 0.03 0.05 0.60 neos-5052403-cygnet 300.00 187.30 300.00 0.62 cvs16r128-89 0.94 1.08 1.72 0.63 cryptanalysiskb128n5obj16 29.53 6.53 10.27 0.64 neos-5195221-niemur 0.50 0.24 0.37 0.65 neos-5188808-nattai 0.30 0.19 0.29 0.66 neos-3004026-krka 0.07 0.08 0.12 0.67 thor50dday 0.27 0.25 0.37 0.68 neos-4300652-rahue 1.22 0.56 0.80 0.70 square47 79.88 89.39 126.92 0.70 n3div36 0.11 0.13 0.18 0.72 blp-ar98 0.10 0.08 0.11 0.73 neos-960392 8.93 1.97 2.66 0.74 tbfp-network 9.06 6.73 9.04 0.74 neos-4647030-tutaki 2.47 2.33 3.09 0.75 decomp2 0.19 0.10 0.12 0.83 wachplan 0.25 0.23 0.26 0.88 netdiversion 7.65 8.61 9.43 0.91 supportcase40 0.24 0.22 0.24 0.92 istanbul-no-cutoff 0.71 0.87 0.94 0.93 supportcase10 300.00 105.36 113.55 0.93 ns1760995 135.95 250.52 269.53 0.93 neos-5104907-jarama 124.70 88.17 89.64 0.98 h80x6320d 0.05 0.04 0.04 1.00 highschool1-aigio 300.00 300.00 300.00 1.00 neos-1456979 0.05 0.04 0.04 1.00 neos-3988577-wolgan 278.89 300.00 300.00 1.00 neos859080 0.01 0.01 0.01 1.00 physiciansched3-3 300.00 300.00 300.00 1.00 pk1 0.02 0.01 0.01 1.00 rail02 300.00 300.00 300.00 1.00 s100 300.00 300.00 300.00 1.00 savsched1 300.00 300.00 300.00 1.00 supportcase19 300.00 300.00 300.00 1.00 swath3 0.03 0.03 0.03 1.00 timtab1 0.01 0.01 0.01 1.00 traininstance2 0.09 0.04 0.04 1.00 traininstance6 0.04 0.03 0.03 1.00 neos-4532248-waihi 2.61 0.92 0.90 1.02 cod105 9.06 7.73 7.46 1.04 square41 28.57 42.48 39.86 1.07 comp21-2idx 1.55 0.38 0.35 1.09 neos-873061 1.46 1.51 1.38 1.09 blp-ic98 0.12 0.11 0.10 1.10 neos-3555904-turama 1.31 1.52 1.37 1.11 piperout-27 0.67 0.29 0.26 1.12 sp97ar 0.40 0.37 0.33 1.12 piperout-08 0.39 0.18 0.16 1.12 triptim1 71.42 58.41 51.43 1.14 bnatt500 0.29 0.16 0.14 1.14 mushroom-best 0.26 0.23 0.20 1.15 leo2 0.14 0.15 0.13 1.15 neos-848589 1.25 1.02 0.85 1.20 neos-1122047 2.09 1.96 1.61 1.22 germanrr 0.31 0.33 0.27 1.22 net12 0.55 0.43 0.35 1.23 drayage-100-23 0.07 0.05 0.04 1.25 neos-3381206-awhea 0.08 0.05 0.04 1.25 neos-4738912-atrato 0.05 0.05 0.04 1.25 uct-subprob 0.11 0.10 0.08 1.25 ns1644855 300.00 300.00 238.30 1.26 leo1 0.10 0.09 0.07 1.29 atlanta-ip 6.85 5.86 4.54 1.29 neos-5107597-kakapo 0.04 0.13 0.10 1.30 air05 0.28 0.24 0.18 1.33 swath1 0.04 0.04 0.03 1.33 sp98ar 0.39 0.42 0.31 1.35 radiationm18-12-05 0.23 0.19 0.14 1.36 bnatt400 0.16 0.11 0.08 1.38 sct2 0.22 0.20 0.14 1.43 mcsched 0.27 0.23 0.16 1.44 supportcase6 7.39 6.03 4.18 1.44 irp 0.11 0.13 0.09 1.44 neos-4722843-widden 1.17 2.25 1.54 1.46 ns1952667 8.95 1.18 0.80 1.47 hypothyroid-k1 4.36 4.47 3.01 1.49 rail507 7.17 4.22 2.84 1.49 neos-3402294-bobin 3.07 1.79 1.20 1.49 drayage-25-23 0.08 0.06 0.04 1.50 icir97_tension 0.03 0.03 0.02 1.50 n5-3 0.03 0.03 0.02 1.50 neos-1582420 0.12 0.09 0.06 1.50 rococoC10-001000 0.04 0.03 0.02 1.50 neos-3216931-puriri 7.33 4.89 3.23 1.51 fast0507 7.37 4.24 2.77 1.53 ns1116954 156.23 16.44 10.73 1.53 neos-4763324-toguru 8.32 7.78 5.00 1.56 neos-3402454-bohle 221.94 115.56 74.15 1.56 nexp-150-20-8-5 0.10 0.11 0.07 1.57 trento1 3.08 3.49 2.21 1.58 qap10 15.57 10.58 6.68 1.58 neos8 0.38 0.42 0.26 1.62 rmatr200-p5 7.50 7.64 4.61 1.66 cmflsp50-24-8-8 0.77 0.73 0.44 1.66 neos-3083819-nubu 0.06 0.05 0.03 1.67 ran14x18-disj-8 0.04 0.05 0.03 1.67 rocI-4-11 0.11 0.10 0.06 1.67 neos-2987310-joes 1.52 1.67 1.00 1.67 neos-662469 1.65 0.91 0.53 1.72 chromaticindex512-7 16.62 37.48 21.24 1.76 k1mushroom 31.00 30.56 16.80 1.82 rococoB10-011000 0.13 0.11 0.06 1.83 comp07-2idx 4.03 2.05 1.10 1.86 reblock115 0.16 0.17 0.09 1.89 opm2-z10-s4 89.23 85.57 44.75 1.91 neos-3024952-loue 0.41 0.39 0.20 1.95 neos-2746589-doon 7.53 5.95 3.02 1.97 50v-10 0.02 0.02 0.01 2.00 b1c1s1 0.05 0.04 0.02 2.00 bppc4-08 0.08 0.04 0.02 2.00 cost266-UUE 0.03 0.04 0.02 2.00 eil33-2 0.05 0.06 0.03 2.00 enlight_hard 0.02 0.02 0.01 2.00 exp-1-500-5-5 0.02 0.02 0.01 2.00 fhnw-binpack4-4 0.03 0.02 0.01 2.00 gen-ip002 0.02 0.02 0.01 2.00 gen-ip054 0.03 0.02 0.01 2.00 glass4 0.02 0.02 0.01 2.00 graphdraw-domain 0.03 0.02 0.01 2.00 mad 0.02 0.02 0.01 2.00 markshare2 0.02 0.02 0.01 2.00 markshare_4_0 0.02 0.02 0.01 2.00 mas74 0.03 0.02 0.01 2.00 mas76 0.02 0.02 0.01 2.00 neos-3046615-murg 0.02 0.02 0.01 2.00 neos-3754480-nidda 0.02 0.02 0.01 2.00 neos-4338804-snowy 0.03 0.02 0.01 2.00 neos-4954672-berkel 0.02 0.02 0.01 2.00 neos-911970 0.03 0.02 0.01 2.00 neos5 0.02 0.02 0.01 2.00 pg 0.03 0.02 0.01 2.00 sp150x300d 0.02 0.02 0.01 2.00 supportcase26 0.03 0.02 0.01 2.00 tr12-30 0.03 0.02 0.01 2.00 mzzv42z 10.10 1.91 0.95 2.01 radiationm40-10-02 1.58 1.44 0.71 2.03 supportcase33 0.99 1.12 0.55 2.04 eilA101-2 2.68 2.86 1.39 2.06 fiball 0.71 0.30 0.14 2.14 chromaticindex1024-7 45.86 201.94 93.65 2.16 roll3000 0.12 0.13 0.06 2.17 n2seq36q 0.46 0.53 0.24 2.21 nursesched-sprint02 0.38 0.51 0.23 2.22 nw04 0.44 1.38 0.61 2.26 physiciansched6-2 11.15 15.62 6.72 2.32 seymour 0.83 0.89 0.38 2.34 ns1830653 0.42 0.33 0.14 2.36 seymour1 0.83 0.91 0.38 2.39 rmatr100-p10 0.27 0.34 0.14 2.43 nursesched-medium-hint03 10.47 11.61 4.73 2.45 neos-1171448 2.35 2.08 0.84 2.48 splice1k1 21.62 22.71 9.17 2.48 neos-4387871-tavua 0.10 0.10 0.04 2.50 nu25-pr12 0.05 0.05 0.02 2.50 unitcal_7 0.96 1.00 0.39 2.56 glass-sc 0.31 0.31 0.12 2.58 buildingenergy 300.00 300.00 115.61 2.59 sing44 9.62 14.79 5.69 2.60 bab2 300.00 76.29 28.97 2.63 graph20-20-1rand 0.23 0.32 0.12 2.67 rail01 223.84 196.04 71.23 2.75 bab6 143.43 38.82 14.06 2.76 CMS750_4 0.38 0.39 0.14 2.79 map16715-04 13.26 19.01 6.80 2.80 neos-2978193-inde 0.16 0.14 0.05 2.80 sorrell3 1.07 1.13 0.40 2.82 assign1-5-8 0.03 0.03 0.01 3.00 binkar10_1 0.02 0.03 0.01 3.00 csched008 0.11 0.09 0.03 3.00 ic97_potential 0.02 0.03 0.01 3.00 lotsize 0.03 0.03 0.01 3.00 mik-250-20-75-4 0.02 0.03 0.01 3.00 neos-2657525-crna 0.03 0.03 0.01 3.00 neos-3627168-kasai 0.04 0.03 0.01 3.00 p200x1188c 0.03 0.03 0.01 3.00 pg5_34 0.03 0.03 0.01 3.00 sing326 9.55 14.16 4.69 3.02 cbs-cta 0.42 0.32 0.10 3.20 csched007 0.19 0.16 0.05 3.20 uccase9 11.54 17.21 5.20 3.31 map10 11.08 21.74 6.25 3.48 neos-1171737 0.70 0.70 0.20 3.50 fastxgemm-n2r6s0t2 0.18 0.29 0.08 3.62 neos-957323 300.00 28.38 7.74 3.67 neos-1445765 0.16 0.35 0.09 3.89 gmu-35-40 0.03 0.04 0.01 4.00 neos17 0.03 0.04 0.01 4.00 s250r10 300.00 300.00 71.74 4.18 neos-1354092 300.00 300.00 70.92 4.23 milo-v12-6-r2-40-1 0.25 0.22 0.05 4.40 ns1208400 3.96 1.60 0.36 4.44 app1-1 0.08 0.09 0.02 4.50 neos-950242 1.05 0.81 0.18 4.50 uccase12 72.32 6.47 1.38 4.69 peg-solitaire-a3 1.88 2.54 0.52 4.88 beasleyC3 0.05 0.05 0.01 5.00 gmu-35-50 0.05 0.05 0.01 5.00 irish-electricity 181.58 300.00 59.53 5.04 academictimetablesmall 14.89 4.15 0.82 5.06 ex10 300.00 300.00 59.25 5.06 dano3_3 46.88 96.86 19.06 5.08 dano3_5 46.79 96.97 19.03 5.10 gfd-schedulen180f7d50m30k18 80.00 39.02 6.81 5.73 neos-2075418-temuka 128.33 300.00 50.57 5.93 mc11 0.06 0.06 0.01 6.00 neos-933966 16.79 18.35 2.80 6.55 neos-827175 9.36 1.91 0.29 6.59 neos-3656078-kumeu 2.37 1.57 0.23 6.83 app1-2 5.03 5.33 0.70 7.61 snp-02-004-104 14.18 23.47 2.83 8.29 neos-4413714-turia 3.97 16.53 1.93 8.56 neos-631710 300.00 300.00 31.97 9.38 satellites2-40 29.70 125.33 10.51 11.92 brazil3 300.00 93.59 7.24 12.93 ex9 300.00 300.00 14.07 21.32 satellites2-60-fs 4.17 171.22 3.34 51.26 ------------------------------------------------------------------------- Geomean Baseline/v5: 1.1452 Shifted(+1s): 1.0759 Geomean v5/HiGHS: 1.5487 Shifted(+1s): 1.1823 (240 problems) --- .../bound_flipping_ratio_test.cpp | 465 +++++---- .../bound_flipping_ratio_test.hpp | 39 +- cpp/src/dual_simplex/phase2.cpp | 898 ++++++++++++------ 3 files changed, 908 insertions(+), 494 deletions(-) diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index 3fbfbd1f82..1086b23ddf 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -11,12 +11,14 @@ #include #include +#include namespace cuopt::mathematical_optimization::simplex { template i_t bound_flipping_ratio_test_t::compute_breakpoints(std::vector& indicies, - std::vector& ratios) + std::vector& ratios, + std::vector& harris_ratios) { i_t n = n_; i_t m = m_; @@ -34,19 +36,21 @@ i_t bound_flipping_ratio_test_t::compute_breakpoints(std::vector& if (vstatus_[j] == variable_status_t::NONBASIC_FIXED) { continue; } if (vstatus_[j] == variable_status_t::NONBASIC_LOWER && delta_z_[j] < -pivot_tol) { indicies[idx] = k; - ratios[idx] = std::max((-dual_tol - z_[j]) / delta_z_[j], 0.0); + ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); + harris_ratios[idx] = std::max((-dual_tol - z_[j]) / delta_z_[j], 0.0); if constexpr (verbose) { settings_.log.printf("ratios[%d] = %e\n", idx, ratios[idx]); } idx++; } if (vstatus_[j] == variable_status_t::NONBASIC_UPPER && delta_z_[j] > pivot_tol) { indicies[idx] = k; - ratios[idx] = std::max((dual_tol - z_[j]) / delta_z_[j], 0.0); + ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); + harris_ratios[idx] = std::max((dual_tol - z_[j]) / delta_z_[j], 0.0); if constexpr (verbose) { settings_.log.printf("ratios[%d] = %e\n", idx, ratios[idx]); } idx++; } } - work_estimate_ += 4 * nz; - work_estimate_ += 4 * idx; + work_estimate_ += 5 * nz; + work_estimate_ += 5 * idx; pivot_tol /= 10; } return idx; @@ -60,7 +64,8 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, f_t& slope, f_t& step_length, i_t& nonbasic_entering, - i_t& entering_index) + i_t& entering_index, + f_t& max_val) { // Find the minimum ratio f_t min_val = inf; @@ -68,27 +73,21 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, i_t candidate = -1; f_t zero_tol = settings_.zero_tol; i_t k_idx = -1; + max_val = 0.0; i_t min_found = 0; - i_t harris_found = 0; for (i_t k = start; k < end; ++k) { if (ratios[k] < min_val) { min_val = ratios[k]; candidate = indicies[k]; k_idx = k; min_found++; - } else if (ratios[k] < min_val + zero_tol) { - // Use Harris to select variables with larger pivots - const i_t j = nonbasic_list_[indicies[k]]; - if (std::abs(delta_z_[j]) > std::abs(delta_z_[candidate])) { - min_val = ratios[k]; - candidate = indicies[k]; - k_idx = k; - } - harris_found++; + } + if (ratios[k] > max_val) { + max_val = ratios[k]; } } - work_estimate_ += (end - start) + 2 * min_found + 6 * harris_found; + work_estimate_ += (end - start) + 2 * min_found; step_length = min_val; nonbasic_entering = candidate; @@ -125,8 +124,19 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // Compute the initial set of breakpoints std::vector indicies(nz); std::vector ratios(nz); - work_estimate_ += 2 * nz; - i_t num_breakpoints = compute_breakpoints(indicies, ratios); + std::vector harris_ratios(nz); + work_estimate_ += 3 * nz; + double t0 = tic(); + i_t num_breakpoints = compute_breakpoints(indicies, ratios, harris_ratios); + time_compute_breakpoints_ += toc(t0); + num_breakpoints_ = num_breakpoints; + // Count zero ratios + num_harris_zero_ = 0; + num_exact_zero_ = 0; + for (i_t k = 0; k < num_breakpoints; k++) { + if (harris_ratios[k] == 0.0) num_harris_zero_++; + if (ratios[k] == 0.0) num_exact_zero_++; + } if constexpr (verbose) { settings_.log.printf("Initial breakpoints %d\n", num_breakpoints); } if (num_breakpoints == 0) { nonbasic_entering = -1; @@ -136,9 +146,12 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, f_t slope = slope_; nonbasic_entering = -1; i_t entering_index = RATIO_TEST_NO_ENTERING_VARIABLE; + f_t max_step_length; + t0 = tic(); i_t k_idx = single_pass( - 0, num_breakpoints, indicies, ratios, slope, step_length, nonbasic_entering, entering_index); + 0, num_breakpoints, indicies, harris_ratios, slope, step_length, nonbasic_entering, entering_index, max_step_length); + time_single_pass_ += toc(t0); if (k_idx == RATIO_TEST_NUMERICAL_ISSUES) { return RATIO_TEST_NUMERICAL_ISSUES; } bool continue_search = k_idx >= 0 && num_breakpoints > 1 && slope > 0.0; if (!continue_search) { @@ -150,6 +163,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, entering_index, std::abs(delta_z_[entering_index])); } + num_buckets_used_ = 0; + step_length_result_ = step_length; return entering_index; } @@ -162,188 +177,270 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, slope); } - // Continue the search using a heap to order the breakpoints - ratios[k_idx] = ratios[num_breakpoints - 1]; - indicies[k_idx] = indicies[num_breakpoints - 1]; - - constexpr bool use_bucket_pass = false; - - if (use_bucket_pass) { - f_t max_ratio = 0.0; - for (i_t k = 0; k < num_breakpoints - 1; ++k) { - if (ratios[k] > max_ratio) { max_ratio = ratios[k]; } + // This code is complicated. There are several important concepts that are needed to understand it. + // + // We are trying to compute the maximum step length we can take while: + // 1) Staying mostly dual feasible (we allow ourselves to be infeasible by dual_tol amount) + // 2) Increasing the dual objective + // 3) Selecting a variable with a large pivot (| delta_z[j] |) + // + // Let alpha be the step length. For each nonbasic variable j, we have + // z_j(alpha) = z_j + alpha * delta_z_j + // + // To stay dual feasible, we either need to keep + // z_j(alpha) >= 0, if j is on it's lower bound, or + // z_j(alpha) <= 0, if j is on it's upper bound. + // + // Consider the equation z_j(alpha) = z_j + alpha * delta_z_j = 0. Each variable j puts a bound on alpha: + // + // alpha_j <= -z_j / delta_z_j, if x_j = l_j (z_j >= 0) and delta_z_j < 0 + // alpha_j <= -z_j / delta_z_j, if x_j = u_j (z_j <= 0) and delta_z_j > 0 + // + // The code refers to these alpha_j as ratios, since they are the ratio of z_j to delta_z_j. + // + // Now we could take alpha = min_j alpha_j, and remain dual feasible. However, we are allowed to increase the step-length + // if j is a variable such that l_j <= x_j <= u_j. To see why imagine that our variable was currenlty on it's lower bound, + // with z_j > 0 and delta_z_j < 0, if we push alpha past alpha_j, than z_j(alpha) < 0. This is fine as long as we flip + // the variable to be on it's upper bound. Thus, we can push alpha past alpha_j for *bounded* variables. + // + // Note that this does not work if we try to increase alpha past alpha_j for a variable with a single bound. We would just + // be making ourselves dual infeasible. So we need to check whether a variable is bounded. + // + // The dual objective as a function of the step-length alpha, is piecewise linear and concave. The breakpoints of this + // piecewise linear function occur at each of the alpha_j values. + // We can keep increasing the step-length as long as the slope remains nonnegative. After that + // we must stop, because we could decrease the dual objective. So the code tracks the cumulative slope of the dual objective. + // + // Now we don't need to exactly feasible: z_j >= 0 if x_j = l_j and z_j <= 0 if x_j = u_j. We can violate these bounds by + // the dual feasibility tolerance eps. We allow ourselves to be infeasible if it would help us get a larger pivot + // (delta_z_j). Small pivots can cause numerical issues, so we would like to avoid them. + // + // With this tolerance we get the equations: + // z_j(alpha) = z_j + alpha * delta_z_j >= -eps if x_j = l_j + // z_j(alpha) = z_j + alpha * delta_z_j <= eps if x_j = u_j + // + // This gives bounds on alpha. We call these alpha_harris_j, for Paula Harris, who proposed this method. + // + // alpha_harris_j <= (-eps - z_j) / delta_z_j, if x_j = l_j and delta_z_j < 0 + // alpha_harris_j <= (eps - z_j) / delta_z_j, if x_j = u_j and delta_z_j > 0 + // + // Let alpha_harris = min_j alpha_harris_j. We can select the variable with the largest | delta_z_j | from those + // candidates { j | alpha_j <= alpha_harris }. + // + // We combine these two ideas (increasing the step length for bounded variables) and allowing ourselves to be slightly dual infeasible + // to choose a larger pivot. + // + // We partition the variables into buckets. Let B_k be the set of variables in bucket k. B_0 is defined as { j | alpha_j <= alpha_harris }. + // We then compute alpha_harris_1 = min_{j not in B_0} alpha_j. And B_1 is defined as { j not in B_0 | alpha_j <= alpha_harris_1 }. And + // so on. + // + // We want to balance two different things: + // 1) Taking a larger step length to increase the dual objective as much as possible, + // 2) Choosing a large pivot for numerical stability. + // + // Let max_pivot = max_j | delta_z_j |. We start working our way backward from the largest bucket to the smallest bucket, + // we choose a variable j that satisfies | delta_z_j | >= 0.1 * max_pivot. Since we can always choose a smaller step length + // for the sake of numerical stability. + // + // Now the final thing to understand is that the ratios alpha_j are not sorted in any particular order. And we don't want to + // pay the O(num_breakpoints * log(num_breakpoints)) cost of sorting them. + // + // So we set a threshold on the step-length and check if all variables j with alpha_j <= threshold have already caused the + // slope to go negative. If so, we just need to consider those candidate variables with alpha_j <= threshold. If not, we + // multiply the threshold by 10. This cost us O(log10(max_step_length/min_step_length) * num_breakpoints) time. So we aren't + // totally linear. But the hope is we are better than a sort. + + // Use a coarse filter to find candidates + f_t minimum_harris_ratio = step_length; + f_t coarse_threshold = (minimum_harris_ratio > 0.0) + ? std::min(10.0 * minimum_harris_ratio, max_step_length) + : max_step_length; + f_t total_slope = slope; + bool found_unbounded = false; + std::vector candidates(num_breakpoints); + std::iota(candidates.begin(), candidates.end(), 0); + work_estimate_ += 2 * num_breakpoints; + i_t scan_start = 0; + i_t num_candidates = 0; + + // This is O( log10(max_step_length/min_step_length) * num_breakpoints) + t0 = tic(); + while (total_slope >= 0.0 && coarse_threshold <= max_step_length && !found_unbounded) { + for (i_t h = scan_start; h < num_breakpoints; ++h) { + const i_t k = candidates[h]; + if (ratios[k] <= coarse_threshold) { + // Candidate is less than coarse threshold, move it to the front of the candidate list + std::swap(candidates[h], candidates[num_candidates]); + num_candidates++; + const i_t j = nonbasic_list_[indicies[k]]; + if (!bounded_variables_[j]) { + found_unbounded = true; + } else { + total_slope -= std::abs(delta_z_[j]) * (upper_[j] - lower_[j]); + } + } } - work_estimate_ += 2 * num_breakpoints; - settings_.log.printf( - "Starting heap passes. %d breakpoints max ratio %e\n", num_breakpoints - 1, max_ratio); - bucket_pass( - indicies, ratios, num_breakpoints - 1, slope, step_length, nonbasic_entering, entering_index); + work_estimate_ += 3 * (num_breakpoints - scan_start); + work_estimate_ += 8 * (num_candidates - scan_start); + scan_start = num_candidates; + coarse_threshold *= 10.0; } + time_coarse_filter_ += toc(t0); - heap_passes( - indicies, ratios, num_breakpoints - 1, slope, step_length, nonbasic_entering, entering_index); - - if constexpr (verbose) { - settings_.log.printf("BFRT step length %e entering index %d non basic entering %d pivot %e\n", - step_length, - entering_index, - nonbasic_entering, - std::abs(delta_z_[entering_index])); - } - return entering_index; -} + candidates.resize(num_candidates); -template -void bound_flipping_ratio_test_t::heap_passes(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_length, - i_t& nonbasic_entering, - i_t& entering_index) -{ - std::vector bare_idx(num_breakpoints); - constexpr bool verbose = false; - const f_t dual_tol = settings_.dual_tol; - const f_t zero_tol = settings_.zero_tol; - const std::vector& delta_z = delta_z_; - const std::vector& nonbasic_list = nonbasic_list_; - const i_t N = num_breakpoints; - for (i_t k = 0; k < N; ++k) { - bare_idx[k] = k; - if constexpr (verbose) { - settings_.log.printf("Adding index %d ratio %e pivot %e to heap\n", - current_indicies[k], - current_ratios[k], - std::abs(delta_z[nonbasic_list[current_indicies[k]]])); + // Check for variables with one sided bounds. These define the maximum step length. + if (found_unbounded) { + for (i_t h = 0; h < num_candidates; h++) { + const i_t k = candidates[h]; + const i_t j = nonbasic_list_[indicies[k]]; + if (!bounded_variables_[j]) { + max_step_length = std::min(max_step_length, harris_ratios[k]); + } } - } - work_estimate_ += N; - - auto compare = [zero_tol, ¤t_ratios, ¤t_indicies, &delta_z, &nonbasic_list]( - const i_t& a, const i_t& b) { - return (current_ratios[a] > current_ratios[b]) || - (current_ratios[b] - current_ratios[a] < zero_tol && - std::abs(delta_z[nonbasic_list[current_indicies[a]]]) > - std::abs(delta_z[nonbasic_list[current_indicies[b]]])); - }; - - std::make_heap(bare_idx.begin(), bare_idx.end(), compare); - work_estimate_ += 10 * bare_idx.size(); - - while (bare_idx.size() > 0 && slope > 0) { - // Remove minimum ratio from the heap and rebalance - i_t heap_index = bare_idx.front(); - std::pop_heap(bare_idx.begin(), bare_idx.end(), compare); - bare_idx.pop_back(); - work_estimate_ += 7 * std::log2(bare_idx.size() + 1); - - nonbasic_entering = current_indicies[heap_index]; - const i_t j = entering_index = nonbasic_list_[nonbasic_entering]; - step_length = current_ratios[heap_index]; - - if (bounded_variables_[j]) { - // We have a bounded variable - const f_t interval = upper_[j] - lower_[j]; - const f_t delta_slope = std::abs(delta_z_[j]) * interval; - const f_t pivot = std::abs(delta_z[j]); - if constexpr (verbose) { - settings_.log.printf( - "heap %d step-length %.12e pivot %e nonbasic entering %d slope %e delta_slope %e new " - "slope %e\n", - bare_idx.size(), - current_ratios[heap_index], - pivot, - nonbasic_entering, - slope, - delta_slope, - slope - delta_slope); + work_estimate_ += 5 * num_candidates; + + // Remove candidates that are greater than the maximum step length + work_estimate_ += 2 * candidates.size(); + for (i_t h = static_cast(candidates.size()) - 1; h >= 0; h--) { + const i_t k = candidates[h]; + const f_t ratio = ratios[k]; + if (ratio > max_step_length) { + // Swap with the last candidate and remove + candidates[h] = candidates.back(); + candidates.pop_back(); } - slope -= delta_slope; - } else { - // The variable is not bounded. Stop the search. - break; } - work_estimate_ += 10; + num_candidates = candidates.size(); + } - if (toc(start_time_) > settings_.time_limit) { - entering_index = RATIO_TEST_TIME_LIMIT; - return; + // Use a bucket sort to partition candidates into buckets by successive Harris breakpoints + // bucket_start[k] = index in candidates[] where bucket k starts + // Bucket k contains candidates[bucket_start[k]] .. candidates[bucket_start[k+1] - 1] + f_t threshold = minimum_harris_ratio; + i_t num_buckets = 0; + std::vector bucket_start(num_candidates + 1, 0); + f_t cumulative_slope = slope; + scan_start = 0; + work_estimate_ += num_candidates + 1; + + // This is O(num_buckets * num_candidates) + i_t slope_breaker_k = -1; // the candidate k that made slope go negative + t0 = tic(); + while (cumulative_slope >= 0.0 && scan_start < num_candidates && threshold <= max_step_length) { + f_t next_threshold = inf; + i_t write = scan_start; + + work_estimate_ += 7 * (num_candidates - scan_start); + for (i_t h = scan_start; h < num_candidates; h++) { + const i_t k = candidates[h]; + const f_t ratio = ratios[k]; + + if (ratio <= threshold) { + const i_t j = nonbasic_list_[indicies[k]]; + if (bounded_variables_[j]) { + cumulative_slope -= std::abs(delta_z_[j]) * (upper_[j] - lower_[j]); + if (cumulative_slope < 0.0 && slope_breaker_k < 0) { + slope_breaker_k = k; + } + } + std::swap(candidates[h], candidates[write]); + write++; + } else { + const i_t j = nonbasic_list_[indicies[k]]; + const f_t harris_ratio = harris_ratios[k]; + next_threshold = std::min(next_threshold, harris_ratio); + } } - if (settings_.concurrent_halt != nullptr && *settings_.concurrent_halt == 1) { - entering_index = CONCURRENT_HALT_RETURN; - return; + + + bucket_start[++num_buckets] = write; + if (write == scan_start) break; // No progress — prevent infinite loop + scan_start = write; + threshold = next_threshold; + + if (cumulative_slope < 0.0) break; + } + time_bucket_sort_ += toc(t0); + bucket0_size_ = (num_buckets > 0) ? bucket_start[1] : 0; + + // Compute the maximum pivot + // This is O(num_candidates) + f_t max_pivot = 0.0; + for (i_t h = 0; h < bucket_start[num_buckets]; h++) { + const i_t k = candidates[h]; + const i_t j = nonbasic_list_[indicies[k]]; + const f_t pivot = std::abs(delta_z_[j]); + if (pivot > max_pivot) { + max_pivot = pivot; } } -} - -template -void bound_flipping_ratio_test_t::bucket_pass(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_length, - i_t& nonbasic_entering, - i_t& entering_index) -{ - const f_t dual_tol = settings_.dual_tol; - const f_t zero_tol = settings_.zero_tol; - const std::vector& delta_z = delta_z_; - const std::vector& nonbasic_list = nonbasic_list_; - const i_t N = num_breakpoints; - - const i_t K = 400; // 0, -16, -15, ...., 0, 1, ...., 400 - 18 = 382 - std::vector buckets(K, 0.0); - std::vector bucket_count(K, 0); - for (i_t k = 0; k < N; ++k) { - const i_t idx = current_indicies[k]; - const f_t ratio = current_ratios[k]; - const f_t min_exponent = -16.0; - const f_t max_exponent = 382.0; - const f_t exponent = std::max(min_exponent, std::min(max_exponent, std::log10(ratio))); - const i_t bucket_idx = ratio == 0.0 ? 0 : static_cast(exponent - min_exponent + 1); - // settings_.log.printf("Ratio %e exponent %e bucket_idx %d\n", ratio, exponent, bucket_idx); - const i_t j = nonbasic_list[idx]; - const f_t interval = upper_[j] - lower_[j]; - const f_t delta_slope = std::abs(delta_z_[j]) * interval; - buckets[bucket_idx] += delta_slope; - bucket_count[bucket_idx]++; + work_estimate_ += 4 * bucket_start[num_buckets]; + + // Select the entering variable + // Scan from last bucket to first. Within each bucket, pick the variable with + // the largest ratio (step length) that has |delta_z| > pivot_threshold + f_t pivot_threshold = std::max(settings_.pivot_tol, std::min(0.1 * max_pivot, 1.0)); + i_t entering_k = -1; + + // This is O(num_candidates) + for (i_t b = num_buckets - 1; b >= 0; b--) { + const i_t b_start = bucket_start[b]; + const i_t b_end = bucket_start[b + 1]; + f_t best_ratio = -1.0; + for (i_t h = b_start; h < b_end; h++) { + const i_t k = candidates[h]; + const i_t j = nonbasic_list_[indicies[k]]; + const f_t pivot = std::abs(delta_z_[j]); + if (pivot > pivot_threshold && ratios[k] > best_ratio) { + best_ratio = ratios[k]; + entering_k = k; + } + } + work_estimate_ += 4 * (bucket_start[b + 1] - bucket_start[b]); + if (entering_k >= 0) break; } - - std::vector cumulative_sum(K, 0.0); - cumulative_sum[0] = buckets[0]; - if (cumulative_sum[0] > slope) { - settings_.log.printf( - "Bucket 0. Count in bucket %d. Slope %e. Cumulative sum %e. Bucket value %e\n", - bucket_count[0], - slope, - cumulative_sum[0], - buckets[0]); - return; + work_estimate_ += 2 * num_buckets; + + // Step = entering variable's breakpoint ratio + num_buckets_used_ = num_buckets; + if (entering_k < 0) { + // Fallback to single_pass result + used_fallback_ = true; + bucket_selected_ = -1; + step_length_result_ = step_length; + selected_is_slope_breaker_ = false; + return entering_index; } - i_t k; - bool exceeded = false; - for (k = 1; k < K; ++k) { - cumulative_sum[k] = cumulative_sum[k - 1] + buckets[k]; - if (cumulative_sum[k] > slope) { - exceeded = true; - break; + step_length = ratios[entering_k]; + nonbasic_entering = indicies[entering_k]; + entering_index = nonbasic_list_[nonbasic_entering]; + + // Record whether we selected the slope breaker + selected_is_slope_breaker_ = (entering_k == slope_breaker_k); + + // Record which bucket was selected + used_fallback_ = false; + for (i_t b = 0; b < num_buckets; b++) { + if (entering_k >= 0) { + // Find which bucket entering_k is in based on its position in candidates + i_t pos = -1; + for (i_t h = 0; h < num_candidates; h++) { + if (candidates[h] == entering_k) { pos = h; break; } + } + if (pos >= bucket_start[b] && pos < bucket_start[b + 1]) { + bucket_selected_ = b; + break; + } } } + step_length_result_ = step_length; + + return entering_index; - if (exceeded) { - settings_.log.printf( - "Value in bucket %d. Count in buckets %d. Slope %e. Cumulative sum %e. Next sum %e Bucket " - "value %e\n", - k, - bucket_count[k], - slope, - cumulative_sum[k - 1], - cumulative_sum[k], - buckets[k - 1]); - } } + #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE template class bound_flipping_ratio_test_t; diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp index 2f73069451..6e9dc647c1 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp @@ -56,8 +56,26 @@ class bound_flipping_ratio_test_t { i_t compute_step_length(f_t& step_length, i_t& nonbasic_entering); f_t work_estimate() const { return work_estimate_; } + // Timing fields (filled by compute_step_length) + f_t time_compute_breakpoints_{0.0}; + f_t time_single_pass_{0.0}; + f_t time_coarse_filter_{0.0}; + f_t time_bucket_sort_{0.0}; + f_t time_pivot_selection_{0.0}; + + // Diagnostic fields + i_t num_buckets_used_{0}; // number of buckets in bucket sort + i_t bucket_selected_{-1}; // which bucket the entering variable came from (-1 = single_pass/fallback) + f_t step_length_result_{0.0}; // the step length chosen + bool used_fallback_{false}; // true if we fell back to single_pass result + i_t bucket0_size_{0}; // size of first bucket (candidates with ratio <= min_harris) + i_t num_breakpoints_{0}; // total breakpoints computed + bool selected_is_slope_breaker_{false}; // true if we selected the variable that made slope go negative + i_t num_harris_zero_{0}; // number of harris_ratios that are exactly 0 + i_t num_exact_zero_{0}; // number of exact ratios that are exactly 0 + private: - i_t compute_breakpoints(std::vector& indices, std::vector& ratios); + i_t compute_breakpoints(std::vector& indices, std::vector& ratios, std::vector& harris_ratios); i_t single_pass(i_t start, i_t end, const std::vector& indices, @@ -65,23 +83,8 @@ class bound_flipping_ratio_test_t { f_t& slope, f_t& step_length, i_t& nonbasic_entering, - i_t& enetering_index); - void heap_passes(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_lenght, - i_t& nonbasic_entering, - i_t& entering_index); - - void bucket_pass(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_length, - i_t& nonbasic_entering, - i_t& entering_index); - + i_t& entering_index, + f_t& max_val); const std::vector& lower_; const std::vector& upper_; const std::vector& bounded_variables_; diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index e2a3c17d11..0b94a67d28 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -464,7 +464,7 @@ void initial_perturbation(const lp_problem_t& lp, max_abs_obj_coeff = std::max(max_abs_obj_coeff, std::abs(lp.objective[j])); } - // Dampen large costs + // Dampen large costs if (max_abs_obj_coeff > 100.0) { max_abs_obj_coeff = std::sqrt(std::sqrt(max_abs_obj_coeff)); } @@ -503,16 +503,15 @@ void initial_perturbation(const lp_problem_t& lp, if (lower == upper || (lower == -inf && upper == inf)) { continue; } - // Skip basic variables - if (vstatus[j] == variable_status_t::BASIC) { - continue; - } const f_t rand_val = random.random(); const f_t cost_factor = std::min(std::abs(obj) + 1.0, max_abs_obj_coeff + 1.0); const f_t perturb = (1.0 + rand_val) * cost_factor * perturbation_base; - if (vstatus[j] == variable_status_t::NONBASIC_LOWER || + if (vstatus[j] == variable_status_t::BASIC) { + // Skip basic variables + continue; + } else if (vstatus[j] == variable_status_t::NONBASIC_LOWER || vstatus[j] == variable_status_t::NONBASIC_FIXED) { // NONBASIC_FIXED from phase 1 for boxed variables — treat as at lower bound objective[j] = obj + perturb; @@ -1572,35 +1571,49 @@ i_t check_steepest_edge_norms(const simplex_solver_settings_t& setting // Remove the perturbation from a variable that is leaving the basis. Since it // is nonbasic, its cost affects only its own reduced cost. If removing the -// perturbation would violate dual feasibility, apply just enough perturbation -// to maintain feasibility (for one-sided variables) or leave it unperturbed -// (for boxed variables, which can be flipped). +// perturbation would violate dual feasibility, the perturbation is left in +// place (for boxed variables) or reduced to the minimum needed (for one-sided +// variables). template void remove_leaving_perturbation(const lp_problem_t& lp, const simplex_solver_settings_t& settings, i_t leaving_index, + i_t direction, std::vector& z, std::vector& objective) { const f_t perturb = objective[leaving_index] - lp.objective[leaving_index]; if (perturb == 0.0) return; - z[leaving_index] -= perturb; - objective[leaving_index] = lp.objective[leaving_index]; - - // Restore dual feasibility if needed const f_t lower = lp.lower[leaving_index]; const f_t upper = lp.upper[leaving_index]; - if (upper == inf && lower > -inf && z[leaving_index] < -settings.tight_tol) { - // At lower bound, needs z >= 0 - const f_t correction = -z[leaving_index]; - z[leaving_index] = 0.0; - objective[leaving_index] += correction; - } else if (lower == -inf && upper < inf && z[leaving_index] > settings.tight_tol) { - // At upper bound, needs z <= 0 - const f_t correction = z[leaving_index]; - z[leaving_index] = 0.0; - objective[leaving_index] -= correction; + const bool boxed = (lower > -inf && upper < inf); + + if (boxed) { + // Only remove if it won't create dual infeasibility. + // direction=1 means going to lower bound (needs z >= 0 after removal) + // direction=-1 means going to upper bound (needs z <= 0 after removal) + const f_t new_z = z[leaving_index] - perturb; + if (direction == 1 && new_z < -settings.tight_tol) { return; } + if (direction == -1 && new_z > settings.tight_tol) { return; } + z[leaving_index] = new_z; + objective[leaving_index] = lp.objective[leaving_index]; + } else { + z[leaving_index] -= perturb; + objective[leaving_index] = lp.objective[leaving_index]; + + // Restore dual feasibility if needed for one-sided variables + if (upper == inf && lower > -inf && z[leaving_index] < -settings.tight_tol) { + // At lower bound, needs z >= 0 + const f_t correction = -z[leaving_index]; + z[leaving_index] = 0.0; + objective[leaving_index] += correction; + } else if (lower == -inf && upper < inf && z[leaving_index] > settings.tight_tol) { + // At upper bound, needs z <= 0 + const f_t correction = z[leaving_index]; + z[leaving_index] = 0.0; + objective[leaving_index] -= correction; + } } } @@ -1608,9 +1621,12 @@ template i_t compute_perturbation(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& delta_z_indices, + const std::vector& vstatus, std::vector& z, std::vector& objective, f_t& sum_perturb, + i_t entering_index, + f_t step_length, f_t& work_estimate) { const i_t n = lp.num_cols; @@ -1626,32 +1642,27 @@ i_t compute_perturbation(const lp_problem_t& lp, objective[j] += violation; num_perturb++; sum_perturb += violation; -#ifdef PERTURBATION_DEBUG - if (violation > 1e-1) { - settings.log.printf( - "perturbation: violation %e j %d lower %e\n", violation, j, lp.lower[j]); - } -#endif } else if (lp.lower[j] == -inf && lp.upper[j] < inf && z[j] > tight_tol) { const f_t violation = z[j]; z[j] -= violation; // z[j] <- 0 objective[j] -= violation; num_perturb++; sum_perturb += violation; -#ifdef PERTURBATION_DEWBUG - if (violation > 1e-1) { - settings.log.printf( - "perturbation: violation %e j %d upper %e\n", violation, j, lp.upper[j]); - } -#endif } } - work_estimate += 7 * delta_z_indices.size(); -#ifdef PERTURBATION_DEBUG - if (num_perturb > 0) { - settings.log.printf("Perturbed %d dual variables by %e\n", num_perturb, sum_perturb); + // On degenerate steps, shift the entering variable's cost (like HiGHS) + // This accumulates shifts that break degeneracy at the next refactorization + if (entering_index >= 0 && step_length == 0.0) { + assert(vstatus[entering_index] != variable_status_t::BASIC); + const f_t shift = -z[entering_index]; + if (shift != 0.0) { + objective[entering_index] += shift; + z[entering_index] = 0.0; + sum_perturb += std::abs(shift); + num_perturb++; + } } -#endif + work_estimate += 7 * delta_z_indices.size(); return 0; } @@ -2273,19 +2284,26 @@ void bound_info(const lp_problem_t& lp, } template -void set_primal_variables_on_bounds(const lp_problem_t& lp, +i_t set_primal_variables_on_bounds(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& z, std::vector& vstatus, - std::vector& x) + std::vector& x, + i_t degen_type = 0) { PHASE2_NVTX_RANGE("DualSimplex::set_primal_variables_on_bounds"); const i_t n = lp.num_cols; f_t tol = 1e-10; + i_t num_fixed_to_lower = 0; + i_t num_fixed_to_upper = 0; + i_t num_lower_to_upper = 0; + i_t num_upper_to_lower = 0; + i_t num_set_fixed = 0; for (i_t j = 0; j < n; ++j) { // We set z_j = 0 for basic variables // But we explicitally skip setting basic variables here if (vstatus[j] == variable_status_t::BASIC) { continue; } + const variable_status_t old_vstatus = vstatus[j]; // We will flip the status of variables between nonbasic lower and nonbasic // upper here to improve dual feasibility const f_t fixed_tolerance = settings.fixed_tol; @@ -2306,29 +2324,69 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, vstatus[j] == variable_status_t::NONBASIC_UPPER) { x[j] = lp.upper[j]; } else if (z[j] >= 0 && lp.lower[j] > -inf) { - if (vstatus[j] != variable_status_t::NONBASIC_LOWER) { - settings.log.debug( - "Setting nonbasic lower variable (zj %e) %d to %e (current %e). vstatus %d\n", - z[j], - j, - lp.lower[j], - x[j], - static_cast(vstatus[j])); + // For boxed variables with degenerate z, use heuristic based on degen_type + if (degen_type >= 1 && std::abs(z[j]) < settings.dual_tol && lp.upper[j] < inf) { + if (degen_type == 1) { + // Column-sum heuristic + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + f_t col_sum = 0.0; + for (i_t k = col_start; k < col_end; k++) { + col_sum += lp.A.x[k]; + } + if (col_sum < 0.0) { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } else { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } + } else { + // degen_type == 3: abs_bound (prefer bound closer to zero, like HiGHS) + if (std::abs(lp.upper[j]) < std::abs(lp.lower[j])) { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } else { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } + } + } else { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; } - x[j] = lp.lower[j]; - vstatus[j] = variable_status_t::NONBASIC_LOWER; } else if (z[j] <= 0 && lp.upper[j] < inf) { - if (vstatus[j] != variable_status_t::NONBASIC_UPPER) { - settings.log.debug( - "Setting nonbasic upper variable (zj %e) %d to %e (current %e). vstatus %d\n", - z[j], - j, - lp.upper[j], - x[j], - static_cast(vstatus[j])); + // For boxed variables with degenerate z, use heuristic based on degen_type + if (degen_type >= 1 && std::abs(z[j]) < settings.dual_tol && lp.lower[j] > -inf) { + if (degen_type == 1) { + // Column-sum heuristic + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + f_t col_sum = 0.0; + for (i_t k = col_start; k < col_end; k++) { + col_sum += lp.A.x[k]; + } + if (col_sum > 0.0) { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } else { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } + } else { + // degen_type == 3: abs_bound (prefer bound closer to zero) + if (std::abs(lp.lower[j]) < std::abs(lp.upper[j])) { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } else { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } + } + } else { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; } - x[j] = lp.upper[j]; - vstatus[j] = variable_status_t::NONBASIC_UPPER; } else if (lp.upper[j] == inf && lp.lower[j] > -inf && z[j] < 0) { // dual infeasible if (vstatus[j] != variable_status_t::NONBASIC_LOWER) { @@ -2362,7 +2420,21 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, } else { assert(1 == 0); } + // Track changes + if (old_vstatus != vstatus[j]) { + if (old_vstatus == variable_status_t::NONBASIC_FIXED && vstatus[j] == variable_status_t::NONBASIC_LOWER) num_fixed_to_lower++; + else if (old_vstatus == variable_status_t::NONBASIC_FIXED && vstatus[j] == variable_status_t::NONBASIC_UPPER) num_fixed_to_upper++; + else if (old_vstatus == variable_status_t::NONBASIC_LOWER && vstatus[j] == variable_status_t::NONBASIC_UPPER) num_lower_to_upper++; + else if (old_vstatus == variable_status_t::NONBASIC_UPPER && vstatus[j] == variable_status_t::NONBASIC_LOWER) num_upper_to_lower++; + else if (vstatus[j] == variable_status_t::NONBASIC_FIXED) num_set_fixed++; + } } + i_t total_changes = num_fixed_to_lower + num_fixed_to_upper + num_lower_to_upper + num_upper_to_lower + num_set_fixed; + if (total_changes > 0) { + settings.log.printf("set_primal_variables_on_bounds: %d changes (fixed->lower=%d, fixed->upper=%d, lower->upper=%d, upper->lower=%d, ->fixed=%d)\n", + total_changes, num_fixed_to_lower, num_fixed_to_upper, num_lower_to_upper, num_upper_to_lower, num_set_fixed); + } + return total_changes; } template @@ -2387,6 +2459,180 @@ f_t amount_of_perturbation(const lp_problem_t& lp, const std::vector +i_t attempt_to_remove_perturbations(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + basis_update_mpf_t& ft, + const std::vector& basic_list, + const std::vector& nonbasic_list, + std::vector& vstatus, + std::vector& objective, + std::vector& z, + std::vector& y, + std::vector& x, + std::vector& xB_workspace, + std::vector& squared_infeasibilities, + std::vector& infeasibility_indices, + f_t& primal_infeasibility, + f_t& primal_infeasibility_squared, + f_t& work_estimate) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + const i_t n_minus_m = n - m; + + // Check if there's any perturbation + const f_t perturbation = amount_of_perturbation(lp, objective); + if (perturbation <= 1e-6) return 0; // OPTIMAL + + // Count perturbations on basic vs nonbasic variables + i_t num_basic_perturbed = 0; + i_t num_nonbasic_boxed_perturbed = 0; + i_t num_nonbasic_other_perturbed = 0; + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + if (objective[j] != lp.objective[j]) num_basic_perturbed++; + } + for (i_t k = 0; k < n_minus_m; ++k) { + const i_t j = nonbasic_list[k]; + if (objective[j] != lp.objective[j]) { + const f_t lower = lp.lower[j]; + const f_t upper = lp.upper[j]; + if (lower > -inf && upper < inf && lower != upper) { + num_nonbasic_boxed_perturbed++; + } else { + num_nonbasic_other_perturbed++; + } + } + } + + if (num_basic_perturbed == 0 && num_nonbasic_other_perturbed == 0) { + // Safe path: perturbation only on nonbasic boxed variables. + // y is unaffected; z[j] - perturb gives exact unperturbed reduced cost. + i_t num_flipped = 0; + for (i_t k = 0; k < n_minus_m; ++k) { + const i_t j = nonbasic_list[k]; + const f_t perturb = objective[j] - lp.objective[j]; + if (perturb == 0.0) continue; + const f_t new_z = z[j] - perturb; + if (vstatus[j] == variable_status_t::NONBASIC_LOWER && new_z < -settings.dual_tol) { + vstatus[j] = variable_status_t::NONBASIC_UPPER; + z[j] = new_z; + objective[j] = lp.objective[j]; + num_flipped++; + } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER && new_z > settings.dual_tol) { + vstatus[j] = variable_status_t::NONBASIC_LOWER; + z[j] = new_z; + objective[j] = lp.objective[j]; + num_flipped++; + } else { + z[j] = new_z; + objective[j] = lp.objective[j]; + } + } + work_estimate += 5 * n_minus_m; + + // Recompute x_B with flipped statuses + compute_primal_solution_from_basis(lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); + work_estimate += 2 * n; + primal_infeasibility_squared = + compute_initial_primal_infeasibilities(lp, settings, basic_list, x, + squared_infeasibilities, infeasibility_indices, + primal_infeasibility); + work_estimate += 4 * m + 2 * n; + + if (primal_infeasibility <= settings.primal_tol) return 0; // OPTIMAL + settings.log.printf("Removed perturbation. Continuing dual simplex (primal_inf=%.2e)\n", primal_infeasibility); + return 1; // CONTINUE_DUAL + } + + // Perturbation on basic (or one-sided nonbasic) variables: need to recompute (y, z). + std::vector unperturbed_y(m); + std::vector unperturbed_z(n); + compute_dual_solution_from_basis(lp, ft, basic_list, nonbasic_list, + unperturbed_y, unperturbed_z, work_estimate); + + // Check if removal is clean (no dual infeasibility) + const f_t dual_infeas = dual_infeasibility(lp, settings, vstatus, unperturbed_z, + settings.tight_tol, settings.dual_tol); + work_estimate += 3 * n; + if (dual_infeas <= settings.dual_tol) { + settings.log.printf("Removed perturbation of %.2e.\n", perturbation); + z = unperturbed_z; + y = unperturbed_y; + objective = lp.objective; + work_estimate += 3 * n + 2 * m; + return 0; // OPTIMAL + } + + // Flip boxed nonbasics that are dual infeasible, and check for one-sided infeasibility + std::vector new_vstatus = vstatus; + i_t num_flipped = 0; + f_t residual_dual_infeas = 0.0; + for (i_t k = 0; k < n_minus_m; ++k) { + const i_t j = nonbasic_list[k]; + const f_t zj = unperturbed_z[j]; + const f_t lower = lp.lower[j]; + const f_t upper = lp.upper[j]; + const bool boxed = (lower > -inf && upper < inf && lower != upper); + + if (new_vstatus[j] == variable_status_t::NONBASIC_LOWER && zj < -settings.dual_tol) { + if (boxed) { + new_vstatus[j] = variable_status_t::NONBASIC_UPPER; + num_flipped++; + } else { + residual_dual_infeas = std::max(residual_dual_infeas, -zj); + } + } else if (new_vstatus[j] == variable_status_t::NONBASIC_UPPER && zj > settings.dual_tol) { + if (boxed) { + new_vstatus[j] = variable_status_t::NONBASIC_LOWER; + num_flipped++; + } else { + residual_dual_infeas = std::max(residual_dual_infeas, zj); + } + } + } + work_estimate += 5 * n_minus_m; + + if (residual_dual_infeas > settings.dual_tol) { + // One-sided infeasibility remains — can't continue with dual simplex. + // new_vstatus is discarded; vstatus unchanged. + settings.log.printf("Perturbation removal: %d flips, residual_dual_infeas=%.2e (PRIMAL_CLEANUP)\n", + num_flipped, residual_dual_infeas); + return 2; // PRIMAL_CLEANUP + } + + // All infeasibility was on boxed variables — accept unperturbed solution + vstatus = new_vstatus; + z = unperturbed_z; + y = unperturbed_y; + objective = lp.objective; + work_estimate += 3 * n + 2 * m; + + // Recompute x_B with flipped statuses + compute_primal_solution_from_basis(lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); + work_estimate += 2 * n; + primal_infeasibility_squared = + compute_initial_primal_infeasibilities(lp, settings, basic_list, x, + squared_infeasibilities, infeasibility_indices, + primal_infeasibility); + work_estimate += 4 * m + 2 * n; + + settings.log.printf("Perturbation removal: %d flips, primal_inf=%.2e\n", num_flipped, primal_infeasibility); + if (primal_infeasibility <= settings.primal_tol) return 0; // OPTIMAL + settings.log.printf("Continuing dual after flip (primal_inf=%.2e)\n", primal_infeasibility); + return 1; // CONTINUE_DUAL +} + template void prepare_optimality(i_t info, f_t orig_primal_infeas, @@ -2414,86 +2660,21 @@ void prepare_optimality(i_t info, sol.objective = compute_objective(lp, sol.x); sol.user_objective = compute_user_objective(lp, sol.objective); - f_t perturbation = amount_of_perturbation(lp, objective); - f_t orig_perturbation = perturbation; - if (perturbation > 1e-6 && phase == 2) { - // Try to remove perturbation - std::vector unperturbed_y(m); - std::vector unperturbed_z(n); - compute_dual_solution_from_basis( - lp, ft, basic_list, nonbasic_list, unperturbed_y, unperturbed_z, work_estimate); - { - const f_t dual_infeas = dual_infeasibility( - lp, settings, vstatus, unperturbed_z, settings.tight_tol, settings.dual_tol); - if (dual_infeas <= settings.dual_tol) { - settings.log.printf("Removed perturbation of %.2e.\n", perturbation); - z = unperturbed_z; - y = unperturbed_y; - perturbation = 0.0; - } else { - settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); - settings.log.printf("Unperturbed dual infeasibility: %.2e\n", dual_infeas); - settings.log.printf("Objective: %+.16e\n", sol.user_objective); - settings.log.printf("Num updates: %d\n", ft.num_updates()); - settings.log.printf("Iterations: %d\n", iter); - - i_t dual_iter = iter; - - // Primal pivots in place, so keep the perturbed solution to fall back on. - // The factor is snapshot rather than refactorized on failure: the copy is - // exact, keeps ft consistent with the restored basis, and cannot itself - // fail the way a refactorization can. - const basis_update_mpf_t saved_ft = ft; - const std::vector saved_x = sol.x; - const std::vector saved_y = sol.y; - const std::vector saved_z = sol.z; - const std::vector saved_vstatus = vstatus; - const std::vector saved_basic_list = basic_list; - const std::vector saved_nonbasic_list = nonbasic_list; - - // Reoptimize the unperturbed objective from this basis. The point is - // primal feasible, so primal simplex stays in phase 2 and pivots only to - // restore dual feasibility. It writes through sol, so x, y and z here see - // the cleaned up solution. It prints no summary; the one below reports the - // final result. - primal_status_t primal_status = primal_phase2_with_advanced_basis(2, - start_time, - lp, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - sol, - iter, - work_estimate, - false); - if (primal_status == primal_status_t::OPTIMAL) { - // z now prices the original objective, so no perturbation remains. - settings.log.printf("Primal cleanup successful. Iterations %d\n", iter - dual_iter); - perturbation = 0.0; - sol.objective = compute_objective(lp, sol.x); - sol.user_objective = compute_user_objective(lp, sol.objective); - } else { - // Restore the perturbed optimum; a partially pivoted basis is worse than - // the dual feasible point we started from. - settings.log.printf("Primal cleanup failed. Reporting the perturbed solution.\n"); - ft = saved_ft; - sol.x = saved_x; - sol.y = saved_y; - sol.z = saved_z; - vstatus = saved_vstatus; - basic_list = saved_basic_list; - nonbasic_list = saved_nonbasic_list; - } - } - } - } + const f_t perturbation = amount_of_perturbation(lp, objective); sol.l2_primal_residual = l2_primal_residual(lp, sol); sol.l2_dual_residual = l2_dual_residual(lp, sol); const f_t dual_infeas = dual_infeasibility(lp, settings, vstatus, z, 0.0, 0.0); - const f_t primal_infeas = primal_infeasibility(lp, settings, vstatus, x); + // Compute max primal infeasibility for reporting + f_t primal_infeas = 0.0; + for (i_t j = 0; j < n; ++j) { + if (x[j] < lp.lower[j]) { + primal_infeas = std::max(primal_infeas, lp.lower[j] - x[j]); + } + if (x[j] > lp.upper[j]) { + primal_infeas = std::max(primal_infeas, x[j] - lp.upper[j]); + } + } if (phase == 1 && iter > 0) { settings.log.printf("Dual phase I complete. Iterations %d. Time %.2f\n", iter, toc(start_time)); } @@ -2521,14 +2702,13 @@ void prepare_optimality(i_t info, primal_infeasibility_breakdown( lp, settings, vstatus, x, basic_infeas, nonbasic_infeas, basic_over); settings.log.printf( - "Primal infeasibility %e/%e (Basic %e, Nonbasic %e, Basic over %e). Perturbation %e/%e. Info " + "Primal infeasibility %e/%e (Basic %e, Nonbasic %e, Basic over %e). Perturbation %e. Info " "%d\n", primal_infeas, orig_primal_infeas, basic_infeas, nonbasic_infeas, basic_over, - orig_perturbation, perturbation, info); } @@ -2618,6 +2798,29 @@ class phase2_timers_t { update_infeasibility_time.work; // clang-format off print_one(settings, "BFRT time", bfrt_time, total_time, total_work); + if (bfrt_time.time > 0.1) { + settings.log.printf(" BFRT breakpoints: %.2fs\n", bfrt_breakpoints_time); + settings.log.printf(" BFRT single_pass: %.2fs\n", bfrt_single_pass_time); + settings.log.printf(" BFRT coarse: %.2fs\n", bfrt_coarse_time); + settings.log.printf(" BFRT bucket: %.2fs\n", bfrt_bucket_time); + settings.log.printf(" BFRT select: %.2fs\n", bfrt_select_time); + } + if (bfrt_calls > 0) { + settings.log.printf(" BFRT calls: %d, zero_steps: %d (%.1f%%), single_pass_only: %d, bucket_used: %d, not_last_bucket: %d, fallback: %d\n", + bfrt_calls, bfrt_zero_steps, 100.0 * bfrt_zero_steps / bfrt_calls, + bfrt_single_pass_only, bfrt_bucket_used, bfrt_not_last_bucket, bfrt_fallback); + settings.log.printf(" BFRT slope_breaker: %d, not_slope_breaker: %d (%.1f%%)\n", + bfrt_selected_slope_breaker, bfrt_not_slope_breaker, + bfrt_bucket_used > 0 ? 100.0 * bfrt_not_slope_breaker / bfrt_bucket_used : 0.0); + if (bfrt_zero_steps > 0) { + settings.log.printf(" BFRT zero-step avg: num_buckets=%.1f, bucket0_size=%.1f, num_breakpoints=%.1f, harris_zero=%.1f, exact_zero=%.1f\n", + 1.0 * bfrt_zero_step_num_buckets_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_bucket0_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_num_breakpoints_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_harris_zero_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_exact_zero_sum / bfrt_zero_steps); + } + } print_one(settings, "Pricing time", pricing_time, total_time, total_work); print_one(settings, "BTran time", btran_time, total_time, total_work); print_one(settings, "FTran time", ftran_time, total_time, total_work); @@ -2638,6 +2841,25 @@ class phase2_timers_t { // clang-format on } work_timer_t bfrt_time; + f_t bfrt_breakpoints_time{0.0}; + f_t bfrt_single_pass_time{0.0}; + f_t bfrt_coarse_time{0.0}; + f_t bfrt_bucket_time{0.0}; + f_t bfrt_select_time{0.0}; + // BFRT diagnostic counters + i_t bfrt_calls{0}; + i_t bfrt_zero_steps{0}; // step_length == 0 + i_t bfrt_single_pass_only{0}; // no bound flips (single_pass decided) + i_t bfrt_bucket_used{0}; // bucket sort was used + i_t bfrt_not_last_bucket{0}; // selected from a bucket other than the last + i_t bfrt_fallback{0}; // fell back to single_pass result after bucket sort + i_t bfrt_zero_step_num_buckets_sum{0}; // sum of num_buckets on zero-step iters + i_t bfrt_zero_step_bucket0_sum{0}; // sum of bucket0 size on zero-step iters + i_t bfrt_zero_step_num_breakpoints_sum{0}; // sum of num_breakpoints on zero-step iters + i_t bfrt_zero_step_harris_zero_sum{0}; // sum of harris_ratios==0 on zero-step iters + i_t bfrt_zero_step_exact_zero_sum{0}; // sum of exact ratios==0 on zero-step iters + i_t bfrt_selected_slope_breaker{0}; // times we selected the slope breaker + i_t bfrt_not_slope_breaker{0}; // times we selected something else work_timer_t pricing_time; work_timer_t btran_time; work_timer_t ftran_time; @@ -2777,10 +2999,6 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, if (toc(start_time) > settings.time_limit) { return dual_status_t::TIME_LIMIT; } } - if (settings.initial_perturbation == 1 && phase == 2) { - phase2::initial_perturbation(lp, settings, vstatus, objective); - } - // Populate c_basic after basis is initialized for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; @@ -2811,8 +3029,117 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, assert(dual_res_norm < 1e-3); #endif - phase2::set_primal_variables_on_bounds(lp, settings, z, vstatus, x); - phase2_work_estimate += 5 * (n - m); + // Count degenerate NONBASIC_FIXED variables before bound assignment + i_t num_degen = 0; + { + i_t num_fixed = 0; + for (i_t j = 0; j < n; ++j) { + if (vstatus[j] == variable_status_t::NONBASIC_FIXED) { + if (std::abs(lp.lower[j] - lp.upper[j]) >= settings.fixed_tol) { + num_fixed++; + if (std::abs(z[j]) < settings.dual_tol) num_degen++; + } + } + } + settings.log.printf("NONBASIC_FIXED boxed: %d, degenerate (|z_j| < dual_tol): %d\n", num_fixed, num_degen); + } + + // Try 3 strategies for degenerate bound assignment, pick best + f_t best_sum_infeas = inf; + i_t best_num_infeas = m; + i_t best_degen_type = 0; + std::vector best_vstatus; + std::vector best_x; + const char* degen_names[] = {"default", "column-sum", "abs-bound"}; + const i_t degen_types[] = {0, 1, 3}; + f_t all_sum_infeas[3]; + i_t all_num_infeas[3]; + + for (i_t di = 0; di < 3; di++) { + const i_t dt = degen_types[di]; + std::vector try_vstatus = vstatus; + std::vector try_x = x; + phase2::set_primal_variables_on_bounds(lp, settings, z, try_vstatus, try_x, dt); + phase2::compute_primal_variables(ft, lp.rhs, lp.A, basic_list, nonbasic_list, + settings.tight_tol, try_x, xB_workspace, phase2_work_estimate); + f_t sum_infeas = 0.0; + i_t num_infeas = 0; + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + f_t infeas = std::max(lp.lower[j] - try_x[j], try_x[j] - lp.upper[j]); + if (infeas > 0.0) { sum_infeas += infeas; num_infeas++; } + } + all_sum_infeas[di] = sum_infeas; + all_num_infeas[di] = num_infeas; + if (di == 0) { + // Default is the baseline + best_sum_infeas = sum_infeas; + best_num_infeas = num_infeas; + best_degen_type = 0; + best_vstatus = try_vstatus; + best_x = try_x; + } else { + // Only pick alternative if BOTH fewer infeasibilities AND lower sum + if (num_infeas <= best_num_infeas && sum_infeas < best_sum_infeas) { + best_sum_infeas = sum_infeas; + best_num_infeas = num_infeas; + best_degen_type = di; + best_vstatus = try_vstatus; + best_x = try_x; + } + } + if (phase == 1 || num_degen == 0) { + for (i_t t = 1; t < 3; t++) { + all_sum_infeas[t] = sum_infeas; + all_num_infeas[t] = num_infeas; + } + break; + } + } + vstatus = best_vstatus; + x = best_x; + settings.log.printf("Bound assignment: default(%d/%.2e) colsum(%d/%.2e) abs-bound(%d/%.2e) -> %s\n", + all_num_infeas[0], all_sum_infeas[0], + all_num_infeas[1], all_sum_infeas[1], + all_num_infeas[2], all_sum_infeas[2], + degen_names[best_degen_type]); + phase2_work_estimate += 15 * (n - m); + + // Near-optimality check: decide whether to apply initial perturbation + if (settings.initial_perturbation != 0 && phase == 2) { + i_t num_primal_infeas = 0; + f_t max_primal_infeas = 0.0; + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + f_t infeas = std::max(lp.lower[j] - x[j], x[j] - lp.upper[j]); + if (infeas > settings.primal_tol) { + num_primal_infeas++; + max_primal_infeas = std::max(max_primal_infeas, infeas); + } + } + bool near_optimal = (num_primal_infeas < 1000 && max_primal_infeas < 1e-3); + bool apply_perturbation = (settings.initial_perturbation == 1) || !near_optimal; + settings.log.printf("Near-optimal check: num_primal_infeas=%d, max_primal_infeas=%.2e, near_optimal=%d, apply_perturbation=%d\n", + num_primal_infeas, max_primal_infeas, near_optimal, apply_perturbation); + if (apply_perturbation) { + phase2::initial_perturbation(lp, settings, vstatus, objective); + // Recompute y, z with perturbed objective + for (i_t k = 0; k < m; ++k) { + c_basic[k] = objective[basic_list[k]]; + } + phase2_work_estimate += 3 * m; + ft.b_transpose_solve(c_basic, y); + phase2::compute_reduced_costs( + objective, lp.A, y, basic_list, nonbasic_list, z, phase2_work_estimate); + // Reassign bounds based on perturbed z (breaks degeneracy) + i_t num_bound_changes2 = phase2::set_primal_variables_on_bounds(lp, settings, z, vstatus, x); + phase2_work_estimate += 5 * (n - m); + if (num_bound_changes2 > 0) { + phase2::compute_primal_variables(ft, lp.rhs, lp.A, basic_list, nonbasic_list, + settings.tight_tol, x, xB_workspace, phase2_work_estimate); + } + } + } #ifdef PRINT_VSTATUS_CHANGES i_t num_vstatus_changes; @@ -2836,16 +3163,6 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } phase2_work_estimate += 3 * n; - phase2::compute_primal_variables(ft, - lp.rhs, - lp.A, - basic_list, - nonbasic_list, - settings.tight_tol, - x, - xB_workspace, - phase2_work_estimate); - if (toc(start_time) > settings.time_limit) { return dual_status_t::TIME_LIMIT; } if (print_norms) { settings.log.printf("|| x || %e\n", vector_norm2(x)); } @@ -2964,6 +3281,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, i_t dense_delta_z = 0; i_t num_refactors = 0; i_t total_bound_flips = 0; + i_t max_bound_flips = 0; f_t delta_y_nz_percentage = 0.0; phase2::phase2_timers_t timers(true); @@ -3144,6 +3462,51 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += ft.work_estimate(); ft.clear_work_estimate(); + + // Before declaring optimal, attempt to remove perturbation. + if (phase == 2) { + i_t removal_status = phase2::attempt_to_remove_perturbations( + lp, settings, ft, basic_list, nonbasic_list, vstatus, objective, + z, y, x, xB_workspace, squared_infeasibilities, infeasibility_indices, + primal_infeasibility, primal_infeasibility_squared, phase2_work_estimate); + if (removal_status == 1) { // CONTINUE_DUAL + obj = phase2::compute_perturbed_objective(objective, x); + phase2_work_estimate += 2 * n; + continue; + } + if (removal_status == 2) { // PRIMAL_CLEANUP + const f_t perturbation = phase2::amount_of_perturbation(lp, objective); + settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); + settings.log.printf("Num updates: %d\n", ft.num_updates()); + settings.log.printf("Iterations: %d\n", iter); + i_t dual_iter = iter; + primal_status_t primal_status = primal_phase2_with_advanced_basis(2, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + phase2_work_estimate, + false); + if (primal_status == primal_status_t::OPTIMAL) { + settings.log.printf("Primal cleanup successful. Iterations %d\n", iter - dual_iter); + objective = lp.objective; + } else { + settings.log.printf("Primal cleanup failed.\n"); + const f_t dual_infeas = phase2::dual_infeasibility( + lp, settings, vstatus, sol.z, settings.tight_tol, settings.dual_tol); + if (dual_infeas > 10.0 * settings.dual_tol) { + return dual_status_t::NUMERICAL; + } + } + } + // removal_status == 0 (OPTIMAL) or primal cleanup done: fall through to prepare_optimality + } + phase2::prepare_optimality(0, primal_infeasibility, lp, @@ -3160,8 +3523,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, iter, x, y, - z, - sol); + z, + sol); status = dual_status_t::OPTIMAL; break; } @@ -3305,6 +3668,36 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, return dual_status_t::NUMERICAL; } timers.bfrt_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); + timers.bfrt_breakpoints_time += bfrt.time_compute_breakpoints_; + timers.bfrt_single_pass_time += bfrt.time_single_pass_; + timers.bfrt_coarse_time += bfrt.time_coarse_filter_; + timers.bfrt_bucket_time += bfrt.time_bucket_sort_; + timers.bfrt_select_time += bfrt.time_pivot_selection_; + // BFRT diagnostics + timers.bfrt_calls++; + if (step_length == 0.0) { + timers.bfrt_zero_steps++; + timers.bfrt_zero_step_num_buckets_sum += bfrt.num_buckets_used_; + timers.bfrt_zero_step_bucket0_sum += bfrt.bucket0_size_; + timers.bfrt_zero_step_num_breakpoints_sum += bfrt.num_breakpoints_; + timers.bfrt_zero_step_harris_zero_sum += bfrt.num_harris_zero_; + timers.bfrt_zero_step_exact_zero_sum += bfrt.num_exact_zero_; + } + if (bfrt.num_buckets_used_ == 0) { + timers.bfrt_single_pass_only++; + } else { + timers.bfrt_bucket_used++; + if (bfrt.used_fallback_) { + timers.bfrt_fallback++; + } else if (bfrt.bucket_selected_ < bfrt.num_buckets_used_ - 1) { + timers.bfrt_not_last_bucket++; + } + if (bfrt.selected_is_slope_breaker_) { + timers.bfrt_selected_slope_breaker++; + } else { + timers.bfrt_not_slope_breaker++; + } + } } else { entering_index = phase2::phase2_ratio_test( lp, settings, vstatus, nonbasic_list, z, delta_z, step_length, nonbasic_entering_index); @@ -3319,137 +3712,51 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += 2 * n; if (perturbation > 0.0 && phase == 2) { - // Try to remove perturbation - std::vector unperturbed_y(m); - std::vector unperturbed_z(n); - phase2_work_estimate += m + n; - phase2::compute_dual_solution_from_basis( - lp, ft, basic_list, nonbasic_list, unperturbed_y, unperturbed_z, phase2_work_estimate); - { - const f_t dual_infeas = phase2::dual_infeasibility( - lp, settings, vstatus, unperturbed_z, settings.tight_tol, settings.dual_tol); - phase2_work_estimate += 3 * n; - settings.log.printf("Dual infeasibility after removing perturbation %e\n", dual_infeas); - if (dual_infeas <= settings.dual_tol) { - settings.log.printf("Removed perturbation of %.2e.\n", perturbation); - z = unperturbed_z; - y = unperturbed_y; - phase2_work_estimate += 2 * n + 2 * m; - perturbation = 0.0; - - std::vector unperturbed_x(n); - phase2_work_estimate += n; - phase2::compute_primal_solution_from_basis(lp, - ft, - basic_list, - nonbasic_list, - vstatus, - unperturbed_x, - xB_workspace, - phase2_work_estimate); - x = unperturbed_x; - primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); - phase2_work_estimate += 4 * m + 2 * n; - settings.log.printf("Updated primal infeasibility: %e\n", primal_infeasibility); - - objective = lp.objective; - phase2_work_estimate += 2 * n; - // Need to reset the objective value, since we have recomputed x - obj = phase2::compute_perturbed_objective(objective, x); - phase2_work_estimate += 2 * n; - if (dual_infeas <= settings.dual_tol && primal_infeasibility <= settings.primal_tol) { - phase2_work_estimate += ft.work_estimate(); - ft.clear_work_estimate(); - phase2::prepare_optimality(1, - primal_infeasibility, - lp, - settings, - ft, - objective, - basic_list, - nonbasic_list, - vstatus, - phase, - start_time, - max_val, - phase2_work_estimate, - iter, - x, - y, - z, - sol); - status = dual_status_t::OPTIMAL; - break; - } - settings.log.printf( - "Continuing with perturbation removed and steepest edge norms reset\n"); - // Clear delta_z before restarting the iteration - phase2_work_estimate += 3 * delta_z_indices.size(); - phase2::clear_delta_z( - entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); - continue; - } else { - std::vector unperturbed_x(n); - phase2_work_estimate += n; - phase2::compute_primal_solution_from_basis(lp, - ft, - basic_list, - nonbasic_list, - vstatus, - unperturbed_x, - xB_workspace, - phase2_work_estimate); - x = unperturbed_x; - phase2_work_estimate += 2 * n; - primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); - phase2_work_estimate += 4 * m + 2 * n; - - const f_t orig_dual_infeas = phase2::dual_infeasibility( - lp, settings, vstatus, z, settings.tight_tol, settings.dual_tol); - phase2_work_estimate += 3 * n; - - if (primal_infeasibility <= settings.primal_tol && - orig_dual_infeas <= settings.dual_tol) { - phase2_work_estimate += ft.work_estimate(); - ft.clear_work_estimate(); - phase2::prepare_optimality(2, - primal_infeasibility, - lp, - settings, - ft, - objective, - basic_list, - nonbasic_list, - vstatus, - phase, - start_time, - max_val, - phase2_work_estimate, - iter, - x, - y, - z, - sol); - status = dual_status_t::OPTIMAL; - break; - } - settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); + i_t removal_status = phase2::attempt_to_remove_perturbations( + lp, settings, ft, basic_list, nonbasic_list, vstatus, objective, + z, y, x, xB_workspace, squared_infeasibilities, infeasibility_indices, + primal_infeasibility, primal_infeasibility_squared, phase2_work_estimate); + if (removal_status == 0) { // OPTIMAL + obj = phase2::compute_perturbed_objective(objective, x); + phase2_work_estimate += 2 * n; + if (primal_infeasibility <= settings.primal_tol) { + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); + phase2::prepare_optimality(1, + primal_infeasibility, + lp, + settings, + ft, + objective, + basic_list, + nonbasic_list, + vstatus, + phase, + start_time, + max_val, + phase2_work_estimate, + iter, + x, + y, + z, + sol); + status = dual_status_t::OPTIMAL; + break; } + settings.log.printf("Continuing with perturbation removed\n"); + phase2_work_estimate += 3 * delta_z_indices.size(); + phase2::clear_delta_z( + entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); + continue; + } else if (removal_status == 1) { // CONTINUE_DUAL + obj = phase2::compute_perturbed_objective(objective, x); + phase2_work_estimate += 2 * n; + phase2_work_estimate += 3 * delta_z_indices.size(); + phase2::clear_delta_z( + entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); + continue; } + // removal_status == 2 (PRIMAL_CLEANUP): fall through to existing logic below } if (perturbation == 0.0 && phase == 2) { @@ -3545,6 +3852,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, timers.flip_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); total_bound_flips += num_flipped; + if (num_flipped > max_bound_flips) max_bound_flips = num_flipped; delta_xB_0_sparse.clear(); if (num_flipped > 0) { @@ -3718,12 +4026,13 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, basic_list, entering_index, scaled_delta_xB_sparse, delta_x, phase2_work_estimate); timers.start_timer(phase2_work_estimate + ft.work_estimate()); - if (settings.remove_perturbation == 1) { - phase2::remove_leaving_perturbation(lp, settings, leaving_index, z, objective); + if (settings.remove_perturbation != 0) { + phase2::remove_leaving_perturbation(lp, settings, leaving_index, direction, z, objective); } f_t sum_perturb = 0.0; phase2::compute_perturbation( - lp, settings, delta_z_indices, z, objective, sum_perturb, phase2_work_estimate); + lp, settings, delta_z_indices, vstatus, z, objective, sum_perturb, + entering_index, step_length, phase2_work_estimate); timers.perturb_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); // Update basis information @@ -3905,13 +4214,13 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, return dual_status_t::WORK_LIMIT; } - if (now > settings.time_limit) { return dual_status_t::TIME_LIMIT; } + if (now > settings.time_limit) { status = dual_status_t::TIME_LIMIT; break; } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return dual_status_t::CONCURRENT_LIMIT; } } - if (iter >= iter_limit) { status = dual_status_t::ITERATION_LIMIT; } + if (status != dual_status_t::TIME_LIMIT && iter >= iter_limit) { status = dual_status_t::ITERATION_LIMIT; } // Flush any remaining work from the basis update into the total work estimate phase2_work_estimate += ft.work_estimate(); @@ -3919,6 +4228,11 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, if (phase == 2) { timers.print_timers(settings); + i_t num_iters = iter - start_iter; + if (num_iters > 0) { + settings.log.printf("Bound flips: total=%d, avg=%.1f, max=%d\n", + total_bound_flips, 1.0 * total_bound_flips / num_iters, max_bound_flips); + } constexpr bool print_stats = false; if constexpr (print_stats) { settings.log.printf("Sparse delta_z %8d %8.2f%\n", From 917ccaffdcca84f05a9c90ea74527865c7aa7a69 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 26 Aug 2026 15:58:38 -0700 Subject: [PATCH 23/34] Avoid double counting slope contribution from variable selected in single_pass --- .../bound_flipping_ratio_test.cpp | 23 +++++-------------- .../bound_flipping_ratio_test.hpp | 1 - 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index 1086b23ddf..d0e6e2c23b 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -61,7 +61,6 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, i_t end, const std::vector& indicies, const std::vector& ratios, - f_t& slope, f_t& step_length, i_t& nonbasic_entering, i_t& entering_index, @@ -95,20 +94,7 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, if (nonbasic_entering == -1) { return RATIO_TEST_NUMERICAL_ISSUES; } const i_t j = entering_index = nonbasic_list_[nonbasic_entering]; - constexpr bool verbose = false; - if (bounded_variables_[j]) { - const f_t interval = upper_[j] - lower_[j]; - const f_t delta_slope = std::abs(delta_z_[j]) * interval; - if constexpr (verbose) { - settings_.log.printf("single pass delta slope %e slope %e after slope %e step length %e\n", - delta_slope, - slope, - slope - delta_slope, - step_length); - } - slope -= delta_slope; - return k_idx; // we should see if we can continue to increase the step-length - } + if (bounded_variables_[j]) { return k_idx; } return -1; // we are done. do not increase the step-length further } @@ -150,10 +136,13 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, t0 = tic(); i_t k_idx = single_pass( - 0, num_breakpoints, indicies, harris_ratios, slope, step_length, nonbasic_entering, entering_index, max_step_length); + 0, num_breakpoints, indicies, harris_ratios, step_length, nonbasic_entering, entering_index, max_step_length); time_single_pass_ += toc(t0); if (k_idx == RATIO_TEST_NUMERICAL_ISSUES) { return RATIO_TEST_NUMERICAL_ISSUES; } - bool continue_search = k_idx >= 0 && num_breakpoints > 1 && slope > 0.0; + // The variable selected by single_pass is guaranteed to be in the first bucket: it + // defines the minimum Harris ratio, and its exact ratio is no greater than its Harris + // ratio. Its slope contribution is therefore applied by the bucket pass below. + bool continue_search = k_idx >= 0 && num_breakpoints > 1; if (!continue_search) { if constexpr (verbose) { settings_.log.printf( diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp index 6e9dc647c1..328f91be0e 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp @@ -80,7 +80,6 @@ class bound_flipping_ratio_test_t { i_t end, const std::vector& indices, const std::vector& ratios, - f_t& slope, f_t& step_length, i_t& nonbasic_entering, i_t& entering_index, From 36ca4d337fb661437b33e3e54e8a6cc615cd65f8 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 27 Aug 2026 16:24:39 -0700 Subject: [PATCH 24/34] Have BFRT decide on bound flips Have the bound-flipping ratio test return the set of variables that must flip at its selected step length. Flip exactly those variables in flip_bounds. The BFRT piecewise-linear objective model changes a bounded variable's bound when its reduced cost crosses zero. Flipping at zero is numerically unstable: small reduced-cost changes between iterations can move a variable repeatedly across zero, reverse its bound, and cause cycling. Use dual_tol / 10 to decide which variables to flip, leaving a small dead zone around zero. flip_bounds flips exactly these variables (previously it decided which bounds to flip using a separate mismatched tolerance). Within that dead zone, the objective represented by the BFRT model may differ from the objective computed using NONBASIC_UPPER/NONBASIC_LOWER. For a bounded variable this discrepancy is at most (upper_j - lower_j) * dual_tol / 10 and the total discrepancy is bounded by the sum of this quantity over the unflipped variables. Also stop the BFRT coarse-filter search after it has scanned all breakpoints. Without the scan_start < num_breakpoints condition, physiciansched3-3 loops forever when coarse_threshold remains zero after all candidates have already been processed. On the 240 MIPLIB LP relaxations, this change prevents five 300-second timeouts (neos-5052403-cygnet, physiciansched3-3, supportcase10, bab2, and brazil3), but introduces two new 300-second timeouts (neos-3988577-wolgan and neos-2075418-temuka). Unsolved runs are capped at 300 seconds. The presolve-only neos-787933 uses its presolve time. The HiGHS times were taken from a faster machine. So the HiGHS advantage is slightly exaggerated. But it still exists. Problem Baseline v5 v8 HiGHS v8/HiGHS ------------------------------------------------------------------------------------ momentum1 0.69 0.73 0.70 300.00 0.00 var-smallemery-m6j6 0.66 0.74 0.72 300.00 0.00 neos-5114902-kasavu 87.40 23.29 4.91 300.00 0.02 supportcase42 0.52 0.77 0.78 36.20 0.02 neos-5049753-cuanza 7.68 3.22 1.91 29.85 0.06 supportcase12 4.68 4.30 4.72 37.11 0.13 proteindesign121hz512p9 0.91 0.48 0.37 2.04 0.18 supportcase22 2.03 1.11 0.73 3.97 0.18 roi2alpha3n4 0.31 0.67 0.22 1.16 0.19 proteindesign122trx11p8 0.64 0.31 0.26 1.26 0.21 mzzv11 40.19 8.46 3.45 16.71 0.21 supportcase18 0.06 0.04 0.03 0.12 0.25 roi5alpha10n8 1.25 3.29 3.42 11.90 0.29 neos-5052403-cygnet 300.00 187.30 110.25 300.00 0.37 supportcase7 1.29 1.46 1.36 3.52 0.39 rd-rplusc-21 0.23 0.25 0.19 0.49 0.39 neos-787933 0.06 0.07 0.07 0.18 0.39 ns1760995 135.94 250.52 114.63 269.53 0.43 rocII-5-11 0.09 0.09 0.10 0.21 0.48 fhnw-binpack4-48 0.07 0.03 0.03 0.06 0.50 neos-5093327-huahum 0.23 0.23 0.25 0.48 0.52 30n20b8 0.08 0.05 0.06 0.11 0.55 lectsched-5-obj 0.13 0.10 0.10 0.18 0.56 neos-4647030-tutaki 2.47 2.33 1.72 3.09 0.56 physiciansched6-2 11.15 15.62 3.84 6.72 0.57 neos-3004026-krka 0.06 0.08 0.07 0.12 0.58 co-100 0.68 0.64 0.75 1.28 0.59 neos-5104907-jarama 124.69 88.16 52.83 89.64 0.59 neos-3402454-bohle 221.86 115.49 44.09 74.15 0.59 dws008-01 0.04 0.03 0.03 0.05 0.60 n3div36 0.11 0.13 0.11 0.18 0.61 neos-5188808-nattai 0.30 0.19 0.18 0.29 0.62 cvs16r128-89 0.93 1.08 1.13 1.72 0.66 neos-860300 0.10 0.07 0.10 0.15 0.67 neos-4300652-rahue 1.22 0.56 0.54 0.80 0.68 cryptanalysiskb128n5obj14 29.78 6.14 9.28 12.54 0.74 square47 79.87 89.37 95.31 126.92 0.75 istanbul-no-cutoff 0.71 0.87 0.71 0.94 0.76 thor50dday 0.27 0.25 0.28 0.37 0.76 ns1952667 8.95 1.18 0.62 0.80 0.77 neos-5195221-niemur 0.50 0.24 0.30 0.37 0.81 tbfp-network 9.06 6.73 7.80 9.04 0.86 cryptanalysiskb128n5obj16 29.53 6.53 8.93 10.27 0.87 blp-ic98 0.12 0.11 0.09 0.10 0.90 neos-3555904-turama 1.31 1.52 1.29 1.37 0.94 physiciansched3-3 300.00 300.00 288.13 300.00 0.96 comp21-2idx 1.55 0.38 0.34 0.35 0.97 square41 28.57 42.48 39.17 39.86 0.98 blp-ar98 0.10 0.08 0.11 0.11 1.00 decomp2 0.19 0.10 0.12 0.12 1.00 highschool1-aigio 300.00 300.00 300.00 300.00 1.00 leo2 0.13 0.15 0.13 0.13 1.00 neos-3988577-wolgan 278.89 300.00 300.00 300.00 1.00 neos-4738912-atrato 0.05 0.05 0.04 0.04 1.00 neos8 0.38 0.42 0.26 0.26 1.00 rail02 300.00 300.00 300.00 300.00 1.00 s100 300.00 300.00 300.00 300.00 1.00 savsched1 300.00 300.00 300.00 300.00 1.00 supportcase19 300.00 300.00 300.00 300.00 1.00 swath3 0.03 0.03 0.03 0.03 1.00 traininstance2 0.09 0.04 0.04 0.04 1.00 traininstance6 0.04 0.03 0.03 0.03 1.00 piperout-08 0.39 0.18 0.17 0.16 1.06 neos-873061 1.46 1.51 1.47 1.38 1.07 sct2 0.22 0.20 0.15 0.14 1.07 piperout-27 0.67 0.29 0.28 0.26 1.08 neos-4532248-waihi 2.61 0.92 0.97 0.90 1.08 neos-3216931-puriri 7.33 4.89 3.50 3.23 1.08 academictimetablesmall 14.89 4.15 0.90 0.82 1.10 germanrr 0.31 0.33 0.30 0.27 1.11 nursesched-sprint02 0.38 0.51 0.26 0.23 1.13 triptim1 71.42 58.41 58.50 51.43 1.14 neos-848589 1.24 1.01 0.97 0.85 1.14 cod105 9.06 7.73 8.56 7.46 1.15 supportcase10 300.00 105.36 133.84 113.55 1.18 sp97ar 0.40 0.37 0.39 0.33 1.18 wachplan 0.25 0.23 0.31 0.26 1.19 sp98ar 0.39 0.41 0.37 0.31 1.19 supportcase40 0.24 0.22 0.29 0.24 1.21 neos-960392 8.93 1.97 3.30 2.66 1.24 drayage-25-23 0.08 0.06 0.05 0.04 1.25 h80x6320d 0.05 0.04 0.05 0.04 1.25 neos-3381206-awhea 0.08 0.05 0.05 0.04 1.25 ns1644855 300.00 300.00 300.00 238.30 1.26 nursesched-medium-hint03 10.47 11.61 6.00 4.73 1.27 radiationm18-12-05 0.23 0.19 0.18 0.14 1.29 mushroom-best 0.26 0.23 0.26 0.20 1.30 supportcase6 7.39 6.03 5.52 4.18 1.32 swath1 0.04 0.04 0.04 0.03 1.33 comp07-2idx 4.03 2.05 1.47 1.10 1.34 neos-2746589-doon 7.53 5.95 4.11 3.02 1.36 neos-5107597-kakapo 0.04 0.13 0.14 0.10 1.40 net12 0.55 0.43 0.50 0.35 1.43 neos-3402294-bobin 3.07 1.79 1.72 1.20 1.43 neos-4722843-widden 1.17 2.25 2.21 1.54 1.44 irp 0.11 0.13 0.13 0.09 1.44 supportcase33 0.99 1.12 0.80 0.55 1.45 fast0507 7.36 4.24 4.05 2.77 1.46 mzzv42z 10.10 1.91 1.42 0.95 1.49 bnatt500 0.29 0.16 0.21 0.14 1.50 drayage-100-23 0.07 0.05 0.06 0.04 1.50 icir97_tension 0.03 0.03 0.03 0.02 1.50 neos-1582420 0.12 0.09 0.09 0.06 1.50 rococoC10-001000 0.04 0.03 0.03 0.02 1.50 air05 0.28 0.24 0.27 0.18 1.50 neos-1122047 2.09 1.96 2.43 1.61 1.51 nexp-150-20-8-5 0.10 0.11 0.11 0.07 1.57 neos-2978193-inde 0.16 0.14 0.08 0.05 1.60 rail507 7.17 4.22 4.64 2.84 1.63 hypothyroid-k1 4.36 4.47 4.93 3.01 1.64 neos-3083819-nubu 0.06 0.05 0.05 0.03 1.67 ran14x18-disj-8 0.04 0.05 0.05 0.03 1.67 cmflsp50-24-8-8 0.77 0.73 0.74 0.44 1.68 leo1 0.10 0.09 0.12 0.07 1.71 trento1 3.08 3.49 3.80 2.21 1.72 neos-662469 1.65 0.91 0.93 0.53 1.75 rmatr200-p5 7.50 7.63 8.13 4.61 1.76 atlanta-ip 6.85 5.86 8.14 4.54 1.79 rocI-4-11 0.11 0.10 0.11 0.06 1.83 mcsched 0.27 0.23 0.30 0.16 1.88 chromaticindex512-7 16.62 37.48 40.10 21.24 1.89 neos-3024952-loue 0.41 0.39 0.38 0.20 1.90 neos-2987310-joes 1.52 1.67 1.91 1.00 1.91 netdiversion 7.64 8.61 18.16 9.43 1.93 qap10 15.57 10.58 12.87 6.68 1.93 sing44 9.62 14.79 11.01 5.69 1.93 k1mushroom 31.00 30.55 32.65 16.80 1.94 opm2-z10-s4 89.23 85.57 87.53 44.75 1.96 neos-4763324-toguru 8.32 7.78 9.86 5.00 1.97 50v-10 0.02 0.02 0.02 0.01 2.00 bnatt400 0.16 0.11 0.16 0.08 2.00 eil33-2 0.05 0.06 0.06 0.03 2.00 enlight_hard 0.02 0.02 0.02 0.01 2.00 exp-1-500-5-5 0.02 0.02 0.02 0.01 2.00 fhnw-binpack4-4 0.03 0.02 0.02 0.01 2.00 gen-ip002 0.02 0.02 0.02 0.01 2.00 gen-ip054 0.03 0.02 0.02 0.01 2.00 glass4 0.02 0.02 0.02 0.01 2.00 mad 0.02 0.02 0.02 0.01 2.00 markshare2 0.02 0.02 0.02 0.01 2.00 markshare_4_0 0.02 0.02 0.02 0.01 2.00 mas74 0.03 0.02 0.02 0.01 2.00 mas76 0.02 0.02 0.02 0.01 2.00 mik-250-20-75-4 0.02 0.03 0.02 0.01 2.00 neos-1456979 0.05 0.04 0.08 0.04 2.00 neos-3046615-murg 0.02 0.02 0.02 0.01 2.00 neos-3754480-nidda 0.02 0.02 0.02 0.01 2.00 neos-4338804-snowy 0.03 0.02 0.02 0.01 2.00 neos-911970 0.03 0.02 0.02 0.01 2.00 neos5 0.02 0.02 0.02 0.01 2.00 neos859080 0.01 0.01 0.02 0.01 2.00 nu25-pr12 0.05 0.05 0.04 0.02 2.00 pg 0.03 0.02 0.02 0.01 2.00 pg5_34 0.03 0.03 0.02 0.01 2.00 pk1 0.02 0.01 0.02 0.01 2.00 sp150x300d 0.02 0.02 0.02 0.01 2.00 supportcase26 0.03 0.02 0.02 0.01 2.00 timtab1 0.01 0.01 0.02 0.01 2.00 radiationm40-10-02 1.58 1.44 1.45 0.71 2.04 ns1830653 0.42 0.33 0.29 0.14 2.07 uct-subprob 0.11 0.10 0.17 0.08 2.12 neos-957323 300.00 28.38 16.74 7.74 2.16 roll3000 0.12 0.13 0.13 0.06 2.17 dano3_3 46.88 96.86 42.60 19.06 2.24 dano3_5 46.79 96.97 42.62 19.03 2.24 neos-4387871-tavua 0.10 0.10 0.09 0.04 2.25 rmatr100-p10 0.27 0.34 0.32 0.14 2.29 unitcal_7 0.96 1.00 0.91 0.39 2.33 ns1116954 156.23 16.44 25.16 10.73 2.34 rail01 223.84 196.04 167.23 71.23 2.35 chromaticindex1024-7 45.86 201.94 220.63 93.65 2.36 csched007 0.19 0.16 0.12 0.05 2.40 eilA101-2 2.68 2.86 3.35 1.39 2.41 bab2 300.00 76.29 70.29 28.97 2.43 reblock115 0.16 0.17 0.22 0.09 2.44 seymour1 0.83 0.91 0.94 0.38 2.47 b1c1s1 0.05 0.04 0.05 0.02 2.50 cost266-UUE 0.03 0.04 0.05 0.02 2.50 n5-3 0.03 0.03 0.05 0.02 2.50 rococoB10-011000 0.13 0.11 0.15 0.06 2.50 uccase9 11.54 17.20 13.22 5.20 2.54 seymour 0.83 0.89 0.98 0.38 2.58 buildingenergy 300.00 300.00 300.00 115.61 2.59 splice1k1 21.62 22.71 23.93 9.17 2.61 sing326 9.54 14.16 12.30 4.69 2.62 neos-1171737 0.70 0.70 0.54 0.20 2.70 satellites2-60-fs 4.17 171.22 9.13 3.34 2.73 glass-sc 0.31 0.31 0.34 0.12 2.83 gfd-schedulen180f7d50m30k18 80.00 39.02 19.47 6.81 2.86 nw04 0.44 1.38 1.75 0.61 2.87 neos-1171448 2.35 2.08 2.41 0.84 2.87 binkar10_1 0.02 0.03 0.03 0.01 3.00 bppc4-08 0.08 0.04 0.06 0.02 3.00 csched008 0.11 0.09 0.09 0.03 3.00 graph20-20-1rand 0.23 0.32 0.36 0.12 3.00 graphdraw-domain 0.02 0.02 0.03 0.01 3.00 ic97_potential 0.02 0.03 0.03 0.01 3.00 neos-2657525-crna 0.03 0.03 0.03 0.01 3.00 neos-4954672-berkel 0.02 0.02 0.03 0.01 3.00 neos17 0.03 0.04 0.03 0.01 3.00 p200x1188c 0.03 0.03 0.03 0.01 3.00 tr12-30 0.03 0.02 0.03 0.01 3.00 map16715-04 13.26 19.01 20.42 6.80 3.00 CMS750_4 0.38 0.39 0.43 0.14 3.07 irish-electricity 181.58 300.00 183.12 59.53 3.08 n2seq36q 0.46 0.53 0.74 0.24 3.08 sorrell3 1.07 1.13 1.25 0.40 3.12 map10 11.08 21.74 19.82 6.25 3.17 bab6 143.43 38.82 44.66 14.06 3.18 ns1208400 3.96 1.60 1.17 0.36 3.25 neos-933966 16.79 18.35 9.31 2.80 3.33 neos-1445765 0.16 0.35 0.30 0.09 3.33 uccase12 72.32 6.47 4.77 1.38 3.46 neos-950242 1.05 0.81 0.66 0.18 3.67 assign1-5-8 0.03 0.03 0.04 0.01 4.00 gmu-35-40 0.03 0.04 0.04 0.01 4.00 gmu-35-50 0.05 0.05 0.04 0.01 4.00 lotsize 0.03 0.03 0.04 0.01 4.00 neos-3627168-kasai 0.04 0.03 0.04 0.01 4.00 s250r10 300.00 300.00 300.00 71.74 4.18 neos-1354092 300.00 300.00 300.00 70.92 4.23 fastxgemm-n2r6s0t2 0.18 0.29 0.34 0.08 4.25 milo-v12-6-r2-40-1 0.25 0.22 0.22 0.05 4.40 app1-1 0.08 0.09 0.09 0.02 4.50 fiball 0.71 0.30 0.66 0.14 4.71 cbs-cta 0.42 0.32 0.48 0.10 4.80 beasleyC3 0.05 0.05 0.05 0.01 5.00 mc11 0.06 0.06 0.05 0.01 5.00 ex10 300.00 300.00 300.00 59.25 5.06 peg-solitaire-a3 1.88 2.54 2.81 0.52 5.40 neos-3656078-kumeu 2.37 1.57 1.27 0.23 5.52 neos-827175 9.36 1.91 1.66 0.29 5.72 neos-2075418-temuka 128.30 300.00 300.00 50.57 5.93 snp-02-004-104 14.17 23.47 19.21 2.83 6.79 app1-2 5.03 5.33 5.26 0.70 7.51 neos-631710 300.00 300.00 300.00 31.97 9.38 neos-4413714-turia 3.97 16.53 20.16 1.93 10.45 brazil3 300.00 93.59 79.55 7.24 10.99 satellites2-40 29.70 125.33 186.43 10.51 17.74 ex9 300.00 300.00 300.00 14.07 21.32 Geomean Baseline/v8: 1.19 Shifted(+1s): 1.14 Geomean v5/v8: 1.05 Shifted(+1s): 1.06 Geomean v8/HiGHS: 1.48 Shifted(+1s): 1.12 (240 problems) --- .../bound_flipping_ratio_test.cpp | 35 +++++++++- .../bound_flipping_ratio_test.hpp | 7 +- cpp/src/dual_simplex/phase2.cpp | 70 +++++++------------ 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index d0e6e2c23b..6fe4ec149f 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -98,14 +98,41 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, return -1; // we are done. do not increase the step-length further } +template +void bound_flipping_ratio_test_t::determine_flips( + f_t step_length, i_t entering_index, std::vector& flip_indices) const +{ + // The piecewise-linear model below assumes that a variable flips bounds as soon as + // its reduced cost crosses zero. In practice, small changes between iterations can + // make a reduced cost oscillate around zero, causing excessive bound flips and + // cycling. We therefore flip only after the violation exceeds dual_tol / 10. + // A bounded variable l_j <= x_j <= u_j contributes l_j*z_j to the dual objective + // when z_j >= 0 and u_j*z_j when z_j < 0. If x_j = l_j and + // -dual_tol/10 <= z_j < 0, the model uses u_j*z_j while the unflipped state uses + // l_j*z_j. Their difference is (u_j - l_j)*|z_j|, bounded by + // (u_j - l_j)*dual_tol/10. For multiple unflipped variables, the discrepancy is + // bounded by sum_j (u_j - l_j)*dual_tol/10. + const f_t flip_tol = settings_.dual_tol / 10; + for (const i_t j : delta_z_indices_) { + if (j == entering_index || !bounded_variables_[j]) { continue; } + const f_t new_z = z_[j] + step_length * delta_z_[j]; + if ((vstatus_[j] == variable_status_t::NONBASIC_LOWER && new_z < -flip_tol) || + (vstatus_[j] == variable_status_t::NONBASIC_UPPER && new_z > flip_tol)) { + flip_indices.push_back(j); + } + } +} + template i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, - i_t& nonbasic_entering) + i_t& nonbasic_entering, + std::vector& flip_indices) { const i_t m = m_; const i_t n = n_; const i_t nz = delta_z_indices_.size(); constexpr bool verbose = false; + flip_indices.clear(); // Compute the initial set of breakpoints std::vector indicies(nz); @@ -154,6 +181,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, } num_buckets_used_ = 0; step_length_result_ = step_length; + determine_flips(step_length, entering_index, flip_indices); return entering_index; } @@ -254,7 +282,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // This is O( log10(max_step_length/min_step_length) * num_breakpoints) t0 = tic(); - while (total_slope >= 0.0 && coarse_threshold <= max_step_length && !found_unbounded) { + while (total_slope >= 0.0 && coarse_threshold <= max_step_length && + scan_start < num_breakpoints && !found_unbounded) { for (i_t h = scan_start; h < num_breakpoints; ++h) { const i_t k = candidates[h]; if (ratios[k] <= coarse_threshold) { @@ -399,6 +428,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, bucket_selected_ = -1; step_length_result_ = step_length; selected_is_slope_breaker_ = false; + determine_flips(step_length, entering_index, flip_indices); return entering_index; } step_length = ratios[entering_k]; @@ -424,6 +454,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, } } step_length_result_ = step_length; + determine_flips(step_length, entering_index, flip_indices); return entering_index; diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp index 328f91be0e..3a87e923db 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp @@ -53,7 +53,9 @@ class bound_flipping_ratio_test_t { { } - i_t compute_step_length(f_t& step_length, i_t& nonbasic_entering); + i_t compute_step_length(f_t& step_length, + i_t& nonbasic_entering, + std::vector& flip_indices); f_t work_estimate() const { return work_estimate_; } // Timing fields (filled by compute_step_length) @@ -84,6 +86,9 @@ class bound_flipping_ratio_test_t { i_t& nonbasic_entering, i_t& entering_index, f_t& max_val); + void determine_flips(f_t step_length, + i_t entering_index, + std::vector& flip_indices) const; const std::vector& lower_; const std::vector& upper_; const std::vector& bounded_variables_; diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 0b94a67d28..123d6354f8 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -1232,30 +1232,19 @@ i_t phase2_ratio_test(const lp_problem_t& lp, template i_t flip_bounds(const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - const std::vector& bounded_variables, - const std::vector& objective, - const std::vector& z, - const std::vector& delta_z_indices, - const std::vector& nonbasic_list, - i_t entering_index, - std::vector& vstatus, + const std::vector& bounded_variables, + const std::vector& flip_indices, + std::vector& vstatus, std::vector& delta_x, std::vector& mark, - std::vector& atilde, - std::vector& atilde_index, - f_t& work_estimate) + std::vector& atilde, + std::vector& atilde_index, + f_t& work_estimate) { i_t num_flipped = 0; - for (i_t k = 0; k < delta_z_indices.size(); ++k) { - const i_t j = delta_z_indices[k]; - if (j == entering_index) { continue; } - if (!bounded_variables[j]) { continue; } - // x_j is now a nonbasic bounded variable that will not enter the basis this - // iteration - const f_t dual_tol = - settings.dual_tol; // lower to 1e-7 or less will cause 25fv47 and d2q06c to cycle - if (vstatus[j] == variable_status_t::NONBASIC_LOWER && z[j] < -dual_tol) { + for (const i_t j : flip_indices) { + assert(bounded_variables[j]); + if (vstatus[j] == variable_status_t::NONBASIC_LOWER) { const f_t delta = lp.upper[j] - lp.lower[j]; const size_t atilde_start_size = atilde_index.size(); scatter_dense(lp.A, j, -delta, atilde, mark, atilde_index); @@ -1263,12 +1252,9 @@ i_t flip_bounds(const lp_problem_t& lp, 4 * (lp.A.col_start[j + 1] - lp.A.col_start[j]) + 10; delta_x[j] += delta; vstatus[j] = variable_status_t::NONBASIC_UPPER; -#ifdef BOUND_FLIP_DEBUG - settings.log.printf( - "Flipping nonbasic %d from lo %e to up %e. z %e\n", j, lp.lower[j], lp.upper[j], z[j]); -#endif num_flipped++; - } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER && z[j] > dual_tol) { + } else { + assert(vstatus[j] == variable_status_t::NONBASIC_UPPER); const f_t delta = lp.lower[j] - lp.upper[j]; const size_t atilde_start_size = atilde_index.size(); scatter_dense(lp.A, j, -delta, atilde, mark, atilde_index); @@ -1276,14 +1262,10 @@ i_t flip_bounds(const lp_problem_t& lp, 4 * (lp.A.col_start[j + 1] - lp.A.col_start[j]) + 10; delta_x[j] += delta; vstatus[j] = variable_status_t::NONBASIC_LOWER; -#ifdef BOUND_FLIP_DEBUG - settings.log.printf( - "Flipping nonbasic %d from up %e to lo %e. z %e\n", j, lp.upper[j], lp.lower[j], z[j]); -#endif num_flipped++; } } - work_estimate += 4 * delta_z_indices.size(); + work_estimate += 2 * flip_indices.size(); return num_flipped; } @@ -3629,6 +3611,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, f_t step_length; i_t entering_index = -1; i_t nonbasic_entering_index = -1; + std::vector flip_indices; const bool harris_ratio = settings.use_harris_ratio; const bool bound_flip_ratio = settings.use_bound_flip_ratio; { @@ -3661,7 +3644,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, delta_z, delta_z_indices, nonbasic_mark); - entering_index = bfrt.compute_step_length(step_length, nonbasic_entering_index); + entering_index = bfrt.compute_step_length( + step_length, nonbasic_entering_index, flip_indices); phase2_work_estimate += bfrt.work_estimate(); if (entering_index == RATIO_TEST_NUMERICAL_ISSUES) { settings.log.printf("Numerical issues encountered in ratio test.\n"); @@ -3835,20 +3819,16 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, timers.start_timer(phase2_work_estimate + ft.work_estimate()); // Update primal variable - const i_t num_flipped = phase2::flip_bounds(lp, - settings, - bounded_variables, - objective, - z, - delta_z_indices, - nonbasic_list, - entering_index, - vstatus, - delta_x_flip, - atilde_mark, - atilde, - atilde_index, - phase2_work_estimate); + const i_t num_flipped = bound_flip_ratio ? phase2::flip_bounds(lp, + bounded_variables, + flip_indices, + vstatus, + delta_x_flip, + atilde_mark, + atilde, + atilde_index, + phase2_work_estimate) + : 0; timers.flip_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); total_bound_flips += num_flipped; From 75a1e97a6c73281d0b3a3210aff4bf9b4d44f1d8 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Thu, 27 Aug 2026 17:10:36 -0700 Subject: [PATCH 25/34] Improve BFRT work estimates --- .../bound_flipping_ratio_test.cpp | 28 ++++++++++--------- .../bound_flipping_ratio_test.hpp | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index 6fe4ec149f..35ef45c0ab 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -49,8 +49,7 @@ i_t bound_flipping_ratio_test_t::compute_breakpoints(std::vector& idx++; } } - work_estimate_ += 5 * nz; - work_estimate_ += 5 * idx; + work_estimate_ += 5 * nz + 5 * idx; pivot_tol /= 10; } return idx; @@ -99,8 +98,9 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, } template -void bound_flipping_ratio_test_t::determine_flips( - f_t step_length, i_t entering_index, std::vector& flip_indices) const +void bound_flipping_ratio_test_t::determine_flips(f_t step_length, + i_t entering_index, + std::vector& flip_indices) { // The piecewise-linear model below assumes that a variable flips bounds as soon as // its reduced cost crosses zero. In practice, small changes between iterations can @@ -121,6 +121,7 @@ void bound_flipping_ratio_test_t::determine_flips( flip_indices.push_back(j); } } + work_estimate_ += 5 * delta_z_indices_.size() + flip_indices.size(); } template @@ -150,6 +151,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, if (harris_ratios[k] == 0.0) num_harris_zero_++; if (ratios[k] == 0.0) num_exact_zero_++; } + work_estimate_ += 2 * num_breakpoints; if constexpr (verbose) { settings_.log.printf("Initial breakpoints %d\n", num_breakpoints); } if (num_breakpoints == 0) { nonbasic_entering = -1; @@ -298,8 +300,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, } } } - work_estimate_ += 3 * (num_breakpoints - scan_start); - work_estimate_ += 8 * (num_candidates - scan_start); + work_estimate_ += 2 * (num_breakpoints - scan_start) + 10 * (num_candidates - scan_start); scan_start = num_candidates; coarse_threshold *= 10.0; } @@ -319,8 +320,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, work_estimate_ += 5 * num_candidates; // Remove candidates that are greater than the maximum step length - work_estimate_ += 2 * candidates.size(); - for (i_t h = static_cast(candidates.size()) - 1; h >= 0; h--) { + const i_t candidates_before_removal = candidates.size(); + for (i_t h = candidates_before_removal - 1; h >= 0; h--) { const i_t k = candidates[h]; const f_t ratio = ratios[k]; if (ratio > max_step_length) { @@ -329,6 +330,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, candidates.pop_back(); } } + work_estimate_ += 2 * candidates_before_removal + 2 * (candidates_before_removal - candidates.size()); num_candidates = candidates.size(); } @@ -349,7 +351,6 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, f_t next_threshold = inf; i_t write = scan_start; - work_estimate_ += 7 * (num_candidates - scan_start); for (i_t h = scan_start; h < num_candidates; h++) { const i_t k = candidates[h]; const f_t ratio = ratios[k]; @@ -370,7 +371,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, next_threshold = std::min(next_threshold, harris_ratio); } } - + work_estimate_ += 3 * (num_candidates - scan_start) + 9 * (write - scan_start); bucket_start[++num_buckets] = write; if (write == scan_start) break; // No progress — prevent infinite loop @@ -415,10 +416,9 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, entering_k = k; } } - work_estimate_ += 4 * (bucket_start[b + 1] - bucket_start[b]); + work_estimate_ += 2 + 5 * (b_end - b_start); if (entering_k >= 0) break; } - work_estimate_ += 2 * num_buckets; // Step = entering variable's breakpoint ratio num_buckets_used_ = num_buckets; @@ -440,10 +440,11 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // Record which bucket was selected used_fallback_ = false; + i_t pos = -1; for (i_t b = 0; b < num_buckets; b++) { if (entering_k >= 0) { // Find which bucket entering_k is in based on its position in candidates - i_t pos = -1; + pos = -1; for (i_t h = 0; h < num_candidates; h++) { if (candidates[h] == entering_k) { pos = h; break; } } @@ -453,6 +454,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, } } } + work_estimate_ += (bucket_selected_ + 1) * (pos + 3); step_length_result_ = step_length; determine_flips(step_length, entering_index, flip_indices); diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp index 3a87e923db..0d78e68ee0 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp @@ -88,7 +88,7 @@ class bound_flipping_ratio_test_t { f_t& max_val); void determine_flips(f_t step_length, i_t entering_index, - std::vector& flip_indices) const; + std::vector& flip_indices); const std::vector& lower_; const std::vector& upper_; const std::vector& bounded_variables_; From d983159ea1dbe0aee14320783f4093209dfee0ae Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 28 Aug 2026 17:46:23 -0700 Subject: [PATCH 26/34] Fix bug in not handling concurrent halt. Use zero_tol in step-length computation to try to remain dual feasible --- cpp/src/branch_and_bound/branch_and_bound.cpp | 91 +++++++++++++++---- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 42d1453833..9b0c083635 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3354,6 +3354,16 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t= settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return cut_pass_action_t::RETURN; + } // Score the cuts f_t score_start_time = tic(); @@ -4032,11 +4042,11 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( nonbasic_list, vstatus); if (refactor_status == CONCURRENT_HALT_RETURN || refactor_status == TIME_LIMIT_RETURN) { - // TODO: On failure vstatus, basic_list, and nonbasic_list are in a bad state. - // We should save copies before the failure and restore them after the failure. return; } if (refactor_status != 0) { + // TODO: On failure vstatus, basic_list, and nonbasic_list are in a bad state. + // We should save copies before the failure and restore them after the failure. settings_.log.printf( "Failed to refactor basis after dual degenerate feasibility pump. " "%d deficient columns.\n", @@ -4790,8 +4800,8 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( f_t work_estimate = 0; const f_t threshold = 100.0 * settings_.integer_tol; const f_t tol = 1e-2; - const f_t pivot_tol = settings_.pivot_tol; - const f_t dual_tol = settings_.dual_tol / 10; + const f_t zero_tol = settings_.zero_tol; + const f_t harris_tol = settings_.dual_tol / 10; i_t num_bounds_added = 0; for (i_t j : degenerate_integer_list) { @@ -4840,12 +4850,12 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( for (i_t jj : delta_z_indices) { if (vstatus[jj] == variable_status_t::NONBASIC_FIXED) { continue; } const f_t dz = scale * delta_z[jj]; - if (vstatus[jj] == variable_status_t::NONBASIC_LOWER && dz < -pivot_tol) { - const f_t ratio = std::max((-dual_tol - soln.z[jj]) / dz, 0.0); + if (vstatus[jj] == variable_status_t::NONBASIC_LOWER && dz < -zero_tol) { + const f_t ratio = std::max((-harris_tol - soln.z[jj]) / dz, 0.0); if (ratio < alpha) { alpha = ratio; } } - if (vstatus[jj] == variable_status_t::NONBASIC_UPPER && dz > pivot_tol) { - const f_t ratio = std::max((dual_tol - soln.z[jj]) / dz, 0.0); + if (vstatus[jj] == variable_status_t::NONBASIC_UPPER && dz > zero_tol) { + const f_t ratio = std::max((harris_tol - soln.z[jj]) / dz, 0.0); if (ratio < alpha) { alpha = ratio; } } } @@ -4855,24 +4865,48 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( // For NONBASIC_LOWER: z_new[jj] >= -dual_tol // For NONBASIC_UPPER: z_new[jj] <= dual_tol { - f_t max_dual_infeas = 0.0; - i_t num_dual_infeas = 0; - i_t worst_j = -1; + f_t max_initial_dual_infeas = 0.0; + f_t max_dual_infeas = 0.0; + f_t worst_old_z = 0.0; + f_t worst_delta_z = 0.0; + f_t worst_step = 0.0; + f_t worst_new_z = 0.0; + i_t num_initial_dual_infeas = 0; + i_t num_dual_infeas = 0; + i_t worst_j = -1; for (i_t jj : delta_z_indices) { if (vstatus[jj] == variable_status_t::NONBASIC_FIXED) { continue; } - const f_t new_zj = soln.z[jj] + alpha * scale * delta_z[jj]; + const f_t old_zj = soln.z[jj]; + const f_t step = alpha * scale * delta_z[jj]; + const f_t new_zj = old_zj + step; + const bool initially_infeasible = + (vstatus[jj] == variable_status_t::NONBASIC_LOWER && + old_zj < -settings_.dual_tol) || + (vstatus[jj] == variable_status_t::NONBASIC_UPPER && old_zj > settings_.dual_tol); + if (initially_infeasible) { + num_initial_dual_infeas++; + max_initial_dual_infeas = std::max(max_initial_dual_infeas, std::abs(old_zj)); + } if (vstatus[jj] == variable_status_t::NONBASIC_LOWER && new_zj < -settings_.dual_tol) { num_dual_infeas++; if (std::abs(new_zj) > max_dual_infeas) { max_dual_infeas = std::abs(new_zj); - worst_j = jj; + worst_j = jj; + worst_old_z = old_zj; + worst_delta_z = scale * delta_z[jj]; + worst_step = step; + worst_new_z = new_zj; } } if (vstatus[jj] == variable_status_t::NONBASIC_UPPER && new_zj > settings_.dual_tol) { num_dual_infeas++; if (std::abs(new_zj) > max_dual_infeas) { max_dual_infeas = std::abs(new_zj); - worst_j = jj; + worst_j = jj; + worst_old_z = old_zj; + worst_delta_z = scale * delta_z[jj]; + worst_step = step; + worst_new_z = new_zj; } } } @@ -4881,9 +4915,24 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( if (num_dual_infeas > 0) { settings_.log.printf( "WARNING pivot_to_improve_rc: dual infeasibility after step! " - "var=%d alpha=%.6e scale=%.0f num_infeas=%d max_infeas=%.6e worst_j=%d " + "var=%d alpha=%.6e scale=%.0f initial_num_infeas=%d " + "initial_max_infeas=%.6e num_infeas=%d max_infeas=%.6e worst_j=%d " + "worst_status=%d old_z=%.16e delta_z=%.16e step=%.16e new_z=%.16e " "new_rc_leaving=%.6e\n", - j, alpha, scale, num_dual_infeas, max_dual_infeas, worst_j, new_zj_leaving); + j, + alpha, + scale, + num_initial_dual_infeas, + max_initial_dual_infeas, + num_dual_infeas, + max_dual_infeas, + worst_j, + static_cast(vstatus[worst_j]), + worst_old_z, + worst_delta_z, + worst_step, + worst_new_z, + new_zj_leaving); } } @@ -5203,6 +5252,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, num_fractional, fractional); + if (received_halt_signal()) { + solver_status_ = mip_status_t::HALT; + set_final_solution(solution, root_objective_); + return solver_status_; + } + if (toc(exploration_stats_.start_time) >= settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } From a55f471d1050e520fea513c962edcdaa9abe2a24 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 2 Sep 2026 11:27:00 -0700 Subject: [PATCH 27/34] Add option to turn of dual degenerate feasibility pump. Silence pivot_out_integer_variables in the tree --- .../mathematical_optimization/constants.h | 1 + .../mip/solver_settings.hpp | 1 + cpp/src/branch_and_bound/branch_and_bound.cpp | 199 +++++++++++------- cpp/src/branch_and_bound/branch_and_bound.hpp | 24 ++- .../dual_simplex/simplex_solver_settings.hpp | 4 +- cpp/src/math_optimization/solver_settings.cu | 1 + cpp/src/mip_heuristics/solver.cu | 4 + 7 files changed, 143 insertions(+), 91 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 534accfe94..5339a58a24 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -81,6 +81,7 @@ #define CUOPT_MIP_ZERO_HALF_CUTS "mip_zero_half_cuts" #define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts" #define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening" +#define CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP "mip_dual_degenerate_feasibility_pump" #define CUOPT_MIP_RINS "mip_rins" #define CUOPT_MIP_RENS "mip_rens" #define CUOPT_MIP_OBJECTIVE_STEP "mip_objective_step" diff --git a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp index 4a363b1dbc..b177e38500 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp @@ -134,6 +134,7 @@ class mip_solver_settings_t { i_t implied_bound_cuts = -1; i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; + i_t dual_degenerate_feasibility_pump = -1; // -1 = automatic (on), 0 = off, 1 = on i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable f_t cut_change_threshold = -1.0; f_t cut_min_orthogonality = 0.5; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 9b0c083635..7369d8586e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1748,6 +1748,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( i_t num_fractional = fractional_variables(settings_, worker->leaf_solution.x, var_types_, fractional); pivot_out_integer_variables(worker->leaf_problem, + lp_settings, worker->basic_list, worker->nonbasic_list, worker->leaf_vstatus, @@ -3337,23 +3338,26 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t::apply_delta_x_for_integer_pivot( } template -void branch_and_bound_t::fast_slack_integer_pivot( +void branch_and_bound_t::fast_slack_integer_pivots( const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, const std::vector& fractional, const std::vector& row_to_slack, const simplex::lp_solution_t& solution, @@ -4254,8 +4259,8 @@ void branch_and_bound_t::fast_slack_integer_pivot( } } - if (fast_candidates.size() > 0) { - settings_.log.printf("Found %ld fast candidates for pivot out integer variables\n", + if (fast_candidates.size() > 0 && settings_.inside_mip < 2) { + settings.log.printf("Found %ld fast candidates for pivot out integer variables\n", fast_candidates.size()); } @@ -4377,22 +4382,26 @@ void branch_and_bound_t::fast_slack_integer_pivot( work_estimate); // apply_delta_x_for_integer_pivot only mutates vstatus when the pivot actually fires, // so entering_index transitioning to BASIC is a reliable success signal. - if (!error) { - settings_.log.printf( + if (!error && settings.inside_mip < 2) { + settings.log.printf( "Fast candidate pivot succeeded: j=%d entering slack=%d row=%d\n", j, entering_index, row); } - if (toc(last_log) > 1.0) { - settings_.log.printf("Fast candidates %d/%d processed in %.2f seconds\n", k + 1, num_candidates, toc(loop_start)); + if (settings.inside_mip < 2 && toc(last_log) > 1.0) { + settings.log.printf("Fast candidates %d/%d processed in %.2f seconds\n", k + 1, num_candidates, toc(loop_start)); last_log = tic(); } } - settings_.log.printf("Fast candidates: %d/%d processed in %.2f seconds\n", num_candidates, num_candidates, toc(loop_start)); + if (settings.inside_mip < 2) + { + settings.log.printf("Fast candidates: %d/%d processed in %.2f seconds\n", num_candidates, num_candidates, toc(loop_start)); + } } template i_t branch_and_bound_t::pivot_out_integer_variables( const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, @@ -4446,34 +4455,40 @@ i_t branch_and_bound_t::pivot_out_integer_variables( } } const f_t degeneracy_fraction = static_cast(num_degenerate) / lp.num_rows; - settings_.log.printf( - "Primal degeneracy: %d/%d basic variables are degenerate (%.1f%%), " - "continuous=%d, integer=%d\n", - num_degenerate, lp.num_rows, - 100.0 * degeneracy_fraction, - num_degenerate_continuous, num_degenerate_integer); + if (settings.inside_mip < 2 && settings.inside_submip == 0) { + settings.log.printf( + "Primal degeneracy: %d/%d basic variables are degenerate (%.1f%%), " + "continuous=%d, integer=%d\n", + num_degenerate, + lp.num_rows, + 100.0 * degeneracy_fraction, + num_degenerate_continuous, + num_degenerate_integer); + } // Skip pivot_out entirely if primal degeneracy is too high — the ratio test // will almost always be won by a degenerate variable, making pivots hopeless. if (degeneracy_fraction > 0.5) { - settings_.log.printf("Skipping pivot_out_integer_variables: degeneracy %.1f%% > 50%%\n", - 100.0 * degeneracy_fraction); + if (settings.inside_mip < 2) { + settings.log.printf("Skipping pivot_out_integer_variables: degeneracy %.1f%% > 50%%\n", + 100.0 * degeneracy_fraction); + } return 0; } std::vector nonbasic_index; - fast_slack_integer_pivot(lp, - fractional, - row_to_slack, - solution, - basic_list_copy, - nonbasic_list_copy, - nonbasic_index, - vstatus_copy, - soln_copy, - basis_update_copy, - work_estimate); - + fast_slack_integer_pivots(lp, + settings, + fractional, + row_to_slack, + solution, + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + soln_copy, + basis_update_copy, + work_estimate); std::vector work_list = fractional; std::vector to_basic_position(lp.num_cols, -1); @@ -4682,13 +4697,21 @@ i_t branch_and_bound_t::pivot_out_integer_variables( } if (toc(worklist_last_log) > 1.0) { - settings_.log.printf( - "Worklist progress: %d/%d processed, %d pivots, %d ratio_fail (unb=%d cont=%d nfint=%d), %d net_inc_fail, %d no_cand, %.2f seconds\n", - worklist_total_processed, static_cast(fractional.size()), - worklist_pivots_succeeded, worklist_ratio_test_fail, - worklist_unbounded, worklist_continuous_won, worklist_nonfrac_int_won, - worklist_net_increase_fail, - worklist_no_candidates, toc(worklist_loop_start)); + if (settings.inside_mip < 2) { + settings.log.printf( + "Worklist progress: %d/%d processed, %d pivots, %d ratio_fail (unb=%d cont=%d nfint=%d), " + "%d net_inc_fail, %d no_cand, %.2f seconds\n", + worklist_total_processed, + static_cast(fractional.size()), + worklist_pivots_succeeded, + worklist_ratio_test_fail, + worklist_unbounded, + worklist_continuous_won, + worklist_nonfrac_int_won, + worklist_net_increase_fail, + worklist_no_candidates, + toc(worklist_loop_start)); + } worklist_last_log = tic(); } } @@ -4706,25 +4729,39 @@ i_t branch_and_bound_t::pivot_out_integer_variables( else { entering_tried_multiple++; } } } - settings_.log.printf( - "Worklist entering stats: unique=%d, tried_once=%d, tried_multiple=%d, " - "max_count=%d, total_ftran=%d, duplication_ratio=%.1fx\n", - unique_entering, entering_tried_once, entering_tried_multiple, - max_entering_count, worklist_ftran_done, - worklist_ftran_done / std::max(1.0, static_cast(unique_entering))); - - settings_.log.printf( - "Worklist stats: processed=%d skipped=%d btran=%d ftran=%d pivots=%d readded=%d " - "btran_time=%.2f dot_time=%.2f ftran_time=%.2f zero_rc_vars=%d " - "no_candidates=%d ratio_test_fail=%d (unbounded=%d continuous_won=%d nonfrac_int_won=%d) " - "net_increase_fail=%d\n", - worklist_total_processed, worklist_skipped, worklist_btran_done, worklist_ftran_done, - worklist_pivots_succeeded, worklist_readded, - worklist_btran_time, worklist_dot_time, worklist_ftran_time, - num_zero_reduced_costs_vars, - worklist_no_candidates, worklist_ratio_test_fail, - worklist_unbounded, worklist_continuous_won, worklist_nonfrac_int_won, - worklist_net_increase_fail); + if (settings.inside_mip < 2) { + settings.log.printf( + "Worklist entering stats: unique=%d, tried_once=%d, tried_multiple=%d, " + "max_count=%d, total_ftran=%d, duplication_ratio=%.1fx\n", + unique_entering, + entering_tried_once, + entering_tried_multiple, + max_entering_count, + worklist_ftran_done, + worklist_ftran_done / std::max(1.0, static_cast(unique_entering))); + + settings.log.printf( + "Worklist stats: processed=%d skipped=%d btran=%d ftran=%d pivots=%d readded=%d " + "btran_time=%.2f dot_time=%.2f ftran_time=%.2f zero_rc_vars=%d " + "no_candidates=%d ratio_test_fail=%d (unbounded=%d continuous_won=%d nonfrac_int_won=%d) " + "net_increase_fail=%d\n", + worklist_total_processed, + worklist_skipped, + worklist_btran_done, + worklist_ftran_done, + worklist_pivots_succeeded, + worklist_readded, + worklist_btran_time, + worklist_dot_time, + worklist_ftran_time, + num_zero_reduced_costs_vars, + worklist_no_candidates, + worklist_ratio_test_fail, + worklist_unbounded, + worklist_continuous_won, + worklist_nonfrac_int_won, + worklist_net_increase_fail); + } std::vector new_fractional; const i_t num_new_fractional = @@ -4733,7 +4770,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( i_t num_integer_increased = start_num_fractional - num_new_fractional; integer_pivots_.fetch_add(num_integer_increased, std::memory_order_release); #if 0 - settings_.log.printf("Pivoted out %d integer variables: %d -> %d in %.2f\n", + settings.log.printf("Pivoted out %d integer variables: %d -> %d in %.2f\n", num_integer_increased, start_num_fractional, num_new_fractional, @@ -5235,6 +5272,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t pivot_out_integer_variables_start_time = tic(); i_t num_integer_increased = pivot_out_integer_variables(original_lp_, + settings_, basic_list, nonbasic_list, root_vstatus_, @@ -5244,14 +5282,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut fractional); settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", num_integer_increased, toc(pivot_out_integer_variables_start_time)); - dual_degenerate_feasibility_pump(original_lp_, - basic_list, - nonbasic_list, - root_vstatus_, - root_relax_soln_, - basis_update, - num_fractional, - fractional); + if (settings_.dual_degenerate_feasibility_pump != 0) { + dual_degenerate_feasibility_pump(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + } + if (received_halt_signal()) { solver_status_ = mip_status_t::HALT; set_final_solution(solution, root_objective_); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index a20e3d7551..52fc57e0de 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -505,19 +505,21 @@ class branch_and_bound_t { std::vector& zero_reduced_costs_vars, std::vector& zero_reduced_costs_vars_nonbasic_index); - void fast_slack_integer_pivot(const simplex::lp_problem_t& lp, - const std::vector& fractional, - const std::vector& row_to_slack, - const simplex::lp_solution_t& solution, - std::vector& basic_list, - std::vector& nonbasic_list, - std::vector& nonbasic_index, - std::vector& vstatus, - simplex::lp_solution_t& soln, - simplex::basis_update_mpf_t& basis_update, - f_t& work_estimate); + void fast_slack_integer_pivots(const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + const std::vector& fractional, + const std::vector& row_to_slack, + const simplex::lp_solution_t& solution, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate); i_t pivot_out_integer_variables(const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 4acfd97107..1f85a1baf3 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -103,6 +103,7 @@ struct simplex_solver_settings_t { strong_chvatal_gomory_cuts(-1), symmetry(-1), reduced_cost_strengthening(-1), + dual_degenerate_feasibility_pump(1), cut_change_threshold(1e-3), cut_min_orthogonality(0.5), mip_batch_pdlp_strong_branching(0), @@ -208,7 +209,8 @@ struct simplex_solver_settings_t { // cuts i_t symmetry; // -1 automatic, 0 to disable, >0 to enable different symmetry methods i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost - // strengthening + // strengthening + i_t dual_degenerate_feasibility_pump; // 0 to disable, 1 to enable f_t cut_change_threshold; // threshold for cut change f_t cut_min_orthogonality; // minimum orthogonality for cuts i_t diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 8288db92ff..8dd5266a73 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -152,6 +152,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_IMPLIED_BOUND_CUTS, &mip_settings.implied_bound_cuts, -1, 1, -1}, {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, + {CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP, &mip_settings.dual_degenerate_feasibility_pump, -1, 1, -1}, {CUOPT_MIP_RINS, &mip_settings.submip_params.rins, -1, 1, -1}, {CUOPT_MIP_RENS, &mip_settings.submip_params.rens, -1, 1, -1}, {CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1}, diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index f8eac0c4d8..05cbf95841 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -388,6 +388,10 @@ solution_t mip_solver_t::run_solver() context.settings.reduced_cost_strengthening == -1 ? 2 : context.settings.reduced_cost_strengthening; + branch_and_bound_settings.dual_degenerate_feasibility_pump = + context.settings.dual_degenerate_feasibility_pump == -1 + ? 1 + : context.settings.dual_degenerate_feasibility_pump; branch_and_bound_settings.symmetry = context.settings.symmetry; branch_and_bound_settings.diving_settings = context.settings.diving_params; From 435689335ffa9fd7ea43f000222f926a7a903c23 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 2 Sep 2026 14:03:00 -0700 Subject: [PATCH 28/34] Fix reduced-cost bounds for degenerate variables Signed-off-by: Christopher Maes --- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 7369d8586e..f6d045bda6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -4876,6 +4876,8 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( const f_t lower_j = lp.lower[j]; const f_t upper_j = lp.upper[j]; + const bool at_lower = soln.x[j] - lower_j <= settings_.primal_tol; + const bool at_upper = upper_j - soln.x[j] <= settings_.primal_tol; // Try both dual rays. scale == +1 uses the computed delta_z (direction -1); // scale == -1 uses -delta_z (direction +1). Either or both may yield an RCS bound. @@ -4984,7 +4986,7 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( // This means l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] <= // u_tilde_j Or equivalently, incumbent_objective <= relaxation_objective + // reduced_costs[j] * (u_tilde_j - l_j) when reduced_costs[j] > 0 - if (lower_j > -inf && new_reduced_cost > threshold) { + if (at_lower && lower_j > -inf && new_reduced_cost > threshold) { const f_t u_tilde_j = var_types_[j] == variable_type_t::INTEGER ? upper_j - tol : std::max(upper_j - 1.0, lower_j); @@ -5007,7 +5009,7 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( // u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j Or // equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * // (l_tilde_j - u_j) when reduced_costs[j] < 0 - if (upper_j < inf && new_reduced_cost < -threshold) { + if (at_upper && upper_j < inf && new_reduced_cost < -threshold) { const f_t l_tilde_j = var_types_[j] == variable_type_t::INTEGER ? lower_j + tol : std::min(lower_j + 1.0, upper_j); From d2fb7fbd4ea7ee715c769989803c66326bd9b67a Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 2 Sep 2026 15:57:12 -0700 Subject: [PATCH 29/34] Equilibrate LP rows and strengthen degenerate perturbations These improvements were discovered through Hiverge's automated exploration of changes to cuOpt's dual simplex solver and then isolated and validated independently against the v8 baseline. Apply row equilibration to imbalanced linear programs before the existing column normalization. For each row, divide the matrix coefficients and right-hand side by the row infinity norm when the maximum-to-minimum row norm ratio exceeds 10. Restrict this equilibration to standalone linear programs. MIP already performs integer-aware row scaling before presolve, and QP and SOCP problems use the existing iterative Ruiz equilibration path. Increase the initial objective perturbation from 5e-7 * max_abs_objective to 1e-5 * max_abs_objective when more than 5% of the nonbasic variables are dual degenerate. Retain the original perturbation strength for other problems. The stronger perturbation separates coincident and nearly coincident reduced costs. Across the benchmark it reduces the BFRT zero-step rate from 34.0% with row scaling alone to 15.4%. Row equilibration improves the numerical conditioning of models with imbalanced constraint rows and prevents several expensive or failed cleanup trajectories. On 240 MIPLIB LP relaxations, v9 improves the raw runtime geometric mean by 1.1865x relative to v8 and the one-second-shifted geometric mean by 1.1037x. It solves 229 problems within 300 seconds, compared with 226 for v8. The HiGHS times were taken from a faster machine, so the apparent HiGHS advantage is slightly exaggerated. Problem v8 v9 Baseline HiGHS v9/HiGHS ------------------------------------------------------------------------------------ momentum1 0.70 0.69 0.69 300.00 0.00 var-smallemery-m6j6 0.72 0.69 0.66 300.00 0.00 neos-5114902-kasavu 4.91 2.74 87.40 300.00 0.01 supportcase42 0.78 0.80 0.52 36.20 0.02 neos-5049753-cuanza 1.91 1.02 7.68 29.85 0.03 roi5alpha10n8 3.42 1.35 1.25 11.90 0.11 mzzv11 3.45 2.40 40.19 16.71 0.14 supportcase12 4.72 6.35 4.68 37.11 0.17 roi2alpha3n4 0.22 0.22 0.31 1.16 0.19 co-100 0.75 0.28 0.68 1.28 0.22 proteindesign121hz512p9 0.37 0.46 0.91 2.04 0.23 neos-5104907-jarama 52.83 21.53 124.69 89.64 0.24 supportcase18 0.03 0.03 0.06 0.12 0.25 ns1644855 300.00 63.39 300.00 238.30 0.27 proteindesign122trx11p8 0.26 0.35 0.64 1.26 0.28 neos-1354092 300.00 20.05 300.00 70.92 0.28 ns1952667 0.62 0.26 8.95 0.80 0.33 rd-rplusc-21 0.19 0.16 0.23 0.49 0.33 neos-787933 0.07 0.06 0.06 0.18 0.33 neos-5052403-cygnet 110.25 109.39 300.00 300.00 0.36 neos-860300 0.10 0.06 0.10 0.15 0.40 ns1760995 114.63 114.33 135.94 269.53 0.42 sct2 0.15 0.06 0.22 0.14 0.43 rocII-5-11 0.10 0.10 0.09 0.21 0.48 supportcase7 1.36 1.68 1.29 3.52 0.48 neos-5093327-huahum 0.25 0.23 0.23 0.48 0.48 satellites2-40 186.43 5.40 29.70 10.51 0.51 physiciansched6-2 3.84 3.60 11.15 6.72 0.54 neos-4647030-tutaki 1.72 1.68 2.47 3.09 0.54 n3div36 0.11 0.10 0.11 0.18 0.56 supportcase22 0.73 2.28 2.03 3.97 0.57 neos-4300652-rahue 0.54 0.46 1.22 0.80 0.57 neos-3004026-krka 0.07 0.07 0.06 0.12 0.58 neos-5107597-kakapo 0.14 0.06 0.04 0.10 0.60 lectsched-5-obj 0.10 0.11 0.13 0.18 0.61 neos-5188808-nattai 0.18 0.18 0.30 0.29 0.62 cvs16r128-89 1.13 1.07 0.93 1.72 0.62 30n20b8 0.06 0.07 0.08 0.11 0.64 blp-ar98 0.11 0.07 0.10 0.11 0.64 decomp2 0.12 0.08 0.19 0.12 0.67 swath3 0.03 0.02 0.03 0.03 0.67 neos-1171448 2.41 0.58 2.35 0.84 0.69 wachplan 0.31 0.18 0.25 0.26 0.69 neos-5195221-niemur 0.30 0.26 0.50 0.37 0.70 thor50dday 0.28 0.26 0.27 0.37 0.70 supportcase10 133.84 79.85 300.00 113.55 0.70 supportcase33 0.80 0.40 0.99 0.55 0.73 square47 95.31 93.64 79.87 126.92 0.74 neos-3381206-awhea 0.05 0.03 0.08 0.04 0.75 buildingenergy 300.00 87.82 300.00 115.61 0.76 cryptanalysiskb128n5obj14 9.28 9.55 29.78 12.54 0.76 neos-2746589-doon 4.11 2.36 7.53 3.02 0.78 nursesched-medium-hint03 6.00 3.71 10.47 4.73 0.78 blp-ic98 0.09 0.08 0.12 0.10 0.80 neos-848589 0.97 0.70 1.24 0.85 0.82 dano3_3 42.60 16.02 46.88 19.06 0.84 dano3_5 42.62 16.04 46.79 19.03 0.84 neos-1171737 0.54 0.17 0.70 0.20 0.85 academictimetablesmall 0.90 0.70 14.89 0.82 0.85 tbfp-network 7.80 7.72 9.06 9.04 0.85 ns1116954 25.16 9.20 156.23 10.73 0.86 neos-3402454-bohle 44.09 64.41 221.86 74.15 0.87 comp21-2idx 0.34 0.31 1.55 0.35 0.89 leo2 0.13 0.12 0.13 0.13 0.92 radiationm18-12-05 0.18 0.13 0.23 0.14 0.93 neos-3555904-turama 1.29 1.31 1.31 1.37 0.96 mzzv42z 1.42 0.91 10.10 0.95 0.96 square41 39.17 38.25 28.57 39.86 0.96 cryptanalysiskb128n5obj16 8.93 10.05 29.53 10.27 0.98 drayage-100-23 0.06 0.04 0.07 0.04 1.00 dws008-01 0.03 0.05 0.04 0.05 1.00 h80x6320d 0.05 0.04 0.05 0.04 1.00 highschool1-aigio 300.00 300.00 300.00 300.00 1.00 icir97_tension 0.03 0.02 0.03 0.02 1.00 leo1 0.12 0.07 0.10 0.07 1.00 neos-3988577-wolgan 300.00 300.00 278.89 300.00 1.00 neos-4738912-atrato 0.04 0.04 0.05 0.04 1.00 neos8 0.26 0.26 0.38 0.26 1.00 nursesched-sprint02 0.26 0.23 0.38 0.23 1.00 physiciansched3-3 288.13 300.00 300.00 300.00 1.00 rail02 300.00 300.00 300.00 300.00 1.00 s100 300.00 300.00 300.00 300.00 1.00 savsched1 300.00 300.00 300.00 300.00 1.00 supportcase19 300.00 300.00 300.00 300.00 1.00 swath1 0.04 0.03 0.04 0.03 1.00 traininstance2 0.04 0.04 0.09 0.04 1.00 s250r10 300.00 71.91 300.00 71.74 1.00 neos-873061 1.47 1.46 1.46 1.38 1.06 fiball 0.66 0.15 0.71 0.14 1.07 neos-3402294-bobin 1.72 1.29 3.07 1.20 1.08 neos-4413714-turia 20.16 2.10 3.97 1.93 1.09 sp98ar 0.37 0.34 0.39 0.31 1.10 germanrr 0.30 0.30 0.31 0.27 1.11 cod105 8.56 8.39 9.06 7.46 1.12 comp07-2idx 1.47 1.26 4.03 1.10 1.15 neos-1122047 2.43 1.87 2.09 1.61 1.16 neos-4763324-toguru 9.86 5.82 8.32 5.00 1.16 supportcase6 5.52 4.93 7.39 4.18 1.18 supportcase40 0.29 0.29 0.24 0.24 1.21 neos-960392 3.30 3.29 8.93 2.66 1.24 drayage-25-23 0.05 0.05 0.08 0.04 1.25 neos-1456979 0.08 0.05 0.05 0.04 1.25 neos-3216931-puriri 3.50 4.20 7.33 3.23 1.30 cmflsp50-24-8-8 0.74 0.58 0.77 0.44 1.32 irp 0.13 0.12 0.11 0.09 1.33 sp97ar 0.39 0.44 0.40 0.33 1.33 neos-1582420 0.09 0.08 0.12 0.06 1.33 traininstance6 0.03 0.04 0.04 0.03 1.33 radiationm40-10-02 1.45 0.97 1.58 0.71 1.37 map10 19.82 8.54 11.08 6.25 1.37 cbs-cta 0.48 0.14 0.42 0.10 1.40 mushroom-best 0.26 0.28 0.26 0.20 1.40 fast0507 4.05 3.95 7.36 2.77 1.43 bnatt500 0.21 0.20 0.29 0.14 1.43 nexp-150-20-8-5 0.11 0.10 0.10 0.07 1.43 map16715-04 20.42 9.87 13.26 6.80 1.45 qap10 12.87 9.80 15.57 6.68 1.47 hypothyroid-k1 4.93 4.42 4.36 3.01 1.47 fhnw-binpack4-48 0.03 0.09 0.07 0.06 1.50 rocI-4-11 0.11 0.09 0.11 0.06 1.50 uct-subprob 0.17 0.12 0.11 0.08 1.50 neos-827175 1.66 0.44 9.36 0.29 1.52 rail507 4.64 4.37 7.17 2.84 1.54 uccase9 13.22 8.04 11.54 5.20 1.55 neos-4532248-waihi 0.97 1.40 2.61 0.90 1.56 reblock115 0.22 0.14 0.16 0.09 1.56 neos-957323 16.74 12.14 300.00 7.74 1.57 trento1 3.80 3.50 3.08 2.21 1.58 neos-2987310-joes 1.91 1.59 1.52 1.00 1.59 neos-3024952-loue 0.38 0.32 0.41 0.20 1.60 air05 0.27 0.29 0.28 0.18 1.61 neos-662469 0.93 0.86 1.65 0.53 1.62 bnatt400 0.16 0.13 0.16 0.08 1.62 neos-3083819-nubu 0.05 0.05 0.06 0.03 1.67 ran14x18-disj-8 0.05 0.05 0.04 0.03 1.67 roll3000 0.13 0.10 0.12 0.06 1.67 k1mushroom 32.65 28.33 31.00 16.80 1.69 opm2-z10-s4 87.53 75.72 89.23 44.75 1.69 istanbul-no-cutoff 0.71 1.63 0.71 0.94 1.73 neos-3656078-kumeu 1.27 0.40 2.37 0.23 1.74 rmatr200-p5 8.13 8.10 7.50 4.61 1.76 ns1830653 0.29 0.25 0.42 0.14 1.79 neos-2978193-inde 0.08 0.09 0.16 0.05 1.80 irish-electricity 183.12 109.17 181.58 59.53 1.83 mcsched 0.30 0.30 0.27 0.16 1.88 sing326 12.30 9.33 9.54 4.69 1.99 assign1-5-8 0.04 0.02 0.03 0.01 2.00 bppc4-08 0.06 0.04 0.08 0.02 2.00 eil33-2 0.06 0.06 0.05 0.03 2.00 ic97_potential 0.03 0.02 0.02 0.01 2.00 mik-250-20-75-4 0.02 0.02 0.02 0.01 2.00 neos-4338804-snowy 0.02 0.02 0.03 0.01 2.00 neos-4387871-tavua 0.09 0.08 0.10 0.04 2.00 neos-4954672-berkel 0.03 0.02 0.02 0.01 2.00 nu25-pr12 0.04 0.04 0.05 0.02 2.00 pg 0.02 0.02 0.03 0.01 2.00 tr12-30 0.03 0.02 0.03 0.01 2.00 satellites2-60-fs 9.13 6.89 4.17 3.34 2.06 ns1208400 1.17 0.76 3.96 0.36 2.11 splice1k1 23.93 19.92 21.62 9.17 2.17 sing44 11.01 12.54 9.62 5.69 2.20 gfd-schedulen180f7d50m30k18 19.47 15.81 80.00 6.81 2.32 rmatr100-p10 0.32 0.33 0.27 0.14 2.36 eilA101-2 3.35 3.29 2.68 1.39 2.37 netdiversion 18.16 22.67 7.64 9.43 2.40 bab6 44.66 34.02 143.43 14.06 2.42 chromaticindex512-7 40.10 53.03 16.62 21.24 2.50 graph20-20-1rand 0.36 0.30 0.23 0.12 2.50 n5-3 0.05 0.05 0.03 0.02 2.50 rococoB10-011000 0.15 0.15 0.13 0.06 2.50 seymour 0.98 0.95 0.83 0.38 2.50 seymour1 0.94 0.97 0.83 0.38 2.55 neos-950242 0.66 0.46 1.05 0.18 2.56 piperout-08 0.17 0.41 0.39 0.16 2.56 triptim1 58.50 132.50 71.42 51.43 2.58 bab2 70.29 75.19 300.00 28.97 2.60 atlanta-ip 8.14 12.20 6.85 4.54 2.69 unitcal_7 0.91 1.05 0.96 0.39 2.69 net12 0.50 0.96 0.55 0.35 2.74 glass-sc 0.34 0.33 0.31 0.12 2.75 neos-1445765 0.30 0.25 0.16 0.09 2.78 chromaticindex1024-7 220.63 262.17 45.86 93.65 2.80 nw04 1.75 1.71 0.44 0.61 2.80 sorrell3 1.25 1.14 1.07 0.40 2.85 n2seq36q 0.74 0.71 0.46 0.24 2.96 50v-10 0.02 0.03 0.02 0.01 3.00 b1c1s1 0.05 0.06 0.05 0.02 3.00 binkar10_1 0.03 0.03 0.02 0.01 3.00 cost266-UUE 0.05 0.06 0.03 0.02 3.00 fhnw-binpack4-4 0.02 0.03 0.03 0.01 3.00 gmu-35-40 0.04 0.03 0.03 0.01 3.00 lotsize 0.04 0.03 0.03 0.01 3.00 neos17 0.03 0.03 0.03 0.01 3.00 pg5_34 0.02 0.03 0.03 0.01 3.00 rococoC10-001000 0.03 0.06 0.04 0.02 3.00 csched007 0.12 0.16 0.19 0.05 3.20 neos-933966 9.31 9.15 16.79 2.80 3.27 milo-v12-6-r2-40-1 0.22 0.18 0.25 0.05 3.60 gmu-35-50 0.04 0.04 0.05 0.01 4.00 neos-3627168-kasai 0.04 0.04 0.04 0.01 4.00 p200x1188c 0.03 0.04 0.03 0.01 4.00 peg-solitaire-a3 2.81 2.14 1.88 0.52 4.12 uccase12 4.77 5.76 72.32 1.38 4.17 rail01 167.23 300.00 223.84 71.23 4.21 csched008 0.09 0.13 0.11 0.03 4.33 CMS750_4 0.43 0.63 0.38 0.14 4.50 piperout-27 0.28 1.23 0.67 0.26 4.73 neos-4722843-widden 2.21 7.67 1.17 1.54 4.98 app1-1 0.09 0.10 0.08 0.02 5.00 fastxgemm-n2r6s0t2 0.34 0.40 0.18 0.08 5.00 ex10 300.00 300.00 300.00 59.25 5.06 neos-2075418-temuka 300.00 300.00 128.30 50.57 5.93 beasleyC3 0.05 0.06 0.05 0.01 6.00 neos-631710 300.00 215.36 300.00 31.97 6.74 mc11 0.05 0.08 0.06 0.01 8.00 app1-2 5.26 5.77 5.03 0.70 8.24 snp-02-004-104 19.21 24.10 14.17 2.83 8.52 brazil3 79.55 61.89 300.00 7.24 8.55 gen-ip002 0.02 0.01 0.02 0.00 10.00 markshare2 0.02 0.01 0.02 0.00 10.00 markshare_4_0 0.02 0.01 0.02 0.00 10.00 mas74 0.02 0.01 0.03 0.00 10.00 neos859080 0.02 0.01 0.01 0.00 10.00 pk1 0.02 0.01 0.02 0.00 10.00 enlight_hard 0.02 0.02 0.02 0.00 20.00 exp-1-500-5-5 0.02 0.02 0.02 0.00 20.00 gen-ip054 0.02 0.02 0.03 0.00 20.00 glass4 0.02 0.02 0.02 0.00 20.00 graphdraw-domain 0.03 0.02 0.02 0.00 20.00 mad 0.02 0.02 0.02 0.00 20.00 mas76 0.02 0.02 0.02 0.00 20.00 neos-3046615-murg 0.02 0.02 0.02 0.00 20.00 neos-3754480-nidda 0.02 0.02 0.02 0.00 20.00 neos5 0.02 0.02 0.02 0.00 20.00 sp150x300d 0.02 0.02 0.02 0.00 20.00 supportcase26 0.02 0.02 0.03 0.00 20.00 timtab1 0.02 0.02 0.01 0.00 20.00 ex9 300.00 300.00 300.00 14.07 21.32 neos-2657525-crna 0.03 0.03 0.03 0.00 30.00 neos-911970 0.02 0.03 0.03 0.00 30.00 Geomean v8/v9: 1.1865 Shifted(+1s): 1.1037 Geomean Baseline/v9: 1.4170 Shifted(+1s): 1.2561 Geomean v9/HiGHS: 1.5257 Shifted(+1s): 1.0133 (240 problems) --- cpp/src/dual_simplex/phase2.cpp | 11 ++++++++--- cpp/src/dual_simplex/scaling.cpp | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 123d6354f8..f7e5d6c7f3 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -455,9 +455,9 @@ template void initial_perturbation(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& vstatus, + bool strongly_degenerate, std::vector& objective) { - const i_t m = lp.num_rows; const i_t n = lp.num_cols; f_t max_abs_obj_coeff = 0.0; for (i_t j = 0; j < n; ++j) { @@ -484,7 +484,11 @@ void initial_perturbation(const lp_problem_t& lp, max_abs_obj_coeff = std::min(max_abs_obj_coeff, f_t(1.0)); } - const f_t perturbation_base = 5e-7 * max_abs_obj_coeff; + // Sub-tolerance perturbations are less disruptive on ordinary problems, but + // are too small to separate reduced costs when a substantial part of the + // nonbasic set is dual degenerate. Use a stronger, still temporary shift in + // that case. The original costs are restored before declaring optimality. + const f_t perturbation_base = (strongly_degenerate ? 1e-5 : 5e-7) * max_abs_obj_coeff; settings.log.printf("Perturbation debug: max_abs_obj_coeff=%e (dampened), perturbation_base=%e, n=%d, num_boxed=%d\n", max_abs_obj_coeff, perturbation_base, n, num_boxed); @@ -3104,7 +3108,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, settings.log.printf("Near-optimal check: num_primal_infeas=%d, max_primal_infeas=%.2e, near_optimal=%d, apply_perturbation=%d\n", num_primal_infeas, max_primal_infeas, near_optimal, apply_perturbation); if (apply_perturbation) { - phase2::initial_perturbation(lp, settings, vstatus, objective); + const bool strongly_degenerate = num_degen > n / 20; + phase2::initial_perturbation(lp, settings, vstatus, strongly_degenerate, objective); // Recompute y, z with perturbed objective for (i_t k = 0; k < m; ++k) { c_basic[k] = objective[basic_list[k]]; diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 98c409a630..8baaa0a8c7 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -253,6 +253,39 @@ i_t scaling(const lp_problem_t& unscaled, return 0; } + // MIP performs integer-aware row scaling before presolve, while QP and SOCP + // use the Ruiz path above. Apply this simpler equilibration only to LPs. + const bool use_lp_row_scaling = !settings.inside_mip && unscaled.second_order_cone_dims.empty() && + unscaled.Q.n == 0; + if (use_lp_row_scaling) { + csr_matrix_t Arow(0, 0, 0); + scaled.A.to_compressed_row(Arow); + std::vector row_norm(m, 1.0); + f_t max_row_norm = 0.0; + f_t min_row_norm = inf; + for (i_t i = 0; i < m; ++i) { + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + row_norm[i] = std::max(row_norm[i], std::abs(Arow.x[p])); + } + max_row_norm = std::max(max_row_norm, row_norm[i]); + min_row_norm = std::min(min_row_norm, row_norm[i]); + } + if (min_row_norm > 0.0 && max_row_norm / min_row_norm > 10.0) { + settings.log.printf("Applying row scaling. Maximum row norm %e, minimum row norm %e\n", + max_row_norm, + min_row_norm); + for (i_t j = 0; j < n; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + scaled.A.x[p] /= row_norm[scaled.A.i[p]]; + } + } + for (i_t i = 0; i < m; ++i) { + scaled.rhs[i] /= row_norm[i]; + row_scaling[i] = row_norm[i]; + } + } + } + column_scaling.resize(n); f_t max = 0; f_t min = std::numeric_limits::max(); From bfcbb158ecaa3c0d835fc7f309a105ca85f3895a Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 2 Sep 2026 17:42:18 -0700 Subject: [PATCH 30/34] Style fixes --- .../mip/solver_settings.hpp | 10 +- cpp/src/branch_and_bound/branch_and_bound.cpp | 350 ++++++++------ cpp/src/branch_and_bound/branch_and_bound.hpp | 74 ++- .../bound_flipping_ratio_test.cpp | 163 +++---- .../bound_flipping_ratio_test.hpp | 32 +- cpp/src/dual_simplex/phase2.cpp | 426 +++++++++++------- cpp/src/dual_simplex/primal.cpp | 16 +- cpp/src/dual_simplex/scaling.cpp | 4 +- .../dual_simplex/simplex_solver_settings.hpp | 14 +- 9 files changed, 622 insertions(+), 467 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp index c296185192..71438bfe8c 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp @@ -133,12 +133,12 @@ class mip_solver_settings_t { i_t clique_cuts = -1; i_t zero_half_cuts = -1; i_t implied_bound_cuts = -1; - i_t strong_chvatal_gomory_cuts = -1; - i_t reduced_cost_strengthening = -1; + i_t strong_chvatal_gomory_cuts = -1; + i_t reduced_cost_strengthening = -1; i_t dual_degenerate_feasibility_pump = -1; // -1 = automatic (on), 0 = off, 1 = on - i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable - f_t cut_change_threshold = -1.0; - f_t cut_min_orthogonality = 0.5; + i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable + f_t cut_change_threshold = -1.0; + f_t cut_min_orthogonality = 0.5; i_t mip_batch_pdlp_strong_branching{ 0}; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only i_t mip_batch_pdlp_reliability_branching{ diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 83d0feb134..3763667c99 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -463,7 +463,6 @@ void branch_and_bound_t::report(const lp_problem_t& lp, settings_.log.printf("%s\n", log_line.c_str()); } - template void branch_and_bound_t::update_reduced_cost_bounds( f_t relaxation_objective, @@ -484,8 +483,9 @@ void branch_and_bound_t::update_reduced_cost_bounds( // Let u_tilde_j = u_j - epsilon, so that floor(u_tilde_j) = u_j - 1 // We want to solve for want the incumbent objective needs to be to make // x_j <= u_tilde_j - // This means l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] <= u_tilde_j - // Or equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * (u_tilde_j - l_j) when reduced_costs[j] > 0 + // This means l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] <= + // u_tilde_j Or equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * + // (u_tilde_j - l_j) when reduced_costs[j] > 0 if (lower_j > -inf && reduced_costs[j] > 0) { const f_t u_tilde_j = var_types_[j] == variable_type_t::INTEGER ? upper_j - tol @@ -495,18 +495,20 @@ void branch_and_bound_t::update_reduced_cost_bounds( const f_t diff = u_tilde_j - lower_j; const f_t objective_j = relaxation_objective + diff * reduced_costs[j]; if (((var_types_[j] == variable_type_t::INTEGER && bound_j == upper_j - 1.0) || - var_types_[j] != variable_type_t::INTEGER) && std::isfinite(objective_j) && std::isfinite(bound_j)) { + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { i_t info = reduced_cost_bounds.add_upper_bound(j, objective_j, bound_j); - //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. Info %d\n", objective_j, bound_j, j, info); + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. + // Info %d\n", objective_j, bound_j, j, info); } } - // x_j >= u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] when reduced_costs[j] < 0 - // Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 - // We want to solve for want the incumbent objective needs to be to make - // x_j >= l_tilde_j - // This means u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j - // Or equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * (l_tilde_j - u_j) when reduced_costs[j] < 0 + // x_j >= u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] when + // reduced_costs[j] < 0 Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 We + // want to solve for want the incumbent objective needs to be to make x_j >= l_tilde_j This + // means u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j Or + // equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * (l_tilde_j + // - u_j) when reduced_costs[j] < 0 if (upper_j < inf && reduced_costs[j] < 0) { const f_t l_tilde_j = var_types_[j] == variable_type_t::INTEGER ? lower_j + tol @@ -516,16 +518,17 @@ void branch_and_bound_t::update_reduced_cost_bounds( const f_t diff = l_tilde_j - upper_j; const f_t objective_j = relaxation_objective + diff * reduced_costs[j]; if (((var_types_[j] == variable_type_t::INTEGER && bound_j == lower_j + 1.0) || - var_types_[j] != variable_type_t::INTEGER) && std::isfinite(objective_j) && std::isfinite(bound_j)) { + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { i_t info = reduced_cost_bounds.add_lower_bound(j, objective_j, bound_j); - //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. Info %d\n", objective_j, bound_j, j, info); + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. + // Info %d\n", objective_j, bound_j, j, info); } } } } } - template i_t branch_and_bound_t::find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, @@ -3470,7 +3473,9 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t::cut_pass_action_t branch_and_bound_t= 1 && upper_bound_.load() < last_upper_bound) { mutex_upper_.lock(); - last_upper_bound = upper_bound_.load(); + last_upper_bound = upper_bound_.load(); std::vector lower_bounds = original_lp_.lower; std::vector upper_bounds = original_lp_.upper; - f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); - i_t new_bounds = reduced_cost_bounds.update_bounds_from_new_incumbent( + f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); + i_t new_bounds = reduced_cost_bounds.update_bounds_from_new_incumbent( upper_bound_.load(), var_types_, lower_bounds, upper_bounds); mutex_upper_.unlock(); mutex_original_lp_.lock(); @@ -3573,7 +3578,12 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t 0) { settings_.log.printf( - "Updated %d integer bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", new_bounds, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); + "Updated %d integer bounds using reduced cost strengthening from new incumbent. Max " + "objective %e Current objective %e Previous max objective %e\n", + new_bounds, + reduced_cost_bounds.get_max_objective(), + upper_bound_.load(), + previous_max_objective); } } @@ -3686,8 +3696,11 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t= 1) { - update_reduced_cost_bounds(root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); - settings_.log.printf("New reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); + update_reduced_cost_bounds( + root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + settings_.log.printf("New reduced cost objective %e (current %e)\n", + reduced_cost_bounds.get_max_objective(), + upper_bound_.load()); pivot_to_improve_reduced_cost_strengthening(original_lp_, basic_list, nonbasic_list, @@ -3695,8 +3708,12 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t::dual_degenerate_feasibility_pump( reduced_basis_update.b_transpose_solve(c_basic_pump, y_pump); // Check if any nonbasic has a violated reduced cost - i_t num_violated = 0; - f_t max_violation = 0.0; + i_t num_violated = 0; + f_t max_violation = 0.0; const i_t num_nonbasics_reduced = reduced_nonbasic_list.size(); for (i_t k = 0; k < num_nonbasics_reduced; k++) { const i_t j = reduced_nonbasic_list[k]; // z[j] = c[j] - y^T * A(:,j) - f_t zj = lp_reduced.objective[j]; + f_t zj = lp_reduced.objective[j]; const i_t col_start = A_reduced.col_start[j]; - const i_t col_end = A_reduced.col_start[j + 1]; + const i_t col_end = A_reduced.col_start[j + 1]; for (i_t p = col_start; p < col_end; p++) { zj -= y_pump[A_reduced.i[p]] * A_reduced.x[p]; } @@ -4020,13 +4037,17 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( f_t pump_call_start_time = tic(); simplex_solver_settings_t primal_settings = settings_; primal_settings.log.log = false; - primal_settings.time_limit = settings_.time_limit; - primal_settings.work_limit = root_relax_work_estimate_ / 10; + primal_settings.time_limit = settings_.time_limit; + primal_settings.work_limit = root_relax_work_estimate_ / 10; settings_.log.printf( "Degenerate feasibility pump: calling primal simplex with %d rows, %d cols, %d nnz, " "%d basis updates, work_limit %.2e, primal_work_estimate %.2e\n", - m, n, A_reduced.col_start[n], reduced_basis_update.num_updates(), - primal_settings.work_limit, primal_work_estimate); + m, + n, + A_reduced.col_start[n], + reduced_basis_update.num_updates(), + primal_settings.work_limit, + primal_work_estimate); simplex::primal_status_t lp_status = simplex::primal_phase2_with_advanced_basis(2, exploration_stats_.start_time, @@ -4039,13 +4060,15 @@ void branch_and_bound_t::dual_degenerate_feasibility_pump( reduced_solution, iter, primal_work_estimate); - f_t pump_call_time = toc(pump_call_start_time); - f_t pump_call_work = primal_work_estimate - primal_work_before; + f_t pump_call_time = toc(pump_call_start_time); + f_t pump_call_work = primal_work_estimate - primal_work_before; i_t pump_call_iters = iter - iter_before; settings_.log.printf( "Degenerate feasibility pump: primal simplex returned status %d, %d iters, " "work %.2e (%.2e/iter), time %.2f (%.2e work/s)\n", - static_cast(lp_status), pump_call_iters, pump_call_work, + static_cast(lp_status), + pump_call_iters, + pump_call_work, pump_call_iters > 0 ? pump_call_work / pump_call_iters : 0.0, pump_call_time, pump_call_time > 0 ? pump_call_work / pump_call_time : 0.0); @@ -4384,7 +4407,7 @@ void branch_and_bound_t::fast_slack_integer_pivots( if (fast_candidates.size() > 0 && settings_.inside_mip < 2) { settings.log.printf("Found %ld fast candidates for pivot out integer variables\n", - fast_candidates.size()); + fast_candidates.size()); } // Build a reverse index nonbasic_index[v] = position of v in nonbasic_list, or -1 if not @@ -4398,8 +4421,8 @@ void branch_and_bound_t::fast_slack_integer_pivots( } const i_t num_candidates = fast_candidates.size(); - f_t last_log = tic(); - f_t loop_start = tic(); + f_t last_log = tic(); + f_t loop_start = tic(); for (i_t k = 0; k < num_candidates; k++) { const i_t j = fast_candidates[k]; const i_t row = fast_rows[k]; @@ -4491,18 +4514,18 @@ void branch_and_bound_t::fast_slack_integer_pivots( utilde_sparse.from_dense(utilde_dense); i_t error = apply_delta_x_for_integer_pivot(lp, - basic_list, - nonbasic_list, - nonbasic_index, - vstatus, - entering_index, - nonbasic_entering, - direction, - delta_x, - utilde_sparse, - soln, - basis_update, - work_estimate); + basic_list, + nonbasic_list, + nonbasic_index, + vstatus, + entering_index, + nonbasic_entering, + direction, + delta_x, + utilde_sparse, + soln, + basis_update, + work_estimate); // apply_delta_x_for_integer_pivot only mutates vstatus when the pivot actually fires, // so entering_index transitioning to BASIC is a reliable success signal. if (!error && settings.inside_mip < 2) { @@ -4511,13 +4534,18 @@ void branch_and_bound_t::fast_slack_integer_pivots( } if (settings.inside_mip < 2 && toc(last_log) > 1.0) { - settings.log.printf("Fast candidates %d/%d processed in %.2f seconds\n", k + 1, num_candidates, toc(loop_start)); + settings.log.printf("Fast candidates %d/%d processed in %.2f seconds\n", + k + 1, + num_candidates, + toc(loop_start)); last_log = tic(); } } - if (settings.inside_mip < 2) - { - settings.log.printf("Fast candidates: %d/%d processed in %.2f seconds\n", num_candidates, num_candidates, toc(loop_start)); + if (settings.inside_mip < 2) { + settings.log.printf("Fast candidates: %d/%d processed in %.2f seconds\n", + num_candidates, + num_candidates, + toc(loop_start)); } } @@ -4561,11 +4589,11 @@ i_t branch_and_bound_t::pivot_out_integer_variables( f_t work_estimate = 0.0; // Count primal degenerate basic variables - i_t num_degenerate = 0; + i_t num_degenerate = 0; i_t num_degenerate_continuous = 0; - i_t num_degenerate_integer = 0; + i_t num_degenerate_integer = 0; for (i_t k = 0; k < lp.num_rows; k++) { - const i_t j = basic_list_copy[k]; + const i_t j = basic_list_copy[k]; const f_t slack_to_lower = soln_copy.x[j] - lp.lower[j]; const f_t slack_to_upper = lp.upper[j] - soln_copy.x[j]; if (slack_to_lower <= settings_.primal_tol || slack_to_upper <= settings_.primal_tol) { @@ -4594,7 +4622,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( if (degeneracy_fraction > 0.5) { if (settings.inside_mip < 2) { settings.log.printf("Skipping pivot_out_integer_variables: degeneracy %.1f%% > 50%%\n", - 100.0 * degeneracy_fraction); + 100.0 * degeneracy_fraction); } return 0; } @@ -4631,24 +4659,24 @@ i_t branch_and_bound_t::pivot_out_integer_variables( // Track which entering variables are actually tried (to detect duplication) std::vector entering_tried_count(lp.num_cols, 0); - i_t worklist_total_processed = 0; - i_t worklist_skipped = 0; - i_t worklist_btran_done = 0; - i_t worklist_ftran_done = 0; - i_t worklist_pivots_succeeded = 0; - i_t worklist_readded = 0; - f_t worklist_btran_time = 0.0; - f_t worklist_dot_time = 0.0; - f_t worklist_ftran_time = 0.0; - i_t worklist_no_candidates = 0; // target had no nonzero dot_q - i_t worklist_ratio_test_fail = 0; // ratio test didn't pick a fractional integer (error -1) - i_t worklist_net_increase_fail = 0; // pivot would net-increase fractionals (error -2) - i_t worklist_unbounded = 0; // entering hit its own bound or unbounded (error -4) - i_t worklist_continuous_won = 0; // continuous variable won ratio test (error -5) - i_t worklist_nonfrac_int_won = 0; // non-fractional integer won ratio test (error -6) + i_t worklist_total_processed = 0; + i_t worklist_skipped = 0; + i_t worklist_btran_done = 0; + i_t worklist_ftran_done = 0; + i_t worklist_pivots_succeeded = 0; + i_t worklist_readded = 0; + f_t worklist_btran_time = 0.0; + f_t worklist_dot_time = 0.0; + f_t worklist_ftran_time = 0.0; + i_t worklist_no_candidates = 0; // target had no nonzero dot_q + i_t worklist_ratio_test_fail = 0; // ratio test didn't pick a fractional integer (error -1) + i_t worklist_net_increase_fail = 0; // pivot would net-increase fractionals (error -2) + i_t worklist_unbounded = 0; // entering hit its own bound or unbounded (error -4) + i_t worklist_continuous_won = 0; // continuous variable won ratio test (error -5) + i_t worklist_nonfrac_int_won = 0; // non-fractional integer won ratio test (error -6) f_t worklist_loop_start = tic(); - f_t worklist_last_log = tic(); + f_t worklist_last_log = tic(); while (!work_list.empty()) { const i_t j = work_list.back(); @@ -4657,9 +4685,18 @@ i_t branch_and_bound_t::pivot_out_integer_variables( worklist_total_processed++; // Skip if j is no longer basic and fractional (may have been fixed by a prior pivot) - if (p < 0) { worklist_skipped++; continue; } - if (vstatus_copy[j] != variable_status_t::BASIC) { worklist_skipped++; continue; } - if (!is_fractional(soln_copy.x[j], var_types_[j], settings_.integer_tol)) { worklist_skipped++; continue; } + if (p < 0) { + worklist_skipped++; + continue; + } + if (vstatus_copy[j] != variable_status_t::BASIC) { + worklist_skipped++; + continue; + } + if (!is_fractional(soln_copy.x[j], var_types_[j], settings_.integer_tol)) { + worklist_skipped++; + continue; + } // We want to pivot variable j out of the basis. // We solve B^T * delta_y = e_p, where p is the position of j in the basis. @@ -4707,9 +4744,9 @@ i_t branch_and_bound_t::pivot_out_integer_variables( // Small nnz means the FTRAN result is likely sparse, so fewer competing // basic variables will have nonzero delta_xB components to block the target. // Skip entering variables that have already been tried (and failed) by prior targets. - f_t values[3] = {0.0, 0.0, 0.0}; + f_t values[3] = {0.0, 0.0, 0.0}; i_t indices[3] = {-1, -1, -1}; - f_t dot_start = tic(); + f_t dot_start = tic(); for (i_t q : zero_reduced_costs_vars) { if (var_types_[q] == variable_type_t::INTEGER) { continue; } if (nonbasic_index[q] < 0) { continue; } @@ -4718,7 +4755,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( const i_t col_start = lp.A.col_start[q]; const i_t col_end = lp.A.col_start[q + 1]; const i_t col_nnz = col_end - col_start; - f_t dot_q = 0.0; + f_t dot_q = 0.0; for (i_t pp = col_start; pp < col_end; pp++) { dot_q += delta_y_dense[lp.A.i[pp]] * lp.A.x[pp]; } @@ -4727,14 +4764,20 @@ i_t branch_and_bound_t::pivot_out_integer_variables( const f_t merit = abs_dot_q / static_cast(col_nnz); if (merit > values[0]) { - indices[2] = indices[1]; values[2] = values[1]; - indices[1] = indices[0]; values[1] = values[0]; - indices[0] = q; values[0] = merit; + indices[2] = indices[1]; + values[2] = values[1]; + indices[1] = indices[0]; + values[1] = values[0]; + indices[0] = q; + values[0] = merit; } else if (merit > values[1]) { - indices[2] = indices[1]; values[2] = values[1]; - indices[1] = q; values[1] = merit; + indices[2] = indices[1]; + values[2] = values[1]; + indices[1] = q; + values[1] = merit; } else if (merit > values[2]) { - indices[2] = q; values[2] = merit; + indices[2] = q; + values[2] = merit; } } worklist_dot_time += toc(dot_start); @@ -4745,7 +4788,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( for (i_t h = 0; h < 3; h++) { if (indices[h] == -1) break; - const i_t q = indices[h]; + const i_t q = indices[h]; const i_t entering_index = q; const i_t nonbasic_entering = nonbasic_index[q]; if (nonbasic_entering < 0) { continue; } @@ -4775,39 +4818,48 @@ i_t branch_and_bound_t::pivot_out_integer_variables( delta_x[q] = direction; i_t error = apply_delta_x_for_integer_pivot(lp, - basic_list_copy, - nonbasic_list_copy, - nonbasic_index, - vstatus_copy, - entering_index, - nonbasic_entering, - direction, - delta_x, - utilde_sparse, - soln_copy, - basis_update_copy, - work_estimate); + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + entering_index, + nonbasic_entering, + direction, + delta_x, + utilde_sparse, + soln_copy, + basis_update_copy, + work_estimate); if (error == -2) { worklist_net_increase_fail++; } - if (error == -4) { worklist_unbounded++; worklist_ratio_test_fail++; } - if (error == -5) { worklist_continuous_won++; worklist_ratio_test_fail++; } - if (error == -6) { worklist_nonfrac_int_won++; worklist_ratio_test_fail++; } + if (error == -4) { + worklist_unbounded++; + worklist_ratio_test_fail++; + } + if (error == -5) { + worklist_continuous_won++; + worklist_ratio_test_fail++; + } + if (error == -6) { + worklist_nonfrac_int_won++; + worklist_ratio_test_fail++; + } if (!error) { worklist_pivots_succeeded++; // Update to_basic_position for the variables that changed status // entering_index is now basic, leaving_index is now nonbasic // Find the leaving variable: it's the one that took entering_index's slot in nonbasic_list - const i_t leaving_index = nonbasic_list_copy[nonbasic_entering]; + const i_t leaving_index = nonbasic_list_copy[nonbasic_entering]; to_basic_position[entering_index] = to_basic_position[leaving_index]; - to_basic_position[leaving_index] = -1; + to_basic_position[leaving_index] = -1; // We did a successful pivot; add fractional variables whose values changed to work list for (i_t k : fractional) { if (vstatus_copy[k] != variable_status_t::BASIC) { continue; } if (std::abs(delta_x[k]) > settings_.zero_tol) { - //work_list.push_back(k); - //worklist_readded++; + // work_list.push_back(k); + // worklist_readded++; } } break; @@ -4840,16 +4892,19 @@ i_t branch_and_bound_t::pivot_out_integer_variables( } // Count unique entering variables and duplication - i_t unique_entering = 0; - i_t max_entering_count = 0; - i_t entering_tried_once = 0; + i_t unique_entering = 0; + i_t max_entering_count = 0; + i_t entering_tried_once = 0; i_t entering_tried_multiple = 0; for (i_t q = 0; q < lp.num_cols; q++) { if (entering_tried_count[q] > 0) { unique_entering++; max_entering_count = std::max(max_entering_count, entering_tried_count[q]); - if (entering_tried_count[q] == 1) { entering_tried_once++; } - else { entering_tried_multiple++; } + if (entering_tried_count[q] == 1) { + entering_tried_once++; + } else { + entering_tried_multiple++; + } } } if (settings.inside_mip < 2) { @@ -4957,9 +5012,9 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( std::vector delta_z_indices; delta_z_indices.reserve(lp.num_cols); - f_t work_estimate = 0; - const f_t threshold = 100.0 * settings_.integer_tol; - const f_t tol = 1e-2; + f_t work_estimate = 0; + const f_t threshold = 100.0 * settings_.integer_tol; + const f_t tol = 1e-2; const f_t zero_tol = settings_.zero_tol; const f_t harris_tol = settings_.dual_tol / 10; @@ -4997,8 +5052,8 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( delta_z, work_estimate); - const f_t lower_j = lp.lower[j]; - const f_t upper_j = lp.upper[j]; + const f_t lower_j = lp.lower[j]; + const f_t upper_j = lp.upper[j]; const bool at_lower = soln.x[j] - lower_j <= settings_.primal_tol; const bool at_upper = upper_j - soln.x[j] <= settings_.primal_tol; @@ -5042,8 +5097,7 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( const f_t step = alpha * scale * delta_z[jj]; const f_t new_zj = old_zj + step; const bool initially_infeasible = - (vstatus[jj] == variable_status_t::NONBASIC_LOWER && - old_zj < -settings_.dual_tol) || + (vstatus[jj] == variable_status_t::NONBASIC_LOWER && old_zj < -settings_.dual_tol) || (vstatus[jj] == variable_status_t::NONBASIC_UPPER && old_zj > settings_.dual_tol); if (initially_infeasible) { num_initial_dual_infeas++; @@ -5122,14 +5176,15 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( std::isfinite(objective_j) && std::isfinite(bound_j)) { i_t info = reduced_cost_bounds.add_upper_bound(j, objective_j, bound_j); if (info > 0) { num_bounds_added++; } - //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. Info %d\n", objective_j, bound_j, j, info); + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. + // Info %d\n", objective_j, bound_j, j, info); } } // x_j >= u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] when - // reduced_costs[j] < 0 Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 We want - // to solve for want the incumbent objective needs to be to make x_j >= l_tilde_j This means - // u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j Or + // reduced_costs[j] < 0 Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 We + // want to solve for want the incumbent objective needs to be to make x_j >= l_tilde_j This + // means u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j Or // equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * // (l_tilde_j - u_j) when reduced_costs[j] < 0 if (at_upper && upper_j < inf && new_reduced_cost < -threshold) { @@ -5145,7 +5200,8 @@ void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( std::isfinite(objective_j) && std::isfinite(bound_j)) { i_t info = reduced_cost_bounds.add_lower_bound(j, objective_j, bound_j); if (info > 0) { num_bounds_added++; } - //settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. Info %d\n", objective_j, bound_j, j, info); + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. + // Info %d\n", objective_j, bound_j, j, info); } } } @@ -5247,7 +5303,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lp_status_t root_status = lp_status_t::UNSET; solving_root_relaxation_ = true; - f_t root_relax_start_time = tic(); + f_t root_relax_start_time = tic(); root_relax_work_estimate_ = 0.0; if (!enable_concurrent_lp_root_solve()) { // RINS/SUBMIP path @@ -5383,8 +5439,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lower_bound_numerical_ = inf; reduced_cost_bounds_t reduced_cost_bounds(original_lp_.num_cols); - update_reduced_cost_bounds(root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); - settings_.log.printf("New reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); + update_reduced_cost_bounds( + root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + settings_.log.printf("New reduced cost objective %e (current %e)\n", + reduced_cost_bounds.get_max_objective(), + upper_bound_.load()); pivot_to_improve_reduced_cost_strengthening(original_lp_, basic_list, nonbasic_list, @@ -5393,20 +5452,25 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, num_fractional, fractional, - root_objective_, reduced_cost_bounds); - settings_.log.printf("After pivoting: new reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); + root_objective_, + reduced_cost_bounds); + settings_.log.printf("After pivoting: new reduced cost objective %e (current %e)\n", + reduced_cost_bounds.get_max_objective(), + upper_bound_.load()); f_t pivot_out_integer_variables_start_time = tic(); - i_t num_integer_increased = pivot_out_integer_variables(original_lp_, - settings_, - basic_list, - nonbasic_list, - root_vstatus_, - root_relax_soln_, - basis_update, - num_fractional, - fractional); - settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", num_integer_increased, toc(pivot_out_integer_variables_start_time)); + i_t num_integer_increased = pivot_out_integer_variables(original_lp_, + settings_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", + num_integer_increased, + toc(pivot_out_integer_variables_start_time)); if (settings_.dual_degenerate_feasibility_pump != 0) { dual_degenerate_feasibility_pump(original_lp_, @@ -5606,10 +5670,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { std::vector lower_bounds = original_lp_.lower; std::vector upper_bounds = original_lp_.upper; - f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); - i_t num_changed = reduced_cost_bounds.update_bounds_from_new_incumbent( + f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); + i_t num_changed = reduced_cost_bounds.update_bounds_from_new_incumbent( upper_bound_.load(), var_types_, lower_bounds, upper_bounds); - settings_.log.printf("Updated %d integer bounds using reduced cost strengthening from new incumbent. Max objective %e Current objective %e Previous max objective %e\n", num_changed, reduced_cost_bounds.get_max_objective(), upper_bound_.load(), previous_max_objective); + settings_.log.printf( + "Updated %d integer bounds using reduced cost strengthening from new incumbent. Max " + "objective %e Current objective %e Previous max objective %e\n", + num_changed, + reduced_cost_bounds.get_max_objective(), + upper_bound_.load(), + previous_max_objective); mutex_original_lp_.lock(); original_lp_.lower = lower_bounds; original_lp_.upper = upper_bounds; diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index a7d1baf32a..9ba0b94128 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -96,12 +96,10 @@ struct deterministic_diving_policy_t; template struct objective_bound_pair_t { objective_bound_pair_t() - : objective(std::numeric_limits::quiet_NaN()), - bound(std::numeric_limits::quiet_NaN()) + : objective(std::numeric_limits::quiet_NaN()), bound(std::numeric_limits::quiet_NaN()) { } - objective_bound_pair_t(f_t objective_in, f_t bound_in) - : objective(objective_in), bound(bound_in) + objective_bound_pair_t(f_t objective_in, f_t bound_in) : objective(objective_in), bound(bound_in) { } bool is_valid() { return objective == objective && bound == bound; } @@ -113,7 +111,9 @@ template class reduced_cost_bounds_t { public: reduced_cost_bounds_t(i_t original_cols) - : max_objective_(-std::numeric_limits::infinity()), lower_bounds_(original_cols), upper_bounds_(original_cols) + : max_objective_(-std::numeric_limits::infinity()), + lower_bounds_(original_cols), + upper_bounds_(original_cols) { } @@ -122,22 +122,16 @@ class reduced_cost_bounds_t { if (col < static_cast(lower_bounds_.size())) { if (!lower_bounds_[col].is_valid()) { lower_bounds_[col] = objective_bound_pair_t(objective, bound); - if (objective > max_objective_) { - max_objective_ = objective; - } + if (objective > max_objective_) { max_objective_ = objective; } return 1; } else { if (bound > lower_bounds_[col].bound) { lower_bounds_[col] = objective_bound_pair_t(objective, bound); - if (objective > max_objective_) { - max_objective_ = objective; - } + if (objective > max_objective_) { max_objective_ = objective; } return 2; } else if (bound == lower_bounds_[col].bound && objective > lower_bounds_[col].objective) { lower_bounds_[col].objective = objective; - if (objective > max_objective_) { - max_objective_ = objective; - } + if (objective > max_objective_) { max_objective_ = objective; } return 1; } else { return -2; @@ -153,22 +147,16 @@ class reduced_cost_bounds_t { if (col < static_cast(upper_bounds_.size())) { if (!upper_bounds_[col].is_valid()) { upper_bounds_[col] = objective_bound_pair_t(objective, bound); - if (objective > max_objective_) { - max_objective_ = objective; - } + if (objective > max_objective_) { max_objective_ = objective; } return 1; } else { if (bound < upper_bounds_[col].bound) { upper_bounds_[col] = objective_bound_pair_t(objective, bound); - if (objective > max_objective_) { - max_objective_ = objective; - } + if (objective > max_objective_) { max_objective_ = objective; } return 2; } else if (bound == upper_bounds_[col].bound && objective > upper_bounds_[col].objective) { upper_bounds_[col].objective = objective; - if (objective > max_objective_) { - max_objective_ = objective; - } + if (objective > max_objective_) { max_objective_ = objective; } return 1; } else { return -2; @@ -184,17 +172,19 @@ class reduced_cost_bounds_t { std::vector& lower_bounds, std::vector& upper_bounds) { - const i_t n = static_cast(lower_bounds_.size()); - f_t max_objective = -std::numeric_limits::infinity(); + const i_t n = static_cast(lower_bounds_.size()); + f_t max_objective = -std::numeric_limits::infinity(); i_t integer_bounds_updated = 0; for (i_t j = 0; j < n; ++j) { if (lower_bounds_[j].is_valid()) { if (incumbent_objective <= lower_bounds_[j].objective && lower_bounds_[j].bound > lower_bounds[j]) { - //printf("RCF Variable %d (%d): lower %e -> %e\n", j, static_cast(var_types[j]), lower_bounds[j], lower_bounds_[j].bound); + // printf("RCF Variable %d (%d): lower %e -> %e\n", j, static_cast(var_types[j]), + // lower_bounds[j], lower_bounds_[j].bound); lower_bounds[j] = lower_bounds_[j].bound; if (var_types[j] == simplex::variable_type_t::INTEGER) { integer_bounds_updated++; } - lower_bounds_[j].bound = lower_bounds_[j].objective = std::numeric_limits::quiet_NaN(); + lower_bounds_[j].bound = lower_bounds_[j].objective = + std::numeric_limits::quiet_NaN(); } if (lower_bounds_[j].objective > max_objective) { max_objective = lower_bounds_[j].objective; @@ -203,10 +193,12 @@ class reduced_cost_bounds_t { if (upper_bounds_[j].is_valid()) { if (incumbent_objective <= upper_bounds_[j].objective && upper_bounds_[j].bound < upper_bounds[j]) { - //printf("RCF Variable %d (%d): upper %e -> %e\n", j, static_cast(var_types[j]), upper_bounds[j], upper_bounds_[j].bound); + // printf("RCF Variable %d (%d): upper %e -> %e\n", j, static_cast(var_types[j]), + // upper_bounds[j], upper_bounds_[j].bound); upper_bounds[j] = upper_bounds_[j].bound; if (var_types[j] == simplex::variable_type_t::INTEGER) { integer_bounds_updated++; } - upper_bounds_[j].bound = upper_bounds_[j].objective = std::numeric_limits::quiet_NaN(); + upper_bounds_[j].bound = upper_bounds_[j].objective = + std::numeric_limits::quiet_NaN(); } if (upper_bounds_[j].objective > max_objective) { max_objective = upper_bounds_[j].objective; @@ -525,18 +517,18 @@ class branch_and_bound_t { std::vector& fractional); i_t apply_delta_x_for_integer_pivot(const simplex::lp_problem_t& lp, - std::vector& basic_list, - std::vector& nonbasic_list, - std::vector& nonbasic_index, - std::vector& vstatus, - i_t entering_index, - i_t nonbasic_entering, - i_t direction, - std::vector& delta_x, - const sparse_vector_t& utilde_sparse, - simplex::lp_solution_t& solution, - simplex::basis_update_mpf_t& basis_update, - f_t& work_estimate); + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + i_t entering_index, + i_t nonbasic_entering, + i_t direction, + std::vector& delta_x, + const sparse_vector_t& utilde_sparse, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate); void dual_degenerate_feasibility_pump(const simplex::lp_problem_t& lp, std::vector& basic_list, diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index 35ef45c0ab..8b9688e021 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -35,16 +35,16 @@ i_t bound_flipping_ratio_test_t::compute_breakpoints(std::vector& const i_t k = nonbasic_mark_[j]; if (vstatus_[j] == variable_status_t::NONBASIC_FIXED) { continue; } if (vstatus_[j] == variable_status_t::NONBASIC_LOWER && delta_z_[j] < -pivot_tol) { - indicies[idx] = k; - ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); - harris_ratios[idx] = std::max((-dual_tol - z_[j]) / delta_z_[j], 0.0); + indicies[idx] = k; + ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); + harris_ratios[idx] = std::max((-dual_tol - z_[j]) / delta_z_[j], 0.0); if constexpr (verbose) { settings_.log.printf("ratios[%d] = %e\n", idx, ratios[idx]); } idx++; } if (vstatus_[j] == variable_status_t::NONBASIC_UPPER && delta_z_[j] > pivot_tol) { - indicies[idx] = k; - ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); - harris_ratios[idx] = std::max((dual_tol - z_[j]) / delta_z_[j], 0.0); + indicies[idx] = k; + ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); + harris_ratios[idx] = std::max((dual_tol - z_[j]) / delta_z_[j], 0.0); if constexpr (verbose) { settings_.log.printf("ratios[%d] = %e\n", idx, ratios[idx]); } idx++; } @@ -71,9 +71,9 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, i_t candidate = -1; f_t zero_tol = settings_.zero_tol; i_t k_idx = -1; - max_val = 0.0; + max_val = 0.0; - i_t min_found = 0; + i_t min_found = 0; for (i_t k = start; k < end; ++k) { if (ratios[k] < min_val) { min_val = ratios[k]; @@ -81,9 +81,7 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, k_idx = k; min_found++; } - if (ratios[k] > max_val) { - max_val = ratios[k]; - } + if (ratios[k] > max_val) { max_val = ratios[k]; } } work_estimate_ += (end - start) + 2 * min_found; @@ -140,13 +138,13 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, std::vector ratios(nz); std::vector harris_ratios(nz); work_estimate_ += 3 * nz; - double t0 = tic(); + double t0 = tic(); i_t num_breakpoints = compute_breakpoints(indicies, ratios, harris_ratios); time_compute_breakpoints_ += toc(t0); num_breakpoints_ = num_breakpoints; // Count zero ratios num_harris_zero_ = 0; - num_exact_zero_ = 0; + num_exact_zero_ = 0; for (i_t k = 0; k < num_breakpoints; k++) { if (harris_ratios[k] == 0.0) num_harris_zero_++; if (ratios[k] == 0.0) num_exact_zero_++; @@ -163,9 +161,15 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, i_t entering_index = RATIO_TEST_NO_ENTERING_VARIABLE; f_t max_step_length; - t0 = tic(); - i_t k_idx = single_pass( - 0, num_breakpoints, indicies, harris_ratios, step_length, nonbasic_entering, entering_index, max_step_length); + t0 = tic(); + i_t k_idx = single_pass(0, + num_breakpoints, + indicies, + harris_ratios, + step_length, + nonbasic_entering, + entering_index, + max_step_length); time_single_pass_ += toc(t0); if (k_idx == RATIO_TEST_NUMERICAL_ISSUES) { return RATIO_TEST_NUMERICAL_ISSUES; } // The variable selected by single_pass is guaranteed to be in the first bucket: it @@ -196,7 +200,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, slope); } - // This code is complicated. There are several important concepts that are needed to understand it. + // This code is complicated. There are several important concepts that are needed to understand + // it. // // We are trying to compute the maximum step length we can take while: // 1) Staying mostly dual feasible (we allow ourselves to be infeasible by dual_tol amount) @@ -210,64 +215,72 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // z_j(alpha) >= 0, if j is on it's lower bound, or // z_j(alpha) <= 0, if j is on it's upper bound. // - // Consider the equation z_j(alpha) = z_j + alpha * delta_z_j = 0. Each variable j puts a bound on alpha: + // Consider the equation z_j(alpha) = z_j + alpha * delta_z_j = 0. Each variable j puts a bound on + // alpha: // // alpha_j <= -z_j / delta_z_j, if x_j = l_j (z_j >= 0) and delta_z_j < 0 // alpha_j <= -z_j / delta_z_j, if x_j = u_j (z_j <= 0) and delta_z_j > 0 // // The code refers to these alpha_j as ratios, since they are the ratio of z_j to delta_z_j. // - // Now we could take alpha = min_j alpha_j, and remain dual feasible. However, we are allowed to increase the step-length - // if j is a variable such that l_j <= x_j <= u_j. To see why imagine that our variable was currenlty on it's lower bound, - // with z_j > 0 and delta_z_j < 0, if we push alpha past alpha_j, than z_j(alpha) < 0. This is fine as long as we flip - // the variable to be on it's upper bound. Thus, we can push alpha past alpha_j for *bounded* variables. + // Now we could take alpha = min_j alpha_j, and remain dual feasible. However, we are allowed to + // increase the step-length if j is a variable such that l_j <= x_j <= u_j. To see why imagine + // that our variable was currenlty on it's lower bound, with z_j > 0 and delta_z_j < 0, if we push + // alpha past alpha_j, than z_j(alpha) < 0. This is fine as long as we flip the variable to be on + // it's upper bound. Thus, we can push alpha past alpha_j for *bounded* variables. // - // Note that this does not work if we try to increase alpha past alpha_j for a variable with a single bound. We would just - // be making ourselves dual infeasible. So we need to check whether a variable is bounded. + // Note that this does not work if we try to increase alpha past alpha_j for a variable with a + // single bound. We would just be making ourselves dual infeasible. So we need to check whether a + // variable is bounded. // - // The dual objective as a function of the step-length alpha, is piecewise linear and concave. The breakpoints of this - // piecewise linear function occur at each of the alpha_j values. - // We can keep increasing the step-length as long as the slope remains nonnegative. After that - // we must stop, because we could decrease the dual objective. So the code tracks the cumulative slope of the dual objective. + // The dual objective as a function of the step-length alpha, is piecewise linear and concave. The + // breakpoints of this piecewise linear function occur at each of the alpha_j values. We can keep + // increasing the step-length as long as the slope remains nonnegative. After that we must stop, + // because we could decrease the dual objective. So the code tracks the cumulative slope of the + // dual objective. // - // Now we don't need to exactly feasible: z_j >= 0 if x_j = l_j and z_j <= 0 if x_j = u_j. We can violate these bounds by - // the dual feasibility tolerance eps. We allow ourselves to be infeasible if it would help us get a larger pivot - // (delta_z_j). Small pivots can cause numerical issues, so we would like to avoid them. + // Now we don't need to exactly feasible: z_j >= 0 if x_j = l_j and z_j <= 0 if x_j = u_j. We can + // violate these bounds by the dual feasibility tolerance eps. We allow ourselves to be infeasible + // if it would help us get a larger pivot (delta_z_j). Small pivots can cause numerical issues, so + // we would like to avoid them. // // With this tolerance we get the equations: // z_j(alpha) = z_j + alpha * delta_z_j >= -eps if x_j = l_j // z_j(alpha) = z_j + alpha * delta_z_j <= eps if x_j = u_j // - // This gives bounds on alpha. We call these alpha_harris_j, for Paula Harris, who proposed this method. + // This gives bounds on alpha. We call these alpha_harris_j, for Paula Harris, who proposed this + // method. // // alpha_harris_j <= (-eps - z_j) / delta_z_j, if x_j = l_j and delta_z_j < 0 // alpha_harris_j <= (eps - z_j) / delta_z_j, if x_j = u_j and delta_z_j > 0 // - // Let alpha_harris = min_j alpha_harris_j. We can select the variable with the largest | delta_z_j | from those - // candidates { j | alpha_j <= alpha_harris }. + // Let alpha_harris = min_j alpha_harris_j. We can select the variable with the largest | + // delta_z_j | from those candidates { j | alpha_j <= alpha_harris }. // - // We combine these two ideas (increasing the step length for bounded variables) and allowing ourselves to be slightly dual infeasible - // to choose a larger pivot. + // We combine these two ideas (increasing the step length for bounded variables) and allowing + // ourselves to be slightly dual infeasible to choose a larger pivot. // - // We partition the variables into buckets. Let B_k be the set of variables in bucket k. B_0 is defined as { j | alpha_j <= alpha_harris }. - // We then compute alpha_harris_1 = min_{j not in B_0} alpha_j. And B_1 is defined as { j not in B_0 | alpha_j <= alpha_harris_1 }. And - // so on. + // We partition the variables into buckets. Let B_k be the set of variables in bucket k. B_0 is + // defined as { j | alpha_j <= alpha_harris }. We then compute alpha_harris_1 = min_{j not in B_0} + // alpha_j. And B_1 is defined as { j not in B_0 | alpha_j <= alpha_harris_1 }. And so on. // // We want to balance two different things: // 1) Taking a larger step length to increase the dual objective as much as possible, // 2) Choosing a large pivot for numerical stability. // - // Let max_pivot = max_j | delta_z_j |. We start working our way backward from the largest bucket to the smallest bucket, - // we choose a variable j that satisfies | delta_z_j | >= 0.1 * max_pivot. Since we can always choose a smaller step length - // for the sake of numerical stability. + // Let max_pivot = max_j | delta_z_j |. We start working our way backward from the largest bucket + // to the smallest bucket, we choose a variable j that satisfies | delta_z_j | >= 0.1 * max_pivot. + // Since we can always choose a smaller step length for the sake of numerical stability. // - // Now the final thing to understand is that the ratios alpha_j are not sorted in any particular order. And we don't want to - // pay the O(num_breakpoints * log(num_breakpoints)) cost of sorting them. + // Now the final thing to understand is that the ratios alpha_j are not sorted in any particular + // order. And we don't want to pay the O(num_breakpoints * log(num_breakpoints)) cost of sorting + // them. // - // So we set a threshold on the step-length and check if all variables j with alpha_j <= threshold have already caused the - // slope to go negative. If so, we just need to consider those candidate variables with alpha_j <= threshold. If not, we - // multiply the threshold by 10. This cost us O(log10(max_step_length/min_step_length) * num_breakpoints) time. So we aren't - // totally linear. But the hope is we are better than a sort. + // So we set a threshold on the step-length and check if all variables j with alpha_j <= threshold + // have already caused the slope to go negative. If so, we just need to consider those candidate + // variables with alpha_j <= threshold. If not, we multiply the threshold by 10. This cost us + // O(log10(max_step_length/min_step_length) * num_breakpoints) time. So we aren't totally linear. + // But the hope is we are better than a sort. // Use a coarse filter to find candidates f_t minimum_harris_ratio = step_length; @@ -279,8 +292,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, std::vector candidates(num_breakpoints); std::iota(candidates.begin(), candidates.end(), 0); work_estimate_ += 2 * num_breakpoints; - i_t scan_start = 0; - i_t num_candidates = 0; + i_t scan_start = 0; + i_t num_candidates = 0; // This is O( log10(max_step_length/min_step_length) * num_breakpoints) t0 = tic(); @@ -292,7 +305,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // Candidate is less than coarse threshold, move it to the front of the candidate list std::swap(candidates[h], candidates[num_candidates]); num_candidates++; - const i_t j = nonbasic_list_[indicies[k]]; + const i_t j = nonbasic_list_[indicies[k]]; if (!bounded_variables_[j]) { found_unbounded = true; } else { @@ -313,16 +326,14 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, for (i_t h = 0; h < num_candidates; h++) { const i_t k = candidates[h]; const i_t j = nonbasic_list_[indicies[k]]; - if (!bounded_variables_[j]) { - max_step_length = std::min(max_step_length, harris_ratios[k]); - } + if (!bounded_variables_[j]) { max_step_length = std::min(max_step_length, harris_ratios[k]); } } work_estimate_ += 5 * num_candidates; // Remove candidates that are greater than the maximum step length const i_t candidates_before_removal = candidates.size(); for (i_t h = candidates_before_removal - 1; h >= 0; h--) { - const i_t k = candidates[h]; + const i_t k = candidates[h]; const f_t ratio = ratios[k]; if (ratio > max_step_length) { // Swap with the last candidate and remove @@ -330,7 +341,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, candidates.pop_back(); } } - work_estimate_ += 2 * candidates_before_removal + 2 * (candidates_before_removal - candidates.size()); + work_estimate_ += + 2 * candidates_before_removal + 2 * (candidates_before_removal - candidates.size()); num_candidates = candidates.size(); } @@ -341,12 +353,12 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, i_t num_buckets = 0; std::vector bucket_start(num_candidates + 1, 0); f_t cumulative_slope = slope; - scan_start = 0; + scan_start = 0; work_estimate_ += num_candidates + 1; // This is O(num_buckets * num_candidates) i_t slope_breaker_k = -1; // the candidate k that made slope go negative - t0 = tic(); + t0 = tic(); while (cumulative_slope >= 0.0 && scan_start < num_candidates && threshold <= max_step_length) { f_t next_threshold = inf; i_t write = scan_start; @@ -359,9 +371,7 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, const i_t j = nonbasic_list_[indicies[k]]; if (bounded_variables_[j]) { cumulative_slope -= std::abs(delta_z_[j]) * (upper_[j] - lower_[j]); - if (cumulative_slope < 0.0 && slope_breaker_k < 0) { - slope_breaker_k = k; - } + if (cumulative_slope < 0.0 && slope_breaker_k < 0) { slope_breaker_k = k; } } std::swap(candidates[h], candidates[write]); write++; @@ -375,8 +385,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, bucket_start[++num_buckets] = write; if (write == scan_start) break; // No progress — prevent infinite loop - scan_start = write; - threshold = next_threshold; + scan_start = write; + threshold = next_threshold; if (cumulative_slope < 0.0) break; } @@ -387,12 +397,10 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // This is O(num_candidates) f_t max_pivot = 0.0; for (i_t h = 0; h < bucket_start[num_buckets]; h++) { - const i_t k = candidates[h]; - const i_t j = nonbasic_list_[indicies[k]]; + const i_t k = candidates[h]; + const i_t j = nonbasic_list_[indicies[k]]; const f_t pivot = std::abs(delta_z_[j]); - if (pivot > max_pivot) { - max_pivot = pivot; - } + if (pivot > max_pivot) { max_pivot = pivot; } } work_estimate_ += 4 * bucket_start[num_buckets]; @@ -405,8 +413,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // This is O(num_candidates) for (i_t b = num_buckets - 1; b >= 0; b--) { const i_t b_start = bucket_start[b]; - const i_t b_end = bucket_start[b + 1]; - f_t best_ratio = -1.0; + const i_t b_end = bucket_start[b + 1]; + f_t best_ratio = -1.0; for (i_t h = b_start; h < b_end; h++) { const i_t k = candidates[h]; const i_t j = nonbasic_list_[indicies[k]]; @@ -424,9 +432,9 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, num_buckets_used_ = num_buckets; if (entering_k < 0) { // Fallback to single_pass result - used_fallback_ = true; - bucket_selected_ = -1; - step_length_result_ = step_length; + used_fallback_ = true; + bucket_selected_ = -1; + step_length_result_ = step_length; selected_is_slope_breaker_ = false; determine_flips(step_length, entering_index, flip_indices); return entering_index; @@ -440,13 +448,16 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // Record which bucket was selected used_fallback_ = false; - i_t pos = -1; + i_t pos = -1; for (i_t b = 0; b < num_buckets; b++) { if (entering_k >= 0) { // Find which bucket entering_k is in based on its position in candidates pos = -1; for (i_t h = 0; h < num_candidates; h++) { - if (candidates[h] == entering_k) { pos = h; break; } + if (candidates[h] == entering_k) { + pos = h; + break; + } } if (pos >= bucket_start[b] && pos < bucket_start[b + 1]) { bucket_selected_ = b; @@ -459,10 +470,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, determine_flips(step_length, entering_index, flip_indices); return entering_index; - } - #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE template class bound_flipping_ratio_test_t; diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp index 0d78e68ee0..4587037889 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp @@ -53,9 +53,7 @@ class bound_flipping_ratio_test_t { { } - i_t compute_step_length(f_t& step_length, - i_t& nonbasic_entering, - std::vector& flip_indices); + i_t compute_step_length(f_t& step_length, i_t& nonbasic_entering, std::vector& flip_indices); f_t work_estimate() const { return work_estimate_; } // Timing fields (filled by compute_step_length) @@ -66,18 +64,22 @@ class bound_flipping_ratio_test_t { f_t time_pivot_selection_{0.0}; // Diagnostic fields - i_t num_buckets_used_{0}; // number of buckets in bucket sort - i_t bucket_selected_{-1}; // which bucket the entering variable came from (-1 = single_pass/fallback) - f_t step_length_result_{0.0}; // the step length chosen - bool used_fallback_{false}; // true if we fell back to single_pass result - i_t bucket0_size_{0}; // size of first bucket (candidates with ratio <= min_harris) - i_t num_breakpoints_{0}; // total breakpoints computed - bool selected_is_slope_breaker_{false}; // true if we selected the variable that made slope go negative - i_t num_harris_zero_{0}; // number of harris_ratios that are exactly 0 - i_t num_exact_zero_{0}; // number of exact ratios that are exactly 0 + i_t num_buckets_used_{0}; // number of buckets in bucket sort + i_t bucket_selected_{ + -1}; // which bucket the entering variable came from (-1 = single_pass/fallback) + f_t step_length_result_{0.0}; // the step length chosen + bool used_fallback_{false}; // true if we fell back to single_pass result + i_t bucket0_size_{0}; // size of first bucket (candidates with ratio <= min_harris) + i_t num_breakpoints_{0}; // total breakpoints computed + bool selected_is_slope_breaker_{ + false}; // true if we selected the variable that made slope go negative + i_t num_harris_zero_{0}; // number of harris_ratios that are exactly 0 + i_t num_exact_zero_{0}; // number of exact ratios that are exactly 0 private: - i_t compute_breakpoints(std::vector& indices, std::vector& ratios, std::vector& harris_ratios); + i_t compute_breakpoints(std::vector& indices, + std::vector& ratios, + std::vector& harris_ratios); i_t single_pass(i_t start, i_t end, const std::vector& indices, @@ -86,9 +88,7 @@ class bound_flipping_ratio_test_t { i_t& nonbasic_entering, i_t& entering_index, f_t& max_val); - void determine_flips(f_t step_length, - i_t entering_index, - std::vector& flip_indices); + void determine_flips(f_t step_length, i_t entering_index, std::vector& flip_indices); const std::vector& lower_; const std::vector& upper_; const std::vector& bounded_variables_; diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index f7e5d6c7f3..9eb3224817 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -465,20 +465,14 @@ void initial_perturbation(const lp_problem_t& lp, } // Dampen large costs - if (max_abs_obj_coeff > 100.0) { - max_abs_obj_coeff = std::sqrt(std::sqrt(max_abs_obj_coeff)); - } + if (max_abs_obj_coeff > 100.0) { max_abs_obj_coeff = std::sqrt(std::sqrt(max_abs_obj_coeff)); } // Ensure a minimum perturbation even for tiny-cost problems - if (max_abs_obj_coeff < 1.0) { - max_abs_obj_coeff = 1.0; - } + if (max_abs_obj_coeff < 1.0) { max_abs_obj_coeff = 1.0; } // If few boxed variables, cap max_abs_obj_coeff at 1.0 i_t num_boxed = 0; for (i_t j = 0; j < n; ++j) { - if (lp.lower[j] > -inf && lp.upper[j] < inf && lp.lower[j] != lp.upper[j]) { - num_boxed++; - } + if (lp.lower[j] > -inf && lp.upper[j] < inf && lp.lower[j] != lp.upper[j]) { num_boxed++; } } if (static_cast(num_boxed) / n < 0.01) { max_abs_obj_coeff = std::min(max_abs_obj_coeff, f_t(1.0)); @@ -490,8 +484,13 @@ void initial_perturbation(const lp_problem_t& lp, // that case. The original costs are restored before declaring optimality. const f_t perturbation_base = (strongly_degenerate ? 1e-5 : 5e-7) * max_abs_obj_coeff; - settings.log.printf("Perturbation debug: max_abs_obj_coeff=%e (dampened), perturbation_base=%e, n=%d, num_boxed=%d\n", - max_abs_obj_coeff, perturbation_base, n, num_boxed); + settings.log.printf( + "Perturbation debug: max_abs_obj_coeff=%e (dampened), perturbation_base=%e, n=%d, " + "num_boxed=%d\n", + max_abs_obj_coeff, + perturbation_base, + n, + num_boxed); objective.resize(n); f_t sum_perturb = 0.0; @@ -504,19 +503,17 @@ void initial_perturbation(const lp_problem_t& lp, const f_t lower = lp.lower[j]; const f_t upper = lp.upper[j]; // Skip truly fixed variables and free variables - if (lower == upper || (lower == -inf && upper == inf)) { - continue; - } + if (lower == upper || (lower == -inf && upper == inf)) { continue; } - const f_t rand_val = random.random(); + const f_t rand_val = random.random(); const f_t cost_factor = std::min(std::abs(obj) + 1.0, max_abs_obj_coeff + 1.0); - const f_t perturb = (1.0 + rand_val) * cost_factor * perturbation_base; + const f_t perturb = (1.0 + rand_val) * cost_factor * perturbation_base; if (vstatus[j] == variable_status_t::BASIC) { // Skip basic variables continue; } else if (vstatus[j] == variable_status_t::NONBASIC_LOWER || - vstatus[j] == variable_status_t::NONBASIC_FIXED) { + vstatus[j] == variable_status_t::NONBASIC_FIXED) { // NONBASIC_FIXED from phase 1 for boxed variables — treat as at lower bound objective[j] = obj + perturb; sum_perturb += perturb; @@ -1236,14 +1233,14 @@ i_t phase2_ratio_test(const lp_problem_t& lp, template i_t flip_bounds(const lp_problem_t& lp, - const std::vector& bounded_variables, - const std::vector& flip_indices, - std::vector& vstatus, + const std::vector& bounded_variables, + const std::vector& flip_indices, + std::vector& vstatus, std::vector& delta_x, std::vector& mark, - std::vector& atilde, - std::vector& atilde_index, - f_t& work_estimate) + std::vector& atilde, + std::vector& atilde_index, + f_t& work_estimate) { i_t num_flipped = 0; for (const i_t j : flip_indices) { @@ -1571,8 +1568,8 @@ void remove_leaving_perturbation(const lp_problem_t& lp, const f_t perturb = objective[leaving_index] - lp.objective[leaving_index]; if (perturb == 0.0) return; - const f_t lower = lp.lower[leaving_index]; - const f_t upper = lp.upper[leaving_index]; + const f_t lower = lp.lower[leaving_index]; + const f_t upper = lp.upper[leaving_index]; const bool boxed = (lower > -inf && upper < inf); if (boxed) { @@ -1582,7 +1579,7 @@ void remove_leaving_perturbation(const lp_problem_t& lp, const f_t new_z = z[leaving_index] - perturb; if (direction == 1 && new_z < -settings.tight_tol) { return; } if (direction == -1 && new_z > settings.tight_tol) { return; } - z[leaving_index] = new_z; + z[leaving_index] = new_z; objective[leaving_index] = lp.objective[leaving_index]; } else { z[leaving_index] -= perturb; @@ -1592,12 +1589,12 @@ void remove_leaving_perturbation(const lp_problem_t& lp, if (upper == inf && lower > -inf && z[leaving_index] < -settings.tight_tol) { // At lower bound, needs z >= 0 const f_t correction = -z[leaving_index]; - z[leaving_index] = 0.0; + z[leaving_index] = 0.0; objective[leaving_index] += correction; } else if (lower == -inf && upper < inf && z[leaving_index] > settings.tight_tol) { // At upper bound, needs z <= 0 const f_t correction = z[leaving_index]; - z[leaving_index] = 0.0; + z[leaving_index] = 0.0; objective[leaving_index] -= correction; } } @@ -2271,20 +2268,20 @@ void bound_info(const lp_problem_t& lp, template i_t set_primal_variables_on_bounds(const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - const std::vector& z, - std::vector& vstatus, - std::vector& x, - i_t degen_type = 0) + const simplex_solver_settings_t& settings, + const std::vector& z, + std::vector& vstatus, + std::vector& x, + i_t degen_type = 0) { PHASE2_NVTX_RANGE("DualSimplex::set_primal_variables_on_bounds"); - const i_t n = lp.num_cols; - f_t tol = 1e-10; + const i_t n = lp.num_cols; + f_t tol = 1e-10; i_t num_fixed_to_lower = 0; i_t num_fixed_to_upper = 0; i_t num_lower_to_upper = 0; i_t num_upper_to_lower = 0; - i_t num_set_fixed = 0; + i_t num_set_fixed = 0; for (i_t j = 0; j < n; ++j) { // We set z_j = 0 for basic variables // But we explicitally skip setting basic variables here @@ -2315,8 +2312,8 @@ i_t set_primal_variables_on_bounds(const lp_problem_t& lp, if (degen_type == 1) { // Column-sum heuristic const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; - f_t col_sum = 0.0; + const i_t col_end = lp.A.col_start[j + 1]; + f_t col_sum = 0.0; for (i_t k = col_start; k < col_end; k++) { col_sum += lp.A.x[k]; } @@ -2347,8 +2344,8 @@ i_t set_primal_variables_on_bounds(const lp_problem_t& lp, if (degen_type == 1) { // Column-sum heuristic const i_t col_start = lp.A.col_start[j]; - const i_t col_end = lp.A.col_start[j + 1]; - f_t col_sum = 0.0; + const i_t col_end = lp.A.col_start[j + 1]; + f_t col_sum = 0.0; for (i_t k = col_start; k < col_end; k++) { col_sum += lp.A.x[k]; } @@ -2408,17 +2405,34 @@ i_t set_primal_variables_on_bounds(const lp_problem_t& lp, } // Track changes if (old_vstatus != vstatus[j]) { - if (old_vstatus == variable_status_t::NONBASIC_FIXED && vstatus[j] == variable_status_t::NONBASIC_LOWER) num_fixed_to_lower++; - else if (old_vstatus == variable_status_t::NONBASIC_FIXED && vstatus[j] == variable_status_t::NONBASIC_UPPER) num_fixed_to_upper++; - else if (old_vstatus == variable_status_t::NONBASIC_LOWER && vstatus[j] == variable_status_t::NONBASIC_UPPER) num_lower_to_upper++; - else if (old_vstatus == variable_status_t::NONBASIC_UPPER && vstatus[j] == variable_status_t::NONBASIC_LOWER) num_upper_to_lower++; - else if (vstatus[j] == variable_status_t::NONBASIC_FIXED) num_set_fixed++; + if (old_vstatus == variable_status_t::NONBASIC_FIXED && + vstatus[j] == variable_status_t::NONBASIC_LOWER) + num_fixed_to_lower++; + else if (old_vstatus == variable_status_t::NONBASIC_FIXED && + vstatus[j] == variable_status_t::NONBASIC_UPPER) + num_fixed_to_upper++; + else if (old_vstatus == variable_status_t::NONBASIC_LOWER && + vstatus[j] == variable_status_t::NONBASIC_UPPER) + num_lower_to_upper++; + else if (old_vstatus == variable_status_t::NONBASIC_UPPER && + vstatus[j] == variable_status_t::NONBASIC_LOWER) + num_upper_to_lower++; + else if (vstatus[j] == variable_status_t::NONBASIC_FIXED) + num_set_fixed++; } } - i_t total_changes = num_fixed_to_lower + num_fixed_to_upper + num_lower_to_upper + num_upper_to_lower + num_set_fixed; + i_t total_changes = num_fixed_to_lower + num_fixed_to_upper + num_lower_to_upper + + num_upper_to_lower + num_set_fixed; if (total_changes > 0) { - settings.log.printf("set_primal_variables_on_bounds: %d changes (fixed->lower=%d, fixed->upper=%d, lower->upper=%d, upper->lower=%d, ->fixed=%d)\n", - total_changes, num_fixed_to_lower, num_fixed_to_upper, num_lower_to_upper, num_upper_to_lower, num_set_fixed); + settings.log.printf( + "set_primal_variables_on_bounds: %d changes (fixed->lower=%d, fixed->upper=%d, " + "lower->upper=%d, upper->lower=%d, ->fixed=%d)\n", + total_changes, + num_fixed_to_lower, + num_fixed_to_upper, + num_lower_to_upper, + num_upper_to_lower, + num_set_fixed); } return total_changes; } @@ -2472,8 +2486,8 @@ i_t attempt_to_remove_perturbations(const lp_problem_t& lp, f_t& primal_infeasibility_squared, f_t& work_estimate) { - const i_t m = lp.num_rows; - const i_t n = lp.num_cols; + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; const i_t n_minus_m = n - m; // Check if there's any perturbation @@ -2481,7 +2495,7 @@ i_t attempt_to_remove_perturbations(const lp_problem_t& lp, if (perturbation <= 1e-6) return 0; // OPTIMAL // Count perturbations on basic vs nonbasic variables - i_t num_basic_perturbed = 0; + i_t num_basic_perturbed = 0; i_t num_nonbasic_boxed_perturbed = 0; i_t num_nonbasic_other_perturbed = 0; for (i_t k = 0; k < m; ++k) { @@ -2506,55 +2520,60 @@ i_t attempt_to_remove_perturbations(const lp_problem_t& lp, // y is unaffected; z[j] - perturb gives exact unperturbed reduced cost. i_t num_flipped = 0; for (i_t k = 0; k < n_minus_m; ++k) { - const i_t j = nonbasic_list[k]; + const i_t j = nonbasic_list[k]; const f_t perturb = objective[j] - lp.objective[j]; if (perturb == 0.0) continue; const f_t new_z = z[j] - perturb; if (vstatus[j] == variable_status_t::NONBASIC_LOWER && new_z < -settings.dual_tol) { - vstatus[j] = variable_status_t::NONBASIC_UPPER; - z[j] = new_z; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + z[j] = new_z; objective[j] = lp.objective[j]; num_flipped++; } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER && new_z > settings.dual_tol) { - vstatus[j] = variable_status_t::NONBASIC_LOWER; - z[j] = new_z; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + z[j] = new_z; objective[j] = lp.objective[j]; num_flipped++; } else { - z[j] = new_z; + z[j] = new_z; objective[j] = lp.objective[j]; } } work_estimate += 5 * n_minus_m; // Recompute x_B with flipped statuses - compute_primal_solution_from_basis(lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); + compute_primal_solution_from_basis( + lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); work_estimate += 2 * n; - primal_infeasibility_squared = - compute_initial_primal_infeasibilities(lp, settings, basic_list, x, - squared_infeasibilities, infeasibility_indices, - primal_infeasibility); + primal_infeasibility_squared = compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); work_estimate += 4 * m + 2 * n; if (primal_infeasibility <= settings.primal_tol) return 0; // OPTIMAL - settings.log.printf("Removed perturbation. Continuing dual simplex (primal_inf=%.2e)\n", primal_infeasibility); + settings.log.printf("Removed perturbation. Continuing dual simplex (primal_inf=%.2e)\n", + primal_infeasibility); return 1; // CONTINUE_DUAL } // Perturbation on basic (or one-sided nonbasic) variables: need to recompute (y, z). std::vector unperturbed_y(m); std::vector unperturbed_z(n); - compute_dual_solution_from_basis(lp, ft, basic_list, nonbasic_list, - unperturbed_y, unperturbed_z, work_estimate); + compute_dual_solution_from_basis( + lp, ft, basic_list, nonbasic_list, unperturbed_y, unperturbed_z, work_estimate); // Check if removal is clean (no dual infeasibility) - const f_t dual_infeas = dual_infeasibility(lp, settings, vstatus, unperturbed_z, - settings.tight_tol, settings.dual_tol); + const f_t dual_infeas = + dual_infeasibility(lp, settings, vstatus, unperturbed_z, settings.tight_tol, settings.dual_tol); work_estimate += 3 * n; if (dual_infeas <= settings.dual_tol) { settings.log.printf("Removed perturbation of %.2e.\n", perturbation); - z = unperturbed_z; - y = unperturbed_y; + z = unperturbed_z; + y = unperturbed_y; objective = lp.objective; work_estimate += 3 * n + 2 * m; return 0; // OPTIMAL @@ -2562,13 +2581,13 @@ i_t attempt_to_remove_perturbations(const lp_problem_t& lp, // Flip boxed nonbasics that are dual infeasible, and check for one-sided infeasibility std::vector new_vstatus = vstatus; - i_t num_flipped = 0; - f_t residual_dual_infeas = 0.0; + i_t num_flipped = 0; + f_t residual_dual_infeas = 0.0; for (i_t k = 0; k < n_minus_m; ++k) { - const i_t j = nonbasic_list[k]; - const f_t zj = unperturbed_z[j]; - const f_t lower = lp.lower[j]; - const f_t upper = lp.upper[j]; + const i_t j = nonbasic_list[k]; + const f_t zj = unperturbed_z[j]; + const f_t lower = lp.lower[j]; + const f_t upper = lp.upper[j]; const bool boxed = (lower > -inf && upper < inf && lower != upper); if (new_vstatus[j] == variable_status_t::NONBASIC_LOWER && zj < -settings.dual_tol) { @@ -2592,28 +2611,35 @@ i_t attempt_to_remove_perturbations(const lp_problem_t& lp, if (residual_dual_infeas > settings.dual_tol) { // One-sided infeasibility remains — can't continue with dual simplex. // new_vstatus is discarded; vstatus unchanged. - settings.log.printf("Perturbation removal: %d flips, residual_dual_infeas=%.2e (PRIMAL_CLEANUP)\n", - num_flipped, residual_dual_infeas); + settings.log.printf( + "Perturbation removal: %d flips, residual_dual_infeas=%.2e (PRIMAL_CLEANUP)\n", + num_flipped, + residual_dual_infeas); return 2; // PRIMAL_CLEANUP } // All infeasibility was on boxed variables — accept unperturbed solution - vstatus = new_vstatus; - z = unperturbed_z; - y = unperturbed_y; + vstatus = new_vstatus; + z = unperturbed_z; + y = unperturbed_y; objective = lp.objective; work_estimate += 3 * n + 2 * m; // Recompute x_B with flipped statuses - compute_primal_solution_from_basis(lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); + compute_primal_solution_from_basis( + lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); work_estimate += 2 * n; - primal_infeasibility_squared = - compute_initial_primal_infeasibilities(lp, settings, basic_list, x, - squared_infeasibilities, infeasibility_indices, - primal_infeasibility); + primal_infeasibility_squared = compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); work_estimate += 4 * m + 2 * n; - settings.log.printf("Perturbation removal: %d flips, primal_inf=%.2e\n", num_flipped, primal_infeasibility); + settings.log.printf( + "Perturbation removal: %d flips, primal_inf=%.2e\n", num_flipped, primal_infeasibility); if (primal_infeasibility <= settings.primal_tol) return 0; // OPTIMAL settings.log.printf("Continuing dual after flip (primal_inf=%.2e)\n", primal_infeasibility); return 1; // CONTINUE_DUAL @@ -2644,22 +2670,18 @@ void prepare_optimality(i_t info, const i_t m = lp.num_rows; const i_t n = lp.num_cols; - sol.objective = compute_objective(lp, sol.x); - sol.user_objective = compute_user_objective(lp, sol.objective); + sol.objective = compute_objective(lp, sol.x); + sol.user_objective = compute_user_objective(lp, sol.objective); const f_t perturbation = amount_of_perturbation(lp, objective); - sol.l2_primal_residual = l2_primal_residual(lp, sol); - sol.l2_dual_residual = l2_dual_residual(lp, sol); - const f_t dual_infeas = dual_infeasibility(lp, settings, vstatus, z, 0.0, 0.0); + sol.l2_primal_residual = l2_primal_residual(lp, sol); + sol.l2_dual_residual = l2_dual_residual(lp, sol); + const f_t dual_infeas = dual_infeasibility(lp, settings, vstatus, z, 0.0, 0.0); // Compute max primal infeasibility for reporting f_t primal_infeas = 0.0; for (i_t j = 0; j < n; ++j) { - if (x[j] < lp.lower[j]) { - primal_infeas = std::max(primal_infeas, lp.lower[j] - x[j]); - } - if (x[j] > lp.upper[j]) { - primal_infeas = std::max(primal_infeas, x[j] - lp.upper[j]); - } + if (x[j] < lp.lower[j]) { primal_infeas = std::max(primal_infeas, lp.lower[j] - x[j]); } + if (x[j] > lp.upper[j]) { primal_infeas = std::max(primal_infeas, x[j] - lp.upper[j]); } } if (phase == 1 && iter > 0) { settings.log.printf("Dual phase I complete. Iterations %d. Time %.2f\n", iter, toc(start_time)); @@ -2834,18 +2856,18 @@ class phase2_timers_t { f_t bfrt_select_time{0.0}; // BFRT diagnostic counters i_t bfrt_calls{0}; - i_t bfrt_zero_steps{0}; // step_length == 0 - i_t bfrt_single_pass_only{0}; // no bound flips (single_pass decided) - i_t bfrt_bucket_used{0}; // bucket sort was used - i_t bfrt_not_last_bucket{0}; // selected from a bucket other than the last - i_t bfrt_fallback{0}; // fell back to single_pass result after bucket sort - i_t bfrt_zero_step_num_buckets_sum{0}; // sum of num_buckets on zero-step iters - i_t bfrt_zero_step_bucket0_sum{0}; // sum of bucket0 size on zero-step iters - i_t bfrt_zero_step_num_breakpoints_sum{0}; // sum of num_breakpoints on zero-step iters - i_t bfrt_zero_step_harris_zero_sum{0}; // sum of harris_ratios==0 on zero-step iters - i_t bfrt_zero_step_exact_zero_sum{0}; // sum of exact ratios==0 on zero-step iters - i_t bfrt_selected_slope_breaker{0}; // times we selected the slope breaker - i_t bfrt_not_slope_breaker{0}; // times we selected something else + i_t bfrt_zero_steps{0}; // step_length == 0 + i_t bfrt_single_pass_only{0}; // no bound flips (single_pass decided) + i_t bfrt_bucket_used{0}; // bucket sort was used + i_t bfrt_not_last_bucket{0}; // selected from a bucket other than the last + i_t bfrt_fallback{0}; // fell back to single_pass result after bucket sort + i_t bfrt_zero_step_num_buckets_sum{0}; // sum of num_buckets on zero-step iters + i_t bfrt_zero_step_bucket0_sum{0}; // sum of bucket0 size on zero-step iters + i_t bfrt_zero_step_num_breakpoints_sum{0}; // sum of num_breakpoints on zero-step iters + i_t bfrt_zero_step_harris_zero_sum{0}; // sum of harris_ratios==0 on zero-step iters + i_t bfrt_zero_step_exact_zero_sum{0}; // sum of exact ratios==0 on zero-step iters + i_t bfrt_selected_slope_breaker{0}; // times we selected the slope breaker + i_t bfrt_not_slope_breaker{0}; // times we selected something else work_timer_t pricing_time; work_timer_t btran_time; work_timer_t ftran_time; @@ -3027,7 +3049,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } } } - settings.log.printf("NONBASIC_FIXED boxed: %d, degenerate (|z_j| < dual_tol): %d\n", num_fixed, num_degen); + settings.log.printf( + "NONBASIC_FIXED boxed: %d, degenerate (|z_j| < dual_tol): %d\n", num_fixed, num_degen); } // Try 3 strategies for degenerate bound assignment, pick best @@ -3037,23 +3060,33 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, std::vector best_vstatus; std::vector best_x; const char* degen_names[] = {"default", "column-sum", "abs-bound"}; - const i_t degen_types[] = {0, 1, 3}; + const i_t degen_types[] = {0, 1, 3}; f_t all_sum_infeas[3]; i_t all_num_infeas[3]; for (i_t di = 0; di < 3; di++) { - const i_t dt = degen_types[di]; + const i_t dt = degen_types[di]; std::vector try_vstatus = vstatus; - std::vector try_x = x; + std::vector try_x = x; phase2::set_primal_variables_on_bounds(lp, settings, z, try_vstatus, try_x, dt); - phase2::compute_primal_variables(ft, lp.rhs, lp.A, basic_list, nonbasic_list, - settings.tight_tol, try_x, xB_workspace, phase2_work_estimate); + phase2::compute_primal_variables(ft, + lp.rhs, + lp.A, + basic_list, + nonbasic_list, + settings.tight_tol, + try_x, + xB_workspace, + phase2_work_estimate); f_t sum_infeas = 0.0; i_t num_infeas = 0; for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; - f_t infeas = std::max(lp.lower[j] - try_x[j], try_x[j] - lp.upper[j]); - if (infeas > 0.0) { sum_infeas += infeas; num_infeas++; } + f_t infeas = std::max(lp.lower[j] - try_x[j], try_x[j] - lp.upper[j]); + if (infeas > 0.0) { + sum_infeas += infeas; + num_infeas++; + } } all_sum_infeas[di] = sum_infeas; all_num_infeas[di] = num_infeas; @@ -3062,16 +3095,16 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, best_sum_infeas = sum_infeas; best_num_infeas = num_infeas; best_degen_type = 0; - best_vstatus = try_vstatus; - best_x = try_x; + best_vstatus = try_vstatus; + best_x = try_x; } else { // Only pick alternative if BOTH fewer infeasibilities AND lower sum if (num_infeas <= best_num_infeas && sum_infeas < best_sum_infeas) { best_sum_infeas = sum_infeas; best_num_infeas = num_infeas; best_degen_type = di; - best_vstatus = try_vstatus; - best_x = try_x; + best_vstatus = try_vstatus; + best_x = try_x; } } if (phase == 1 || num_degen == 0) { @@ -3083,12 +3116,16 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } } vstatus = best_vstatus; - x = best_x; - settings.log.printf("Bound assignment: default(%d/%.2e) colsum(%d/%.2e) abs-bound(%d/%.2e) -> %s\n", - all_num_infeas[0], all_sum_infeas[0], - all_num_infeas[1], all_sum_infeas[1], - all_num_infeas[2], all_sum_infeas[2], - degen_names[best_degen_type]); + x = best_x; + settings.log.printf( + "Bound assignment: default(%d/%.2e) colsum(%d/%.2e) abs-bound(%d/%.2e) -> %s\n", + all_num_infeas[0], + all_sum_infeas[0], + all_num_infeas[1], + all_sum_infeas[1], + all_num_infeas[2], + all_sum_infeas[2], + degen_names[best_degen_type]); phase2_work_estimate += 15 * (n - m); // Near-optimality check: decide whether to apply initial perturbation @@ -3097,16 +3134,21 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, f_t max_primal_infeas = 0.0; for (i_t k = 0; k < m; ++k) { const i_t j = basic_list[k]; - f_t infeas = std::max(lp.lower[j] - x[j], x[j] - lp.upper[j]); + f_t infeas = std::max(lp.lower[j] - x[j], x[j] - lp.upper[j]); if (infeas > settings.primal_tol) { num_primal_infeas++; max_primal_infeas = std::max(max_primal_infeas, infeas); } } - bool near_optimal = (num_primal_infeas < 1000 && max_primal_infeas < 1e-3); + bool near_optimal = (num_primal_infeas < 1000 && max_primal_infeas < 1e-3); bool apply_perturbation = (settings.initial_perturbation == 1) || !near_optimal; - settings.log.printf("Near-optimal check: num_primal_infeas=%d, max_primal_infeas=%.2e, near_optimal=%d, apply_perturbation=%d\n", - num_primal_infeas, max_primal_infeas, near_optimal, apply_perturbation); + settings.log.printf( + "Near-optimal check: num_primal_infeas=%d, max_primal_infeas=%.2e, near_optimal=%d, " + "apply_perturbation=%d\n", + num_primal_infeas, + max_primal_infeas, + near_optimal, + apply_perturbation); if (apply_perturbation) { const bool strongly_degenerate = num_degen > n / 20; phase2::initial_perturbation(lp, settings, vstatus, strongly_degenerate, objective); @@ -3122,8 +3164,15 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, i_t num_bound_changes2 = phase2::set_primal_variables_on_bounds(lp, settings, z, vstatus, x); phase2_work_estimate += 5 * (n - m); if (num_bound_changes2 > 0) { - phase2::compute_primal_variables(ft, lp.rhs, lp.A, basic_list, nonbasic_list, - settings.tight_tol, x, xB_workspace, phase2_work_estimate); + phase2::compute_primal_variables(ft, + lp.rhs, + lp.A, + basic_list, + nonbasic_list, + settings.tight_tol, + x, + xB_workspace, + phase2_work_estimate); } } } @@ -3452,10 +3501,22 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, // Before declaring optimal, attempt to remove perturbation. if (phase == 2) { - i_t removal_status = phase2::attempt_to_remove_perturbations( - lp, settings, ft, basic_list, nonbasic_list, vstatus, objective, - z, y, x, xB_workspace, squared_infeasibilities, infeasibility_indices, - primal_infeasibility, primal_infeasibility_squared, phase2_work_estimate); + i_t removal_status = phase2::attempt_to_remove_perturbations(lp, + settings, + ft, + basic_list, + nonbasic_list, + vstatus, + objective, + z, + y, + x, + xB_workspace, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility, + primal_infeasibility_squared, + phase2_work_estimate); if (removal_status == 1) { // CONTINUE_DUAL obj = phase2::compute_perturbed_objective(objective, x); phase2_work_estimate += 2 * n; @@ -3466,19 +3527,19 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); settings.log.printf("Num updates: %d\n", ft.num_updates()); settings.log.printf("Iterations: %d\n", iter); - i_t dual_iter = iter; + i_t dual_iter = iter; primal_status_t primal_status = primal_phase2_with_advanced_basis(2, - start_time, - lp, - settings, - vstatus, - ft, - basic_list, - nonbasic_list, - sol, - iter, - phase2_work_estimate, - false); + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + phase2_work_estimate, + false); if (primal_status == primal_status_t::OPTIMAL) { settings.log.printf("Primal cleanup successful. Iterations %d\n", iter - dual_iter); objective = lp.objective; @@ -3486,9 +3547,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, settings.log.printf("Primal cleanup failed.\n"); const f_t dual_infeas = phase2::dual_infeasibility( lp, settings, vstatus, sol.z, settings.tight_tol, settings.dual_tol); - if (dual_infeas > 10.0 * settings.dual_tol) { - return dual_status_t::NUMERICAL; - } + if (dual_infeas > 10.0 * settings.dual_tol) { return dual_status_t::NUMERICAL; } } } // removal_status == 0 (OPTIMAL) or primal cleanup done: fall through to prepare_optimality @@ -3510,8 +3569,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, iter, x, y, - z, - sol); + z, + sol); status = dual_status_t::OPTIMAL; break; } @@ -3649,8 +3708,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, delta_z, delta_z_indices, nonbasic_mark); - entering_index = bfrt.compute_step_length( - step_length, nonbasic_entering_index, flip_indices); + entering_index = + bfrt.compute_step_length(step_length, nonbasic_entering_index, flip_indices); phase2_work_estimate += bfrt.work_estimate(); if (entering_index == RATIO_TEST_NUMERICAL_ISSUES) { settings.log.printf("Numerical issues encountered in ratio test.\n"); @@ -3701,10 +3760,22 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += 2 * n; if (perturbation > 0.0 && phase == 2) { - i_t removal_status = phase2::attempt_to_remove_perturbations( - lp, settings, ft, basic_list, nonbasic_list, vstatus, objective, - z, y, x, xB_workspace, squared_infeasibilities, infeasibility_indices, - primal_infeasibility, primal_infeasibility_squared, phase2_work_estimate); + i_t removal_status = phase2::attempt_to_remove_perturbations(lp, + settings, + ft, + basic_list, + nonbasic_list, + vstatus, + objective, + z, + y, + x, + xB_workspace, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility, + primal_infeasibility_squared, + phase2_work_estimate); if (removal_status == 0) { // OPTIMAL obj = phase2::compute_perturbed_objective(objective, x); phase2_work_estimate += 2 * n; @@ -3833,7 +3904,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, atilde, atilde_index, phase2_work_estimate) - : 0; + : 0; timers.flip_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); total_bound_flips += num_flipped; @@ -4004,7 +4075,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2::check_primal_infeasibilities( lp, settings, basic_list, x, squared_infeasibilities, infeasibility_indices); #endif - timers.update_infeasibility_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); + timers.update_infeasibility_time += + timers.stop_timer(phase2_work_estimate + ft.work_estimate()); // Clear delta_x phase2::clear_delta_x( @@ -4015,9 +4087,16 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2::remove_leaving_perturbation(lp, settings, leaving_index, direction, z, objective); } f_t sum_perturb = 0.0; - phase2::compute_perturbation( - lp, settings, delta_z_indices, vstatus, z, objective, sum_perturb, - entering_index, step_length, phase2_work_estimate); + phase2::compute_perturbation(lp, + settings, + delta_z_indices, + vstatus, + z, + objective, + sum_perturb, + entering_index, + step_length, + phase2_work_estimate); timers.perturb_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); // Update basis information @@ -4165,8 +4244,8 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, if ((iter % FEATURE_LOG_INTERVAL) == 0 && work_unit_context) { [[maybe_unused]] i_t iters_elapsed = iter - last_feature_log_iter; - work_unit_context->record_work_sync_on_horizon( - (phase2_work_estimate - last_work_reported) / 1e8); + work_unit_context->record_work_sync_on_horizon((phase2_work_estimate - last_work_reported) / + 1e8); last_work_reported = phase2_work_estimate; last_feature_log_iter = iter; @@ -4199,13 +4278,18 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, return dual_status_t::WORK_LIMIT; } - if (now > settings.time_limit) { status = dual_status_t::TIME_LIMIT; break; } + if (now > settings.time_limit) { + status = dual_status_t::TIME_LIMIT; + break; + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return dual_status_t::CONCURRENT_LIMIT; } } - if (status != dual_status_t::TIME_LIMIT && iter >= iter_limit) { status = dual_status_t::ITERATION_LIMIT; } + if (status != dual_status_t::TIME_LIMIT && iter >= iter_limit) { + status = dual_status_t::ITERATION_LIMIT; + } // Flush any remaining work from the basis update into the total work estimate phase2_work_estimate += ft.work_estimate(); @@ -4216,7 +4300,9 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, i_t num_iters = iter - start_iter; if (num_iters > 0) { settings.log.printf("Bound flips: total=%d, avg=%.1f, max=%d\n", - total_bound_flips, 1.0 * total_bound_flips / num_iters, max_bound_flips); + total_bound_flips, + 1.0 * total_bound_flips / num_iters, + max_bound_flips); } constexpr bool print_stats = false; if constexpr (print_stats) { diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 7548e2d28f..1a67956e47 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -575,10 +575,10 @@ i_t primal_ratio_test(const lp_problem_t& lp, i_t direction, f_t& work_estimate) { - const i_t m = lp.num_rows; - basic_leaving = -1; - i_t leaving_index = -1; - constexpr f_t pivot_tol = 1e-8; + const i_t m = lp.num_rows; + basic_leaving = -1; + i_t leaving_index = -1; + constexpr f_t pivot_tol = 1e-8; constexpr f_t harris_tol = 1e-8; // Harris ratio test: two passes. @@ -963,9 +963,7 @@ primal_status_t primal_phase2_with_advanced_basis( work_estimate += basis_update.work_estimate(); basis_update.clear_work_estimate(); - if (work_estimate > settings.work_limit) { - return primal_status_t::WORK_LIMIT; - } + if (work_estimate > settings.work_limit) { return primal_status_t::WORK_LIMIT; } primal_timers_t timers(false); @@ -1328,8 +1326,8 @@ primal_status_t primal_phase2_with_advanced_basis( // After compute_delta_z and update_z, delta_z[j] still holds the raw // pivot row entries (update_z multiplies by dual_step_length into z, not delta_z) for (i_t k = 0; k < n - m; ++k) { - const i_t j = nonbasic_list[k]; - const f_t a_j = delta_z[j]; + const i_t j = nonbasic_list[k]; + const f_t a_j = delta_z[j]; const f_t candidate = (a_j * a_j / pivot_sq) * w_enter; if (candidate > devex_weight[j]) { devex_weight[j] = candidate; } } diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 8baaa0a8c7..102036e635 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -255,8 +255,8 @@ i_t scaling(const lp_problem_t& unscaled, // MIP performs integer-aware row scaling before presolve, while QP and SOCP // use the Ruiz path above. Apply this simpler equilibration only to LPs. - const bool use_lp_row_scaling = !settings.inside_mip && unscaled.second_order_cone_dims.empty() && - unscaled.Q.n == 0; + const bool use_lp_row_scaling = + !settings.inside_mip && unscaled.second_order_cone_dims.empty() && unscaled.Q.n == 0; if (use_lp_row_scaling) { csr_matrix_t Arow(0, 0, 0); scaled.A.to_compressed_row(Arow); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index ef9804e46c..5324b7f0e6 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -193,9 +193,9 @@ struct simplex_solver_settings_t { i_t augmented; // -1 automatic, 0 to solve with ADAT, 1 to solve with augmented system i_t dualize; // -1 automatic, 0 to not dualize, 1 to dualize i_t ordering; // -1 automatic, 0 to use nested dissection, 1 to use AMD - i_t initial_perturbation; // -1 automatic, 0 to not perturb, 1 to perturb - i_t remove_perturbation; // -1 automatic, 0 disabled, 1 enabled - i_t primal_pricing; // 0 Dantzig (default), 1 Devex + i_t initial_perturbation; // -1 automatic, 0 to not perturb, 1 to perturb + i_t remove_perturbation; // -1 automatic, 0 disabled, 1 enabled + i_t primal_pricing; // 0 Dantzig (default), 1 Devex barrier_dual_initial_point_t barrier_dual_initial_point; // -1 automatic, 0 Lustig-Marsten-Shanno, // 1 dual least squares, 2 SeDuMi mu-based @@ -224,11 +224,11 @@ struct simplex_solver_settings_t { i_t strong_chvatal_gomory_cuts; // -1 automatic, 0 to disable, >0 to enable strong Chvatal Gomory // cuts i_t symmetry; // -1 automatic, 0 to disable, >0 to enable different symmetry methods - i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost - // strengthening + i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost + // strengthening i_t dual_degenerate_feasibility_pump; // 0 to disable, 1 to enable - f_t cut_change_threshold; // threshold for cut change - f_t cut_min_orthogonality; // minimum orthogonality for cuts + f_t cut_change_threshold; // threshold for cut change + f_t cut_min_orthogonality; // minimum orthogonality for cuts i_t mip_batch_pdlp_strong_branching; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only i_t mip_batch_pdlp_reliability_branching; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch From c3a025139a51c23b0a2a24885da8be724dfc9e62 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 2 Sep 2026 21:29:35 -0700 Subject: [PATCH 31/34] Prefer large pivots with Harris ratio buckets This improvement was discovered through Hiverge's automated exploration of changes to cuOpt's dual simplex solver and then isolated on top of the v9 row-equilibration and perturbation changes. The bound-flipping ratio test searches Harris buckets from the latest to the earliest so that it favors a longer dual step. Within the selected bucket, choose the candidate with the largest absolute pivot instead of the largest exact breakpoint ratio. Use the exact ratio only to break ties between equal pivots. The change recovers ex9 and neos-3988577-wolgan, which time out in v9, but introduces a timeout on irish-electricity after failed primal cleanup. The larger pivots reduce total BFRT zero steps by 22.8% and improve the aggregate benchmark despite that cleanup regression. Problem v9 v10 Baseline HiGHS v10/HiGHS ------------------------------------------------------------------------------------- var-smallemery-m6j6 0.69 0.69 0.66 300.00 0.00 momentum1 0.69 0.70 0.69 300.00 0.00 neos-5114902-kasavu 2.74 2.73 87.40 300.00 0.01 supportcase42 0.80 0.76 0.52 36.20 0.02 neos-5049753-cuanza 1.02 1.05 7.68 29.85 0.04 supportcase12 6.35 4.11 4.68 37.11 0.11 roi5alpha10n8 1.35 1.32 1.25 11.90 0.11 mzzv11 2.40 2.12 40.19 16.71 0.13 ns1760995 114.33 44.10 135.94 269.53 0.16 ns1952667 0.26 0.15 8.95 0.80 0.19 proteindesign121hz512p9 0.46 0.40 0.91 2.04 0.20 roi2alpha3n4 0.22 0.24 0.31 1.16 0.21 co-100 0.28 0.27 0.68 1.28 0.21 neos-5104907-jarama 21.53 19.35 124.69 89.64 0.22 neos-5052403-cygnet 109.39 72.96 300.00 300.00 0.24 proteindesign122trx11p8 0.35 0.33 0.64 1.26 0.26 neos-1354092 20.05 20.09 300.00 70.92 0.28 supportcase18 0.03 0.04 0.06 0.12 0.33 rd-rplusc-21 0.16 0.17 0.23 0.49 0.35 30n20b8 0.07 0.04 0.08 0.11 0.36 ns1644855 63.39 88.00 300.00 238.30 0.37 neos-787933 0.06 0.07 0.06 0.18 0.39 sct2 0.06 0.06 0.22 0.14 0.43 supportcase7 1.68 1.53 1.29 3.52 0.43 wachplan 0.18 0.12 0.25 0.26 0.46 neos-860300 0.06 0.07 0.10 0.15 0.47 rocII-5-11 0.10 0.10 0.09 0.21 0.48 neos-5093327-huahum 0.23 0.23 0.23 0.48 0.48 supportcase22 2.28 1.92 2.03 3.97 0.48 physiciansched6-2 3.60 3.25 11.15 6.72 0.48 neos-5107597-kakapo 0.06 0.05 0.04 0.10 0.50 cvs16r128-89 1.07 0.88 0.93 1.72 0.51 satellites2-40 5.40 5.56 29.70 10.51 0.53 neos-4647030-tutaki 1.68 1.69 2.47 3.09 0.55 lectsched-5-obj 0.11 0.10 0.13 0.18 0.56 supportcase10 79.85 66.38 300.00 113.55 0.58 buildingenergy 87.82 68.31 300.00 115.61 0.59 n3div36 0.10 0.11 0.11 0.18 0.61 neos-5188808-nattai 0.18 0.19 0.30 0.29 0.66 tbfp-network 7.72 6.01 9.06 9.04 0.66 neos-5195221-niemur 0.26 0.25 0.50 0.37 0.68 thor50dday 0.26 0.25 0.27 0.37 0.68 nursesched-medium-hint03 3.71 3.23 10.47 4.73 0.68 square47 93.64 87.67 79.87 126.92 0.69 cryptanalysiskb128n5obj14 9.55 8.73 29.78 12.54 0.70 ns1116954 9.20 7.52 156.23 10.73 0.70 neos-1171448 0.58 0.60 2.35 0.84 0.71 academictimetablesmall 0.70 0.59 14.89 0.82 0.72 neos-3402454-bohle 64.41 55.07 221.86 74.15 0.74 neos-2746589-doon 2.36 2.25 7.53 3.02 0.75 neos-4300652-rahue 0.46 0.60 1.22 0.80 0.75 neos-3004026-krka 0.07 0.09 0.06 0.12 0.75 neos-3381206-awhea 0.03 0.03 0.08 0.04 0.75 square41 38.25 31.75 28.57 39.86 0.80 blp-ic98 0.08 0.08 0.12 0.10 0.80 dws008-01 0.05 0.04 0.04 0.05 0.80 supportcase33 0.40 0.44 0.99 0.55 0.80 neos-848589 0.70 0.68 1.24 0.85 0.80 comp21-2idx 0.31 0.28 1.55 0.35 0.80 cod105 8.39 6.13 9.06 7.46 0.82 decomp2 0.08 0.10 0.19 0.12 0.83 cryptanalysiskb128n5obj16 10.05 8.70 29.53 10.27 0.85 fiball 0.15 0.12 0.71 0.14 0.86 blp-ar98 0.07 0.10 0.10 0.11 0.91 dano3_3 16.02 17.37 46.88 19.06 0.91 dano3_5 16.04 17.42 46.79 19.03 0.92 neos-3555904-turama 1.31 1.27 1.31 1.37 0.93 neos-3988577-wolgan 300.00 281.89 278.89 300.00 0.94 neos-1171737 0.17 0.19 0.70 0.20 0.95 drayage-25-23 0.05 0.04 0.08 0.04 1.00 h80x6320d 0.04 0.04 0.05 0.04 1.00 highschool1-aigio 300.00 300.00 300.00 300.00 1.00 icir97_tension 0.02 0.02 0.03 0.02 1.00 leo1 0.07 0.07 0.10 0.07 1.00 leo2 0.12 0.13 0.13 0.13 1.00 neos-1456979 0.05 0.04 0.05 0.04 1.00 neos8 0.26 0.26 0.38 0.26 1.00 nursesched-sprint02 0.23 0.23 0.38 0.23 1.00 physiciansched3-3 300.00 300.00 300.00 300.00 1.00 radiationm18-12-05 0.13 0.14 0.23 0.14 1.00 rail02 300.00 300.00 300.00 300.00 1.00 s100 300.00 300.00 300.00 300.00 1.00 savsched1 300.00 300.00 300.00 300.00 1.00 supportcase19 300.00 300.00 300.00 300.00 1.00 swath3 0.02 0.03 0.03 0.03 1.00 traininstance6 0.04 0.03 0.04 0.03 1.00 neos-873061 1.46 1.43 1.46 1.38 1.04 neos-957323 12.14 8.37 300.00 7.74 1.08 mzzv42z 0.91 1.03 10.10 0.95 1.08 neos-3402294-bobin 1.29 1.33 3.07 1.20 1.11 germanrr 0.30 0.30 0.31 0.27 1.11 s250r10 71.91 81.59 300.00 71.74 1.14 supportcase6 4.93 4.83 7.39 4.18 1.16 neos-1582420 0.08 0.07 0.12 0.06 1.17 supportcase40 0.29 0.28 0.24 0.24 1.17 neos-4763324-toguru 5.82 5.85 8.32 5.00 1.17 neos-1122047 1.87 1.90 2.09 1.61 1.18 sp98ar 0.34 0.37 0.39 0.31 1.19 neos-4413714-turia 2.10 2.33 3.97 1.93 1.21 rail507 4.37 3.52 7.17 2.84 1.24 drayage-100-23 0.04 0.05 0.07 0.04 1.25 neos-4738912-atrato 0.04 0.05 0.05 0.04 1.25 traininstance2 0.04 0.05 0.09 0.04 1.25 nexp-150-20-8-5 0.10 0.09 0.10 0.07 1.29 fast0507 3.95 3.58 7.36 2.77 1.29 sp97ar 0.44 0.43 0.40 0.33 1.30 irp 0.12 0.12 0.11 0.09 1.33 swath1 0.03 0.04 0.04 0.03 1.33 radiationm40-10-02 0.97 0.95 1.58 0.71 1.34 cmflsp50-24-8-8 0.58 0.59 0.77 0.44 1.34 map16715-04 9.87 9.21 13.26 6.80 1.35 comp07-2idx 1.26 1.51 4.03 1.10 1.37 map10 8.54 8.67 11.08 6.25 1.39 cbs-cta 0.14 0.14 0.42 0.10 1.40 neos-2978193-inde 0.09 0.07 0.16 0.05 1.40 trento1 3.50 3.10 3.08 2.21 1.40 hypothyroid-k1 4.42 4.40 4.36 3.01 1.46 uccase9 8.04 7.66 11.54 5.20 1.47 qap10 9.80 9.87 15.57 6.68 1.48 neos-827175 0.44 0.43 9.36 0.29 1.48 neos-2987310-joes 1.59 1.49 1.52 1.00 1.49 bnatt500 0.20 0.21 0.29 0.14 1.50 mushroom-best 0.28 0.30 0.26 0.20 1.50 neos-960392 3.29 3.99 8.93 2.66 1.50 air05 0.29 0.27 0.28 0.18 1.50 reblock115 0.14 0.14 0.16 0.09 1.56 neos-4532248-waihi 1.40 1.43 2.61 0.90 1.59 uct-subprob 0.12 0.13 0.11 0.08 1.62 ns1830653 0.25 0.23 0.42 0.14 1.64 fhnw-binpack4-48 0.09 0.10 0.07 0.06 1.67 neos-3083819-nubu 0.05 0.05 0.06 0.03 1.67 ran14x18-disj-8 0.05 0.05 0.04 0.03 1.67 rocI-4-11 0.09 0.10 0.11 0.06 1.67 roll3000 0.10 0.10 0.12 0.06 1.67 opm2-z10-s4 75.72 75.53 89.23 44.75 1.69 istanbul-no-cutoff 1.63 1.62 0.71 0.94 1.72 neos-3656078-kumeu 0.40 0.40 2.37 0.23 1.74 neos-662469 0.86 0.93 1.65 0.53 1.75 k1mushroom 28.33 29.61 31.00 16.80 1.76 rmatr200-p5 8.10 8.20 7.50 4.61 1.78 sing326 9.33 8.37 9.54 4.69 1.78 neos-933966 9.15 5.17 16.79 2.80 1.85 rmatr100-p10 0.33 0.26 0.27 0.14 1.86 bnatt400 0.13 0.15 0.16 0.08 1.88 mcsched 0.30 0.30 0.27 0.16 1.88 atlanta-ip 12.20 9.04 6.85 4.54 1.99 assign1-5-8 0.02 0.02 0.03 0.01 2.00 b1c1s1 0.06 0.04 0.05 0.02 2.00 bppc4-08 0.04 0.04 0.08 0.02 2.00 eil33-2 0.06 0.06 0.05 0.03 2.00 fhnw-binpack4-4 0.03 0.02 0.03 0.01 2.00 ic97_potential 0.02 0.02 0.02 0.01 2.00 mik-250-20-75-4 0.02 0.02 0.02 0.01 2.00 neos-3024952-loue 0.32 0.40 0.41 0.20 2.00 neos-4954672-berkel 0.02 0.02 0.02 0.01 2.00 pg5_34 0.03 0.02 0.03 0.01 2.00 tr12-30 0.02 0.02 0.03 0.01 2.00 graph20-20-1rand 0.30 0.25 0.23 0.12 2.08 neos-3216931-puriri 4.20 6.83 7.33 3.23 2.11 ns1208400 0.76 0.78 3.96 0.36 2.17 chromaticindex512-7 53.03 46.15 16.62 21.24 2.17 splice1k1 19.92 20.18 21.62 9.17 2.20 eilA101-2 3.29 3.08 2.68 1.39 2.22 nw04 1.71 1.36 0.44 0.61 2.23 sing44 12.54 12.80 9.62 5.69 2.25 gfd-schedulen180f7d50m30k18 15.81 15.35 80.00 6.81 2.25 n2seq36q 0.71 0.56 0.46 0.24 2.33 triptim1 132.50 121.77 71.42 51.43 2.37 netdiversion 22.67 22.84 7.64 9.43 2.42 piperout-08 0.41 0.39 0.39 0.16 2.44 chromaticindex1024-7 262.17 232.72 45.86 93.65 2.48 satellites2-60-fs 6.89 8.34 4.17 3.34 2.50 neos-4387871-tavua 0.08 0.10 0.10 0.04 2.50 neos-950242 0.46 0.45 1.05 0.18 2.50 nu25-pr12 0.04 0.05 0.05 0.02 2.50 rococoB10-011000 0.15 0.15 0.13 0.06 2.50 rococoC10-001000 0.06 0.05 0.04 0.02 2.50 glass-sc 0.33 0.31 0.31 0.12 2.58 seymour1 0.97 1.00 0.83 0.38 2.63 seymour 0.95 1.01 0.83 0.38 2.66 unitcal_7 1.05 1.04 0.96 0.39 2.67 sorrell3 1.14 1.10 1.07 0.40 2.75 bab2 75.19 80.63 300.00 28.97 2.78 bab6 34.02 40.19 143.43 14.06 2.86 neos-1445765 0.25 0.26 0.16 0.09 2.89 net12 0.96 1.03 0.55 0.35 2.94 50v-10 0.03 0.03 0.02 0.01 3.00 binkar10_1 0.03 0.03 0.02 0.01 3.00 cost266-UUE 0.06 0.06 0.03 0.02 3.00 gmu-35-40 0.03 0.03 0.03 0.01 3.00 gmu-35-50 0.04 0.03 0.05 0.01 3.00 lotsize 0.03 0.03 0.03 0.01 3.00 n5-3 0.05 0.06 0.03 0.02 3.00 neos-4338804-snowy 0.02 0.03 0.03 0.01 3.00 pg 0.02 0.03 0.03 0.01 3.00 csched007 0.16 0.17 0.19 0.05 3.40 peg-solitaire-a3 2.14 1.78 1.88 0.52 3.42 milo-v12-6-r2-40-1 0.18 0.18 0.25 0.05 3.60 uccase12 5.76 5.36 72.32 1.38 3.88 app1-1 0.10 0.08 0.08 0.02 4.00 csched008 0.13 0.12 0.11 0.03 4.00 neos-3627168-kasai 0.04 0.04 0.04 0.01 4.00 neos17 0.03 0.04 0.03 0.01 4.00 p200x1188c 0.04 0.04 0.03 0.01 4.00 rail01 300.00 300.00 223.84 71.23 4.21 CMS750_4 0.63 0.63 0.38 0.14 4.50 piperout-27 1.23 1.26 0.67 0.26 4.85 neos-631710 215.36 155.73 300.00 31.97 4.87 neos-4722843-widden 7.67 7.75 1.17 1.54 5.03 irish-electricity 109.17 300.00 181.58 59.53 5.04 ex10 300.00 300.00 300.00 59.25 5.06 fastxgemm-n2r6s0t2 0.40 0.47 0.18 0.08 5.87 neos-2075418-temuka 300.00 300.00 128.30 50.57 5.93 beasleyC3 0.06 0.06 0.05 0.01 6.00 snp-02-004-104 24.10 21.83 14.17 2.83 7.71 app1-2 5.77 5.58 5.03 0.70 7.97 mc11 0.08 0.08 0.06 0.01 8.00 brazil3 61.89 63.40 300.00 7.24 8.76 ex9 300.00 132.66 300.00 14.07 9.43 enlight_hard 0.02 0.01 0.02 0.00 10.00 gen-ip054 0.02 0.01 0.03 0.00 10.00 pk1 0.01 0.01 0.02 0.00 10.00 exp-1-500-5-5 0.02 0.02 0.02 0.00 20.00 gen-ip002 0.01 0.02 0.02 0.00 20.00 glass4 0.02 0.02 0.02 0.00 20.00 graphdraw-domain 0.02 0.02 0.02 0.00 20.00 mad 0.02 0.02 0.02 0.00 20.00 markshare2 0.01 0.02 0.02 0.00 20.00 markshare_4_0 0.01 0.02 0.02 0.00 20.00 mas74 0.01 0.02 0.03 0.00 20.00 mas76 0.02 0.02 0.02 0.00 20.00 neos-2657525-crna 0.03 0.02 0.03 0.00 20.00 neos-3046615-murg 0.02 0.02 0.02 0.00 20.00 neos-911970 0.03 0.02 0.03 0.00 20.00 neos5 0.02 0.02 0.02 0.00 20.00 neos859080 0.01 0.02 0.01 0.00 20.00 supportcase26 0.02 0.02 0.03 0.00 20.00 timtab1 0.02 0.02 0.01 0.00 20.00 neos-3754480-nidda 0.02 0.03 0.02 0.00 30.00 sp150x300d 0.02 0.03 0.02 0.00 30.00 Geomean v9/v10: 1.0157 Shifted(+1s): 1.0209 Geomean Baseline/v10: 1.4393 Shifted(+1s): 1.2823 Geomean v10/HiGHS: 1.5021 Shifted(+1s): 0.9925 (240 problems) --- cpp/src/dual_simplex/bound_flipping_ratio_test.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index 8b9688e021..d18ed95e90 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -406,7 +406,8 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, // Select the entering variable // Scan from last bucket to first. Within each bucket, pick the variable with - // the largest ratio (step length) that has |delta_z| > pivot_threshold + // the largest |delta_z|, provided |delta_z| > pivot_threshold, breaking ties + // by preferring the larger step length. f_t pivot_threshold = std::max(settings_.pivot_tol, std::min(0.1 * max_pivot, 1.0)); i_t entering_k = -1; @@ -414,12 +415,15 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, for (i_t b = num_buckets - 1; b >= 0; b--) { const i_t b_start = bucket_start[b]; const i_t b_end = bucket_start[b + 1]; + f_t best_pivot = -1.0; f_t best_ratio = -1.0; for (i_t h = b_start; h < b_end; h++) { const i_t k = candidates[h]; const i_t j = nonbasic_list_[indicies[k]]; const f_t pivot = std::abs(delta_z_[j]); - if (pivot > pivot_threshold && ratios[k] > best_ratio) { + if (pivot > pivot_threshold && + (pivot > best_pivot || (pivot == best_pivot && ratios[k] > best_ratio))) { + best_pivot = pivot; best_ratio = ratios[k]; entering_k = k; } From ef5ff666e288491ece37e31eb251091c54de09f0 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 2 Sep 2026 21:31:33 -0700 Subject: [PATCH 32/34] Add parameters to control degenerate pivots --- .../mathematical_optimization/constants.h | 2 + .../mip/solver_settings.hpp | 2 + cpp/src/branch_and_bound/branch_and_bound.cpp | 106 ++++++++++-------- .../dual_simplex/simplex_solver_settings.hpp | 4 + cpp/src/math_optimization/solver_settings.cpp | 2 + cpp/src/mip_heuristics/solver.cu | 4 + 6 files changed, 73 insertions(+), 47 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 4868c01dc4..9079e2a23d 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -84,6 +84,8 @@ #define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts" #define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening" #define CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP "mip_dual_degenerate_feasibility_pump" +#define CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS "mip_primal_degenerate_pivots" +#define CUOPT_MIP_DUAL_DEGENERATE_PIVOTS "mip_dual_degenerate_pivots" #define CUOPT_MIP_RINS "mip_rins" #define CUOPT_MIP_RENS "mip_rens" #define CUOPT_MIP_OBJECTIVE_STEP "mip_objective_step" diff --git a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp index 71438bfe8c..f12c33e818 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp @@ -136,6 +136,8 @@ class mip_solver_settings_t { i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; i_t dual_degenerate_feasibility_pump = -1; // -1 = automatic (on), 0 = off, 1 = on + i_t primal_degenerate_pivots = -1; // -1 = automatic (on), 0 = off, 1 = on + i_t dual_degenerate_pivots = -1; // -1 = automatic (on), 0 = off, 1 = on i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable f_t cut_change_threshold = -1.0; f_t cut_min_orthogonality = 0.5; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3763667c99..e9bf0cbd2e 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1785,15 +1785,17 @@ dual_status_t branch_and_bound_t::solve_node_lp( std::vector fractional; i_t num_fractional = fractional_variables(settings_, worker->leaf_solution.x, var_types_, fractional); - pivot_out_integer_variables(worker->leaf_problem, - lp_settings, - worker->basic_list, - worker->nonbasic_list, - worker->leaf_vstatus, - worker->leaf_solution, - worker->basis_factors, - num_fractional, - fractional); + if (settings_.dual_degenerate_pivots != 0) { + pivot_out_integer_variables(worker->leaf_problem, + lp_settings, + worker->basic_list, + worker->nonbasic_list, + worker->leaf_vstatus, + worker->leaf_solution, + worker->basis_factors, + num_fractional, + fractional); + } } } } @@ -3464,15 +3466,18 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t::cut_pass_action_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.printf("New reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); - pivot_to_improve_reduced_cost_strengthening(original_lp_, - basic_list, - nonbasic_list, - root_vstatus_, - root_relax_soln_, - basis_update, - num_fractional, - fractional, - root_objective_, - reduced_cost_bounds); + if (settings_.primal_degenerate_pivots != 0) { + pivot_to_improve_reduced_cost_strengthening(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional, + root_objective_, + reduced_cost_bounds); + } settings_.log.printf("After pivoting: new reduced cost objective %e (current %e)\n", reduced_cost_bounds.get_max_objective(), upper_bound_.load()); f_t pivot_out_integer_variables_start_time = tic(); - i_t num_integer_increased = pivot_out_integer_variables(original_lp_, - settings_, - basic_list, - nonbasic_list, - root_vstatus_, - root_relax_soln_, - basis_update, - num_fractional, - fractional); + i_t num_integer_increased = 0; + if (settings_.dual_degenerate_pivots != 0) { + num_integer_increased = pivot_out_integer_variables(original_lp_, + settings_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + } settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", num_integer_increased, toc(pivot_out_integer_variables_start_time)); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 5324b7f0e6..3d9c439e5a 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -108,6 +108,8 @@ struct simplex_solver_settings_t { symmetry(-1), reduced_cost_strengthening(-1), dual_degenerate_feasibility_pump(1), + primal_degenerate_pivots(1), + dual_degenerate_pivots(1), cut_change_threshold(1e-3), cut_min_orthogonality(0.5), mip_batch_pdlp_strong_branching(0), @@ -227,6 +229,8 @@ struct simplex_solver_settings_t { i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost // strengthening i_t dual_degenerate_feasibility_pump; // 0 to disable, 1 to enable + i_t primal_degenerate_pivots; // 0 to disable, 1 to enable + i_t dual_degenerate_pivots; // 0 to disable, 1 to enable f_t cut_change_threshold; // threshold for cut change f_t cut_min_orthogonality; // minimum orthogonality for cuts i_t diff --git a/cpp/src/math_optimization/solver_settings.cpp b/cpp/src/math_optimization/solver_settings.cpp index bff005a90d..6646e09bbf 100644 --- a/cpp/src/math_optimization/solver_settings.cpp +++ b/cpp/src/math_optimization/solver_settings.cpp @@ -157,6 +157,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, {CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP, &mip_settings.dual_degenerate_feasibility_pump, -1, 1, -1}, + {CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS, &mip_settings.primal_degenerate_pivots, -1, 1, -1}, + {CUOPT_MIP_DUAL_DEGENERATE_PIVOTS, &mip_settings.dual_degenerate_pivots, -1, 1, -1}, {CUOPT_MIP_RINS, &mip_settings.submip_params.rins, -1, 1, -1}, {CUOPT_MIP_RENS, &mip_settings.submip_params.rens, -1, 1, -1}, {CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1}, diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 6173aa6354..e27a94c64a 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -403,6 +403,10 @@ solution_t mip_solver_t::run_solver() context.settings.dual_degenerate_feasibility_pump == -1 ? 1 : context.settings.dual_degenerate_feasibility_pump; + branch_and_bound_settings.primal_degenerate_pivots = + context.settings.primal_degenerate_pivots == -1 ? 1 : context.settings.primal_degenerate_pivots; + branch_and_bound_settings.dual_degenerate_pivots = + context.settings.dual_degenerate_pivots == -1 ? 1 : context.settings.dual_degenerate_pivots; branch_and_bound_settings.symmetry = context.settings.symmetry; branch_and_bound_settings.diving_settings = context.settings.diving_params; From 2e02e232000f2c7568eaec10ca001bc84bf6be40 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Wed, 2 Sep 2026 21:32:08 -0700 Subject: [PATCH 33/34] Style fixes --- cpp/include/cuopt/mathematical_optimization/constants.h | 4 ++-- cpp/src/branch_and_bound/branch_and_bound.cpp | 4 ++-- cpp/src/dual_simplex/simplex_solver_settings.hpp | 4 ++-- cpp/src/mip_heuristics/solver.cu | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 9079e2a23d..aae5c1ce6f 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -84,8 +84,8 @@ #define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts" #define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening" #define CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP "mip_dual_degenerate_feasibility_pump" -#define CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS "mip_primal_degenerate_pivots" -#define CUOPT_MIP_DUAL_DEGENERATE_PIVOTS "mip_dual_degenerate_pivots" +#define CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS "mip_primal_degenerate_pivots" +#define CUOPT_MIP_DUAL_DEGENERATE_PIVOTS "mip_dual_degenerate_pivots" #define CUOPT_MIP_RINS "mip_rins" #define CUOPT_MIP_RENS "mip_rens" #define CUOPT_MIP_OBJECTIVE_STEP "mip_objective_step" diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e9bf0cbd2e..5534360fb6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -3466,7 +3466,7 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t::solve(mip_solution_t& solut upper_bound_.load()); f_t pivot_out_integer_variables_start_time = tic(); - i_t num_integer_increased = 0; + i_t num_integer_increased = 0; if (settings_.dual_degenerate_pivots != 0) { num_integer_increased = pivot_out_integer_variables(original_lp_, settings_, diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 3d9c439e5a..bed2c4eee5 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -229,8 +229,8 @@ struct simplex_solver_settings_t { i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost // strengthening i_t dual_degenerate_feasibility_pump; // 0 to disable, 1 to enable - i_t primal_degenerate_pivots; // 0 to disable, 1 to enable - i_t dual_degenerate_pivots; // 0 to disable, 1 to enable + i_t primal_degenerate_pivots; // 0 to disable, 1 to enable + i_t dual_degenerate_pivots; // 0 to disable, 1 to enable f_t cut_change_threshold; // threshold for cut change f_t cut_min_orthogonality; // minimum orthogonality for cuts i_t diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index e27a94c64a..645de93cd6 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -404,7 +404,8 @@ solution_t mip_solver_t::run_solver() ? 1 : context.settings.dual_degenerate_feasibility_pump; branch_and_bound_settings.primal_degenerate_pivots = - context.settings.primal_degenerate_pivots == -1 ? 1 : context.settings.primal_degenerate_pivots; + context.settings.primal_degenerate_pivots == -1 ? 1 + : context.settings.primal_degenerate_pivots; branch_and_bound_settings.dual_degenerate_pivots = context.settings.dual_degenerate_pivots == -1 ? 1 : context.settings.dual_degenerate_pivots; branch_and_bound_settings.symmetry = context.settings.symmetry; From 0f95053204b3ddef63ac0d5bf784ddca41d63f6f Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 4 Sep 2026 11:49:54 -0700 Subject: [PATCH 34/34] Snapshot slack columns for branch-and-bound workers Signed-off-by: Christopher Maes --- cpp/src/branch_and_bound/branch_and_bound.cpp | 35 +++++++---- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + .../deterministic_workers.hpp | 59 ++++++++++++------- cpp/src/branch_and_bound/worker.hpp | 7 +++ cpp/src/branch_and_bound/worker_pool.hpp | 20 +++++-- cpp/src/mip_heuristics/root_heuristics.hpp | 19 +++++- 6 files changed, 99 insertions(+), 42 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 5534360fb6..83bc52b581 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1788,6 +1788,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( if (settings_.dual_degenerate_pivots != 0) { pivot_out_integer_variables(worker->leaf_problem, lp_settings, + worker->new_slacks, worker->basic_list, worker->nonbasic_list, worker->leaf_vstatus, @@ -3117,7 +3118,7 @@ void branch_and_bound_t::launch_root_heuristics( // Using shared_ptr here, so the lifetime of the object is tied to the related task. This allows // the solver to send the stop signal and immediately continue the execution. auto current_heuristic = root_heuristics.create_new_cut_pass_heuristic( - Arow_, var_types_, lp_solution.x, edge_norms_, settings_); + Arow_, var_types_, lp_solution.x, edge_norms_, new_slacks_, settings_); auto worker_count = root_heuristics.worker_count_; current_heuristic->initialize_pseudocost( @@ -3470,6 +3471,7 @@ typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t i_t branch_and_bound_t::pivot_out_integer_variables( const simplex::lp_problem_t& lp, const simplex::simplex_solver_settings_t& settings, + const std::vector& new_slacks, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, @@ -4587,7 +4590,7 @@ i_t branch_and_bound_t::pivot_out_integer_variables( const i_t num_zero_reduced_costs_vars = zero_reduced_costs_vars.size(); std::vector row_to_slack(lp.num_rows, -1); - for (i_t j : new_slacks_) { + for (i_t j : new_slacks) { if (lp.lower[j] != 0 || lp.upper[j] != inf) { continue; } const i_t p = lp.A.col_start[j]; row_to_slack[lp.A.i[p]] = j; @@ -5472,6 +5475,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.dual_degenerate_pivots != 0) { num_integer_increased = pivot_out_integer_variables(original_lp_, settings_, + new_slacks_, basic_list, nonbasic_list, root_vstatus_, @@ -5788,19 +5792,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut var_types_, symmetry_, settings_, - pc_, - root_relax_soln_.x, - edge_norms_); + pc_, + root_relax_soln_.x, + edge_norms_, + new_slacks_); submip_worker_pool_.init(num_submip_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, - pc_, - root_relax_soln_.x, - edge_norms_, - num_bfs_workers); + pc_, + root_relax_soln_.x, + edge_norms_, + new_slacks_, + num_bfs_workers); if (num_diving_workers > 0) { diving_worker_pool_.init(num_diving_workers, @@ -5812,6 +5818,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut pc_, root_relax_soln_.x, edge_norms_, + new_slacks_, num_bfs_workers + num_submip_workers); } @@ -6011,9 +6018,10 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri Arow, var_types_, settings_, - pc_, - root_relax_soln_.x, - edge_norms_); + pc_, + root_relax_soln_.x, + edge_norms_, + new_slacks_); if (num_diving_workers > 0) { // Extract diving types from search_strategies (skip BEST_FIRST at index 0) @@ -6030,7 +6038,8 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri settings_, pc_, root_relax_soln_.x, - edge_norms_); + edge_norms_, + new_slacks_); } } diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 9ba0b94128..fdc0cf41d5 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -508,6 +508,7 @@ class branch_and_bound_t { i_t pivot_out_integer_variables(const simplex::lp_problem_t& lp, const simplex::simplex_solver_settings_t& settings, + const std::vector& new_slacks, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index fae259ac3f..7c31d023cc 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -89,11 +89,13 @@ class deterministic_worker_base_t : public branch_and_bound_worker_t { const csr_matrix_t& Arow, const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, - pseudo_costs_t& pc, - const std::vector& root_solution, - const std::vector& root_edge_norm, - const std::string& context_name) - : base_t(id, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm), + pseudo_costs_t& pc, + const std::vector& root_solution, + const std::vector& root_edge_norm, + const std::vector& new_slacks, + const std::string& context_name) + : base_t( + id, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm, new_slacks), work_context(context_name), pc_snapshot(1, settings) { @@ -144,18 +146,20 @@ class deterministic_bfs_worker_t const csr_matrix_t& Arow, const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, - pseudo_costs_t& pc, - const std::vector& root_solution, - const std::vector& root_edge_norm) + pseudo_costs_t& pc, + const std::vector& root_solution, + const std::vector& root_edge_norm, + const std::vector& new_slacks) : base_t(id, original_lp, Arow, var_types, settings, - pc, - root_solution, - root_edge_norm, - "BB_Worker_" + std::to_string(id)) + pc, + root_solution, + root_edge_norm, + new_slacks, + "BB_Worker_" + std::to_string(id)) { } @@ -313,16 +317,18 @@ class deterministic_diving_worker_t const simplex::simplex_solver_settings_t& settings, pseudo_costs_t& pc, const std::vector& root_solution, - const std::vector& root_edge_norm) + const std::vector& root_edge_norm, + const std::vector& new_slacks) : base_t(id, original_lp, Arow, var_types, settings, - pc, - root_solution, - root_edge_norm, - "Diving_Worker_" + std::to_string(id)), + pc, + root_solution, + root_edge_norm, + new_slacks, + "Diving_Worker_" + std::to_string(id)), diving_type(type) { dive_lower = original_lp.lower; @@ -430,12 +436,13 @@ class deterministic_bfs_worker_pool_t const simplex::simplex_solver_settings_t& settings, pseudo_costs_t& pc, const std::vector& root_solution, - const std::vector& root_edge_norm) + const std::vector& root_edge_norm, + const std::vector& new_slacks) { this->workers_.reserve(num_workers); for (int i = 0; i < num_workers; ++i) { this->workers_.emplace_back( - i, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm); + i, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm, new_slacks); } } @@ -469,13 +476,23 @@ class deterministic_diving_worker_pool_t const simplex::simplex_solver_settings_t& settings, pseudo_costs_t& pc, const std::vector& root_solution, - const std::vector& root_edge_norm) + const std::vector& root_edge_norm, + const std::vector& new_slacks) { this->workers_.reserve(num_workers); for (int i = 0; i < num_workers; ++i) { search_strategy_t type = diving_types[i % diving_types.size()]; this->workers_.emplace_back( - i, type, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm); + i, + type, + original_lp, + Arow, + var_types, + settings, + pc, + root_solution, + root_edge_norm, + new_slacks); } } diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 0ec0f74bf9..5c705087d2 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -98,6 +98,7 @@ class branch_and_bound_worker_t { const std::vector& root_solution; const std::vector& root_edge_norm; const std::vector& var_types; + const std::vector& new_slacks; pseudo_costs_t& pseudo_costs; @@ -120,6 +121,7 @@ class branch_and_bound_worker_t { pseudo_costs_t& pc, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, uint64_t rng_offset = 0) : worker_id(worker_id), search_strategy(search_strategy_t::BEST_FIRST), @@ -138,6 +140,7 @@ class branch_and_bound_worker_t { root_solution(root_solution), root_edge_norm(root_edge_norm), var_types(var_type), + new_slacks(new_slacks), pseudo_costs(pc) { } @@ -179,6 +182,7 @@ class bfs_worker_t : public branch_and_bound_worker_t { pseudo_costs_t& pc, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, uint64_t rng_offset = 0) : Base(worker_id, original_lp, @@ -188,6 +192,7 @@ class bfs_worker_t : public branch_and_bound_worker_t { pc, root_solution, root_edge_norm, + new_slacks, rng_offset) { this->start_lower = original_lp.lower; @@ -265,6 +270,7 @@ class diving_worker_t : public branch_and_bound_worker_t { pseudo_costs_t& pc, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, uint64_t rng_offset = 0) : Base(worker_id, original_lp, @@ -274,6 +280,7 @@ class diving_worker_t : public branch_and_bound_worker_t { pc, root_solution, root_edge_norm, + new_slacks, rng_offset) { this->start_lower = original_lp.lower; diff --git a/cpp/src/branch_and_bound/worker_pool.hpp b/cpp/src/branch_and_bound/worker_pool.hpp index c4e54a61f1..bdb420a405 100644 --- a/cpp/src/branch_and_bound/worker_pool.hpp +++ b/cpp/src/branch_and_bound/worker_pool.hpp @@ -24,10 +24,11 @@ class worker_pool_t { const std::vector& var_type, mip_symmetry_t* symmetry, const simplex::simplex_solver_settings_t& settings, - pseudo_costs_t& pc, - const std::vector& root_solution, - const std::vector& root_edge_norm, - const uint64_t rng_offset = 0) + pseudo_costs_t& pc, + const std::vector& root_solution, + const std::vector& root_edge_norm, + const std::vector& new_slacks, + const uint64_t rng_offset = 0) { assert(!is_initialized_); assert(num_workers > 0); @@ -37,7 +38,16 @@ class worker_pool_t { idle_workers_.clear_resize(num_workers); for (i_t i = 0; i < num_workers; ++i) { workers_[i] = std::make_unique( - i, original_lp, Arow, var_type, settings, pc, root_solution, root_edge_norm, rng_offset); + i, + original_lp, + Arow, + var_type, + settings, + pc, + root_solution, + root_edge_norm, + new_slacks, + rng_offset); idle_workers_.push_back(i); // Propagate the (possibly null) symmetry pointer; workers lazily build // their orbital_fixing/lexical_reduction state via ensure_orbital_fixing(). diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index b9645579b5..2a893bcc19 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -19,6 +19,7 @@ struct cut_pass_heuristics_t { csr_matrix_t Arow_; std::vector root_solution_; std::vector root_edge_norm_; + std::vector new_slacks_; pseudo_costs_t pseudo_costs_; omp_atomic_t active_workers_; std::atomic halt_; @@ -31,11 +32,13 @@ struct cut_pass_heuristics_t { const std::vector& var_types, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, const simplex::simplex_solver_settings_t& settings) : var_types_(var_types), Arow_(Arow), root_solution_(root_solution), root_edge_norm_(root_edge_norm), + new_slacks_(new_slacks), pseudo_costs_(root_solution.size(), settings), active_workers_(0), halt_(false), @@ -82,7 +85,15 @@ struct cut_pass_heuristics_t { search_strategy_t type) { submip_worker_ = std::make_unique>( - id, lp, Arow_, var_types_, settings, pseudo_costs_, root_solution_, root_edge_norm_); + id, + lp, + Arow_, + var_types_, + settings, + pseudo_costs_, + root_solution_, + root_edge_norm_, + new_slacks_); submip_worker_->start_node = mip_node_t(root_obj, root_vstatus); submip_worker_->leaf_vstatus = root_vstatus; submip_worker_->leaf_solution.x = sol; @@ -121,7 +132,8 @@ struct cut_pass_heuristics_t { settings, pseudo_costs_, root_solution_, - root_edge_norm_)); + root_edge_norm_, + new_slacks_)); worker->start_node = root_node.detach_copy(); worker->start_lower = lp.lower; worker->start_upper = lp.upper; @@ -201,10 +213,11 @@ struct root_heuristics_t { const std::vector& var_types, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, const simplex::simplex_solver_settings_t& settings) { return cut_passes_heuristics_.emplace_back(std::make_shared>( - Arow, var_types, root_solution, root_edge_norm, settings)); + Arow, var_types, root_solution, root_edge_norm, new_slacks, settings)); } };