Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cpp/include/cuopt/linear_programming/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
#define CUOPT_MIP_CLIQUE_CUTS "mip_clique_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_OBJECTIVE_STEP "mip_objective_step"
#define CUOPT_MIP_CUT_CHANGE_THRESHOLD "mip_cut_change_threshold"
#define CUOPT_MIP_CUT_MIN_ORTHOGONALITY "mip_cut_min_orthogonality"
#define CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING "mip_batch_pdlp_strong_branching"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class mip_solver_settings_t {
i_t implied_bound_cuts = -1;
i_t strong_chvatal_gomory_cuts = -1;
i_t reduced_cost_strengthening = -1;
i_t 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{
Expand Down
22 changes: 19 additions & 3 deletions cpp/src/branch_and_bound/branch_and_bound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,13 @@ std::pair<node_status_t, rounding_direction_t> branch_and_bound_t<i_t, f_t>::upd
policy.graphviz(search_tree, node_ptr, "lower bound", leaf_obj);
policy.update_pseudo_costs(node_ptr, leaf_obj);
node_ptr->lower_bound = leaf_obj;
if (original_lp_.objective_is_integral) {
if (original_lp_.objective_step.has_step()) {
f_t step = original_lp_.objective_step.step_size;
f_t bias = original_lp_.objective_step.bias;
// Round up to next value on the lattice: k * step + bias >= leaf_obj
f_t k = std::ceil((leaf_obj - bias) / step - settings_.integer_tol);
node_ptr->lower_bound = k * step + bias;
} else if (original_lp_.objective_is_integral) {
node_ptr->lower_bound = std::ceil(leaf_obj - settings_.integer_tol);
Comment on lines +1230 to 1237
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fathom against the tightened node bound, not the raw LP objective.

After Lines 1230-1237, node_ptr->lower_bound may already be snapped up to the incumbent lattice value, but the branch/fathom check still uses leaf_obj. That keeps branching nodes that cannot produce a strictly improving integer solution anymore (for example, UB = 2, step = 0.5, leaf_obj = 1.7 snaps to LB = 2.0 and still branches here). Drive this decision from the tightened bound instead.

Also applies to: 1247-1273

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` around lines 1230 - 1237, The
code computes a tightened lattice-aware lower bound into node_ptr->lower_bound
(inside the original_lp_.objective_step/objective_is_integral branches) but the
subsequent fathom/branch decision still uses leaf_obj; update the branch/fathom
comparisons to use node_ptr->lower_bound instead of leaf_obj so decisions are
driven by the tightened bound (replace occurrences of leaf_obj in the
fathom/branch checks near the first block and also in the similar logic in the
1247-1273 section); ensure you keep the same tolerance logic
(settings_.integer_tol) and the existing comparison direction when swapping to
node_ptr->lower_bound.

}
policy.on_optimal_callback(leaf_solution.x, leaf_obj);
Expand Down Expand Up @@ -1345,8 +1351,18 @@ dual::status_t branch_and_bound_t<i_t, f_t>::solve_node_lp(
lp_settings.concurrent_halt = &node_concurrent_halt_;
lp_settings.set_log(false);
f_t cutoff = upper_bound_.load();
if (original_lp_.objective_is_integral) {
lp_settings.cut_off = std::ceil(cutoff - settings_.integer_tol) + settings_.dual_tol;
if (original_lp_.objective_step.has_step()) {
f_t step = original_lp_.objective_step.step_size;
f_t bias = original_lp_.objective_step.bias;
// Any improving feasible solution must have objective <= cutoff - step.
f_t k = std::floor((cutoff - bias) / step + settings_.integer_tol);
lp_settings.cut_off = (k - 1) * step + bias + settings_.dual_tol;
} else if (original_lp_.objective_is_integral) {
// If the objective is integral, any feasible solution should produce an upper bound that is
// (approximately) integral. We add a small tolerance and floor this value to get an integer,
// we then subtract 1, to stop simplex on problems that cannot improve the primal objective.
lp_settings.cut_off =
std::floor(cutoff + settings_.integer_tol) - 1 + settings_.dual_tol;
Comment on lines +1354 to +1365
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The cutoff formula drops one attainable value when cutoff is off-lattice.

(floor((cutoff - bias) / step + tol) - 1) * step + bias is only correct when cutoff is already on the objective lattice. If the incumbent is between lattice points—or just slightly below one because upper_bound_ stores the raw compute_objective(...) sum—you skip the next improving objective as well. The same off-by-one exists in the integral fallback.

Possible fix
   if (original_lp_.objective_step.has_step()) {
     f_t step = original_lp_.objective_step.step_size;
     f_t bias = original_lp_.objective_step.bias;
-    // Any improving feasible solution must have objective <= cutoff - step.
-    f_t k               = std::floor((cutoff - bias) / step + settings_.integer_tol);
+    // Largest lattice value strictly below `cutoff`.
+    f_t k               = std::ceil((cutoff - bias) / step - settings_.integer_tol);
     lp_settings.cut_off = (k - 1) * step + bias + settings_.dual_tol;
   } else if (original_lp_.objective_is_integral) {
-    lp_settings.cut_off =
-      std::floor(cutoff + settings_.integer_tol) - 1 + settings_.dual_tol;
+    lp_settings.cut_off =
+      std::ceil(cutoff - settings_.integer_tol) - 1 + settings_.dual_tol;
   } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` around lines 1354 - 1365, The
current cutoff computation in the objective_step and integral branches subtracts
an extra 1 (via (k - 1) and -1) which drops the next attainable lattice value
when cutoff is off-lattice; to fix, compute the lattice index k =
std::floor((cutoff - bias) / step + settings_.integer_tol) (for objective_step)
and use lp_settings.cut_off = k * step + bias + settings_.dual_tol (remove the
"-1" adjustment), and in the original_lp_.objective_is_integral fallback use
lp_settings.cut_off = std::floor(cutoff + settings_.integer_tol) +
settings_.dual_tol (remove the "-1"); update code around
original_lp_.objective_step, lp_settings.cut_off, settings_.integer_tol,
settings_.dual_tol, original_lp_.objective_is_integral and k accordingly.

} else {
lp_settings.cut_off = cutoff + settings_.dual_tol;
}
Expand Down
1 change: 1 addition & 0 deletions cpp/src/dual_simplex/presolve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ void convert_user_problem(const user_problem_t<i_t, f_t>& user_problem,
problem.obj_scale = user_problem.obj_scale;
problem.obj_constant = user_problem.obj_constant;
problem.objective_is_integral = user_problem.objective_is_integral;
problem.objective_step = user_problem.objective_step;
problem.rhs = user_problem.rhs;
problem.lower = user_problem.lower;
problem.upper = user_problem.upper;
Expand Down
1 change: 1 addition & 0 deletions cpp/src/dual_simplex/presolve.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ struct lp_problem_t {
f_t obj_constant;
f_t obj_scale; // 1.0 for min, -1.0 for max
bool objective_is_integral{false};
objective_step_t<f_t> objective_step;

void write_mps(const std::string& path) const
{
Expand Down
11 changes: 11 additions & 0 deletions cpp/src/dual_simplex/user_problem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ enum class variable_type_t : int8_t {
INTEGER = 2,
};

// The objective function takes values on a lattice: k * step_size + bias
// for integer k. A step_size of 0 means no lattice structure is known.
template <typename f_t>
struct objective_step_t {
f_t step_size{0};
f_t bias{0};

bool has_step() const { return step_size > 0; }
};

template <typename i_t, typename f_t>
struct user_problem_t {
user_problem_t(raft::handle_t const* handle_ptr_)
Expand All @@ -48,6 +58,7 @@ struct user_problem_t {
f_t obj_constant;
f_t obj_scale; // positive for min, netagive for max
bool objective_is_integral{false};
objective_step_t<f_t> objective_step;
std::vector<variable_type_t> var_types;
std::vector<i_t> Q_offsets;
std::vector<i_t> Q_indices;
Expand Down
1 change: 1 addition & 0 deletions cpp/src/math_optimization/solver_settings.cu
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ solver_settings_t<i_t, f_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<i_t>::max(), -1},
{CUOPT_MIP_OBJECTIVE_STEP, &mip_settings.objective_step, 0, 1, 1},
{CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, 1, 2, 1},
{CUOPT_NUM_GPUS, &mip_settings.num_gpus, 1, 2, 1},
{CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING, &mip_settings.mip_batch_pdlp_strong_branching, 0, 2, 0},
Expand Down
Loading