diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 64da263a4d..aae5c1ce6f 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -57,6 +57,9 @@ #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_REMOVE_PERTURBATION "remove_perturbation" +#define CUOPT_PRIMAL_PRICING "primal_pricing" #define CUOPT_MIP_PROBING "mip_probing" #define CUOPT_DUAL_POSTSOLVE "dual_postsolve" #define CUOPT_MIP_DETERMINISM_MODE "mip_determinism_mode" @@ -80,6 +83,9 @@ #define CUOPT_MIP_ZERO_HALF_CUTS "mip_zero_half_cuts" #define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts" #define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening" +#define CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP "mip_dual_degenerate_feasibility_pump" +#define CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS "mip_primal_degenerate_pivots" +#define CUOPT_MIP_DUAL_DEGENERATE_PIVOTS "mip_dual_degenerate_pivots" #define CUOPT_MIP_RINS "mip_rins" #define CUOPT_MIP_RENS "mip_rens" #define CUOPT_MIP_OBJECTIVE_STEP "mip_objective_step" @@ -211,7 +217,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 #define CUOPT_BARRIER_DUAL_INITIAL_POINT_AUTOMATIC -1 #define CUOPT_BARRIER_DUAL_INITIAL_POINT_LUSTIG_MARSTEN_SHANNO 0 diff --git a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp index ed1080d9a3..f12c33e818 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/solver_settings.hpp @@ -133,11 +133,14 @@ class mip_solver_settings_t { i_t clique_cuts = -1; i_t zero_half_cuts = -1; i_t implied_bound_cuts = -1; - i_t strong_chvatal_gomory_cuts = -1; - i_t reduced_cost_strengthening = -1; - i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable - f_t cut_change_threshold = -1.0; - f_t cut_min_orthogonality = 0.5; + i_t strong_chvatal_gomory_cuts = -1; + i_t reduced_cost_strengthening = -1; + i_t dual_degenerate_feasibility_pump = -1; // -1 = automatic (on), 0 = off, 1 = on + i_t primal_degenerate_pivots = -1; // -1 = automatic (on), 0 = off, 1 = on + i_t dual_degenerate_pivots = -1; // -1 = automatic (on), 0 = off, 1 = on + i_t objective_step = 1; // 0 = disable objective step tightening, 1 = enable + f_t cut_change_threshold = -1.0; + f_t cut_min_orthogonality = 0.5; i_t mip_batch_pdlp_strong_branching{ 0}; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only i_t mip_batch_pdlp_reliability_branching{ diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index bd07aaac32..cf6424fb6f 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -59,6 +59,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. @@ -68,6 +69,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 }; @@ -79,6 +81,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"; } } @@ -294,6 +297,9 @@ class pdlp_solver_settings_t { i_t augmented{-1}; i_t dualize{-1}; i_t ordering{-1}; + i_t initial_perturbation{-1}; + i_t remove_perturbation{-1}; + i_t primal_pricing{0}; barrier_dual_initial_point_t barrier_dual_initial_point{barrier_dual_initial_point_t::Automatic}; i_t postsolve_info{-1}; i_t barrier_presolve_bound_free_variables{-1}; // -1 automatic, 0 disabled, 1 enabled diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index a320cc0602..83bc52b581 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -26,8 +26,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -97,6 +99,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); } } @@ -460,6 +463,72 @@ void branch_and_bound_t::report(const lp_problem_t& lp, settings_.log.printf("%s\n", log_line.c_str()); } +template +void branch_and_bound_t::update_reduced_cost_bounds( + f_t relaxation_objective, + const std::vector& reduced_costs, + const std::vector& var_status, + reduced_cost_bounds_t& reduced_cost_bounds) +{ + const i_t n = reduced_cost_bounds.num_cols(); + const f_t threshold = 100.0 * settings_.integer_tol; + const f_t tol = 1e-2; + for (i_t j = 0; j < n; ++j) { + if (std::isfinite(reduced_costs[j]) && std::abs(reduced_costs[j]) > threshold && + var_status[j] != variable_status_t::BASIC) { + const f_t lower_j = original_lp_.lower[j]; + const f_t upper_j = original_lp_.upper[j]; + + // x_j <= l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] + // Let u_tilde_j = u_j - epsilon, so that floor(u_tilde_j) = u_j - 1 + // We want to solve for want the incumbent objective needs to be to make + // x_j <= u_tilde_j + // This means l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] <= + // u_tilde_j Or equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * + // (u_tilde_j - l_j) when reduced_costs[j] > 0 + if (lower_j > -inf && reduced_costs[j] > 0) { + const f_t u_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? upper_j - tol + : std::max(upper_j - 1.0, lower_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::floor(u_tilde_j) : u_tilde_j; + const f_t diff = u_tilde_j - lower_j; + const f_t objective_j = relaxation_objective + diff * reduced_costs[j]; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == upper_j - 1.0) || + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_upper_bound(j, objective_j, bound_j); + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. + // Info %d\n", objective_j, bound_j, j, info); + } + } + + // x_j >= u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] when + // reduced_costs[j] < 0 Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 We + // want to solve for want the incumbent objective needs to be to make x_j >= l_tilde_j This + // means u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j Or + // equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * (l_tilde_j + // - u_j) when reduced_costs[j] < 0 + if (upper_j < inf && reduced_costs[j] < 0) { + const f_t l_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? lower_j + tol + : std::min(lower_j + 1.0, upper_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::ceil(l_tilde_j) : l_tilde_j; + const f_t diff = l_tilde_j - upper_j; + const f_t objective_j = relaxation_objective + diff * reduced_costs[j]; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == lower_j + 1.0) || + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_lower_bound(j, objective_j, bound_j); + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. + // Info %d\n", objective_j, bound_j, j, info); + } + } + } + } +} + template i_t branch_and_bound_t::find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, @@ -733,10 +802,19 @@ 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); - repaired_solution = lp_solution.x; + 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) { f_t primal_error; @@ -895,6 +973,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; @@ -1661,8 +1742,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, @@ -1676,6 +1758,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) { @@ -1689,13 +1772,32 @@ 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); } stats.total_lp_solve_time += toc(lp_start_time); stats.total_simplex_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); + if (settings_.dual_degenerate_pivots != 0) { + pivot_out_integer_variables(worker->leaf_problem, + lp_settings, + worker->new_slacks, + worker->basic_list, + worker->nonbasic_list, + worker->leaf_vstatus, + worker->leaf_solution, + worker->basis_factors, + num_fractional, + fractional); + } + } } } @@ -3016,7 +3118,7 @@ void branch_and_bound_t::launch_root_heuristics( // Using shared_ptr here, so the lifetime of the object is tied to the related task. This allows // the solver to send the stop signal and immediately continue the execution. auto current_heuristic = root_heuristics.create_new_cut_pass_heuristic( - Arow_, var_types_, lp_solution.x, edge_norms_, settings_); + Arow_, var_types_, lp_solution.x, edge_norms_, new_slacks_, settings_); auto worker_count = root_heuristics.worker_count_; current_heuristic->initialize_pseudocost( @@ -3156,7 +3258,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; @@ -3172,6 +3275,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( nonbasic_list, root_vstatus_, edge_norms_, + work_estimate, nullptr); } @@ -3296,7 +3400,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( } template -auto branch_and_bound_t::do_cut_pass( +typename branch_and_bound_t::cut_pass_action_t branch_and_bound_t::do_cut_pass( [[maybe_unused]] i_t cut_pass, mip_solution_t& solution, i_t& num_fractional, @@ -3313,8 +3417,9 @@ auto branch_and_bound_t::do_cut_pass( f_t& last_upper_bound, f_t& last_objective, f_t root_relax_objective, + reduced_cost_bounds_t& reduced_cost_bounds, i_t& cut_pool_size, - [[maybe_unused]] const std::vector& saved_solution) -> cut_pass_action_t + [[maybe_unused]] const std::vector& saved_solution) { #ifdef PRINT_FRACTIONAL_INFO settings_.log.printf("Found %d fractional variables on cut pass %d\n", num_fractional, cut_pass); @@ -3358,6 +3463,47 @@ auto branch_and_bound_t::do_cut_pass( if (cut_generation_time > 1.0) { settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); } + + num_fractional = fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); + + f_t pivot_out_integer_variables_start_time = tic(); + i_t num_integer_increased = 0; + if (settings_.dual_degenerate_pivots != 0) { + num_integer_increased = pivot_out_integer_variables(original_lp_, + settings_, + new_slacks_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + } + settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", + num_integer_increased, + toc(pivot_out_integer_variables_start_time)); + if (settings_.dual_degenerate_feasibility_pump != 0) { + dual_degenerate_feasibility_pump(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + } + if (received_halt_signal()) { + solver_status_ = mip_status_t::HALT; + set_final_solution(solution, root_objective_); + return cut_pass_action_t::RETURN; + } + if (toc(exploration_stats_.start_time) >= settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return cut_pass_action_t::RETURN; + } + // Score the cuts f_t score_start_time = tic(); cut_pool.score_cuts(root_relax_soln_.x); @@ -3426,15 +3572,26 @@ auto branch_and_bound_t::do_cut_pass( if (settings_.reduced_cost_strengthening >= 1 && upper_bound_.load() < last_upper_bound) { mutex_upper_.lock(); - last_upper_bound = upper_bound_.load(); - std::vector lower_bounds; - std::vector upper_bounds; - find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); + last_upper_bound = upper_bound_.load(); + std::vector lower_bounds = original_lp_.lower; + std::vector upper_bounds = original_lp_.upper; + f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); + i_t new_bounds = reduced_cost_bounds.update_bounds_from_new_incumbent( + upper_bound_.load(), var_types_, lower_bounds, upper_bounds); mutex_upper_.unlock(); mutex_original_lp_.lock(); original_lp_.lower = lower_bounds; original_lp_.upper = upper_bounds; mutex_original_lp_.unlock(); + if (1 || new_bounds > 0) { + settings_.log.printf( + "Updated %d integer bounds using reduced cost strengthening from new incumbent. Max " + "objective %e Current objective %e Previous max objective %e\n", + new_bounds, + reduced_cost_bounds.get_max_objective(), + upper_bound_.load(), + previous_max_objective); + } } // Try to do bound strengthening @@ -3478,6 +3635,7 @@ auto branch_and_bound_t::do_cut_pass( i_t iter = 0; bool initialize_basis = false; 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, @@ -3490,6 +3648,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); @@ -3519,7 +3678,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); @@ -3542,6 +3702,32 @@ auto branch_and_bound_t::do_cut_pass( } root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + if (settings_.reduced_cost_strengthening >= 1) { + update_reduced_cost_bounds( + root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + settings_.log.printf("New reduced cost objective %e (current %e)\n", + reduced_cost_bounds.get_max_objective(), + upper_bound_.load()); + if (settings_.primal_degenerate_pivots != 0) { + pivot_to_improve_reduced_cost_strengthening(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional, + root_objective_, + reduced_cost_bounds); + } + settings_.log.printf("After pivoting: new reduced cost objective %e (current %e)\n", + reduced_cost_bounds.get_max_objective(), + upper_bound_.load()); + } + + // 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); + if (settings_.benchmark_info_ptr != nullptr) { settings_.benchmark_info_ptr->root_lp_with_cuts = compute_user_objective(original_lp_, root_objective_); @@ -3621,6 +3807,1429 @@ auto branch_and_bound_t::do_cut_pass( return cut_pass_action_t::CONTINUE; } +template +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]) <= settings_.tight_tol) { + 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& soln, + simplex::basis_update_mpf_t& basis_update, + 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 + // 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]) <= settings_.tight_tol) { + 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; + 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]) <= 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]; + 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]) <= settings_.tight_tol) { + // 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_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]; + } + } + + 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]) <= settings_.tight_tol) { + 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]) <= settings_.tight_tol) { + reduced_edge_norms[reduced_col++] = edge_norms_[j]; + } + } + + simplex::basis_update_mpf_t reduced_basis_update = basis_update; + reduced_basis_update.clear_work_estimate(); + for (i_t k = 0; k < m; k++) { + reduced_basic_list[k] = original_col_to_reduced_col[basic_list[k]]; + } + + f_t primal_work_estimate = 0.0; + 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); + 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]) <= settings_.tight_tol) { + 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] = 0.1; + } else if (reduced_vstatus[reduced_col] == variable_status_t::NONBASIC_UPPER) { + lp_reduced.objective[reduced_col] = -0.1; + } + } + reduced_col++; + } + } + + // Check reduced costs before calling primal simplex. + // Compute y = B^{-T} * c_B (BTRAN with the pump objective on basic variables) + std::vector c_basic_pump(m, 0.0); + for (i_t k = 0; k < m; k++) { + c_basic_pump[k] = lp_reduced.objective[reduced_basic_list[k]]; + } + std::vector y_pump(m); + reduced_basis_update.b_transpose_solve(c_basic_pump, y_pump); + + // Check if any nonbasic has a violated reduced cost + i_t num_violated = 0; + f_t max_violation = 0.0; + const i_t num_nonbasics_reduced = reduced_nonbasic_list.size(); + for (i_t k = 0; k < num_nonbasics_reduced; k++) { + const i_t j = reduced_nonbasic_list[k]; + // z[j] = c[j] - y^T * A(:,j) + f_t zj = lp_reduced.objective[j]; + const i_t col_start = A_reduced.col_start[j]; + const i_t col_end = A_reduced.col_start[j + 1]; + for (i_t p = col_start; p < col_end; p++) { + zj -= y_pump[A_reduced.i[p]] * A_reduced.x[p]; + } + // Check pricing condition + bool violated = false; + if (reduced_vstatus[j] == variable_status_t::NONBASIC_LOWER || + reduced_vstatus[j] == variable_status_t::NONBASIC_FIXED) { + if (zj < -settings_.dual_tol) { violated = true; } + } else if (reduced_vstatus[j] == variable_status_t::NONBASIC_UPPER) { + if (zj > settings_.dual_tol) { violated = true; } + } + if (violated) { + num_violated++; + max_violation = std::max(max_violation, std::abs(zj)); + } + } + + if (num_violated == 0) { + settings_.log.printf( + "Degenerate feasibility pump (%d/%d): skipping primal simplex, no violated reduced costs " + "(%d nonbasics checked)\n", + pump_iter, + max_pump_iter, + num_nonbasics_reduced); + primal_work_estimate += reduced_basis_update.work_estimate(); + reduced_basis_update.clear_work_estimate(); + // Don't count this as a pump iteration, but break if we've skipped twice + // in a row (perturbation isn't helping) + if (stalled) { break; } + stalled = true; + pump_iter--; + continue; + } + settings_.log.printf( + "Degenerate feasibility pump (%d/%d): %d violated reduced costs (max %.2e) out of %d " + "nonbasics\n", + pump_iter, + max_pump_iter, + num_violated, + max_violation, + num_nonbasics_reduced); + + bool recompute_basis = false; + const i_t iter_before = iter; + f_t primal_work_before = primal_work_estimate; + f_t pump_call_start_time = tic(); + simplex_solver_settings_t primal_settings = settings_; + primal_settings.log.log = false; + primal_settings.time_limit = settings_.time_limit; + primal_settings.work_limit = root_relax_work_estimate_ / 10; + settings_.log.printf( + "Degenerate feasibility pump: calling primal simplex with %d rows, %d cols, %d nnz, " + "%d basis updates, work_limit %.2e, primal_work_estimate %.2e\n", + m, + n, + A_reduced.col_start[n], + reduced_basis_update.num_updates(), + primal_settings.work_limit, + primal_work_estimate); + simplex::primal_status_t lp_status = + simplex::primal_phase2_with_advanced_basis(2, + exploration_stats_.start_time, + lp_reduced, + primal_settings, + reduced_vstatus, + reduced_basis_update, + reduced_basic_list, + reduced_nonbasic_list, + reduced_solution, + iter, + primal_work_estimate); + f_t pump_call_time = toc(pump_call_start_time); + f_t pump_call_work = primal_work_estimate - primal_work_before; + i_t pump_call_iters = iter - iter_before; + settings_.log.printf( + "Degenerate feasibility pump: primal simplex returned status %d, %d iters, " + "work %.2e (%.2e/iter), time %.2f (%.2e work/s)\n", + static_cast(lp_status), + pump_call_iters, + pump_call_work, + pump_call_iters > 0 ? pump_call_work / pump_call_iters : 0.0, + pump_call_time, + pump_call_time > 0 ? pump_call_work / pump_call_time : 0.0); + // Detect a stall: the solve made no pivots, so the incumbent vertex was + // already optimal for this objective and x did not move. Perturb next pass. + stalled = (iter == iter_before); + + 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]) <= settings_.tight_tol) { + 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); + const f_t primal_residual = 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( + "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); + if (num_fractional_reduced < best_num_fractional) { + best_num_fractional = num_fractional_reduced; + best_reduced_vstatus = reduced_vstatus; + } + } else { + settings_.log.printf( + "Degenerate feasibility pump: primal simplex returned non-optimal status %d at pump_iter " + "%d. Work estimate %.2e\n", + static_cast(lp_status), + pump_iter, + primal_work_estimate); + // Even if we hit work/time limit, the solution may have improved. + // Check fractional count before breaking. + if (lp_status == simplex::primal_status_t::WORK_LIMIT || + lp_status == simplex::primal_status_t::TIME_LIMIT) { + std::vector adjusted_solution(lp.num_cols, 0.0); + reduced_col = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (vstatus[j] == variable_status_t::BASIC || + std::abs(soln.z[j]) <= settings_.tight_tol) { + adjusted_solution[j] = reduced_solution.x[reduced_col++]; + } else { + adjusted_solution[j] = soln.x[j]; + } + } + std::vector residual = lp.rhs; + matrix_vector_multiply(lp.A, 1.0, adjusted_solution, -1.0, residual); + const f_t primal_residual = vector_norm_inf(residual); + if (primal_residual <= 1e-6) { + std::vector tmp_fractional; + i_t num_fractional_reduced = + fractional_variables(settings_, adjusted_solution, var_types_, tmp_fractional); + settings_.log.printf( + "Degenerate feasibility pump (%d/%d): after work/time limit, fractional " + "variables %d/%d\n", + pump_iter, + max_pump_iter, + num_fractional_reduced, + num_fractional); + if (num_fractional_reduced < best_num_fractional) { + best_num_fractional = num_fractional_reduced; + best_reduced_vstatus = reduced_vstatus; + } + } + } + break; + } + } + + settings_.log.printf( + "Degenerate feasibility pump: Simplex iterations %d, Best number of fractional variables " + "%d/%d. Work estimate %.2e, Time %.2f, Basis updates %d\n", + iter, + best_num_fractional, + num_fractional, + primal_work_estimate, + toc(dual_degenerate_feasibility_pump_start_time), + reduced_basis_update.num_updates()); + if (best_num_fractional < num_fractional) { + // Translate the vstatus from the reduced problem to the vstatus for the original problem + i_t reduced_cols = 0; + 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) { + 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) { + return; + } + if (refactor_status != 0) { + // TODO: On failure vstatus, basic_list, and nonbasic_list are in a bad state. + // We should save copies before the failure and restore them after the failure. + settings_.log.printf( + "Failed to refactor basis after dual degenerate feasibility pump. " + "%d deficient columns.\n", + 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 { + 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]; + + 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; + } + } + + // Then solve B xB = rhs + std::vector xB(lp.num_rows); + basis_update.b_solve(rhs, xB); + + // 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); + } +} + +template +i_t branch_and_bound_t::apply_delta_x_for_integer_pivot( + const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + 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) { + if (leaving_index == -1) { + return -4; // unbounded or entering hit its own bound + } else if (var_types_[leaving_index] != variable_type_t::INTEGER) { + return -5; // continuous variable won ratio test + } else { + return -6; // integer variable won but it's not fractional (already at integer value) + } + } + + std::vector test_x = solution.x; + i_t integer_destroyed = 0; + 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 -2; } + + 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 -3; } + if (rank < 0 || rank != lp.num_rows) { return -3; } + simplex::reorder_basic_list(q, basic_list); + basis_update.reset(L, U, p); + } + + return 0; +} + +template +void branch_and_bound_t::fast_slack_integer_pivots( + const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + const std::vector& fractional, + const std::vector& row_to_slack, + const simplex::lp_solution_t& solution, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate) +{ + 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; + 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[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); + fast_nonbasic_slacks.push_back(nonbasic_slack); + } + } + + if (fast_candidates.size() > 0 && settings_.inside_mip < 2) { + settings.log.printf("Found %ld fast candidates for pivot out integer variables\n", + fast_candidates.size()); + } + + // Build a reverse index nonbasic_index[v] = position of v in nonbasic_list, or -1 if not + // present. Used to locate the entering variable's slot in the fast-candidate path. + // apply_delta_x_for_integer_pivot keeps this index consistent by applying an O(1) fix-up + // on each successful pivot; the two variables whose (non)basic status changes are the only + // entries that need to be updated. + nonbasic_index.assign(lp.num_cols, -1); + for (i_t p = 0; p < static_cast(nonbasic_list.size()); ++p) { + nonbasic_index[nonbasic_list[p]] = p; + } + + const i_t num_candidates = fast_candidates.size(); + f_t last_log = tic(); + f_t loop_start = tic(); + for (i_t k = 0; k < num_candidates; k++) { + const i_t j = fast_candidates[k]; + const i_t row = fast_rows[k]; + const i_t nonbasic_slack = fast_nonbasic_slacks[k]; + // Skip if state changed by a prior successful pivot. + if (vstatus[j] != variable_status_t::BASIC) { continue; } + if (vstatus[nonbasic_slack] == variable_status_t::BASIC) { continue; } + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + f_t a_ij = 0.0; + 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; + } + } + 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.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 + // 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); + 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 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); + } + + // 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_sparse.i[h]; + if (jj == j) continue; + const f_t val = delta_x_sparse.x[h]; + const f_t slack_value = soln.x[jj]; + if (val < -slack_value) { + ok = false; + break; + } + } + 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); + + // 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[h]]; + } + std::vector utilde_dense; + basis_update.u_multiply(b_inv_abar, utilde_dense); + sparse_vector_t utilde_sparse; + utilde_sparse.from_dense(utilde_dense); + + i_t error = apply_delta_x_for_integer_pivot(lp, + basic_list, + nonbasic_list, + nonbasic_index, + vstatus, + entering_index, + nonbasic_entering, + direction, + delta_x, + utilde_sparse, + soln, + basis_update, + work_estimate); + // apply_delta_x_for_integer_pivot only mutates vstatus when the pivot actually fires, + // so entering_index transitioning to BASIC is a reliable success signal. + if (!error && settings.inside_mip < 2) { + settings.log.printf( + "Fast candidate pivot succeeded: j=%d entering slack=%d row=%d\n", j, entering_index, row); + } + + if (settings.inside_mip < 2 && toc(last_log) > 1.0) { + settings.log.printf("Fast candidates %d/%d processed in %.2f seconds\n", + k + 1, + num_candidates, + toc(loop_start)); + last_log = tic(); + } + } + if (settings.inside_mip < 2) { + settings.log.printf("Fast candidates: %d/%d processed in %.2f seconds\n", + num_candidates, + num_candidates, + toc(loop_start)); + } +} + +template +i_t branch_and_bound_t::pivot_out_integer_variables( + const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + const std::vector& new_slacks, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& solution, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional) +{ + if (num_fractional == 0) { return 0; } + f_t pivot_out_integer_variables_start_time = tic(); + std::vector zero_reduced_costs_vars; + std::vector zero_reduced_costs_vars_nonbasic_index; + bool dual_degenerate = check_for_dual_degeneracy( + solution, nonbasic_list, zero_reduced_costs_vars, zero_reduced_costs_vars_nonbasic_index); + if (!dual_degenerate) { return 0; } + + lp_solution_t soln_copy = solution; + std::vector basic_list_copy = basic_list; + std::vector nonbasic_list_copy = nonbasic_list; + std::vector vstatus_copy = vstatus; + simplex::basis_update_mpf_t basis_update_copy = basis_update; + + const i_t start_num_fractional = num_fractional; + + const i_t num_zero_reduced_costs_vars = zero_reduced_costs_vars.size(); + + std::vector row_to_slack(lp.num_rows, -1); + for (i_t j : new_slacks) { + if (lp.lower[j] != 0 || lp.upper[j] != inf) { continue; } + const i_t p = lp.A.col_start[j]; + row_to_slack[lp.A.i[p]] = j; + } + + f_t work_estimate = 0.0; + + // Count primal degenerate basic variables + i_t num_degenerate = 0; + i_t num_degenerate_continuous = 0; + i_t num_degenerate_integer = 0; + for (i_t k = 0; k < lp.num_rows; k++) { + const i_t j = basic_list_copy[k]; + const f_t slack_to_lower = soln_copy.x[j] - lp.lower[j]; + const f_t slack_to_upper = lp.upper[j] - soln_copy.x[j]; + if (slack_to_lower <= settings_.primal_tol || slack_to_upper <= settings_.primal_tol) { + num_degenerate++; + if (var_types_[j] == variable_type_t::INTEGER) { + num_degenerate_integer++; + } else { + num_degenerate_continuous++; + } + } + } + const f_t degeneracy_fraction = static_cast(num_degenerate) / lp.num_rows; + if (settings.inside_mip < 2 && settings.inside_submip == 0) { + settings.log.printf( + "Primal degeneracy: %d/%d basic variables are degenerate (%.1f%%), " + "continuous=%d, integer=%d\n", + num_degenerate, + lp.num_rows, + 100.0 * degeneracy_fraction, + num_degenerate_continuous, + num_degenerate_integer); + } + + // Skip pivot_out entirely if primal degeneracy is too high — the ratio test + // will almost always be won by a degenerate variable, making pivots hopeless. + if (degeneracy_fraction > 0.5) { + if (settings.inside_mip < 2) { + settings.log.printf("Skipping pivot_out_integer_variables: degeneracy %.1f%% > 50%%\n", + 100.0 * degeneracy_fraction); + } + return 0; + } + + std::vector nonbasic_index; + fast_slack_integer_pivots(lp, + settings, + fractional, + row_to_slack, + solution, + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + soln_copy, + basis_update_copy, + work_estimate); + + std::vector work_list = fractional; + std::vector to_basic_position(lp.num_cols, -1); + + for (i_t k = 0; k < lp.num_rows; k++) { + to_basic_position[basic_list_copy[k]] = k; + } + + sparse_vector_t ep; + ep.n = lp.num_rows; + ep.i.resize(1); + ep.x.resize(1); + ep.x[0] = 1.0; + + std::vector delta_y_dense(lp.num_rows, 0.0); + + // Track which entering variables are actually tried (to detect duplication) + std::vector entering_tried_count(lp.num_cols, 0); + + i_t worklist_total_processed = 0; + i_t worklist_skipped = 0; + i_t worklist_btran_done = 0; + i_t worklist_ftran_done = 0; + i_t worklist_pivots_succeeded = 0; + i_t worklist_readded = 0; + f_t worklist_btran_time = 0.0; + f_t worklist_dot_time = 0.0; + f_t worklist_ftran_time = 0.0; + i_t worklist_no_candidates = 0; // target had no nonzero dot_q + i_t worklist_ratio_test_fail = 0; // ratio test didn't pick a fractional integer (error -1) + i_t worklist_net_increase_fail = 0; // pivot would net-increase fractionals (error -2) + i_t worklist_unbounded = 0; // entering hit its own bound or unbounded (error -4) + i_t worklist_continuous_won = 0; // continuous variable won ratio test (error -5) + i_t worklist_nonfrac_int_won = 0; // non-fractional integer won ratio test (error -6) + + f_t worklist_loop_start = tic(); + f_t worklist_last_log = tic(); + + while (!work_list.empty()) { + const i_t j = work_list.back(); + const i_t p = to_basic_position[j]; + work_list.pop_back(); + worklist_total_processed++; + + // Skip if j is no longer basic and fractional (may have been fixed by a prior pivot) + if (p < 0) { + worklist_skipped++; + continue; + } + if (vstatus_copy[j] != variable_status_t::BASIC) { + worklist_skipped++; + continue; + } + if (!is_fractional(soln_copy.x[j], var_types_[j], settings_.integer_tol)) { + worklist_skipped++; + continue; + } + + // We want to pivot variable j out of the basis. + // We solve B^T * delta_y = e_p, where p is the position of j in the basis. + // Or delta_y = B^{-T} e_p, or delta_y^T = e_p^T B^{-T} + + ep.i[0] = p; + sparse_vector_t delta_y_sparse; + sparse_vector_t UTsol_sparse; + f_t btran_start = tic(); + basis_update_copy.b_transpose_solve(ep, delta_y_sparse, UTsol_sparse); + worklist_btran_time += toc(btran_start); + worklist_btran_done++; + + // Scatter delta_y_sparse into dense workspace for dot product computation + const i_t delta_y_nz = delta_y_sparse.i.size(); + for (i_t h = 0; h < delta_y_nz; h++) { + delta_y_dense[delta_y_sparse.i[h]] = delta_y_sparse.x[h]; + } + + // We also have that + // B*delta_xB + N*delta_xN = 0 + // So delta_xB = -B^{-1} N * delta_xN + // And delta_xB[p] = e_p^T * delta_xB = -e_p^T B^{-1} N * delta_xN + // = -delta_y^T N * delta_xN + // Recall that delta_xN = e_q where q is the entering variables + // So delta_xB[p] = -delta_y^T A(:, q) + // + // For p to be the leaving variable, we need it to be the binding + // member in the ratio test + // x_B + alpha * delta_xB >= l_B + // x_B + alpha * delta_xB <= u_B + // + // Or alpha <= (l_B[p] - x_B[p]) / delta_xB[p] when delta_xB[p] < 0 + // Or alpha <= (u_B[p] - x_B[p]) / delta_xB[p] when delta_xB[p] > 0 + // + // Thus, if we want to push x_B[p] up to u_B[p], we want + // alpha = (u_B[p] - x_B[p]) / delta_xB[p] to be small + // And if we want to push x_B[p] down to l_B[p], we want + // alpha = (l_B[p] - x_B[p]) / delta_xB[p] to be small + // + // Or equivalently, we want delta_xB[p] to be large + + // Find top 3 candidates by merit = |dot_q| / nnz(A(:,q)) + // Large |dot_q| means the target moves a lot (small step to hit bound). + // Small nnz means the FTRAN result is likely sparse, so fewer competing + // basic variables will have nonzero delta_xB components to block the target. + // Skip entering variables that have already been tried (and failed) by prior targets. + f_t values[3] = {0.0, 0.0, 0.0}; + i_t indices[3] = {-1, -1, -1}; + f_t dot_start = tic(); + for (i_t q : zero_reduced_costs_vars) { + if (var_types_[q] == variable_type_t::INTEGER) { continue; } + if (nonbasic_index[q] < 0) { continue; } + if (entering_tried_count[q] > 0) { continue; } + // Compute dot_q = delta_y^T * A(:, q) using dense delta_y + const i_t col_start = lp.A.col_start[q]; + const i_t col_end = lp.A.col_start[q + 1]; + const i_t col_nnz = col_end - col_start; + f_t dot_q = 0.0; + for (i_t pp = col_start; pp < col_end; pp++) { + dot_q += delta_y_dense[lp.A.i[pp]] * lp.A.x[pp]; + } + const f_t abs_dot_q = std::abs(dot_q); + if (abs_dot_q <= 1e-12) { continue; } + const f_t merit = abs_dot_q / static_cast(col_nnz); + + if (merit > values[0]) { + indices[2] = indices[1]; + values[2] = values[1]; + indices[1] = indices[0]; + values[1] = values[0]; + indices[0] = q; + values[0] = merit; + } else if (merit > values[1]) { + indices[2] = indices[1]; + values[2] = values[1]; + indices[1] = q; + values[1] = merit; + } else if (merit > values[2]) { + indices[2] = q; + values[2] = merit; + } + } + worklist_dot_time += toc(dot_start); + + if (indices[0] == -1) { worklist_no_candidates++; } + + // Try the top 3 candidates + for (i_t h = 0; h < 3; h++) { + if (indices[h] == -1) break; + + const i_t q = indices[h]; + const i_t entering_index = q; + const i_t nonbasic_entering = nonbasic_index[q]; + if (nonbasic_entering < 0) { continue; } + entering_tried_count[q]++; + + // Determine direction based on entering variable's status + const i_t direction = (vstatus_copy[q] == variable_status_t::NONBASIC_LOWER || + vstatus_copy[q] == variable_status_t::NONBASIC_FIXED) + ? 1 + : -1; + + // Solve B * delta_xB = A(:, q) so utilde is valid for the MPF update. + sparse_vector_t rhs(lp.A, q); + sparse_vector_t delta_xB; + sparse_vector_t utilde_sparse; + f_t ftran_start = tic(); + basis_update_copy.b_solve(rhs, delta_xB, utilde_sparse); + worklist_ftran_time += toc(ftran_start); + worklist_ftran_done++; + + std::vector delta_xB_dense; + delta_xB.to_dense(delta_xB_dense); + std::vector delta_x(lp.num_cols, 0.0); + for (i_t i = 0; i < lp.num_rows; i++) { + delta_x[basic_list_copy[i]] = -direction * delta_xB_dense[i]; + } + delta_x[q] = direction; + + i_t error = apply_delta_x_for_integer_pivot(lp, + basic_list_copy, + nonbasic_list_copy, + nonbasic_index, + vstatus_copy, + entering_index, + nonbasic_entering, + direction, + delta_x, + utilde_sparse, + soln_copy, + basis_update_copy, + work_estimate); + + if (error == -2) { worklist_net_increase_fail++; } + if (error == -4) { + worklist_unbounded++; + worklist_ratio_test_fail++; + } + if (error == -5) { + worklist_continuous_won++; + worklist_ratio_test_fail++; + } + if (error == -6) { + worklist_nonfrac_int_won++; + worklist_ratio_test_fail++; + } + + if (!error) { + worklist_pivots_succeeded++; + // Update to_basic_position for the variables that changed status + // entering_index is now basic, leaving_index is now nonbasic + // Find the leaving variable: it's the one that took entering_index's slot in nonbasic_list + const i_t leaving_index = nonbasic_list_copy[nonbasic_entering]; + to_basic_position[entering_index] = to_basic_position[leaving_index]; + to_basic_position[leaving_index] = -1; + + // We did a successful pivot; add fractional variables whose values changed to work list + for (i_t k : fractional) { + if (vstatus_copy[k] != variable_status_t::BASIC) { continue; } + if (std::abs(delta_x[k]) > settings_.zero_tol) { + // work_list.push_back(k); + // worklist_readded++; + } + } + break; + } + } + + // Clear dense workspace for next target + for (i_t h = 0; h < delta_y_nz; h++) { + delta_y_dense[delta_y_sparse.i[h]] = 0.0; + } + + if (toc(worklist_last_log) > 1.0) { + if (settings.inside_mip < 2) { + settings.log.printf( + "Worklist progress: %d/%d processed, %d pivots, %d ratio_fail (unb=%d cont=%d nfint=%d), " + "%d net_inc_fail, %d no_cand, %.2f seconds\n", + worklist_total_processed, + static_cast(fractional.size()), + worklist_pivots_succeeded, + worklist_ratio_test_fail, + worklist_unbounded, + worklist_continuous_won, + worklist_nonfrac_int_won, + worklist_net_increase_fail, + worklist_no_candidates, + toc(worklist_loop_start)); + } + worklist_last_log = tic(); + } + } + + // Count unique entering variables and duplication + i_t unique_entering = 0; + i_t max_entering_count = 0; + i_t entering_tried_once = 0; + i_t entering_tried_multiple = 0; + for (i_t q = 0; q < lp.num_cols; q++) { + if (entering_tried_count[q] > 0) { + unique_entering++; + max_entering_count = std::max(max_entering_count, entering_tried_count[q]); + if (entering_tried_count[q] == 1) { + entering_tried_once++; + } else { + entering_tried_multiple++; + } + } + } + if (settings.inside_mip < 2) { + settings.log.printf( + "Worklist entering stats: unique=%d, tried_once=%d, tried_multiple=%d, " + "max_count=%d, total_ftran=%d, duplication_ratio=%.1fx\n", + unique_entering, + entering_tried_once, + entering_tried_multiple, + max_entering_count, + worklist_ftran_done, + worklist_ftran_done / std::max(1.0, static_cast(unique_entering))); + + settings.log.printf( + "Worklist stats: processed=%d skipped=%d btran=%d ftran=%d pivots=%d readded=%d " + "btran_time=%.2f dot_time=%.2f ftran_time=%.2f zero_rc_vars=%d " + "no_candidates=%d ratio_test_fail=%d (unbounded=%d continuous_won=%d nonfrac_int_won=%d) " + "net_increase_fail=%d\n", + worklist_total_processed, + worklist_skipped, + worklist_btran_done, + worklist_ftran_done, + worklist_pivots_succeeded, + worklist_readded, + worklist_btran_time, + worklist_dot_time, + worklist_ftran_time, + num_zero_reduced_costs_vars, + worklist_no_candidates, + worklist_ratio_test_fail, + worklist_unbounded, + worklist_continuous_won, + worklist_nonfrac_int_won, + worklist_net_increase_fail); + } + + std::vector new_fractional; + const i_t num_new_fractional = + 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); +#if 0 + settings.log.printf("Pivoted out %d integer variables: %d -> %d in %.2f\n", + num_integer_increased, + start_num_fractional, + num_new_fractional, + toc(pivot_out_integer_variables_start_time)); +#endif + num_fractional = num_new_fractional; + fractional = new_fractional; + basic_list = basic_list_copy; + nonbasic_list = nonbasic_list_copy; + vstatus = vstatus_copy; + basis_update = basis_update_copy; + solution = soln_copy; + return num_integer_increased; + } + return 0; +} + +template +void branch_and_bound_t::pivot_to_improve_reduced_cost_strengthening( + const simplex::lp_problem_t& lp, + const std::vector& basic_list, + const std::vector& nonbasic_list, + const std::vector& vstatus, + const simplex::lp_solution_t& soln, + const simplex::basis_update_mpf_t& basis_update, + const i_t num_fractional, + const std::vector& fractional, + const f_t relaxation_objective, + reduced_cost_bounds_t& reduced_cost_bounds) +{ + // Count primal degenerate basic variables + i_t num_degenerate = 0; + i_t num_degenerate_continuous = 0; + i_t num_degenerate_integer = 0; + std::vector degenerate_integer_list; + degenerate_integer_list.reserve(lp.num_rows); + for (i_t k = 0; k < lp.num_rows; k++) { + const i_t j = basic_list[k]; + const f_t slack_to_lower = soln.x[j] - lp.lower[j]; + const f_t slack_to_upper = lp.upper[j] - soln.x[j]; + if (slack_to_lower <= settings_.primal_tol || slack_to_upper <= settings_.primal_tol) { + num_degenerate++; + if (var_types_[j] == variable_type_t::INTEGER) { + num_degenerate_integer++; + degenerate_integer_list.push_back(j); + } else { + num_degenerate_continuous++; + } + } + } + + if (num_degenerate_integer == 0) return; + + std::vector variable_to_basic_position(lp.num_cols, -1); + for (i_t k = 0; k < lp.num_rows; k++) { + variable_to_basic_position[basic_list[k]] = k; + } + std::vector delta_y(lp.num_rows, 0); + std::vector delta_z(lp.num_cols, 0); + std::vector delta_z_mark(lp.num_cols, 0); + std::vector delta_z_indices; + delta_z_indices.reserve(lp.num_cols); + + f_t work_estimate = 0; + const f_t threshold = 100.0 * settings_.integer_tol; + const f_t tol = 1e-2; + const f_t zero_tol = settings_.zero_tol; + const f_t harris_tol = settings_.dual_tol / 10; + + i_t num_bounds_added = 0; + for (i_t j : degenerate_integer_list) { + // x_j is a degenerate integer basic variable. + // We would like a dual-feasible point where x_j is nonbasic with a nonzero + // reduced cost that may be used for reduced cost strengthening. + // We do not need to take the pivot; a dual step along either ray is enough. + // + // One BTRAN: B^T * delta_y = e_p, which matches direction == -1 in + // compute_reduced_cost_update (B^T * delta_y = -direction * e_p). + // The opposite direction is the negated (delta_y, delta_z) ray. + const i_t leaving_index = j; + const i_t p = variable_to_basic_position[j]; + if (p == -1) continue; + + sparse_vector_t ep(lp.num_rows, 1); + ep.i[0] = p; + ep.x[0] = 1.0; + sparse_vector_t delta_y_sparse; + sparse_vector_t UTsol_sparse; + basis_update.b_transpose_solve(ep, delta_y_sparse, UTsol_sparse); + + // delta_zN = -N^T * delta_y, delta_z[leaving] = -1 + delta_y_sparse.to_dense(delta_y); + simplex::compute_reduced_cost_update(lp, + basic_list, + nonbasic_list, + delta_y, + leaving_index, + /*direction=*/-1, + delta_z_mark, + delta_z_indices, + delta_z, + work_estimate); + + const f_t lower_j = lp.lower[j]; + const f_t upper_j = lp.upper[j]; + const bool at_lower = soln.x[j] - lower_j <= settings_.primal_tol; + const bool at_upper = upper_j - soln.x[j] <= settings_.primal_tol; + + // Try both dual rays. scale == +1 uses the computed delta_z (direction -1); + // scale == -1 uses -delta_z (direction +1). Either or both may yield an RCS bound. + for (const f_t scale : {1.0, -1.0}) { + // Maximum dual step-length alpha that keeps dual feasibility on this ray. + // zl_j + alpha * delta_zN_j >= 0 for nonbasic j on lower bound + // zu_j + alpha * delta_zN_j <= 0 for nonbasic j on upper bound + f_t alpha = inf; + for (i_t jj : delta_z_indices) { + if (vstatus[jj] == variable_status_t::NONBASIC_FIXED) { continue; } + const f_t dz = scale * delta_z[jj]; + if (vstatus[jj] == variable_status_t::NONBASIC_LOWER && dz < -zero_tol) { + const f_t ratio = std::max((-harris_tol - soln.z[jj]) / dz, 0.0); + if (ratio < alpha) { alpha = ratio; } + } + if (vstatus[jj] == variable_status_t::NONBASIC_UPPER && dz > zero_tol) { + const f_t ratio = std::max((harris_tol - soln.z[jj]) / dz, 0.0); + if (ratio < alpha) { alpha = ratio; } + } + } + if (alpha == 0.0 || !std::isfinite(alpha)) { continue; } + + // Verify dual feasibility of the new point z_new = z + alpha * scale * delta_z + // For NONBASIC_LOWER: z_new[jj] >= -dual_tol + // For NONBASIC_UPPER: z_new[jj] <= dual_tol + { + f_t max_initial_dual_infeas = 0.0; + f_t max_dual_infeas = 0.0; + f_t worst_old_z = 0.0; + f_t worst_delta_z = 0.0; + f_t worst_step = 0.0; + f_t worst_new_z = 0.0; + i_t num_initial_dual_infeas = 0; + i_t num_dual_infeas = 0; + i_t worst_j = -1; + for (i_t jj : delta_z_indices) { + if (vstatus[jj] == variable_status_t::NONBASIC_FIXED) { continue; } + const f_t old_zj = soln.z[jj]; + const f_t step = alpha * scale * delta_z[jj]; + const f_t new_zj = old_zj + step; + const bool initially_infeasible = + (vstatus[jj] == variable_status_t::NONBASIC_LOWER && old_zj < -settings_.dual_tol) || + (vstatus[jj] == variable_status_t::NONBASIC_UPPER && old_zj > settings_.dual_tol); + if (initially_infeasible) { + num_initial_dual_infeas++; + max_initial_dual_infeas = std::max(max_initial_dual_infeas, std::abs(old_zj)); + } + if (vstatus[jj] == variable_status_t::NONBASIC_LOWER && new_zj < -settings_.dual_tol) { + num_dual_infeas++; + if (std::abs(new_zj) > max_dual_infeas) { + max_dual_infeas = std::abs(new_zj); + worst_j = jj; + worst_old_z = old_zj; + worst_delta_z = scale * delta_z[jj]; + worst_step = step; + worst_new_z = new_zj; + } + } + if (vstatus[jj] == variable_status_t::NONBASIC_UPPER && new_zj > settings_.dual_tol) { + num_dual_infeas++; + if (std::abs(new_zj) > max_dual_infeas) { + max_dual_infeas = std::abs(new_zj); + worst_j = jj; + worst_old_z = old_zj; + worst_delta_z = scale * delta_z[jj]; + worst_step = step; + worst_new_z = new_zj; + } + } + } + // Also check the leaving variable itself + const f_t new_zj_leaving = soln.z[j] + alpha * scale * delta_z[j]; + if (num_dual_infeas > 0) { + settings_.log.printf( + "WARNING pivot_to_improve_rc: dual infeasibility after step! " + "var=%d alpha=%.6e scale=%.0f initial_num_infeas=%d " + "initial_max_infeas=%.6e num_infeas=%d max_infeas=%.6e worst_j=%d " + "worst_status=%d old_z=%.16e delta_z=%.16e step=%.16e new_z=%.16e " + "new_rc_leaving=%.6e\n", + j, + alpha, + scale, + num_initial_dual_infeas, + max_initial_dual_infeas, + num_dual_infeas, + max_dual_infeas, + worst_j, + static_cast(vstatus[worst_j]), + worst_old_z, + worst_delta_z, + worst_step, + worst_new_z, + new_zj_leaving); + } + } + + // Claim: We don't actually need to take a pivot if all we want to do is add a bound + // coming from reduced cost strengthening + const f_t new_reduced_cost = soln.z[j] + alpha * scale * delta_z[j]; + + // x_j <= l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] + // Let u_tilde_j = u_j - epsilon, so that floor(u_tilde_j) = u_j - 1 + // We want to solve for want the incumbent objective needs to be to make + // x_j <= u_tilde_j + // This means l_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] <= + // u_tilde_j Or equivalently, incumbent_objective <= relaxation_objective + + // reduced_costs[j] * (u_tilde_j - l_j) when reduced_costs[j] > 0 + if (at_lower && lower_j > -inf && new_reduced_cost > threshold) { + const f_t u_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? upper_j - tol + : std::max(upper_j - 1.0, lower_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::floor(u_tilde_j) : u_tilde_j; + const f_t diff = u_tilde_j - lower_j; + const f_t objective_j = relaxation_objective + diff * new_reduced_cost; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == upper_j - 1.0) || + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_upper_bound(j, objective_j, bound_j); + if (info > 0) { num_bounds_added++; } + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d upper bound. + // Info %d\n", objective_j, bound_j, j, info); + } + } + + // x_j >= u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] when + // reduced_costs[j] < 0 Let l_tilde_j = l_j + epsilon, so that ceil(l_tilde_j) = l_j + 1 We + // want to solve for want the incumbent objective needs to be to make x_j >= l_tilde_j This + // means u_j + (incumbent_objective - relaxation_objective) / reduced_costs[j] >= l_tilde_j Or + // equivalently, incumbent_objective <= relaxation_objective + reduced_costs[j] * + // (l_tilde_j - u_j) when reduced_costs[j] < 0 + if (at_upper && upper_j < inf && new_reduced_cost < -threshold) { + const f_t l_tilde_j = var_types_[j] == variable_type_t::INTEGER + ? lower_j + tol + : std::min(lower_j + 1.0, upper_j); + const f_t bound_j = + var_types_[j] == variable_type_t::INTEGER ? std::ceil(l_tilde_j) : l_tilde_j; + const f_t diff = l_tilde_j - upper_j; + const f_t objective_j = relaxation_objective + diff * new_reduced_cost; + if (((var_types_[j] == variable_type_t::INTEGER && bound_j == lower_j + 1.0) || + var_types_[j] != variable_type_t::INTEGER) && + std::isfinite(objective_j) && std::isfinite(bound_j)) { + i_t info = reduced_cost_bounds.add_lower_bound(j, objective_j, bound_j); + if (info > 0) { num_bounds_added++; } + // settings_.log.printf("Added objective bound pair (%e, %e) for variable %d lower bound. + // Info %d\n", objective_j, bound_j, j, info); + } + } + } + + // Clear arrays for next iteration + for (i_t k : delta_z_indices) { + delta_z_mark[k] = 0; + delta_z[k] = 0.0; + } + delta_z[leaving_index] = 0.0; + delta_z_indices.clear(); + for (i_t k : delta_y_sparse.i) { + delta_y[k] = 0.0; + } + } + settings_.log.printf("Added %d bounds for reduced cost strengthening\n", num_bounds_added); +} + template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -3705,7 +5314,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut solving_root_relaxation_ = true; f_t root_relax_start_time = tic(); - + root_relax_work_estimate_ = 0.0; if (!enable_concurrent_lp_root_solve()) { // RINS/SUBMIP path settings_.log.printf("\n"); @@ -3720,7 +5329,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; @@ -3733,13 +5343,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_); } settings_.log.printf("\n"); 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; // This stops the clique table generation when the solve exit early. Note that we cannot // do this in the destructor since we store the clique_table as a pointer. @@ -3792,6 +5404,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("Root relaxation objective %+.8e\n\n", root_relax_soln_.user_objective); assert(root_vstatus_.size() == original_lp_.num_cols); @@ -3832,6 +5448,68 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut is_running_ = true; lower_bound_numerical_ = inf; + reduced_cost_bounds_t reduced_cost_bounds(original_lp_.num_cols); + update_reduced_cost_bounds( + root_objective_, root_relax_soln_.z, root_vstatus_, reduced_cost_bounds); + settings_.log.printf("New reduced cost objective %e (current %e)\n", + reduced_cost_bounds.get_max_objective(), + upper_bound_.load()); + if (settings_.primal_degenerate_pivots != 0) { + pivot_to_improve_reduced_cost_strengthening(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional, + root_objective_, + reduced_cost_bounds); + } + settings_.log.printf("After pivoting: new reduced cost objective %e (current %e)\n", + reduced_cost_bounds.get_max_objective(), + upper_bound_.load()); + + f_t pivot_out_integer_variables_start_time = tic(); + i_t num_integer_increased = 0; + if (settings_.dual_degenerate_pivots != 0) { + num_integer_increased = pivot_out_integer_variables(original_lp_, + settings_, + new_slacks_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + } + settings_.log.printf("Pivoted out %d integer variables in %e seconds\n", + num_integer_increased, + toc(pivot_out_integer_variables_start_time)); + + if (settings_.dual_degenerate_feasibility_pump != 0) { + dual_degenerate_feasibility_pump(original_lp_, + basic_list, + nonbasic_list, + root_vstatus_, + root_relax_soln_, + basis_update, + num_fractional, + fractional); + } + + if (received_halt_signal()) { + solver_status_ = mip_status_t::HALT; + set_final_solution(solution, root_objective_); + return solver_status_; + } + if (toc(exploration_stats_.start_time) >= settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } + if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } cut_pool_t cut_pool(original_lp_.num_cols, settings_); @@ -3920,6 +5598,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut last_upper_bound, last_objective, root_relax_objective, + reduced_cost_bounds, cut_pool_size, saved_solution); @@ -4005,10 +5684,23 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (settings_.reduced_cost_strengthening >= 2 && upper_bound_.load() < last_upper_bound) { - std::vector lower_bounds; - std::vector upper_bounds; - i_t num_fixed = find_reduced_cost_fixings(upper_bound_.load(), lower_bounds, upper_bounds); - if (num_fixed > 0) { + std::vector lower_bounds = original_lp_.lower; + std::vector upper_bounds = original_lp_.upper; + f_t previous_max_objective = reduced_cost_bounds.get_max_objective(); + i_t num_changed = reduced_cost_bounds.update_bounds_from_new_incumbent( + upper_bound_.load(), var_types_, lower_bounds, upper_bounds); + settings_.log.printf( + "Updated %d integer bounds using reduced cost strengthening from new incumbent. Max " + "objective %e Current objective %e Previous max objective %e\n", + num_changed, + reduced_cost_bounds.get_max_objective(), + upper_bound_.load(), + previous_max_objective); + mutex_original_lp_.lock(); + original_lp_.lower = lower_bounds; + original_lp_.upper = upper_bounds; + mutex_original_lp_.unlock(); + if (num_changed > 0) { std::vector bounds_changed(original_lp_.num_cols, true); std::vector row_sense; @@ -4100,19 +5792,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut var_types_, symmetry_, settings_, - pc_, - root_relax_soln_.x, - edge_norms_); + pc_, + root_relax_soln_.x, + edge_norms_, + new_slacks_); submip_worker_pool_.init(num_submip_workers, original_lp_, Arow_, var_types_, symmetry_, settings_, - pc_, - root_relax_soln_.x, - edge_norms_, - num_bfs_workers); + pc_, + root_relax_soln_.x, + edge_norms_, + new_slacks_, + num_bfs_workers); if (num_diving_workers > 0) { diving_worker_pool_.init(num_diving_workers, @@ -4124,6 +5818,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut pc_, root_relax_soln_.x, edge_norms_, + new_slacks_, num_bfs_workers + num_submip_workers); } @@ -4323,9 +6018,10 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri Arow, var_types_, settings_, - pc_, - root_relax_soln_.x, - edge_norms_); + pc_, + root_relax_soln_.x, + edge_norms_, + new_slacks_); if (num_diving_workers > 0) { // Extract diving types from search_strategies (skip BEST_FIRST at index 0) @@ -4342,7 +6038,8 @@ void branch_and_bound_t::run_deterministic_coordinator(const csr_matri settings_, pc_, root_relax_soln_.x, - edge_norms_); + edge_norms_, + new_slacks_); } } @@ -4723,8 +6420,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_; - - 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, @@ -4736,6 +6433,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); @@ -4751,6 +6449,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); } @@ -5337,6 +7036,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); @@ -5352,6 +7052,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); @@ -5365,6 +7066,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 3ce193a30f..fdc0cf41d5 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -92,6 +93,144 @@ struct deterministic_bfs_policy_t; template struct deterministic_diving_policy_t; +template +struct objective_bound_pair_t { + objective_bound_pair_t() + : objective(std::numeric_limits::quiet_NaN()), bound(std::numeric_limits::quiet_NaN()) + { + } + objective_bound_pair_t(f_t objective_in, f_t bound_in) : objective(objective_in), bound(bound_in) + { + } + bool is_valid() { return objective == objective && bound == bound; } + f_t objective; + f_t bound; +}; + +template +class reduced_cost_bounds_t { + public: + reduced_cost_bounds_t(i_t original_cols) + : max_objective_(-std::numeric_limits::infinity()), + lower_bounds_(original_cols), + upper_bounds_(original_cols) + { + } + + i_t add_lower_bound(i_t col, f_t objective, f_t bound) + { + if (col < static_cast(lower_bounds_.size())) { + if (!lower_bounds_[col].is_valid()) { + lower_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { max_objective_ = objective; } + return 1; + } else { + if (bound > lower_bounds_[col].bound) { + lower_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { max_objective_ = objective; } + return 2; + } else if (bound == lower_bounds_[col].bound && objective > lower_bounds_[col].objective) { + lower_bounds_[col].objective = objective; + if (objective > max_objective_) { max_objective_ = objective; } + return 1; + } else { + return -2; + } + } + } else { + return -1; + } + } + + i_t add_upper_bound(i_t col, f_t objective, f_t bound) + { + if (col < static_cast(upper_bounds_.size())) { + if (!upper_bounds_[col].is_valid()) { + upper_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { max_objective_ = objective; } + return 1; + } else { + if (bound < upper_bounds_[col].bound) { + upper_bounds_[col] = objective_bound_pair_t(objective, bound); + if (objective > max_objective_) { max_objective_ = objective; } + return 2; + } else if (bound == upper_bounds_[col].bound && objective > upper_bounds_[col].objective) { + upper_bounds_[col].objective = objective; + if (objective > max_objective_) { max_objective_ = objective; } + return 1; + } else { + return -2; + } + } + } else { + return -1; + } + } + + i_t update_bounds_from_new_incumbent(f_t incumbent_objective, + const std::vector& var_types, + std::vector& lower_bounds, + std::vector& upper_bounds) + { + const i_t n = static_cast(lower_bounds_.size()); + f_t max_objective = -std::numeric_limits::infinity(); + i_t integer_bounds_updated = 0; + for (i_t j = 0; j < n; ++j) { + if (lower_bounds_[j].is_valid()) { + if (incumbent_objective <= lower_bounds_[j].objective && + lower_bounds_[j].bound > lower_bounds[j]) { + // printf("RCF Variable %d (%d): lower %e -> %e\n", j, static_cast(var_types[j]), + // lower_bounds[j], lower_bounds_[j].bound); + lower_bounds[j] = lower_bounds_[j].bound; + if (var_types[j] == simplex::variable_type_t::INTEGER) { integer_bounds_updated++; } + lower_bounds_[j].bound = lower_bounds_[j].objective = + std::numeric_limits::quiet_NaN(); + } + if (lower_bounds_[j].objective > max_objective) { + max_objective = lower_bounds_[j].objective; + } + } + if (upper_bounds_[j].is_valid()) { + if (incumbent_objective <= upper_bounds_[j].objective && + upper_bounds_[j].bound < upper_bounds[j]) { + // printf("RCF Variable %d (%d): upper %e -> %e\n", j, static_cast(var_types[j]), + // upper_bounds[j], upper_bounds_[j].bound); + upper_bounds[j] = upper_bounds_[j].bound; + if (var_types[j] == simplex::variable_type_t::INTEGER) { integer_bounds_updated++; } + upper_bounds_[j].bound = upper_bounds_[j].objective = + std::numeric_limits::quiet_NaN(); + } + if (upper_bounds_[j].objective > max_objective) { + max_objective = upper_bounds_[j].objective; + } + } + } + max_objective_ = max_objective; + return integer_bounds_updated; + } + + f_t get_current_lower_bound(i_t col) + { + if (col < static_cast(lower_bounds_.size())) { return lower_bounds_[col].bound; } + return std::numeric_limits::quiet_NaN(); + } + + f_t get_current_upper_bound(i_t col) + { + if (col < static_cast(upper_bounds_.size())) { return upper_bounds_[col].bound; } + return std::numeric_limits::quiet_NaN(); + } + + i_t num_cols() { return static_cast(lower_bounds_.size()); } + + f_t get_max_objective() { return max_objective_; } + + private: + f_t max_objective_; + std::vector> lower_bounds_; + std::vector> upper_bounds_; +}; + template class branch_and_bound_t { public: @@ -175,8 +314,13 @@ 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); + void update_reduced_cost_bounds(f_t relaxation_objective, + const std::vector& reduced_costs, + const std::vector& var_status, + reduced_cost_bounds_t& reduced_cost_bounds); i_t find_reduced_cost_fixings(f_t upper_bound, std::vector& lower_bounds, std::vector& upper_bounds); @@ -246,6 +390,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_; @@ -322,6 +467,7 @@ class branch_and_bound_t { f_t& last_upper_bound, f_t& last_objective, f_t root_relax_objective, + reduced_cost_bounds_t& reduced_cost_bounds, i_t& cut_pool_size, const std::vector& saved_solution); @@ -341,6 +487,71 @@ 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 fast_slack_integer_pivots(const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + const std::vector& fractional, + const std::vector& row_to_slack, + const simplex::lp_solution_t& solution, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + f_t& work_estimate); + + i_t pivot_out_integer_variables(const simplex::lp_problem_t& lp, + const simplex::simplex_solver_settings_t& settings, + const std::vector& new_slacks, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional); + + i_t apply_delta_x_for_integer_pivot(const simplex::lp_problem_t& lp, + std::vector& basic_list, + std::vector& nonbasic_list, + std::vector& nonbasic_index, + 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, + std::vector& vstatus, + simplex::lp_solution_t& soln, + simplex::basis_update_mpf_t& basis_update, + i_t& num_fractional, + std::vector& fractional); + + void pivot_to_improve_reduced_cost_strengthening( + const simplex::lp_problem_t& lp, + const std::vector& basic_list, + const std::vector& nonbasic_list, + const std::vector& vstatus, + const simplex::lp_solution_t& soln, + const simplex::basis_update_mpf_t& basis_update, + i_t num_fractional, + const std::vector& fractional, + f_t relaxation_objective, + reduced_cost_bounds_t& reduced_cost_bounds); + // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(); diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index fae259ac3f..7c31d023cc 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -89,11 +89,13 @@ class deterministic_worker_base_t : public branch_and_bound_worker_t { const csr_matrix_t& Arow, const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, - pseudo_costs_t& pc, - const std::vector& root_solution, - const std::vector& root_edge_norm, - const std::string& context_name) - : base_t(id, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm), + pseudo_costs_t& pc, + const std::vector& root_solution, + const std::vector& root_edge_norm, + const std::vector& new_slacks, + const std::string& context_name) + : base_t( + id, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm, new_slacks), work_context(context_name), pc_snapshot(1, settings) { @@ -144,18 +146,20 @@ class deterministic_bfs_worker_t const csr_matrix_t& Arow, const std::vector& var_types, const simplex::simplex_solver_settings_t& settings, - pseudo_costs_t& pc, - const std::vector& root_solution, - const std::vector& root_edge_norm) + pseudo_costs_t& pc, + const std::vector& root_solution, + const std::vector& root_edge_norm, + const std::vector& new_slacks) : base_t(id, original_lp, Arow, var_types, settings, - pc, - root_solution, - root_edge_norm, - "BB_Worker_" + std::to_string(id)) + pc, + root_solution, + root_edge_norm, + new_slacks, + "BB_Worker_" + std::to_string(id)) { } @@ -313,16 +317,18 @@ class deterministic_diving_worker_t const simplex::simplex_solver_settings_t& settings, pseudo_costs_t& pc, const std::vector& root_solution, - const std::vector& root_edge_norm) + const std::vector& root_edge_norm, + const std::vector& new_slacks) : base_t(id, original_lp, Arow, var_types, settings, - pc, - root_solution, - root_edge_norm, - "Diving_Worker_" + std::to_string(id)), + pc, + root_solution, + root_edge_norm, + new_slacks, + "Diving_Worker_" + std::to_string(id)), diving_type(type) { dive_lower = original_lp.lower; @@ -430,12 +436,13 @@ class deterministic_bfs_worker_pool_t const simplex::simplex_solver_settings_t& settings, pseudo_costs_t& pc, const std::vector& root_solution, - const std::vector& root_edge_norm) + const std::vector& root_edge_norm, + const std::vector& new_slacks) { this->workers_.reserve(num_workers); for (int i = 0; i < num_workers; ++i) { this->workers_.emplace_back( - i, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm); + i, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm, new_slacks); } } @@ -469,13 +476,23 @@ class deterministic_diving_worker_pool_t const simplex::simplex_solver_settings_t& settings, pseudo_costs_t& pc, const std::vector& root_solution, - const std::vector& root_edge_norm) + const std::vector& root_edge_norm, + const std::vector& new_slacks) { this->workers_.reserve(num_workers); for (int i = 0; i < num_workers; ++i) { search_strategy_t type = diving_types[i % diving_types.size()]; this->workers_.emplace_back( - i, type, original_lp, Arow, var_types, settings, pc, root_solution, root_edge_norm); + i, + type, + original_lp, + Arow, + var_types, + settings, + pc, + root_solution, + root_edge_norm, + new_slacks); } } diff --git a/cpp/src/branch_and_bound/pseudo_costs.cpp b/cpp/src/branch_and_bound/pseudo_costs.cpp index c4071bf3b8..8f86594e79 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/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 0ec0f74bf9..5c705087d2 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -98,6 +98,7 @@ class branch_and_bound_worker_t { const std::vector& root_solution; const std::vector& root_edge_norm; const std::vector& var_types; + const std::vector& new_slacks; pseudo_costs_t& pseudo_costs; @@ -120,6 +121,7 @@ class branch_and_bound_worker_t { pseudo_costs_t& pc, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, uint64_t rng_offset = 0) : worker_id(worker_id), search_strategy(search_strategy_t::BEST_FIRST), @@ -138,6 +140,7 @@ class branch_and_bound_worker_t { root_solution(root_solution), root_edge_norm(root_edge_norm), var_types(var_type), + new_slacks(new_slacks), pseudo_costs(pc) { } @@ -179,6 +182,7 @@ class bfs_worker_t : public branch_and_bound_worker_t { pseudo_costs_t& pc, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, uint64_t rng_offset = 0) : Base(worker_id, original_lp, @@ -188,6 +192,7 @@ class bfs_worker_t : public branch_and_bound_worker_t { pc, root_solution, root_edge_norm, + new_slacks, rng_offset) { this->start_lower = original_lp.lower; @@ -265,6 +270,7 @@ class diving_worker_t : public branch_and_bound_worker_t { pseudo_costs_t& pc, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, uint64_t rng_offset = 0) : Base(worker_id, original_lp, @@ -274,6 +280,7 @@ class diving_worker_t : public branch_and_bound_worker_t { pc, root_solution, root_edge_norm, + new_slacks, rng_offset) { this->start_lower = original_lp.lower; diff --git a/cpp/src/branch_and_bound/worker_pool.hpp b/cpp/src/branch_and_bound/worker_pool.hpp index c4e54a61f1..bdb420a405 100644 --- a/cpp/src/branch_and_bound/worker_pool.hpp +++ b/cpp/src/branch_and_bound/worker_pool.hpp @@ -24,10 +24,11 @@ class worker_pool_t { const std::vector& var_type, mip_symmetry_t* symmetry, const simplex::simplex_solver_settings_t& settings, - pseudo_costs_t& pc, - const std::vector& root_solution, - const std::vector& root_edge_norm, - const uint64_t rng_offset = 0) + pseudo_costs_t& pc, + const std::vector& root_solution, + const std::vector& root_edge_norm, + const std::vector& new_slacks, + const uint64_t rng_offset = 0) { assert(!is_initialized_); assert(num_workers > 0); @@ -37,7 +38,16 @@ class worker_pool_t { idle_workers_.clear_resize(num_workers); for (i_t i = 0; i < num_workers; ++i) { workers_[i] = std::make_unique( - i, original_lp, Arow, var_type, settings, pc, root_solution, root_edge_norm, rng_offset); + i, + original_lp, + Arow, + var_type, + settings, + pc, + root_solution, + root_edge_norm, + new_slacks, + rng_offset); idle_workers_.push_back(i); // Propagate the (possibly null) symmetry pointer; workers lazily build // their orbital_fixing/lexical_reduction state via ensure_orbital_fixing(). diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index f81962d054..c2a7027548 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1507,7 +1507,7 @@ f_t basis_update_mpf_t::dot_product(i_t col, nz_mark++; } } - work_estimate_ += 2 * nz_mark + (col_end - col_start); + work_estimate_ += 2 * (col_end - col_start) + 2 * nz_mark; return dot; } @@ -1524,7 +1524,7 @@ void basis_update_mpf_t::add_sparse_column(const csc_matrix_t @@ -1549,7 +1549,7 @@ void basis_update_mpf_t::add_sparse_column(const csc_matrix_t @@ -2009,6 +2009,34 @@ 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 @@ -2202,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/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/bound_flipping_ratio_test.cpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp index cb0964dc05..d18ed95e90 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.cpp @@ -11,12 +11,14 @@ #include #include +#include namespace cuopt::mathematical_optimization::simplex { template i_t bound_flipping_ratio_test_t::compute_breakpoints(std::vector& indicies, - std::vector& ratios) + std::vector& ratios, + std::vector& harris_ratios) { i_t n = n_; i_t m = m_; @@ -33,20 +35,21 @@ i_t bound_flipping_ratio_test_t::compute_breakpoints(std::vector& const i_t k = nonbasic_mark_[j]; if (vstatus_[j] == variable_status_t::NONBASIC_FIXED) { continue; } if (vstatus_[j] == variable_status_t::NONBASIC_LOWER && delta_z_[j] < -pivot_tol) { - indicies[idx] = k; - ratios[idx] = std::max((-dual_tol - z_[j]) / delta_z_[j], 0.0); + indicies[idx] = k; + ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); + harris_ratios[idx] = std::max((-dual_tol - z_[j]) / delta_z_[j], 0.0); if constexpr (verbose) { settings_.log.printf("ratios[%d] = %e\n", idx, ratios[idx]); } idx++; } if (vstatus_[j] == variable_status_t::NONBASIC_UPPER && delta_z_[j] > pivot_tol) { - indicies[idx] = k; - ratios[idx] = std::max((dual_tol - z_[j]) / delta_z_[j], 0.0); + indicies[idx] = k; + ratios[idx] = std::max((-z_[j]) / delta_z_[j], 0.0); + harris_ratios[idx] = std::max((dual_tol - z_[j]) / delta_z_[j], 0.0); if constexpr (verbose) { settings_.log.printf("ratios[%d] = %e\n", idx, ratios[idx]); } idx++; } } - work_estimate_ += 4 * nz; - work_estimate_ += 4 * idx; + work_estimate_ += 5 * nz + 5 * idx; pivot_tol /= 10; } return idx; @@ -57,10 +60,10 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, i_t end, const std::vector& indicies, const std::vector& ratios, - f_t& slope, f_t& step_length, i_t& nonbasic_entering, - i_t& entering_index) + i_t& entering_index, + f_t& max_val) { // Find the minimum ratio f_t min_val = inf; @@ -68,27 +71,19 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, i_t candidate = -1; f_t zero_tol = settings_.zero_tol; i_t k_idx = -1; + max_val = 0.0; - i_t min_found = 0; - i_t harris_found = 0; + i_t min_found = 0; for (i_t k = start; k < end; ++k) { if (ratios[k] < min_val) { min_val = ratios[k]; candidate = indicies[k]; k_idx = k; min_found++; - } else if (ratios[k] < min_val + zero_tol) { - // Use Harris to select variables with larger pivots - const i_t j = nonbasic_list_[indicies[k]]; - if (std::abs(delta_z_[j]) > std::abs(delta_z_[candidate])) { - min_val = ratios[k]; - candidate = indicies[k]; - k_idx = k; - } - harris_found++; } + if (ratios[k] > max_val) { max_val = ratios[k]; } } - work_estimate_ += (end - start) + 2 * min_found + 6 * harris_found; + work_estimate_ += (end - start) + 2 * min_found; step_length = min_val; nonbasic_entering = candidate; @@ -96,37 +91,65 @@ i_t bound_flipping_ratio_test_t::single_pass(i_t start, if (nonbasic_entering == -1) { return RATIO_TEST_NUMERICAL_ISSUES; } const i_t j = entering_index = nonbasic_list_[nonbasic_entering]; - constexpr bool verbose = false; - if (bounded_variables_[j]) { - const f_t interval = upper_[j] - lower_[j]; - const f_t delta_slope = std::abs(delta_z_[j]) * interval; - if constexpr (verbose) { - settings_.log.printf("single pass delta slope %e slope %e after slope %e step length %e\n", - delta_slope, - slope, - slope - delta_slope, - step_length); + if (bounded_variables_[j]) { return k_idx; } + return -1; // we are done. do not increase the step-length further +} + +template +void bound_flipping_ratio_test_t::determine_flips(f_t step_length, + i_t entering_index, + std::vector& flip_indices) +{ + // The piecewise-linear model below assumes that a variable flips bounds as soon as + // its reduced cost crosses zero. In practice, small changes between iterations can + // make a reduced cost oscillate around zero, causing excessive bound flips and + // cycling. We therefore flip only after the violation exceeds dual_tol / 10. + // A bounded variable l_j <= x_j <= u_j contributes l_j*z_j to the dual objective + // when z_j >= 0 and u_j*z_j when z_j < 0. If x_j = l_j and + // -dual_tol/10 <= z_j < 0, the model uses u_j*z_j while the unflipped state uses + // l_j*z_j. Their difference is (u_j - l_j)*|z_j|, bounded by + // (u_j - l_j)*dual_tol/10. For multiple unflipped variables, the discrepancy is + // bounded by sum_j (u_j - l_j)*dual_tol/10. + const f_t flip_tol = settings_.dual_tol / 10; + for (const i_t j : delta_z_indices_) { + if (j == entering_index || !bounded_variables_[j]) { continue; } + const f_t new_z = z_[j] + step_length * delta_z_[j]; + if ((vstatus_[j] == variable_status_t::NONBASIC_LOWER && new_z < -flip_tol) || + (vstatus_[j] == variable_status_t::NONBASIC_UPPER && new_z > flip_tol)) { + flip_indices.push_back(j); } - slope -= delta_slope; - return k_idx; // we should see if we can continue to increase the step-length } - return -1; // we are done. do not increase the step-length further + work_estimate_ += 5 * delta_z_indices_.size() + flip_indices.size(); } template i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, - i_t& nonbasic_entering) + i_t& nonbasic_entering, + std::vector& flip_indices) { const i_t m = m_; const i_t n = n_; const i_t nz = delta_z_indices_.size(); constexpr bool verbose = false; + flip_indices.clear(); // Compute the initial set of breakpoints std::vector indicies(nz); std::vector ratios(nz); - work_estimate_ += 2 * nz; - i_t num_breakpoints = compute_breakpoints(indicies, ratios); + std::vector harris_ratios(nz); + work_estimate_ += 3 * nz; + double t0 = tic(); + i_t num_breakpoints = compute_breakpoints(indicies, ratios, harris_ratios); + time_compute_breakpoints_ += toc(t0); + num_breakpoints_ = num_breakpoints; + // Count zero ratios + num_harris_zero_ = 0; + num_exact_zero_ = 0; + for (i_t k = 0; k < num_breakpoints; k++) { + if (harris_ratios[k] == 0.0) num_harris_zero_++; + if (ratios[k] == 0.0) num_exact_zero_++; + } + work_estimate_ += 2 * num_breakpoints; if constexpr (verbose) { settings_.log.printf("Initial breakpoints %d\n", num_breakpoints); } if (num_breakpoints == 0) { nonbasic_entering = -1; @@ -136,11 +159,23 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, f_t slope = slope_; nonbasic_entering = -1; i_t entering_index = RATIO_TEST_NO_ENTERING_VARIABLE; - - i_t k_idx = single_pass( - 0, num_breakpoints, indicies, ratios, slope, step_length, nonbasic_entering, entering_index); + f_t max_step_length; + + t0 = tic(); + i_t k_idx = single_pass(0, + num_breakpoints, + indicies, + harris_ratios, + step_length, + nonbasic_entering, + entering_index, + max_step_length); + time_single_pass_ += toc(t0); if (k_idx == RATIO_TEST_NUMERICAL_ISSUES) { return RATIO_TEST_NUMERICAL_ISSUES; } - bool continue_search = k_idx >= 0 && num_breakpoints > 1 && slope > 0.0; + // The variable selected by single_pass is guaranteed to be in the first bucket: it + // defines the minimum Harris ratio, and its exact ratio is no greater than its Harris + // ratio. Its slope contribution is therefore applied by the bucket pass below. + bool continue_search = k_idx >= 0 && num_breakpoints > 1; if (!continue_search) { if constexpr (verbose) { settings_.log.printf( @@ -150,6 +185,9 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, entering_index, std::abs(delta_z_[entering_index])); } + num_buckets_used_ = 0; + step_length_result_ = step_length; + determine_flips(step_length, entering_index, flip_indices); return entering_index; } @@ -162,185 +200,280 @@ i_t bound_flipping_ratio_test_t::compute_step_length(f_t& step_length, slope); } - // Continue the search using a heap to order the breakpoints - ratios[k_idx] = ratios[num_breakpoints - 1]; - indicies[k_idx] = indicies[num_breakpoints - 1]; - - constexpr bool use_bucket_pass = false; - - if (use_bucket_pass) { - f_t max_ratio = 0.0; - for (i_t k = 0; k < num_breakpoints - 1; ++k) { - if (ratios[k] > max_ratio) { max_ratio = ratios[k]; } + // This code is complicated. There are several important concepts that are needed to understand + // it. + // + // We are trying to compute the maximum step length we can take while: + // 1) Staying mostly dual feasible (we allow ourselves to be infeasible by dual_tol amount) + // 2) Increasing the dual objective + // 3) Selecting a variable with a large pivot (| delta_z[j] |) + // + // Let alpha be the step length. For each nonbasic variable j, we have + // z_j(alpha) = z_j + alpha * delta_z_j + // + // To stay dual feasible, we either need to keep + // z_j(alpha) >= 0, if j is on it's lower bound, or + // z_j(alpha) <= 0, if j is on it's upper bound. + // + // Consider the equation z_j(alpha) = z_j + alpha * delta_z_j = 0. Each variable j puts a bound on + // alpha: + // + // alpha_j <= -z_j / delta_z_j, if x_j = l_j (z_j >= 0) and delta_z_j < 0 + // alpha_j <= -z_j / delta_z_j, if x_j = u_j (z_j <= 0) and delta_z_j > 0 + // + // The code refers to these alpha_j as ratios, since they are the ratio of z_j to delta_z_j. + // + // Now we could take alpha = min_j alpha_j, and remain dual feasible. However, we are allowed to + // increase the step-length if j is a variable such that l_j <= x_j <= u_j. To see why imagine + // that our variable was currenlty on it's lower bound, with z_j > 0 and delta_z_j < 0, if we push + // alpha past alpha_j, than z_j(alpha) < 0. This is fine as long as we flip the variable to be on + // it's upper bound. Thus, we can push alpha past alpha_j for *bounded* variables. + // + // Note that this does not work if we try to increase alpha past alpha_j for a variable with a + // single bound. We would just be making ourselves dual infeasible. So we need to check whether a + // variable is bounded. + // + // The dual objective as a function of the step-length alpha, is piecewise linear and concave. The + // breakpoints of this piecewise linear function occur at each of the alpha_j values. We can keep + // increasing the step-length as long as the slope remains nonnegative. After that we must stop, + // because we could decrease the dual objective. So the code tracks the cumulative slope of the + // dual objective. + // + // Now we don't need to exactly feasible: z_j >= 0 if x_j = l_j and z_j <= 0 if x_j = u_j. We can + // violate these bounds by the dual feasibility tolerance eps. We allow ourselves to be infeasible + // if it would help us get a larger pivot (delta_z_j). Small pivots can cause numerical issues, so + // we would like to avoid them. + // + // With this tolerance we get the equations: + // z_j(alpha) = z_j + alpha * delta_z_j >= -eps if x_j = l_j + // z_j(alpha) = z_j + alpha * delta_z_j <= eps if x_j = u_j + // + // This gives bounds on alpha. We call these alpha_harris_j, for Paula Harris, who proposed this + // method. + // + // alpha_harris_j <= (-eps - z_j) / delta_z_j, if x_j = l_j and delta_z_j < 0 + // alpha_harris_j <= (eps - z_j) / delta_z_j, if x_j = u_j and delta_z_j > 0 + // + // Let alpha_harris = min_j alpha_harris_j. We can select the variable with the largest | + // delta_z_j | from those candidates { j | alpha_j <= alpha_harris }. + // + // We combine these two ideas (increasing the step length for bounded variables) and allowing + // ourselves to be slightly dual infeasible to choose a larger pivot. + // + // We partition the variables into buckets. Let B_k be the set of variables in bucket k. B_0 is + // defined as { j | alpha_j <= alpha_harris }. We then compute alpha_harris_1 = min_{j not in B_0} + // alpha_j. And B_1 is defined as { j not in B_0 | alpha_j <= alpha_harris_1 }. And so on. + // + // We want to balance two different things: + // 1) Taking a larger step length to increase the dual objective as much as possible, + // 2) Choosing a large pivot for numerical stability. + // + // Let max_pivot = max_j | delta_z_j |. We start working our way backward from the largest bucket + // to the smallest bucket, we choose a variable j that satisfies | delta_z_j | >= 0.1 * max_pivot. + // Since we can always choose a smaller step length for the sake of numerical stability. + // + // Now the final thing to understand is that the ratios alpha_j are not sorted in any particular + // order. And we don't want to pay the O(num_breakpoints * log(num_breakpoints)) cost of sorting + // them. + // + // So we set a threshold on the step-length and check if all variables j with alpha_j <= threshold + // have already caused the slope to go negative. If so, we just need to consider those candidate + // variables with alpha_j <= threshold. If not, we multiply the threshold by 10. This cost us + // O(log10(max_step_length/min_step_length) * num_breakpoints) time. So we aren't totally linear. + // But the hope is we are better than a sort. + + // Use a coarse filter to find candidates + f_t minimum_harris_ratio = step_length; + f_t coarse_threshold = (minimum_harris_ratio > 0.0) + ? std::min(10.0 * minimum_harris_ratio, max_step_length) + : max_step_length; + f_t total_slope = slope; + bool found_unbounded = false; + std::vector candidates(num_breakpoints); + std::iota(candidates.begin(), candidates.end(), 0); + work_estimate_ += 2 * num_breakpoints; + i_t scan_start = 0; + i_t num_candidates = 0; + + // This is O( log10(max_step_length/min_step_length) * num_breakpoints) + t0 = tic(); + while (total_slope >= 0.0 && coarse_threshold <= max_step_length && + scan_start < num_breakpoints && !found_unbounded) { + for (i_t h = scan_start; h < num_breakpoints; ++h) { + const i_t k = candidates[h]; + if (ratios[k] <= coarse_threshold) { + // Candidate is less than coarse threshold, move it to the front of the candidate list + std::swap(candidates[h], candidates[num_candidates]); + num_candidates++; + const i_t j = nonbasic_list_[indicies[k]]; + if (!bounded_variables_[j]) { + found_unbounded = true; + } else { + total_slope -= std::abs(delta_z_[j]) * (upper_[j] - lower_[j]); + } + } } - work_estimate_ += 2 * num_breakpoints; - settings_.log.printf( - "Starting heap passes. %d breakpoints max ratio %e\n", num_breakpoints - 1, max_ratio); - bucket_pass( - indicies, ratios, num_breakpoints - 1, slope, step_length, nonbasic_entering, entering_index); + work_estimate_ += 2 * (num_breakpoints - scan_start) + 10 * (num_candidates - scan_start); + scan_start = num_candidates; + coarse_threshold *= 10.0; } + time_coarse_filter_ += toc(t0); - heap_passes( - indicies, ratios, num_breakpoints - 1, slope, step_length, nonbasic_entering, entering_index); + candidates.resize(num_candidates); - if constexpr (verbose) { - settings_.log.printf("BFRT step length %e entering index %d non basic entering %d pivot %e\n", - step_length, - entering_index, - nonbasic_entering, - std::abs(delta_z_[entering_index])); - } - return entering_index; -} - -template -void bound_flipping_ratio_test_t::heap_passes(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_length, - i_t& nonbasic_entering, - i_t& entering_index) -{ - std::vector bare_idx(num_breakpoints); - constexpr bool verbose = false; - const f_t dual_tol = settings_.dual_tol; - const f_t zero_tol = settings_.zero_tol; - const std::vector& delta_z = delta_z_; - const std::vector& nonbasic_list = nonbasic_list_; - const i_t N = num_breakpoints; - for (i_t k = 0; k < N; ++k) { - bare_idx[k] = k; - if constexpr (verbose) { - settings_.log.printf("Adding index %d ratio %e pivot %e to heap\n", - current_indicies[k], - current_ratios[k], - std::abs(delta_z[nonbasic_list[current_indicies[k]]])); + // Check for variables with one sided bounds. These define the maximum step length. + if (found_unbounded) { + for (i_t h = 0; h < num_candidates; h++) { + const i_t k = candidates[h]; + const i_t j = nonbasic_list_[indicies[k]]; + if (!bounded_variables_[j]) { max_step_length = std::min(max_step_length, harris_ratios[k]); } } - } - work_estimate_ += N; - - auto compare = [zero_tol, ¤t_ratios, ¤t_indicies, &delta_z, &nonbasic_list]( - const i_t& a, const i_t& b) { - return (current_ratios[a] > current_ratios[b]) || - (current_ratios[b] - current_ratios[a] < zero_tol && - std::abs(delta_z[nonbasic_list[current_indicies[a]]]) > - std::abs(delta_z[nonbasic_list[current_indicies[b]]])); - }; - - std::make_heap(bare_idx.begin(), bare_idx.end(), compare); - work_estimate_ += 3 * 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(); - - nonbasic_entering = current_indicies[heap_index]; - const i_t j = entering_index = nonbasic_list_[nonbasic_entering]; - step_length = current_ratios[heap_index]; - - if (bounded_variables_[j]) { - // We have a bounded variable - const f_t interval = upper_[j] - lower_[j]; - const f_t delta_slope = std::abs(delta_z_[j]) * interval; - const f_t pivot = std::abs(delta_z[j]); - if constexpr (verbose) { - settings_.log.printf( - "heap %d step-length %.12e pivot %e nonbasic entering %d slope %e delta_slope %e new " - "slope %e\n", - bare_idx.size(), - current_ratios[heap_index], - pivot, - nonbasic_entering, - slope, - delta_slope, - slope - delta_slope); + work_estimate_ += 5 * num_candidates; + + // Remove candidates that are greater than the maximum step length + const i_t candidates_before_removal = candidates.size(); + for (i_t h = candidates_before_removal - 1; h >= 0; h--) { + const i_t k = candidates[h]; + const f_t ratio = ratios[k]; + if (ratio > max_step_length) { + // Swap with the last candidate and remove + candidates[h] = candidates.back(); + candidates.pop_back(); } - slope -= delta_slope; - } else { - // The variable is not bounded. Stop the search. - break; } + work_estimate_ += + 2 * candidates_before_removal + 2 * (candidates_before_removal - candidates.size()); + num_candidates = candidates.size(); + } - if (toc(start_time_) > settings_.time_limit) { - entering_index = RATIO_TEST_TIME_LIMIT; - return; - } - if (settings_.concurrent_halt != nullptr && *settings_.concurrent_halt == 1) { - entering_index = CONCURRENT_HALT_RETURN; - return; + // Use a bucket sort to partition candidates into buckets by successive Harris breakpoints + // bucket_start[k] = index in candidates[] where bucket k starts + // Bucket k contains candidates[bucket_start[k]] .. candidates[bucket_start[k+1] - 1] + f_t threshold = minimum_harris_ratio; + i_t num_buckets = 0; + std::vector bucket_start(num_candidates + 1, 0); + f_t cumulative_slope = slope; + scan_start = 0; + work_estimate_ += num_candidates + 1; + + // This is O(num_buckets * num_candidates) + i_t slope_breaker_k = -1; // the candidate k that made slope go negative + t0 = tic(); + while (cumulative_slope >= 0.0 && scan_start < num_candidates && threshold <= max_step_length) { + f_t next_threshold = inf; + i_t write = scan_start; + + for (i_t h = scan_start; h < num_candidates; h++) { + const i_t k = candidates[h]; + const f_t ratio = ratios[k]; + + if (ratio <= threshold) { + const i_t j = nonbasic_list_[indicies[k]]; + if (bounded_variables_[j]) { + cumulative_slope -= std::abs(delta_z_[j]) * (upper_[j] - lower_[j]); + if (cumulative_slope < 0.0 && slope_breaker_k < 0) { slope_breaker_k = k; } + } + std::swap(candidates[h], candidates[write]); + write++; + } else { + const i_t j = nonbasic_list_[indicies[k]]; + const f_t harris_ratio = harris_ratios[k]; + next_threshold = std::min(next_threshold, harris_ratio); + } } - } -} + work_estimate_ += 3 * (num_candidates - scan_start) + 9 * (write - scan_start); -template -void bound_flipping_ratio_test_t::bucket_pass(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_length, - i_t& nonbasic_entering, - i_t& entering_index) -{ - const f_t dual_tol = settings_.dual_tol; - const f_t zero_tol = settings_.zero_tol; - const std::vector& delta_z = delta_z_; - const std::vector& nonbasic_list = nonbasic_list_; - const i_t N = num_breakpoints; - - const i_t K = 400; // 0, -16, -15, ...., 0, 1, ...., 400 - 18 = 382 - std::vector buckets(K, 0.0); - std::vector bucket_count(K, 0); - for (i_t k = 0; k < N; ++k) { - const i_t idx = current_indicies[k]; - const f_t ratio = current_ratios[k]; - const f_t min_exponent = -16.0; - const f_t max_exponent = 382.0; - const f_t exponent = std::max(min_exponent, std::min(max_exponent, std::log10(ratio))); - const i_t bucket_idx = ratio == 0.0 ? 0 : static_cast(exponent - min_exponent + 1); - // settings_.log.printf("Ratio %e exponent %e bucket_idx %d\n", ratio, exponent, bucket_idx); - const i_t j = nonbasic_list[idx]; - const f_t interval = upper_[j] - lower_[j]; - const f_t delta_slope = std::abs(delta_z_[j]) * interval; - buckets[bucket_idx] += delta_slope; - bucket_count[bucket_idx]++; - } + bucket_start[++num_buckets] = write; + if (write == scan_start) break; // No progress — prevent infinite loop + scan_start = write; + threshold = next_threshold; - std::vector cumulative_sum(K, 0.0); - cumulative_sum[0] = buckets[0]; - if (cumulative_sum[0] > slope) { - settings_.log.printf( - "Bucket 0. Count in bucket %d. Slope %e. Cumulative sum %e. Bucket value %e\n", - bucket_count[0], - slope, - cumulative_sum[0], - buckets[0]); - return; + if (cumulative_slope < 0.0) break; + } + time_bucket_sort_ += toc(t0); + bucket0_size_ = (num_buckets > 0) ? bucket_start[1] : 0; + + // Compute the maximum pivot + // This is O(num_candidates) + f_t max_pivot = 0.0; + for (i_t h = 0; h < bucket_start[num_buckets]; h++) { + const i_t k = candidates[h]; + const i_t j = nonbasic_list_[indicies[k]]; + const f_t pivot = std::abs(delta_z_[j]); + if (pivot > max_pivot) { max_pivot = pivot; } } - i_t k; - bool exceeded = false; - for (k = 1; k < K; ++k) { - cumulative_sum[k] = cumulative_sum[k - 1] + buckets[k]; - if (cumulative_sum[k] > slope) { - exceeded = true; - break; + work_estimate_ += 4 * bucket_start[num_buckets]; + + // Select the entering variable + // Scan from last bucket to first. Within each bucket, pick the variable with + // the largest |delta_z|, provided |delta_z| > pivot_threshold, breaking ties + // by preferring the larger step length. + f_t pivot_threshold = std::max(settings_.pivot_tol, std::min(0.1 * max_pivot, 1.0)); + i_t entering_k = -1; + + // This is O(num_candidates) + for (i_t b = num_buckets - 1; b >= 0; b--) { + const i_t b_start = bucket_start[b]; + const i_t b_end = bucket_start[b + 1]; + f_t best_pivot = -1.0; + f_t best_ratio = -1.0; + for (i_t h = b_start; h < b_end; h++) { + const i_t k = candidates[h]; + const i_t j = nonbasic_list_[indicies[k]]; + const f_t pivot = std::abs(delta_z_[j]); + if (pivot > pivot_threshold && + (pivot > best_pivot || (pivot == best_pivot && ratios[k] > best_ratio))) { + best_pivot = pivot; + best_ratio = ratios[k]; + entering_k = k; + } } + work_estimate_ += 2 + 5 * (b_end - b_start); + if (entering_k >= 0) break; } - if (exceeded) { - settings_.log.printf( - "Value in bucket %d. Count in buckets %d. Slope %e. Cumulative sum %e. Next sum %e Bucket " - "value %e\n", - k, - bucket_count[k], - slope, - cumulative_sum[k - 1], - cumulative_sum[k], - buckets[k - 1]); + // Step = entering variable's breakpoint ratio + num_buckets_used_ = num_buckets; + if (entering_k < 0) { + // Fallback to single_pass result + used_fallback_ = true; + bucket_selected_ = -1; + step_length_result_ = step_length; + selected_is_slope_breaker_ = false; + determine_flips(step_length, entering_index, flip_indices); + return entering_index; } + step_length = ratios[entering_k]; + nonbasic_entering = indicies[entering_k]; + entering_index = nonbasic_list_[nonbasic_entering]; + + // Record whether we selected the slope breaker + selected_is_slope_breaker_ = (entering_k == slope_breaker_k); + + // Record which bucket was selected + used_fallback_ = false; + i_t pos = -1; + for (i_t b = 0; b < num_buckets; b++) { + if (entering_k >= 0) { + // Find which bucket entering_k is in based on its position in candidates + pos = -1; + for (i_t h = 0; h < num_candidates; h++) { + if (candidates[h] == entering_k) { + pos = h; + break; + } + } + if (pos >= bucket_start[b] && pos < bucket_start[b + 1]) { + bucket_selected_ = b; + break; + } + } + } + work_estimate_ += (bucket_selected_ + 1) * (pos + 3); + step_length_result_ = step_length; + determine_flips(step_length, entering_index, flip_indices); + + return entering_index; } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE diff --git a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp index 2e73d05eff..4587037889 100644 --- a/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp +++ b/cpp/src/dual_simplex/bound_flipping_ratio_test.hpp @@ -53,35 +53,42 @@ class bound_flipping_ratio_test_t { { } - i_t compute_step_length(f_t& step_length, i_t& nonbasic_entering); + i_t compute_step_length(f_t& step_length, i_t& nonbasic_entering, std::vector& flip_indices); f_t work_estimate() const { return work_estimate_; } + // Timing fields (filled by compute_step_length) + f_t time_compute_breakpoints_{0.0}; + f_t time_single_pass_{0.0}; + f_t time_coarse_filter_{0.0}; + f_t time_bucket_sort_{0.0}; + f_t time_pivot_selection_{0.0}; + + // Diagnostic fields + i_t num_buckets_used_{0}; // number of buckets in bucket sort + i_t bucket_selected_{ + -1}; // which bucket the entering variable came from (-1 = single_pass/fallback) + f_t step_length_result_{0.0}; // the step length chosen + bool used_fallback_{false}; // true if we fell back to single_pass result + i_t bucket0_size_{0}; // size of first bucket (candidates with ratio <= min_harris) + i_t num_breakpoints_{0}; // total breakpoints computed + bool selected_is_slope_breaker_{ + false}; // true if we selected the variable that made slope go negative + i_t num_harris_zero_{0}; // number of harris_ratios that are exactly 0 + i_t num_exact_zero_{0}; // number of exact ratios that are exactly 0 + private: - i_t compute_breakpoints(std::vector& indices, std::vector& ratios); + i_t compute_breakpoints(std::vector& indices, + std::vector& ratios, + std::vector& harris_ratios); i_t single_pass(i_t start, i_t end, const std::vector& indices, const std::vector& ratios, - f_t& slope, f_t& step_length, i_t& nonbasic_entering, - i_t& enetering_index); - void heap_passes(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_lenght, - i_t& nonbasic_entering, - i_t& entering_index); - - void bucket_pass(const std::vector& current_indicies, - const std::vector& current_ratios, - i_t num_breakpoints, - f_t& slope, - f_t& step_length, - i_t& nonbasic_entering, - i_t& entering_index); - + i_t& entering_index, + f_t& max_val); + void determine_flips(f_t step_length, i_t entering_index, std::vector& flip_indices); const std::vector& lower_; const std::vector& upper_; const std::vector& bounded_variables_; @@ -100,7 +107,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/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index e1ba272adf..977f5e5511 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; - dual_status_t status = - dual_phase2(2, 0, start_time, lp, settings, vstatus, solution, dual_iter, 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, 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,33 @@ 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 +1499,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 +1624,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 a5f10c3229..9eb3224817 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 @@ -160,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 @@ -454,43 +455,70 @@ template void initial_perturbation(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& vstatus, + bool strongly_degenerate, std::vector& objective) { - const i_t m = lp.num_rows; const i_t n = lp.num_cols; f_t max_abs_obj_coeff = 0.0; for (i_t j = 0; j < n; ++j) { max_abs_obj_coeff = std::max(max_abs_obj_coeff, std::abs(lp.objective[j])); } - const f_t dual_tol = settings.dual_tol; + // Dampen large costs + if (max_abs_obj_coeff > 100.0) { max_abs_obj_coeff = std::sqrt(std::sqrt(max_abs_obj_coeff)); } + // Ensure a minimum perturbation even for tiny-cost problems + if (max_abs_obj_coeff < 1.0) { max_abs_obj_coeff = 1.0; } + + // If few boxed variables, cap max_abs_obj_coeff at 1.0 + i_t num_boxed = 0; + for (i_t j = 0; j < n; ++j) { + if (lp.lower[j] > -inf && lp.upper[j] < inf && lp.lower[j] != lp.upper[j]) { num_boxed++; } + } + if (static_cast(num_boxed) / n < 0.01) { + max_abs_obj_coeff = std::min(max_abs_obj_coeff, f_t(1.0)); + } + + // Sub-tolerance perturbations are less disruptive on ordinary problems, but + // are too small to separate reduced costs when a substantial part of the + // nonbasic set is dual degenerate. Use a stronger, still temporary shift in + // that case. The original costs are restored before declaring optimality. + const f_t perturbation_base = (strongly_degenerate ? 1e-5 : 5e-7) * max_abs_obj_coeff; + + settings.log.printf( + "Perturbation debug: max_abs_obj_coeff=%e (dampened), perturbation_base=%e, n=%d, " + "num_boxed=%d\n", + max_abs_obj_coeff, + perturbation_base, + n, + num_boxed); objective.resize(n); 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]; const f_t lower = lp.lower[j]; const f_t upper = lp.upper[j]; - if (vstatus[j] == variable_status_t::NONBASIC_FIXED || - vstatus[j] == variable_status_t::NONBASIC_FREE || lower == upper || - lower == -inf && upper == inf) { - continue; - } + // Skip truly fixed variables and free variables + if (lower == upper || (lower == -inf && upper == inf)) { continue; } - const f_t rand_val = random.random(); - const f_t perturb = - (1e-5 * std::abs(obj) + 1e-7 * max_abs_obj_coeff + 10 * dual_tol) * (1.0 + rand_val); + const f_t rand_val = random.random(); + const f_t cost_factor = std::min(std::abs(obj) + 1.0, max_abs_obj_coeff + 1.0); + const f_t perturb = (1.0 + rand_val) * cost_factor * perturbation_base; - if (vstatus[j] == variable_status_t::NONBASIC_LOWER || lower > -inf && upper < inf && obj > 0) { + if (vstatus[j] == variable_status_t::BASIC) { + // Skip basic variables + continue; + } else if (vstatus[j] == variable_status_t::NONBASIC_LOWER || + vstatus[j] == variable_status_t::NONBASIC_FIXED) { + // NONBASIC_FIXED from phase 1 for boxed variables — treat as at lower bound objective[j] = obj + perturb; sum_perturb += perturb; num_perturb++; - } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER || - lower > -inf && upper < inf && obj < 0) { + } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER) { objective[j] = obj - perturb; sum_perturb += perturb; num_perturb++; @@ -904,7 +932,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; } @@ -1205,13 +1233,8 @@ i_t phase2_ratio_test(const lp_problem_t& lp, template i_t flip_bounds(const lp_problem_t& lp, - const simplex_solver_settings_t& settings, const std::vector& bounded_variables, - const std::vector& objective, - const std::vector& z, - const std::vector& delta_z_indices, - const std::vector& nonbasic_list, - i_t entering_index, + const std::vector& flip_indices, std::vector& vstatus, std::vector& delta_x, std::vector& mark, @@ -1220,15 +1243,9 @@ i_t flip_bounds(const lp_problem_t& lp, f_t& work_estimate) { i_t num_flipped = 0; - for (i_t k = 0; k < delta_z_indices.size(); ++k) { - const i_t j = delta_z_indices[k]; - if (j == entering_index) { continue; } - if (!bounded_variables[j]) { continue; } - // x_j is now a nonbasic bounded variable that will not enter the basis this - // iteration - const f_t dual_tol = - settings.dual_tol; // lower to 1e-7 or less will cause 25fv47 and d2q06c to cycle - if (vstatus[j] == variable_status_t::NONBASIC_LOWER && z[j] < -dual_tol) { + for (const i_t j : flip_indices) { + assert(bounded_variables[j]); + if (vstatus[j] == variable_status_t::NONBASIC_LOWER) { const f_t delta = lp.upper[j] - lp.lower[j]; const size_t atilde_start_size = atilde_index.size(); scatter_dense(lp.A, j, -delta, atilde, mark, atilde_index); @@ -1236,12 +1253,9 @@ i_t flip_bounds(const lp_problem_t& lp, 4 * (lp.A.col_start[j + 1] - lp.A.col_start[j]) + 10; delta_x[j] += delta; vstatus[j] = variable_status_t::NONBASIC_UPPER; -#ifdef BOUND_FLIP_DEBUG - settings.log.printf( - "Flipping nonbasic %d from lo %e to up %e. z %e\n", j, lp.lower[j], lp.upper[j], z[j]); -#endif num_flipped++; - } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER && z[j] > dual_tol) { + } else { + assert(vstatus[j] == variable_status_t::NONBASIC_UPPER); const f_t delta = lp.lower[j] - lp.upper[j]; const size_t atilde_start_size = atilde_index.size(); scatter_dense(lp.A, j, -delta, atilde, mark, atilde_index); @@ -1249,13 +1263,10 @@ i_t flip_bounds(const lp_problem_t& lp, 4 * (lp.A.col_start[j + 1] - lp.A.col_start[j]) + 10; delta_x[j] += delta; vstatus[j] = variable_status_t::NONBASIC_LOWER; -#ifdef BOUND_FLIP_DEBUG - settings.log.printf( - "Flipping nonbasic %d from up %e to lo %e. z %e\n", j, lp.upper[j], lp.lower[j], z[j]); -#endif num_flipped++; } } + work_estimate += 2 * flip_indices.size(); return num_flipped; } @@ -1454,7 +1465,7 @@ i_t update_steepest_edge_norms(const simplex_solver_settings_t& settin work_estimate += 2 * v_sparse.i.size(); } v_sparse.scatter(v); - work_estimate += 2 * v_sparse.i.size(); + work_estimate += 4 * v_sparse.i.size(); const i_t leaving_index = basic_list[basic_leaving_index]; const f_t prev_dy_norm_squared = delta_y_steepest_edge[leaving_index]; @@ -1506,7 +1517,7 @@ i_t update_steepest_edge_norms(const simplex_solver_settings_t& settin delta_y_steepest_edge[j] = new_val; } } - work_estimate += 5 * scaled_delta_xB_nz; + work_estimate += 6 * scaled_delta_xB_nz; const i_t v_nz = v_sparse.i.size(); for (i_t k = 0; k < v_nz; ++k) { @@ -1541,13 +1552,64 @@ i_t check_steepest_edge_norms(const simplex_solver_settings_t& setting return 0; } +// Remove the perturbation from a variable that is leaving the basis. Since it +// is nonbasic, its cost affects only its own reduced cost. If removing the +// perturbation would violate dual feasibility, the perturbation is left in +// place (for boxed variables) or reduced to the minimum needed (for one-sided +// variables). +template +void remove_leaving_perturbation(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + i_t leaving_index, + i_t direction, + std::vector& z, + std::vector& objective) +{ + const f_t perturb = objective[leaving_index] - lp.objective[leaving_index]; + if (perturb == 0.0) return; + + const f_t lower = lp.lower[leaving_index]; + const f_t upper = lp.upper[leaving_index]; + const bool boxed = (lower > -inf && upper < inf); + + if (boxed) { + // Only remove if it won't create dual infeasibility. + // direction=1 means going to lower bound (needs z >= 0 after removal) + // direction=-1 means going to upper bound (needs z <= 0 after removal) + const f_t new_z = z[leaving_index] - perturb; + if (direction == 1 && new_z < -settings.tight_tol) { return; } + if (direction == -1 && new_z > settings.tight_tol) { return; } + z[leaving_index] = new_z; + objective[leaving_index] = lp.objective[leaving_index]; + } else { + z[leaving_index] -= perturb; + objective[leaving_index] = lp.objective[leaving_index]; + + // Restore dual feasibility if needed for one-sided variables + if (upper == inf && lower > -inf && z[leaving_index] < -settings.tight_tol) { + // At lower bound, needs z >= 0 + const f_t correction = -z[leaving_index]; + z[leaving_index] = 0.0; + objective[leaving_index] += correction; + } else if (lower == -inf && upper < inf && z[leaving_index] > settings.tight_tol) { + // At upper bound, needs z <= 0 + const f_t correction = z[leaving_index]; + z[leaving_index] = 0.0; + objective[leaving_index] -= correction; + } + } +} + template i_t compute_perturbation(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& delta_z_indices, + const std::vector& vstatus, std::vector& z, std::vector& objective, f_t& sum_perturb, + i_t entering_index, + f_t step_length, f_t& work_estimate) { const i_t n = lp.num_cols; @@ -1563,32 +1625,27 @@ i_t compute_perturbation(const lp_problem_t& lp, objective[j] += violation; num_perturb++; sum_perturb += violation; -#ifdef PERTURBATION_DEBUG - if (violation > 1e-1) { - settings.log.printf( - "perturbation: violation %e j %d lower %e\n", violation, j, lp.lower[j]); - } -#endif } else if (lp.lower[j] == -inf && lp.upper[j] < inf && z[j] > tight_tol) { const f_t violation = z[j]; z[j] -= violation; // z[j] <- 0 objective[j] -= violation; num_perturb++; sum_perturb += violation; -#ifdef PERTURBATION_DEWBUG - if (violation > 1e-1) { - settings.log.printf( - "perturbation: violation %e j %d upper %e\n", violation, j, lp.upper[j]); - } -#endif } } - work_estimate += 7 * delta_z_indices.size(); -#ifdef PERTURBATION_DEBUG - if (num_perturb > 0) { - settings.log.printf("Perturbed %d dual variables by %e\n", num_perturb, sum_perturb); + // On degenerate steps, shift the entering variable's cost (like HiGHS) + // This accumulates shifts that break degeneracy at the next refactorization + if (entering_index >= 0 && step_length == 0.0) { + assert(vstatus[entering_index] != variable_status_t::BASIC); + const f_t shift = -z[entering_index]; + if (shift != 0.0) { + objective[entering_index] += shift; + z[entering_index] = 0.0; + sum_perturb += std::abs(shift); + num_perturb++; + } } -#endif + work_estimate += 7 * delta_z_indices.size(); return 0; } @@ -2210,19 +2267,26 @@ void bound_info(const lp_problem_t& lp, } 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) +i_t set_primal_variables_on_bounds(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& z, + std::vector& vstatus, + std::vector& x, + i_t degen_type = 0) { PHASE2_NVTX_RANGE("DualSimplex::set_primal_variables_on_bounds"); - const i_t n = lp.num_cols; - f_t tol = 1e-10; + const i_t n = lp.num_cols; + f_t tol = 1e-10; + i_t num_fixed_to_lower = 0; + i_t num_fixed_to_upper = 0; + i_t num_lower_to_upper = 0; + i_t num_upper_to_lower = 0; + i_t num_set_fixed = 0; for (i_t j = 0; j < n; ++j) { // We set z_j = 0 for basic variables // But we explicitally skip setting basic variables here if (vstatus[j] == variable_status_t::BASIC) { continue; } + const variable_status_t old_vstatus = vstatus[j]; // We will flip the status of variables between nonbasic lower and nonbasic // upper here to improve dual feasibility const f_t fixed_tolerance = settings.fixed_tol; @@ -2243,29 +2307,69 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, vstatus[j] == variable_status_t::NONBASIC_UPPER) { x[j] = lp.upper[j]; } else if (z[j] >= 0 && lp.lower[j] > -inf) { - if (vstatus[j] != variable_status_t::NONBASIC_LOWER) { - settings.log.debug( - "Setting nonbasic lower variable (zj %e) %d to %e (current %e). vstatus %d\n", - z[j], - j, - lp.lower[j], - x[j], - static_cast(vstatus[j])); + // For boxed variables with degenerate z, use heuristic based on degen_type + if (degen_type >= 1 && std::abs(z[j]) < settings.dual_tol && lp.upper[j] < inf) { + if (degen_type == 1) { + // Column-sum heuristic + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + f_t col_sum = 0.0; + for (i_t k = col_start; k < col_end; k++) { + col_sum += lp.A.x[k]; + } + if (col_sum < 0.0) { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } else { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } + } else { + // degen_type == 3: abs_bound (prefer bound closer to zero, like HiGHS) + if (std::abs(lp.upper[j]) < std::abs(lp.lower[j])) { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } else { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } + } + } else { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; } - x[j] = lp.lower[j]; - vstatus[j] = variable_status_t::NONBASIC_LOWER; } else if (z[j] <= 0 && lp.upper[j] < inf) { - if (vstatus[j] != variable_status_t::NONBASIC_UPPER) { - settings.log.debug( - "Setting nonbasic upper variable (zj %e) %d to %e (current %e). vstatus %d\n", - z[j], - j, - lp.upper[j], - x[j], - static_cast(vstatus[j])); + // For boxed variables with degenerate z, use heuristic based on degen_type + if (degen_type >= 1 && std::abs(z[j]) < settings.dual_tol && lp.lower[j] > -inf) { + if (degen_type == 1) { + // Column-sum heuristic + const i_t col_start = lp.A.col_start[j]; + const i_t col_end = lp.A.col_start[j + 1]; + f_t col_sum = 0.0; + for (i_t k = col_start; k < col_end; k++) { + col_sum += lp.A.x[k]; + } + if (col_sum > 0.0) { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } else { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } + } else { + // degen_type == 3: abs_bound (prefer bound closer to zero) + if (std::abs(lp.lower[j]) < std::abs(lp.upper[j])) { + x[j] = lp.lower[j]; + vstatus[j] = variable_status_t::NONBASIC_LOWER; + } else { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; + } + } + } else { + x[j] = lp.upper[j]; + vstatus[j] = variable_status_t::NONBASIC_UPPER; } - x[j] = lp.upper[j]; - vstatus[j] = variable_status_t::NONBASIC_UPPER; } else if (lp.upper[j] == inf && lp.lower[j] > -inf && z[j] < 0) { // dual infeasible if (vstatus[j] != variable_status_t::NONBASIC_LOWER) { @@ -2299,7 +2403,38 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, } else { assert(1 == 0); } + // Track changes + if (old_vstatus != vstatus[j]) { + if (old_vstatus == variable_status_t::NONBASIC_FIXED && + vstatus[j] == variable_status_t::NONBASIC_LOWER) + num_fixed_to_lower++; + else if (old_vstatus == variable_status_t::NONBASIC_FIXED && + vstatus[j] == variable_status_t::NONBASIC_UPPER) + num_fixed_to_upper++; + else if (old_vstatus == variable_status_t::NONBASIC_LOWER && + vstatus[j] == variable_status_t::NONBASIC_UPPER) + num_lower_to_upper++; + else if (old_vstatus == variable_status_t::NONBASIC_UPPER && + vstatus[j] == variable_status_t::NONBASIC_LOWER) + num_upper_to_lower++; + else if (vstatus[j] == variable_status_t::NONBASIC_FIXED) + num_set_fixed++; + } + } + i_t total_changes = num_fixed_to_lower + num_fixed_to_upper + num_lower_to_upper + + num_upper_to_lower + num_set_fixed; + if (total_changes > 0) { + settings.log.printf( + "set_primal_variables_on_bounds: %d changes (fixed->lower=%d, fixed->upper=%d, " + "lower->upper=%d, upper->lower=%d, ->fixed=%d)\n", + total_changes, + num_fixed_to_lower, + num_fixed_to_upper, + num_lower_to_upper, + num_upper_to_lower, + num_set_fixed); } + return total_changes; } template @@ -2324,6 +2459,192 @@ f_t amount_of_perturbation(const lp_problem_t& lp, const std::vector +i_t attempt_to_remove_perturbations(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + basis_update_mpf_t& ft, + const std::vector& basic_list, + const std::vector& nonbasic_list, + std::vector& vstatus, + std::vector& objective, + std::vector& z, + std::vector& y, + std::vector& x, + std::vector& xB_workspace, + std::vector& squared_infeasibilities, + std::vector& infeasibility_indices, + f_t& primal_infeasibility, + f_t& primal_infeasibility_squared, + f_t& work_estimate) +{ + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + const i_t n_minus_m = n - m; + + // Check if there's any perturbation + const f_t perturbation = amount_of_perturbation(lp, objective); + if (perturbation <= 1e-6) return 0; // OPTIMAL + + // Count perturbations on basic vs nonbasic variables + i_t num_basic_perturbed = 0; + i_t num_nonbasic_boxed_perturbed = 0; + i_t num_nonbasic_other_perturbed = 0; + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + if (objective[j] != lp.objective[j]) num_basic_perturbed++; + } + for (i_t k = 0; k < n_minus_m; ++k) { + const i_t j = nonbasic_list[k]; + if (objective[j] != lp.objective[j]) { + const f_t lower = lp.lower[j]; + const f_t upper = lp.upper[j]; + if (lower > -inf && upper < inf && lower != upper) { + num_nonbasic_boxed_perturbed++; + } else { + num_nonbasic_other_perturbed++; + } + } + } + + if (num_basic_perturbed == 0 && num_nonbasic_other_perturbed == 0) { + // Safe path: perturbation only on nonbasic boxed variables. + // y is unaffected; z[j] - perturb gives exact unperturbed reduced cost. + i_t num_flipped = 0; + for (i_t k = 0; k < n_minus_m; ++k) { + const i_t j = nonbasic_list[k]; + const f_t perturb = objective[j] - lp.objective[j]; + if (perturb == 0.0) continue; + const f_t new_z = z[j] - perturb; + if (vstatus[j] == variable_status_t::NONBASIC_LOWER && new_z < -settings.dual_tol) { + vstatus[j] = variable_status_t::NONBASIC_UPPER; + z[j] = new_z; + objective[j] = lp.objective[j]; + num_flipped++; + } else if (vstatus[j] == variable_status_t::NONBASIC_UPPER && new_z > settings.dual_tol) { + vstatus[j] = variable_status_t::NONBASIC_LOWER; + z[j] = new_z; + objective[j] = lp.objective[j]; + num_flipped++; + } else { + z[j] = new_z; + objective[j] = lp.objective[j]; + } + } + work_estimate += 5 * n_minus_m; + + // Recompute x_B with flipped statuses + compute_primal_solution_from_basis( + lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); + work_estimate += 2 * n; + primal_infeasibility_squared = compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); + work_estimate += 4 * m + 2 * n; + + if (primal_infeasibility <= settings.primal_tol) return 0; // OPTIMAL + settings.log.printf("Removed perturbation. Continuing dual simplex (primal_inf=%.2e)\n", + primal_infeasibility); + return 1; // CONTINUE_DUAL + } + + // Perturbation on basic (or one-sided nonbasic) variables: need to recompute (y, z). + std::vector unperturbed_y(m); + std::vector unperturbed_z(n); + compute_dual_solution_from_basis( + lp, ft, basic_list, nonbasic_list, unperturbed_y, unperturbed_z, work_estimate); + + // Check if removal is clean (no dual infeasibility) + const f_t dual_infeas = + dual_infeasibility(lp, settings, vstatus, unperturbed_z, settings.tight_tol, settings.dual_tol); + work_estimate += 3 * n; + if (dual_infeas <= settings.dual_tol) { + settings.log.printf("Removed perturbation of %.2e.\n", perturbation); + z = unperturbed_z; + y = unperturbed_y; + objective = lp.objective; + work_estimate += 3 * n + 2 * m; + return 0; // OPTIMAL + } + + // Flip boxed nonbasics that are dual infeasible, and check for one-sided infeasibility + std::vector new_vstatus = vstatus; + i_t num_flipped = 0; + f_t residual_dual_infeas = 0.0; + for (i_t k = 0; k < n_minus_m; ++k) { + const i_t j = nonbasic_list[k]; + const f_t zj = unperturbed_z[j]; + const f_t lower = lp.lower[j]; + const f_t upper = lp.upper[j]; + const bool boxed = (lower > -inf && upper < inf && lower != upper); + + if (new_vstatus[j] == variable_status_t::NONBASIC_LOWER && zj < -settings.dual_tol) { + if (boxed) { + new_vstatus[j] = variable_status_t::NONBASIC_UPPER; + num_flipped++; + } else { + residual_dual_infeas = std::max(residual_dual_infeas, -zj); + } + } else if (new_vstatus[j] == variable_status_t::NONBASIC_UPPER && zj > settings.dual_tol) { + if (boxed) { + new_vstatus[j] = variable_status_t::NONBASIC_LOWER; + num_flipped++; + } else { + residual_dual_infeas = std::max(residual_dual_infeas, zj); + } + } + } + work_estimate += 5 * n_minus_m; + + if (residual_dual_infeas > settings.dual_tol) { + // One-sided infeasibility remains — can't continue with dual simplex. + // new_vstatus is discarded; vstatus unchanged. + settings.log.printf( + "Perturbation removal: %d flips, residual_dual_infeas=%.2e (PRIMAL_CLEANUP)\n", + num_flipped, + residual_dual_infeas); + return 2; // PRIMAL_CLEANUP + } + + // All infeasibility was on boxed variables — accept unperturbed solution + vstatus = new_vstatus; + z = unperturbed_z; + y = unperturbed_y; + objective = lp.objective; + work_estimate += 3 * n + 2 * m; + + // Recompute x_B with flipped statuses + compute_primal_solution_from_basis( + lp, ft, basic_list, nonbasic_list, vstatus, x, xB_workspace, work_estimate); + work_estimate += 2 * n; + primal_infeasibility_squared = compute_initial_primal_infeasibilities(lp, + settings, + basic_list, + x, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility); + work_estimate += 4 * m + 2 * n; + + settings.log.printf( + "Perturbation removal: %d flips, primal_inf=%.2e\n", num_flipped, primal_infeasibility); + if (primal_infeasibility <= settings.primal_tol) return 0; // OPTIMAL + settings.log.printf("Continuing dual after flip (primal_inf=%.2e)\n", primal_infeasibility); + return 1; // CONTINUE_DUAL +} + template void prepare_optimality(i_t info, f_t orig_primal_infeas, @@ -2331,54 +2652,44 @@ 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, + f_t& work_estimate, + i_t& iter, const std::vector& x, std::vector& y, std::vector& z, lp_solution_t& sol) { - 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); - f_t perturbation = amount_of_perturbation(lp, objective); - f_t orig_perturbation = perturbation; - if (perturbation > 1e-6 && phase == 2) { - // Try to remove perturbation - std::vector unperturbed_y(m); - std::vector unperturbed_z(n); - compute_dual_solution_from_basis( - lp, ft, basic_list, nonbasic_list, unperturbed_y, unperturbed_z, work_estimate); - { - const f_t dual_infeas = dual_infeasibility( - lp, settings, vstatus, unperturbed_z, settings.tight_tol, settings.dual_tol); - if (dual_infeas <= settings.dual_tol) { - settings.log.printf("Removed perturbation of %.2e.\n", perturbation); - z = unperturbed_z; - y = unperturbed_y; - perturbation = 0.0; - } else { - settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); - } - } - } + 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); + const f_t perturbation = amount_of_perturbation(lp, objective); - sol.l2_primal_residual = l2_primal_residual(lp, sol); - sol.l2_dual_residual = l2_dual_residual(lp, sol); - const f_t dual_infeas = dual_infeasibility(lp, settings, vstatus, z, 0.0, 0.0); - const f_t primal_infeas = primal_infeasibility(lp, settings, vstatus, x); + sol.l2_primal_residual = l2_primal_residual(lp, sol); + sol.l2_dual_residual = l2_dual_residual(lp, sol); + const f_t dual_infeas = dual_infeasibility(lp, settings, vstatus, z, 0.0, 0.0); + // Compute max primal infeasibility for reporting + f_t primal_infeas = 0.0; + for (i_t j = 0; j < n; ++j) { + if (x[j] < lp.lower[j]) { primal_infeas = std::max(primal_infeas, lp.lower[j] - x[j]); } + if (x[j] > lp.upper[j]) { primal_infeas = std::max(primal_infeas, x[j] - lp.upper[j]); } + } if (phase == 1 && iter > 0) { 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( @@ -2399,20 +2710,34 @@ void prepare_optimality(i_t info, primal_infeasibility_breakdown( lp, settings, vstatus, x, basic_infeas, nonbasic_infeas, basic_over); settings.log.printf( - "Primal infeasibility %e/%e (Basic %e, Nonbasic %e, Basic over %e). Perturbation %e/%e. Info " + "Primal infeasibility %e/%e (Basic %e, Nonbasic %e, Basic over %e). Perturbation %e. Info " "%d\n", primal_infeas, orig_primal_infeas, basic_infeas, nonbasic_infeas, basic_over, - orig_perturbation, perturbation, info); } #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: @@ -2435,60 +2760,131 @@ 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); + if (bfrt_time.time > 0.1) { + settings.log.printf(" BFRT breakpoints: %.2fs\n", bfrt_breakpoints_time); + settings.log.printf(" BFRT single_pass: %.2fs\n", bfrt_single_pass_time); + settings.log.printf(" BFRT coarse: %.2fs\n", bfrt_coarse_time); + settings.log.printf(" BFRT bucket: %.2fs\n", bfrt_bucket_time); + settings.log.printf(" BFRT select: %.2fs\n", bfrt_select_time); + } + if (bfrt_calls > 0) { + settings.log.printf(" BFRT calls: %d, zero_steps: %d (%.1f%%), single_pass_only: %d, bucket_used: %d, not_last_bucket: %d, fallback: %d\n", + bfrt_calls, bfrt_zero_steps, 100.0 * bfrt_zero_steps / bfrt_calls, + bfrt_single_pass_only, bfrt_bucket_used, bfrt_not_last_bucket, bfrt_fallback); + settings.log.printf(" BFRT slope_breaker: %d, not_slope_breaker: %d (%.1f%%)\n", + bfrt_selected_slope_breaker, bfrt_not_slope_breaker, + bfrt_bucket_used > 0 ? 100.0 * bfrt_not_slope_breaker / bfrt_bucket_used : 0.0); + if (bfrt_zero_steps > 0) { + settings.log.printf(" BFRT zero-step avg: num_buckets=%.1f, bucket0_size=%.1f, num_breakpoints=%.1f, harris_zero=%.1f, exact_zero=%.1f\n", + 1.0 * bfrt_zero_step_num_buckets_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_bucket0_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_num_breakpoints_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_harris_zero_sum / bfrt_zero_steps, + 1.0 * bfrt_zero_step_exact_zero_sum / bfrt_zero_steps); + } + } + print_one(settings, "Pricing time", pricing_time, total_time, total_work); + print_one(settings, "BTran time", btran_time, total_time, total_work); + print_one(settings, "FTran time", ftran_time, total_time, total_work); + 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; + f_t bfrt_breakpoints_time{0.0}; + f_t bfrt_single_pass_time{0.0}; + f_t bfrt_coarse_time{0.0}; + f_t bfrt_bucket_time{0.0}; + f_t bfrt_select_time{0.0}; + // BFRT diagnostic counters + i_t bfrt_calls{0}; + i_t bfrt_zero_steps{0}; // step_length == 0 + i_t bfrt_single_pass_only{0}; // no bound flips (single_pass decided) + i_t bfrt_bucket_used{0}; // bucket sort was used + i_t bfrt_not_last_bucket{0}; // selected from a bucket other than the last + i_t bfrt_fallback{0}; // fell back to single_pass result after bucket sort + i_t bfrt_zero_step_num_buckets_sum{0}; // sum of num_buckets on zero-step iters + i_t bfrt_zero_step_bucket0_sum{0}; // sum of bucket0 size on zero-step iters + i_t bfrt_zero_step_num_breakpoints_sum{0}; // sum of num_breakpoints on zero-step iters + i_t bfrt_zero_step_harris_zero_sum{0}; // sum of harris_ratios==0 on zero-step iters + i_t bfrt_zero_step_exact_zero_sum{0}; // sum of exact ratios==0 on zero-step iters + i_t bfrt_selected_slope_breaker{0}; // times we selected the slope breaker + i_t bfrt_not_slope_breaker{0}; // times we selected something else + work_timer_t pricing_time; + work_timer_t btran_time; + work_timer_t ftran_time; + 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; }; @@ -2503,6 +2899,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) { @@ -2525,6 +2922,7 @@ dual_status_t dual_phase2(i_t phase, nonbasic_list, sol, iter, + work_estimate, delta_y_steepest_edge, work_unit_context); } @@ -2542,6 +2940,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) { @@ -2556,7 +2955,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; @@ -2639,8 +3037,145 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, assert(dual_res_norm < 1e-3); #endif - phase2::set_primal_variables_on_bounds(lp, settings, z, vstatus, x); - phase2_work_estimate += 5 * (n - m); + // Count degenerate NONBASIC_FIXED variables before bound assignment + i_t num_degen = 0; + { + i_t num_fixed = 0; + for (i_t j = 0; j < n; ++j) { + if (vstatus[j] == variable_status_t::NONBASIC_FIXED) { + if (std::abs(lp.lower[j] - lp.upper[j]) >= settings.fixed_tol) { + num_fixed++; + if (std::abs(z[j]) < settings.dual_tol) num_degen++; + } + } + } + settings.log.printf( + "NONBASIC_FIXED boxed: %d, degenerate (|z_j| < dual_tol): %d\n", num_fixed, num_degen); + } + + // Try 3 strategies for degenerate bound assignment, pick best + f_t best_sum_infeas = inf; + i_t best_num_infeas = m; + i_t best_degen_type = 0; + std::vector best_vstatus; + std::vector best_x; + const char* degen_names[] = {"default", "column-sum", "abs-bound"}; + const i_t degen_types[] = {0, 1, 3}; + f_t all_sum_infeas[3]; + i_t all_num_infeas[3]; + + for (i_t di = 0; di < 3; di++) { + const i_t dt = degen_types[di]; + std::vector try_vstatus = vstatus; + std::vector try_x = x; + phase2::set_primal_variables_on_bounds(lp, settings, z, try_vstatus, try_x, dt); + phase2::compute_primal_variables(ft, + lp.rhs, + lp.A, + basic_list, + nonbasic_list, + settings.tight_tol, + try_x, + xB_workspace, + phase2_work_estimate); + f_t sum_infeas = 0.0; + i_t num_infeas = 0; + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + f_t infeas = std::max(lp.lower[j] - try_x[j], try_x[j] - lp.upper[j]); + if (infeas > 0.0) { + sum_infeas += infeas; + num_infeas++; + } + } + all_sum_infeas[di] = sum_infeas; + all_num_infeas[di] = num_infeas; + if (di == 0) { + // Default is the baseline + best_sum_infeas = sum_infeas; + best_num_infeas = num_infeas; + best_degen_type = 0; + best_vstatus = try_vstatus; + best_x = try_x; + } else { + // Only pick alternative if BOTH fewer infeasibilities AND lower sum + if (num_infeas <= best_num_infeas && sum_infeas < best_sum_infeas) { + best_sum_infeas = sum_infeas; + best_num_infeas = num_infeas; + best_degen_type = di; + best_vstatus = try_vstatus; + best_x = try_x; + } + } + if (phase == 1 || num_degen == 0) { + for (i_t t = 1; t < 3; t++) { + all_sum_infeas[t] = sum_infeas; + all_num_infeas[t] = num_infeas; + } + break; + } + } + vstatus = best_vstatus; + x = best_x; + settings.log.printf( + "Bound assignment: default(%d/%.2e) colsum(%d/%.2e) abs-bound(%d/%.2e) -> %s\n", + all_num_infeas[0], + all_sum_infeas[0], + all_num_infeas[1], + all_sum_infeas[1], + all_num_infeas[2], + all_sum_infeas[2], + degen_names[best_degen_type]); + phase2_work_estimate += 15 * (n - m); + + // Near-optimality check: decide whether to apply initial perturbation + if (settings.initial_perturbation != 0 && phase == 2) { + i_t num_primal_infeas = 0; + f_t max_primal_infeas = 0.0; + for (i_t k = 0; k < m; ++k) { + const i_t j = basic_list[k]; + f_t infeas = std::max(lp.lower[j] - x[j], x[j] - lp.upper[j]); + if (infeas > settings.primal_tol) { + num_primal_infeas++; + max_primal_infeas = std::max(max_primal_infeas, infeas); + } + } + bool near_optimal = (num_primal_infeas < 1000 && max_primal_infeas < 1e-3); + bool apply_perturbation = (settings.initial_perturbation == 1) || !near_optimal; + settings.log.printf( + "Near-optimal check: num_primal_infeas=%d, max_primal_infeas=%.2e, near_optimal=%d, " + "apply_perturbation=%d\n", + num_primal_infeas, + max_primal_infeas, + near_optimal, + apply_perturbation); + if (apply_perturbation) { + const bool strongly_degenerate = num_degen > n / 20; + phase2::initial_perturbation(lp, settings, vstatus, strongly_degenerate, objective); + // Recompute y, z with perturbed objective + for (i_t k = 0; k < m; ++k) { + c_basic[k] = objective[basic_list[k]]; + } + phase2_work_estimate += 3 * m; + ft.b_transpose_solve(c_basic, y); + phase2::compute_reduced_costs( + objective, lp.A, y, basic_list, nonbasic_list, z, phase2_work_estimate); + // Reassign bounds based on perturbed z (breaks degeneracy) + i_t num_bound_changes2 = phase2::set_primal_variables_on_bounds(lp, settings, z, vstatus, x); + phase2_work_estimate += 5 * (n - m); + if (num_bound_changes2 > 0) { + phase2::compute_primal_variables(ft, + lp.rhs, + lp.A, + basic_list, + nonbasic_list, + settings.tight_tol, + x, + xB_workspace, + phase2_work_estimate); + } + } + } #ifdef PRINT_VSTATUS_CHANGES i_t num_vstatus_changes; @@ -2664,16 +3199,6 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } phase2_work_estimate += 3 * n; - phase2::compute_primal_variables(ft, - lp.rhs, - lp.A, - basic_list, - nonbasic_list, - settings.tight_tol, - x, - xB_workspace, - phase2_work_estimate); - if (toc(start_time) > settings.time_limit) { return dual_status_t::TIME_LIMIT; } if (print_norms) { settings.log.printf("|| x || %e\n", vector_norm2(x)); } @@ -2792,8 +3317,9 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, i_t dense_delta_z = 0; i_t num_refactors = 0; i_t total_bound_flips = 0; + i_t max_bound_flips = 0; f_t delta_y_nz_percentage = 0.0; - phase2::phase2_timers_t timers(false); + phase2::phase2_timers_t timers(true); // Sparse vectors for main loop (declared outside loop for instrumentation) sparse_vector_t delta_y_sparse(m, 0); @@ -2811,10 +3337,11 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += ft.work_estimate(); ft.clear_work_estimate(); + f_t last_work_reported = 0.0; if (work_unit_context) { work_unit_context->record_work_sync_on_horizon((phase2_work_estimate) / 1e8); + last_work_reported = phase2_work_estimate; } - phase2_work_estimate = 0.0; if (phase == 2) { settings.log.printf("%5d %+.16e %7d %.8e %.2e %.2f\n", @@ -2835,7 +3362,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) { @@ -2856,7 +3383,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++) { @@ -2969,6 +3496,63 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, } } + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); + + // Before declaring optimal, attempt to remove perturbation. + if (phase == 2) { + i_t removal_status = phase2::attempt_to_remove_perturbations(lp, + settings, + ft, + basic_list, + nonbasic_list, + vstatus, + objective, + z, + y, + x, + xB_workspace, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility, + primal_infeasibility_squared, + phase2_work_estimate); + if (removal_status == 1) { // CONTINUE_DUAL + obj = phase2::compute_perturbed_objective(objective, x); + phase2_work_estimate += 2 * n; + continue; + } + if (removal_status == 2) { // PRIMAL_CLEANUP + const f_t perturbation = phase2::amount_of_perturbation(lp, objective); + settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); + settings.log.printf("Num updates: %d\n", ft.num_updates()); + settings.log.printf("Iterations: %d\n", iter); + i_t dual_iter = iter; + primal_status_t primal_status = primal_phase2_with_advanced_basis(2, + start_time, + lp, + settings, + vstatus, + ft, + basic_list, + nonbasic_list, + sol, + iter, + phase2_work_estimate, + false); + if (primal_status == primal_status_t::OPTIMAL) { + settings.log.printf("Primal cleanup successful. Iterations %d\n", iter - dual_iter); + objective = lp.objective; + } else { + settings.log.printf("Primal cleanup failed.\n"); + const f_t dual_infeas = phase2::dual_infeasibility( + lp, settings, vstatus, sol.z, settings.tight_tol, settings.dual_tol); + if (dual_infeas > 10.0 * settings.dual_tol) { return dual_status_t::NUMERICAL; } + } + } + // removal_status == 0 (OPTIMAL) or primal cleanup done: fall through to prepare_optimality + } + phase2::prepare_optimality(0, primal_infeasibility, lp, @@ -2981,6 +3565,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase, start_time, max_val, + phase2_work_estimate, iter, x, y, @@ -2998,7 +3583,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(); @@ -3006,7 +3591,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) { @@ -3030,7 +3615,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++) { @@ -3069,7 +3654,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; } @@ -3090,6 +3675,7 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, f_t step_length; i_t entering_index = -1; i_t nonbasic_entering_index = -1; + std::vector flip_indices; const bool harris_ratio = settings.use_harris_ratio; const bool bound_flip_ratio = settings.use_bound_flip_ratio; { @@ -3105,7 +3691,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, @@ -3122,13 +3708,44 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, delta_z, delta_z_indices, nonbasic_mark); - entering_index = bfrt.compute_step_length(step_length, nonbasic_entering_index); + entering_index = + bfrt.compute_step_length(step_length, nonbasic_entering_index, flip_indices); phase2_work_estimate += bfrt.work_estimate(); if (entering_index == RATIO_TEST_NUMERICAL_ISSUES) { settings.log.printf("Numerical issues encountered in ratio test.\n"); return dual_status_t::NUMERICAL; } - timers.bfrt_time += timers.stop_timer(); + timers.bfrt_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); + timers.bfrt_breakpoints_time += bfrt.time_compute_breakpoints_; + timers.bfrt_single_pass_time += bfrt.time_single_pass_; + timers.bfrt_coarse_time += bfrt.time_coarse_filter_; + timers.bfrt_bucket_time += bfrt.time_bucket_sort_; + timers.bfrt_select_time += bfrt.time_pivot_selection_; + // BFRT diagnostics + timers.bfrt_calls++; + if (step_length == 0.0) { + timers.bfrt_zero_steps++; + timers.bfrt_zero_step_num_buckets_sum += bfrt.num_buckets_used_; + timers.bfrt_zero_step_bucket0_sum += bfrt.bucket0_size_; + timers.bfrt_zero_step_num_breakpoints_sum += bfrt.num_breakpoints_; + timers.bfrt_zero_step_harris_zero_sum += bfrt.num_harris_zero_; + timers.bfrt_zero_step_exact_zero_sum += bfrt.num_exact_zero_; + } + if (bfrt.num_buckets_used_ == 0) { + timers.bfrt_single_pass_only++; + } else { + timers.bfrt_bucket_used++; + if (bfrt.used_fallback_) { + timers.bfrt_fallback++; + } else if (bfrt.bucket_selected_ < bfrt.num_buckets_used_ - 1) { + timers.bfrt_not_last_bucket++; + } + if (bfrt.selected_is_slope_breaker_) { + timers.bfrt_selected_slope_breaker++; + } else { + timers.bfrt_not_slope_breaker++; + } + } } else { entering_index = phase2::phase2_ratio_test( lp, settings, vstatus, nonbasic_list, z, delta_z, step_length, nonbasic_entering_index); @@ -3143,131 +3760,63 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += 2 * n; if (perturbation > 0.0 && phase == 2) { - // Try to remove perturbation - std::vector unperturbed_y(m); - std::vector unperturbed_z(n); - phase2_work_estimate += m + n; - phase2::compute_dual_solution_from_basis( - lp, ft, basic_list, nonbasic_list, unperturbed_y, unperturbed_z, phase2_work_estimate); - { - const f_t dual_infeas = phase2::dual_infeasibility( - lp, settings, vstatus, unperturbed_z, settings.tight_tol, settings.dual_tol); - phase2_work_estimate += 3 * n; - settings.log.printf("Dual infeasibility after removing perturbation %e\n", dual_infeas); - if (dual_infeas <= settings.dual_tol) { - settings.log.printf("Removed perturbation of %.2e.\n", perturbation); - z = unperturbed_z; - y = unperturbed_y; - phase2_work_estimate += 2 * n + 2 * m; - perturbation = 0.0; - - std::vector unperturbed_x(n); - phase2_work_estimate += n; - phase2::compute_primal_solution_from_basis(lp, - ft, - basic_list, - nonbasic_list, - vstatus, - unperturbed_x, - xB_workspace, - phase2_work_estimate); - x = unperturbed_x; - primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); - phase2_work_estimate += 4 * m + 2 * n; - settings.log.printf("Updated primal infeasibility: %e\n", primal_infeasibility); - - objective = lp.objective; - phase2_work_estimate += 2 * n; - // Need to reset the objective value, since we have recomputed x - obj = phase2::compute_perturbed_objective(objective, x); - phase2_work_estimate += 2 * n; - if (dual_infeas <= settings.dual_tol && primal_infeasibility <= settings.primal_tol) { - phase2::prepare_optimality(1, - primal_infeasibility, - lp, - settings, - ft, - objective, - basic_list, - nonbasic_list, - vstatus, - phase, - start_time, - max_val, - iter, - x, - y, - z, - sol); - status = dual_status_t::OPTIMAL; - break; - } - settings.log.printf( - "Continuing with perturbation removed and steepest edge norms reset\n"); - // Clear delta_z before restarting the iteration - phase2_work_estimate += 3 * delta_z_indices.size(); - phase2::clear_delta_z( - entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); - continue; - } else { - std::vector unperturbed_x(n); - phase2_work_estimate += n; - phase2::compute_primal_solution_from_basis(lp, - ft, - basic_list, - nonbasic_list, - vstatus, - unperturbed_x, - xB_workspace, - phase2_work_estimate); - x = unperturbed_x; - phase2_work_estimate += 2 * n; - primal_infeasibility_squared = - phase2::compute_initial_primal_infeasibilities(lp, - settings, - basic_list, - x, - squared_infeasibilities, - infeasibility_indices, - primal_infeasibility); - phase2_work_estimate += 4 * m + 2 * n; - - const f_t orig_dual_infeas = phase2::dual_infeasibility( - lp, settings, vstatus, z, settings.tight_tol, settings.dual_tol); - phase2_work_estimate += 3 * n; - - if (primal_infeasibility <= settings.primal_tol && - orig_dual_infeas <= settings.dual_tol) { - phase2::prepare_optimality(2, - primal_infeasibility, - lp, - settings, - ft, - objective, - basic_list, - nonbasic_list, - vstatus, - phase, - start_time, - max_val, - iter, - x, - y, - z, - sol); - status = dual_status_t::OPTIMAL; - break; - } - settings.log.printf("Failed to remove perturbation of %.2e.\n", perturbation); + i_t removal_status = phase2::attempt_to_remove_perturbations(lp, + settings, + ft, + basic_list, + nonbasic_list, + vstatus, + objective, + z, + y, + x, + xB_workspace, + squared_infeasibilities, + infeasibility_indices, + primal_infeasibility, + primal_infeasibility_squared, + phase2_work_estimate); + if (removal_status == 0) { // OPTIMAL + obj = phase2::compute_perturbed_objective(objective, x); + phase2_work_estimate += 2 * n; + if (primal_infeasibility <= settings.primal_tol) { + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); + phase2::prepare_optimality(1, + primal_infeasibility, + lp, + settings, + ft, + objective, + basic_list, + nonbasic_list, + vstatus, + phase, + start_time, + max_val, + phase2_work_estimate, + iter, + x, + y, + z, + sol); + status = dual_status_t::OPTIMAL; + break; } + settings.log.printf("Continuing with perturbation removed\n"); + phase2_work_estimate += 3 * delta_z_indices.size(); + phase2::clear_delta_z( + entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); + continue; + } else if (removal_status == 1) { // CONTINUE_DUAL + obj = phase2::compute_perturbed_objective(objective, x); + phase2_work_estimate += 2 * n; + phase2_work_estimate += 3 * delta_z_indices.size(); + phase2::clear_delta_z( + entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); + continue; } + // removal_status == 2 (PRIMAL_CLEANUP): fall through to existing logic below } if (perturbation == 0.0 && phase == 2) { @@ -3317,7 +3866,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 @@ -3333,7 +3882,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; @@ -3344,29 +3893,26 @@ 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, - bounded_variables, - objective, - z, - delta_z_indices, - nonbasic_list, - entering_index, - vstatus, - delta_x_flip, - atilde_mark, - atilde, - atilde_index, - phase2_work_estimate); - - timers.flip_time += timers.stop_timer(); + const i_t num_flipped = bound_flip_ratio ? phase2::flip_bounds(lp, + bounded_variables, + flip_indices, + vstatus, + delta_x_flip, + atilde_mark, + atilde, + atilde_index, + phase2_work_estimate) + : 0; + + timers.flip_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); total_bound_flips += num_flipped; + if (num_flipped > max_bound_flips) max_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, @@ -3378,10 +3924,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); @@ -3408,7 +3954,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; } @@ -3420,7 +3966,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, @@ -3442,18 +3988,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; @@ -3464,7 +4010,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, @@ -3474,9 +4020,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 @@ -3529,17 +4075,29 @@ 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()); + if (settings.remove_perturbation != 0) { + phase2::remove_leaving_perturbation(lp, settings, leaving_index, direction, z, objective); + } f_t sum_perturb = 0.0; - phase2::compute_perturbation( - lp, settings, delta_z_indices, z, objective, sum_perturb, phase2_work_estimate); - timers.perturb_time += timers.stop_timer(); + phase2::compute_perturbation(lp, + settings, + delta_z_indices, + vstatus, + z, + objective, + sum_perturb, + entering_index, + step_length, + phase2_work_estimate); + timers.perturb_time += timers.stop_timer(phase2_work_estimate + ft.work_estimate()); // Update basis information vstatus[entering_index] = variable_status_t::BASIC; @@ -3562,7 +4120,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"); @@ -3578,8 +4136,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 @@ -3657,7 +4215,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)) @@ -3676,16 +4234,19 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, phase2_work_estimate += 3 * delta_z_indices.size(); phase2::clear_delta_z(entering_index, leaving_index, delta_z_mark, delta_z_indices, delta_z); + // Flush basis update work into the total work estimate every iteration + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); + f_t now = toc(start_time); // Feature logging for regression training (every FEATURE_LOG_INTERVAL iterations) if ((iter % FEATURE_LOG_INTERVAL) == 0 && work_unit_context) { [[maybe_unused]] i_t iters_elapsed = iter - last_feature_log_iter; - phase2_work_estimate += ft.work_estimate(); - ft.clear_work_estimate(); - work_unit_context->record_work_sync_on_horizon(phase2_work_estimate / 1e8); - phase2_work_estimate = 0.0; + work_unit_context->record_work_sync_on_horizon((phase2_work_estimate - last_work_reported) / + 1e8); + last_work_reported = phase2_work_estimate; last_feature_log_iter = iter; } @@ -3717,16 +4278,32 @@ dual_status_t dual_phase2_with_advanced_basis(i_t phase, return dual_status_t::WORK_LIMIT; } - if (now > settings.time_limit) { return dual_status_t::TIME_LIMIT; } + if (now > settings.time_limit) { + status = dual_status_t::TIME_LIMIT; + break; + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return dual_status_t::CONCURRENT_LIMIT; } } - if (iter >= iter_limit) { status = dual_status_t::ITERATION_LIMIT; } + if (status != dual_status_t::TIME_LIMIT && iter >= iter_limit) { + status = dual_status_t::ITERATION_LIMIT; + } + + // Flush any remaining work from the basis update into the total work estimate + phase2_work_estimate += ft.work_estimate(); + ft.clear_work_estimate(); if (phase == 2) { timers.print_timers(settings); + i_t num_iters = iter - start_iter; + if (num_iters > 0) { + settings.log.printf("Bound flips: total=%d, avg=%.1f, max=%d\n", + total_bound_flips, + 1.0 * total_bound_flips / num_iters, + max_bound_flips); + } constexpr bool print_stats = false; if constexpr (print_stats) { settings.log.printf("Sparse delta_z %8d %8.2f%\n", @@ -3737,10 +4314,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; } @@ -3756,6 +4329,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); @@ -3772,6 +4346,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/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 78c7107ca3..1a67956e47 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -14,18 +14,128 @@ #include #include +#include + 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 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) + 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; } @@ -53,18 +163,20 @@ void set_primal_variables_on_bounds(const lp_problem_t& lp, assert(1 == 0); } } + work_estimate += n + 3.0 * (n - m); } 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, + f_t& work_estimate) { 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; @@ -102,6 +214,7 @@ f_t dual_infeasibility(const lp_problem_t& lp, non_basic_upper_inf++; } } + work_estimate += 8 * n; return sum_infeasible; } @@ -111,9 +224,11 @@ 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) + f_t& dual_inf, + f_t& work_estimate) { const i_t m = lp.num_rows; const i_t n = lp.num_cols; @@ -121,8 +236,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) && @@ -148,69 +262,78 @@ i_t phase2_pricing(const lp_problem_t& lp, } } } + work_estimate += 5 * (n - m); 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 devex_pricing(const lp_problem_t& lp, + const std::vector& z, + const std::vector& devex_weight, + const std::vector& nonbasic_list, + const std::vector& vstatus, + f_t dual_tol, + i_t& direction, + i_t& basic_entering, + f_t& dual_inf, + f_t& work_estimate) { - const i_t m = lp.num_rows; - const i_t n = lp.num_cols; - basic_leaving = -1; - i_t leaving_index = -1; - f_t min_val = inf; - constexpr f_t pivot_tol = 1e-8; - 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) { - // 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) { - min_val = ratio; - basic_leaving = k; - leaving_index = j; - } + const i_t m = lp.num_rows; + const i_t n = lp.num_cols; + i_t entering_index = -1; + f_t max_score = 0.0; + dual_inf = 0.0; + for (i_t k = 0; k < n - m; ++k) { + const i_t j = nonbasic_list[k]; + if (vstatus[j] == variable_status_t::NONBASIC_FIXED) { continue; } + f_t infeas = 0.0; + i_t dir = 0; + if ((vstatus[j] == variable_status_t::NONBASIC_LOWER || + vstatus[j] == variable_status_t::NONBASIC_FREE) && + z[j] < -dual_tol) { + infeas = -z[j]; + dir = 1; + } else if ((vstatus[j] == variable_status_t::NONBASIC_UPPER || + vstatus[j] == variable_status_t::NONBASIC_FREE) && + z[j] > dual_tol) { + infeas = z[j]; + dir = -1; } - if (lp.upper[j] < inf && x[j] <= lp.upper[j] && 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) { - min_val = ratio; - basic_leaving = k; - leaving_index = j; + if (infeas > 0.0) { + dual_inf += infeas; + const f_t score = (infeas * infeas) / devex_weight[j]; + if (score > max_score) { + max_score = score; + basic_entering = k; + entering_index = j; + direction = dir; } } } - step_length = min_val; - return leaving_index; + work_estimate += 7 * (n - m); + return entering_index; } 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, + 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; for (i_t j = 0; j < n; ++j) { - if (x[j] < lp.lower[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, @@ -221,10 +344,11 @@ 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; + num_infeasible++; if (infeas > 1e-6) { settings.log.debug("x %d infeas %e lo %e val %e up %e vstatus %hhd\n", j, @@ -236,15 +360,366 @@ f_t primal_infeasibility(const lp_problem_t& lp, } } } + work_estimate += n + 4 * m; 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, + f_t& work_estimate) +{ + i_t num_infeasible = 0; + 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, + 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) { + 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; + } else { + objective[j] = 0.0; + } + } + work_estimate += n - m + 4 * m; +} + +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, + 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]; + 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; } + } + work_estimate += 5 * (row_end - row_start); + } + work_estimate += 4 * delta_y.i.size(); +} + +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, + 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 +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, + 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; +} + +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_mpf_t& ft, + std::vector& c_basic, + std::vector& y, + std::vector& z, + f_t& work_estimate) +{ + 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]; + } + work_estimate += 3 * m; + 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]]; + } + 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 +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, + f_t& work_estimate) +{ + 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]; + } + 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 +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 -// 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 +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, + f_t& work_estimate) +{ + const i_t m = lp.num_rows; + basic_leaving = -1; + i_t leaving_index = -1; + constexpr f_t pivot_tol = 1e-8; + 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 < 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 <= 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 (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 <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = ratio; + basic_leaving = k; + leaving_index = j; + } + } + // 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 <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = ratio; + basic_leaving = k; + leaving_index = j; + } + } + + if (lp.lower[j] > -inf && delta_x[j] < -pivot_tol) { + // xj + step * delta_x[j] >= lp.lower[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. 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 <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = 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 <= (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 on the bound. + if (neum < 0 && -neum <= settings.primal_tol) { neum = 0.0; } + f_t ratio = neum / delta_x[j]; + if (ratio >= 0 && ratio <= alpha_1 && abs_dx > best_pivot) { + best_pivot = abs_dx; + step_length = ratio; + basic_leaving = k; + leaving_index = j; + } + } + } + + work_estimate += 10 * m; + return leaving_index; +} + template primal_status_t primal_phase2(i_t phase, f_t start_time, @@ -256,34 +731,14 @@ 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); + work_estimate += 2 * n; assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); @@ -308,6 +763,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,47 +808,64 @@ 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]; - } + basis_update_mpf_t ft(L, U, p, settings.refactor_frequency); - // 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)); + 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, + bool print_summary) +{ + 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); - set_primal_variables_on_bounds(lp, settings, z, vstatus, x); + std::vector& x = sol.x; + std::vector& y = sol.y; + std::vector& z = sol.z; - const f_t init_dual_inf = dual_infeasibility(lp, vstatus, z); - settings.log.printf("Initial dual infeasibility %e\n", init_dual_inf); + std::vector incoming_x = x; + std::vector incoming_vstatus = vstatus; + work_estimate += 2.0 * n; + settings.log.printf("Primal Simplex\n"); + settings.log.printf("Pricing: %s\n", settings.primal_pricing == 1 ? "Devex" : "Dantzig"); + // Nonbasics must be on their bounds before forming B x_B = b - A_N x_N. + // Setting them after the solve leaves ||A*x - b|| large whenever x_N != 0. + set_primal_variables_on_bounds(lp, settings, vstatus, x, work_estimate); 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) { @@ -403,150 +876,601 @@ primal_status_t primal_phase2(i_t phase, 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); - ft.b_solve(rhs, xB); + 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]; } - settings.log.printf("|| x || %e\n", vector_norm2(x)); + 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); - 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); + 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); + if (primal_inf > primal_tol) { + // We are primal infeasible. Switch to phase 1 + 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; + } else { + settings.log.printf("Phase 2\n"); + phase = 2; + } - const i_t iter_limit = iter + 1000; - std::vector delta_y(m); + std::vector c_basic(m); + 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)); } + + 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); } + + 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; + sparse_vector_t delta_y(m, 0); + sparse_vector_t etilde(m, 0); std::vector delta_z(n); std::vector delta_x(n); + std::vector devex_weight(n, 1.0); + i_t num_bad_devex_weight = 0; + work_estimate += 2 * m + 3 * n; + + f_t dual_inf = init_dual_inf; + f_t obj = compute_objective(lp, x); + 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); + 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; + + 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); - settings.log.printf("Iter Objective Primal inf Dual Inf. Step Entering Leaving\n"); while (iter < iter_limit) { + timers.start_timer(work_estimate + basis_update.work_estimate()); 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; + if (settings.primal_pricing == 1) { + entering_index = devex_pricing(lp, + z, + devex_weight, + nonbasic_list, + vstatus, + pricing_dual_tol, + direction, + nonbasic_entering, + dual_inf, + work_estimate); + } else { + entering_index = phase2_pricing(lp, + z, + nonbasic_list, + vstatus, + pricing_dual_tol, + direction, + nonbasic_entering, + dual_inf, + work_estimate); + } + timers.pricing_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); if (entering_index == -1) { - 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) { + // 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(); + 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); + 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); + if (primal_inf > primal_tol) { + 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); + settings.log.printf( + "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"); + 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, 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 + // 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, + work_estimate); + } + 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); + work_estimate += 2 * n; + sol.objective = obj; + sol.user_objective = compute_user_objective(lp, obj); + 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)); + 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)); + } + timers.print_timers(settings); + return primal_status_t::OPTIMAL; + } else { + 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 + // 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, 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); + 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, + work_estimate); + } + if (entering_index == -1) { + settings.log.printf( + "No entering variable found with large " + "infeasibility %e (%d).\n", + primal_inf, + num_primal_inf); + return primal_status_t::PRIMAL_INFEASIBLE; + } + pricing_dual_tol = retry_dual_tol; + } else { + // Restore the objective to the original objective + 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, + 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++; + // 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); + 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); - 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); + 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; + timers.ftran_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); - 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 + timers.start_timer(work_estimate + basis_update.work_estimate()); 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 = primal_ratio_test(lp, + settings, + vstatus, + basic_list, + x, + delta_x, + step_length, + basic_leaving, + 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; } - assert(step_length >= 0.0); - // Update the primal variables + 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); + 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 - // Update the factorization - ft.update(utilde, basic_leaving); + 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) { + 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); + } + + 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; + leave_bound = lp.lower[leaving_index]; + } else { + // 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; - // 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; + 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()); + + // Devex weight update (only when using Devex pricing) + if (settings.primal_pricing == 1) { + const f_t pivot = scaled_delta_xB[basic_leaving]; + const f_t pivot_sq = pivot * pivot; + const f_t w_enter = devex_weight[entering_index]; + // Exact pivot weight for entering variable is 1/pivot_sq + // Check if stored weight was a bad approximation + const f_t exact_pivot_weight = 1.0 / pivot_sq; + if (w_enter > 3.0 * exact_pivot_weight) { num_bad_devex_weight++; } + // Update weights for all nonbasic columns using the pivot row (delta_z) + // After compute_delta_z and update_z, delta_z[j] still holds the raw + // pivot row entries (update_z multiplies by dual_step_length into z, not delta_z) + for (i_t k = 0; k < n - m; ++k) { + const i_t j = nonbasic_list[k]; + const f_t a_j = delta_z[j]; + const f_t candidate = (a_j * a_j / pivot_sq) * w_enter; + if (candidate > devex_weight[j]) { devex_weight[j] = candidate; } + } + // Weight for leaving variable (now nonbasic) + devex_weight[leaving_index] = std::max(1.0 / pivot_sq, f_t(1e-4)); + // Weight for entering variable (now basic) — reset + devex_weight[entering_index] = 1.0; + work_estimate += 5 * (n - m); + // Reset framework if too many bad weights + if (num_bad_devex_weight > 3) { + std::fill(devex_weight.begin(), devex_weight.end(), f_t(1.0)); + num_bad_devex_weight = 0; + } + } + + timers.start_timer(work_estimate + basis_update.work_estimate()); + should_refactor = basis_update.update(utilde_sparse, etilde, basic_leaving) == 1; + timers.lu_update_time += timers.stop_timer(work_estimate + basis_update.work_estimate()); + } + 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; } + 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; + } + 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, 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. + compute_basic_primal_variables( + lp, basis_update, basic_list, nonbasic_list, x, work_estimate); + } } 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]]; + primal_inf = primal_infeasibility(lp, settings, vstatus, x, num_primal_inf, work_estimate); + if (primal_inf > primal_tol) { + 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; } - z[j] -= dot; + compute_phase1_objective(lp, settings, vstatus, x, objective, work_estimate); + phase = 1; + recompute_duals = true; + } else if (phase == 1) { + 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; } - // zB = 0 - for (i_t k = 0; k < m; ++k) { - z[basic_list[k]] = 0.0; + + if (recompute_duals) { + compute_dual_variables(lp, + settings, + objective, + basic_list, + nonbasic_list, + basis_update, + c_basic, + y, + z, + work_estimate); } - 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", - iter, - compute_user_objective(lp, obj), - primal_inf, - dual_inf, - step_length, - entering_index, - leaving_index); + 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++; + + f_t now = toc(start_time); + 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", + iter, + user_obj, + phase == 1 ? num_primal_inf : num_dual_inf, + phase == 1 ? primal_inf : dual_inf, + now); + switched_phase = false; + } + + work_estimate += basis_update.work_estimate(); + basis_update.clear_work_estimate(); + + 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; + } } - if (iter == iter_limit) { return primal_status_t::ITERATION_LIMIT; } + timers.print_timers(settings); + if (iter >= iter_limit) { return primal_status_t::ITERATION_LIMIT; } return primal_status_t::NUMERICAL; } #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 primal_status_t primal_phase2( int phase, double start_time, @@ -556,6 +1480,20 @@ 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, + bool print_summary); + #endif } // namespace cuopt::mathematical_optimization::simplex diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index 930958a802..fc47d90368 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 @@ -18,15 +19,47 @@ 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, + WORK_LIMIT = 8, + NOT_LOADED = 9 }; +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, + f_t& work_estimate); + +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, + // 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, f_t start_time, diff --git a/cpp/src/dual_simplex/right_looking_lu.cpp b/cpp/src/dual_simplex/right_looking_lu.cpp index 6a717cd257..63f5cb7c0f 100644 --- a/cpp/src/dual_simplex/right_looking_lu.cpp +++ b/cpp/src/dual_simplex/right_looking_lu.cpp @@ -209,7 +209,8 @@ class trailing_matrix_t { const f_t max_in_col = max_in_column_[j]; const i_t c_start = col_start_[j]; const i_t c_end = col_end_[j]; - for (i_t p = c_start; p < c_end; p++) { + i_t p; + for (p = c_start; p < c_end; p++) { const i_t i = c_i_[p]; const f_t val = c_x_[p]; const i_t rdeg = row_counts_.get_count(i); @@ -224,7 +225,7 @@ class trailing_matrix_t { if (markowitz <= markowitz_lower_bound) { break; } } } - work_estimate_ += 3 * (c_end - c_start); + work_estimate_ += 3 * (p - c_start); nsearch++; if (markowitz <= markowitz_lower_bound) { break; } } @@ -241,19 +242,21 @@ class trailing_matrix_t { assert(rdeg == nz); const i_t r_start = row_start_[i]; const i_t r_end = row_end_[i]; - for (i_t p = r_start; p < r_end; p++) { + i_t p; + for (p = r_start; p < r_end; p++) { const i_t j = r_j_[p]; // Look up the value from the column copy of j f_t val = 0; const i_t c_start = col_start_[j]; const i_t c_end = col_end_[j]; - for (i_t q = c_start; q < c_end; q++) { + i_t q; + for (q = c_start; q < c_end; q++) { if (c_i_[q] == i) { val = c_x_[q]; break; } } - work_estimate_ += 2 * (c_end - c_start); + work_estimate_ += 2 * (q - c_start); const f_t max_in_col = max_in_column_[j]; const i_t cdeg = col_counts_.get_count(j); assert(cdeg >= 0); @@ -267,7 +270,7 @@ class trailing_matrix_t { if (markowitz <= markowitz_lower_bound) { break; } } } - work_estimate_ += 5 * (r_end - r_start); + work_estimate_ += 5 * (p - r_start); nsearch++; if (markowitz <= markowitz_lower_bound) { break; } } @@ -334,7 +337,7 @@ class trailing_matrix_t { } } } - work_estimate_ += 2 * (c_end - c_start) + 6 * (pivot_col_count - n_fillin); + work_estimate_ += 2 * (c_end - c_start) + 5 * (pivot_col_count - n_fillin); // Step 2b: Remove cancellations (entries that became zero). if (n_cancel > 0) { @@ -1285,12 +1288,14 @@ class symmetric_trailing_matrix_t { const i_t j = r_j_[rp]; // Look up A(pivot_p, j) from column j f_t val = 0; - for (i_t q = col_start_[j]; q < col_end_[j]; q++) { + i_t q; + for (q = col_start_[j]; q < col_end_[j]; q++) { if (c_i_[q] == pivot_p) { val = c_x_[q]; break; } } + work_estimate_ += 2 * (q - col_start_[j]); const f_t lj = val / pivot_val; pivot_col_val_[j] = lj; pivot_col_mark_[j] = 1; diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 98c409a630..102036e635 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -253,6 +253,39 @@ i_t scaling(const lp_problem_t& unscaled, return 0; } + // MIP performs integer-aware row scaling before presolve, while QP and SOCP + // use the Ruiz path above. Apply this simpler equilibration only to LPs. + const bool use_lp_row_scaling = + !settings.inside_mip && unscaled.second_order_cone_dims.empty() && unscaled.Q.n == 0; + if (use_lp_row_scaling) { + csr_matrix_t Arow(0, 0, 0); + scaled.A.to_compressed_row(Arow); + std::vector row_norm(m, 1.0); + f_t max_row_norm = 0.0; + f_t min_row_norm = inf; + for (i_t i = 0; i < m; ++i) { + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + row_norm[i] = std::max(row_norm[i], std::abs(Arow.x[p])); + } + max_row_norm = std::max(max_row_norm, row_norm[i]); + min_row_norm = std::min(min_row_norm, row_norm[i]); + } + if (min_row_norm > 0.0 && max_row_norm / min_row_norm > 10.0) { + settings.log.printf("Applying row scaling. Maximum row norm %e, minimum row norm %e\n", + max_row_norm, + min_row_norm); + for (i_t j = 0; j < n; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + scaled.A.x[p] /= row_norm[scaled.A.i[p]]; + } + } + for (i_t i = 0; i < m; ++i) { + scaled.rhs[i] /= row_norm[i]; + row_scaling[i] = row_norm[i]; + } + } + } + column_scaling.resize(n); f_t max = 0; f_t min = std::numeric_limits::max(); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 8b3eba56d3..bed2c4eee5 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -81,6 +81,9 @@ struct simplex_solver_settings_t { augmented(0), dualize(-1), ordering(-1), + initial_perturbation(-1), + remove_perturbation(-1), + primal_pricing(0), barrier_dual_initial_point(barrier_dual_initial_point_t::Automatic), postsolve_info(-1), barrier_presolve_bound_free_variables(-1), @@ -88,6 +91,7 @@ struct simplex_solver_settings_t { barrier_initial_point_safeguard(10.0), check_Q(false), crossover(false), + unscaled_max_abs_obj_coeff(-1.0), refactor_frequency(100), iteration_log_frequency(1000), first_iteration_log(2), @@ -103,6 +107,9 @@ struct simplex_solver_settings_t { strong_chvatal_gomory_cuts(-1), symmetry(-1), reduced_cost_strengthening(-1), + dual_degenerate_feasibility_pump(1), + primal_degenerate_pivots(1), + dual_degenerate_pivots(1), cut_change_threshold(1e-3), cut_min_orthogonality(0.5), mip_batch_pdlp_strong_branching(0), @@ -188,6 +195,9 @@ struct simplex_solver_settings_t { i_t augmented; // -1 automatic, 0 to solve with ADAT, 1 to solve with augmented system i_t dualize; // -1 automatic, 0 to not dualize, 1 to dualize i_t ordering; // -1 automatic, 0 to use nested dissection, 1 to use AMD + i_t initial_perturbation; // -1 automatic, 0 to not perturb, 1 to perturb + i_t remove_perturbation; // -1 automatic, 0 disabled, 1 enabled + i_t primal_pricing; // 0 Dantzig (default), 1 Devex barrier_dual_initial_point_t barrier_dual_initial_point; // -1 automatic, 0 Lustig-Marsten-Shanno, // 1 dual least squares, 2 SeDuMi mu-based @@ -198,6 +208,7 @@ struct simplex_solver_settings_t { // the interior of the nonnegative orthant / SOC bool check_Q; // true to check if Q is positive semidefinite bool crossover; // true to do crossover, false to not + f_t unscaled_max_abs_obj_coeff; // max |c_j| before scaling (-1 = not set, compute from lp) i_t refactor_frequency; // number of basis updates before refactorization i_t iteration_log_frequency; // number of iterations between log updates i_t first_iteration_log; // number of iterations to log at beginning of solve @@ -215,10 +226,13 @@ struct simplex_solver_settings_t { i_t strong_chvatal_gomory_cuts; // -1 automatic, 0 to disable, >0 to enable strong Chvatal Gomory // cuts i_t symmetry; // -1 automatic, 0 to disable, >0 to enable different symmetry methods - i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost - // strengthening - f_t cut_change_threshold; // threshold for cut change - f_t cut_min_orthogonality; // minimum orthogonality for cuts + i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost + // strengthening + i_t dual_degenerate_feasibility_pump; // 0 to disable, 1 to enable + i_t primal_degenerate_pivots; // 0 to disable, 1 to enable + i_t dual_degenerate_pivots; // 0 to disable, 1 to enable + f_t cut_change_threshold; // threshold for cut change + f_t cut_min_orthogonality; // minimum orthogonality for cuts i_t mip_batch_pdlp_strong_branching; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch PDLP only i_t mip_batch_pdlp_reliability_branching; // 0 = DS only, 1 = cooperative DS + PDLP, 2 = batch diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 388bb43b35..607a4f798f 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -61,6 +61,53 @@ 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 @@ -117,6 +164,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"); @@ -135,6 +183,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; } @@ -150,6 +199,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; @@ -177,6 +227,14 @@ lp_status_t solve_linear_program_with_advanced_basis( presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; std::vector row_scales_simplex; + // Compute max |c_j| before scaling for perturbation calibration + if (settings.unscaled_max_abs_obj_coeff < 0.0) { + f_t max_obj = 0.0; + for (i_t j = 0; j < presolved_lp.num_cols; ++j) { + max_obj = std::max(max_obj, std::abs(presolved_lp.objective[j])); + } + const_cast&>(settings).unscaled_max_abs_obj_coeff = max_obj; + } { raft::common::nvtx::range scope_scaling("DualSimplex::scaling"); scaling(presolved_lp, settings, lp, column_scales, row_scales_simplex); @@ -217,6 +275,7 @@ lp_status_t solve_linear_program_with_advanced_basis( phase1_vstatus, phase1_solution, iter, + work_estimate, edge_norms, work_unit_context); } @@ -255,6 +314,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) { @@ -275,6 +335,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; @@ -291,12 +352,19 @@ lp_status_t solve_linear_program_with_advanced_basis( nonbasic_list, solution, iter, + work_estimate, edge_norms, work_unit_context); } 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); + // TODO: We need to update ft if the basis changed + } + if (settings.inside_mip && settings.concurrent_halt != nullptr) { + settings.log.debug("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); @@ -704,6 +772,104 @@ 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, @@ -718,8 +884,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; @@ -771,8 +938,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; @@ -828,6 +996,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( @@ -840,6 +1009,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( @@ -853,6 +1023,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_with_barrier( const user_problem_t& user_problem, const simplex_solver_settings_t& settings, diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 308c462de5..f91fff29e0 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -73,6 +73,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` @@ -88,6 +89,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 @@ -102,6 +104,11 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us 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_with_barrier(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, f_t start_time, diff --git a/cpp/src/math_optimization/solver_settings.cpp b/cpp/src/math_optimization/solver_settings.cpp index c913ee121a..6646e09bbf 100644 --- a/cpp/src/math_optimization/solver_settings.cpp +++ b/cpp/src/math_optimization/solver_settings.cpp @@ -134,13 +134,16 @@ 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_METHOD, reinterpret_cast(&mip_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_BARRIER, 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}, {CUOPT_DUALIZE, &pdlp_settings.dualize, -1, 1, -1}, {CUOPT_ORDERING, &pdlp_settings.ordering, -1, 1, -1}, + {CUOPT_INITIAL_PERTURBATION, &pdlp_settings.initial_perturbation, -1, 1, -1}, + {CUOPT_REMOVE_PERTURBATION, &pdlp_settings.remove_perturbation, -1, 1, -1}, + {CUOPT_PRIMAL_PRICING, &pdlp_settings.primal_pricing, 0, 1, 0}, {CUOPT_BARRIER_DUAL_INITIAL_POINT, reinterpret_cast(&pdlp_settings.barrier_dual_initial_point), -1, 2, -1}, {CUOPT_POSTSOLVE_INFO, &pdlp_settings.postsolve_info, -1, 1, -1}, {CUOPT_MIP_CUT_PASSES, &mip_settings.max_cut_passes, -1, std::numeric_limits::max(), 10}, @@ -153,6 +156,9 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_IMPLIED_BOUND_CUTS, &mip_settings.implied_bound_cuts, -1, 1, -1}, {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, + {CUOPT_MIP_DUAL_DEGENERATE_FEASIBILITY_PUMP, &mip_settings.dual_degenerate_feasibility_pump, -1, 1, -1}, + {CUOPT_MIP_PRIMAL_DEGENERATE_PIVOTS, &mip_settings.primal_degenerate_pivots, -1, 1, -1}, + {CUOPT_MIP_DUAL_DEGENERATE_PIVOTS, &mip_settings.dual_degenerate_pivots, -1, 1, -1}, {CUOPT_MIP_RINS, &mip_settings.submip_params.rins, -1, 1, -1}, {CUOPT_MIP_RENS, &mip_settings.submip_params.rens, -1, 1, -1}, {CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1}, diff --git a/cpp/src/mip_heuristics/root_heuristics.hpp b/cpp/src/mip_heuristics/root_heuristics.hpp index b9645579b5..2a893bcc19 100644 --- a/cpp/src/mip_heuristics/root_heuristics.hpp +++ b/cpp/src/mip_heuristics/root_heuristics.hpp @@ -19,6 +19,7 @@ struct cut_pass_heuristics_t { csr_matrix_t Arow_; std::vector root_solution_; std::vector root_edge_norm_; + std::vector new_slacks_; pseudo_costs_t pseudo_costs_; omp_atomic_t active_workers_; std::atomic halt_; @@ -31,11 +32,13 @@ struct cut_pass_heuristics_t { const std::vector& var_types, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, const simplex::simplex_solver_settings_t& settings) : var_types_(var_types), Arow_(Arow), root_solution_(root_solution), root_edge_norm_(root_edge_norm), + new_slacks_(new_slacks), pseudo_costs_(root_solution.size(), settings), active_workers_(0), halt_(false), @@ -82,7 +85,15 @@ struct cut_pass_heuristics_t { search_strategy_t type) { submip_worker_ = std::make_unique>( - id, lp, Arow_, var_types_, settings, pseudo_costs_, root_solution_, root_edge_norm_); + id, + lp, + Arow_, + var_types_, + settings, + pseudo_costs_, + root_solution_, + root_edge_norm_, + new_slacks_); submip_worker_->start_node = mip_node_t(root_obj, root_vstatus); submip_worker_->leaf_vstatus = root_vstatus; submip_worker_->leaf_solution.x = sol; @@ -121,7 +132,8 @@ struct cut_pass_heuristics_t { settings, pseudo_costs_, root_solution_, - root_edge_norm_)); + root_edge_norm_, + new_slacks_)); worker->start_node = root_node.detach_copy(); worker->start_lower = lp.lower; worker->start_upper = lp.upper; @@ -201,10 +213,11 @@ struct root_heuristics_t { const std::vector& var_types, const std::vector& root_solution, const std::vector& root_edge_norm, + const std::vector& new_slacks, const simplex::simplex_solver_settings_t& settings) { return cut_passes_heuristics_.emplace_back(std::make_shared>( - Arow, var_types, root_solution, root_edge_norm, settings)); + Arow, var_types, root_solution, root_edge_norm, new_slacks, settings)); } }; diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 22b4672496..645de93cd6 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -399,6 +399,15 @@ solution_t mip_solver_t::run_solver() context.settings.reduced_cost_strengthening == -1 ? 2 : context.settings.reduced_cost_strengthening; + branch_and_bound_settings.dual_degenerate_feasibility_pump = + context.settings.dual_degenerate_feasibility_pump == -1 + ? 1 + : context.settings.dual_degenerate_feasibility_pump; + branch_and_bound_settings.primal_degenerate_pivots = + context.settings.primal_degenerate_pivots == -1 ? 1 + : context.settings.primal_degenerate_pivots; + branch_and_bound_settings.dual_degenerate_pivots = + context.settings.dual_degenerate_pivots == -1 ? 1 : context.settings.dual_degenerate_pivots; branch_and_bound_settings.symmetry = context.settings.symmetry; branch_and_bound_settings.diving_settings = context.settings.diving_params; diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index d08a36d178..53c10b6a83 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -433,7 +433,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()); } @@ -600,9 +600,12 @@ 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; + dual_simplex_settings.remove_perturbation = settings.remove_perturbation; + dual_simplex_settings.primal_pricing = settings.primal_pricing; if (dual_simplex_settings.concurrent_halt != nullptr) { // Don't show the dual simplex log in concurrent mode. Show the PDLP log instead dual_simplex_settings.log.log = false; @@ -645,6 +648,61 @@ 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; + primal_settings.primal_pricing = settings.primal_pricing; + if (primal_settings.concurrent_halt != nullptr) { + // Don't show the primal simplex log in concurrent mode. Show the PDLP log instead + primal_settings.log.log = false; + } + + 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 @@ -1812,19 +1870,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); } } 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..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 @@ -62,6 +62,7 @@ class SolverMethod(IntEnum): PDLP = auto() DualSimplex = auto() Barrier = auto() + Primal = auto() Unset = auto() def __str__(self):