From 63d1ba4e74fe6a22d51bc39330fe187e3e7a6e11 Mon Sep 17 00:00:00 2001 From: Christopher Maes Date: Fri, 27 Feb 2026 22:24:50 -0800 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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