Skip to content

Unified pseudocost object for the regular and deterministic mode#1020

Draft
nguidotti wants to merge 53 commits intoNVIDIA:mainfrom
nguidotti:simplify-pseudocost
Draft

Unified pseudocost object for the regular and deterministic mode#1020
nguidotti wants to merge 53 commits intoNVIDIA:mainfrom
nguidotti:simplify-pseudocost

Conversation

@nguidotti
Copy link
Copy Markdown
Contributor

@nguidotti nguidotti commented Apr 1, 2026

This PR simplify the pseudo cost class in such a way that both regular and deterministic B&B use the same code path as much as possible. It also disable mutexes and atomics when running in deterministic mode as each thread has its own snapshot of the pseudocost.

It also move the routines related with the diving heuristics back to the diving_heuristics.cpp.

It includes #979 and #956.

Pending: MIPLIB benchmark

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
    • NA
  • Documentation
    • The documentation is up to date with these changes
    • Added new documentation
    • NA

nguidotti and others added 30 commits March 13, 2026 14:55
…er and worker pool in two files. removed local reduced cost fixing.
…tions. fix incorrect LP problem passed to the coefficient diving.
… improve-reliable-branching

# Conflicts:
#	cpp/src/branch_and_bound/pseudo_costs.cpp
…ange instead of the objective. fixed candidate ranking in reliability branching.
…. fix incorrect check for infeasible nodes in the tree.
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
nguidotti and others added 19 commits March 25, 2026 15:45
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
…unction).

Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
…g branching as a setting

Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
…fixing

# Conflicts:
#	cpp/src/branch_and_bound/branch_and_bound.cpp
…nd deterministic mode.

Signed-off-by: Nicolas Guidotti <224634272+nguidotti@users.noreply.github.com>
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot bot commented Apr 1, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@nguidotti nguidotti changed the title Simplify pseudocost Unified pseudocost object for the regular and deterministic mode Apr 1, 2026
@nguidotti
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nguidotti nguidotti added non-breaking Introduces a non-breaking change do not merge Do not merge if this flag is set improvement Improves an existing functionality labels Apr 1, 2026
@nguidotti nguidotti added this to the 26.06 milestone Apr 1, 2026
@nguidotti nguidotti self-assigned this Apr 1, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

This pull request restructures the branch-and-bound solver architecture by extracting worker management into separate headers, introducing mode-parameterized pseudo-cost structures, adding reduced-cost fixing functionality, and refactoring bounds propagation logic. Key changes include removal of mip_node.cpp, restructuring of worker infrastructure, and significant updates to core algorithm implementations.

Changes

Cohort / File(s) Summary
Build & Compilation
cpp/src/branch_and_bound/CMakeLists.txt, cpp/src/branch_and_bound/mip_node.cpp
Removed mip_node.cpp from compilation source list; deleted the entire implementation file containing inactive_status() function.
Constants & Enums
cpp/src/branch_and_bound/constants.hpp
New header extracting num_search_strategies, search_strategy_t enum, rounding_direction_t, and branch_and_bound_mode_t enums from previously scattered locations.
Worker Infrastructure
cpp/src/branch_and_bound/branch_and_bound_worker.hpp, cpp/src/branch_and_bound/worker.hpp, cpp/src/branch_and_bound/worker_pool.hpp
Removed old monolithic worker header; replaced with new modular worker.hpp (stats, single worker, initialization methods) and worker_pool.hpp (pool management, idle-worker coordination, strategy distribution helpers).
Core Branch-and-Bound
cpp/src/branch_and_bound/branch_and_bound.hpp, cpp/src/branch_and_bound/branch_and_bound.cpp
Major refactoring of root reduced-cost fixing (new root_reduced_cost_fixing() method), updated bounds strengthening flow after incumbent updates, worker-local LP state routing, deterministic snapshot handling, and loop termination gating.
Node Bounds Propagation
cpp/src/branch_and_bound/mip_node.hpp
Reworked get_variable_bounds() and update_branched_variable_bounds() to return feasibility flags, added start-bound parameters for incremental propagation, and inlined inactive_status() definition; imported rounding_direction_t from constants.hpp.
Pseudo-Costs & Strong Branching
cpp/src/branch_and_bound/pseudo_costs.hpp, cpp/src/branch_and_bound/pseudo_costs.cpp
Introduced mode-parameterized pseudo_costs_t<i_t, f_t, BnBMode> with conditional atomic/mutex types; refactored strong branching to support pivot estimation, updated score calculation via pseudo_cost_averages_t, and changed root_solution to lp_solution_t with basis metadata.
Diving Heuristics
cpp/src/branch_and_bound/diving_heuristics.hpp, cpp/src/branch_and_bound/diving_heuristics.cpp
Added branch_and_bound_mode_t Mode template parameter to pseudocost_diving() and guided_diving(); inlined scoring logic, replaced helper function calls with direct computation, and added explicit template instantiations for deterministic mode.
Reduced-Cost Fixing
cpp/src/branch_and_bound/reduced_cost_fixing.hpp
New header defining reduced_cost_fixing() function that tightens variable bounds based on reduced costs and returns counts of fixed/improved variables.
Deterministic Execution
cpp/src/branch_and_bound/deterministic_workers.hpp
Updated header includes (replacing branch_and_bound_worker.hpp with worker.hpp), changed total_lp_iters type to int64_t, removed variable selection helpers, and switched to node-based bounds retrieval with infeasibility handling.
Dual Simplex Helpers
cpp/src/dual_simplex/phase2.cpp, cpp/src/dual_simplex/phase2.hpp
Reorganized compute_reduced_cost_update() and compute_delta_z() helper functions; moved implementations to achieve better namespace/scope resolution; added public template declarations in header.
Solver Settings
cpp/src/dual_simplex/simplex_solver_settings.hpp, cpp/src/math_optimization/solver_settings.cu
Updated reduced_cost_strengthening semantics documentation (now supports levels 1–3 for different pass schedules); added mip_strong_branching_use_pivot_estimation boolean flag; updated CUOPT_MIP_BATCH_PDLP_STRONG_BRANCHING max value from 1 to 2.
MIP Heuristics Configuration
cpp/src/mip_heuristics/solver.cu, cpp/src/mip_heuristics/diversity/lns/rins.cu, cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
Updated reduced-cost strengthening configuration: default mapping now uses level 3 instead of 2; sub-MIP heuristics explicitly set reduced_cost_strengthening = 0.
Utility Helpers
cpp/src/utilities/omp_helpers.hpp
Added fake_omp_mutex_t class providing static no-op lock/unlock methods; extended omp_atomic_t<T> with get_no_atomic() accessors for non-atomic value retrieval.
Test Configuration
cpp/tests/mip/determinism_test.cu
Increased seymour1.mps test time limit from 60.0 to 120.0 seconds.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.87% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main objective: unifying the pseudocost object for regular and deterministic modes, which is the primary focus of the changeset.
Description check ✅ Passed The PR description clearly describes the main objectives: simplifying the pseudocost class for unified regular and deterministic B&B, disabling mutexes/atomics in deterministic mode, and moving diving heuristics routines.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/src/branch_and_bound/pseudo_costs.cpp (1)

835-874: ⚠️ Potential issue | 🟡 Minor

Initialize averages even when reliable_threshold != 0.

averages is only populated in the reliable_threshold == 0 branch, but the final score recomputation can still fall back to it if one side stayed uninitialized after trial branching. That leaves this path reading indeterminate values.

🩹 Minimal fix
-  pseudo_cost_averages_t<i_t, f_t> averages;
+  pseudo_cost_averages_t<i_t, f_t> averages = compute_averages();
@@
-  if (reliable_threshold == 0) {
-    averages = compute_averages();
+  if (reliable_threshold == 0) {
     log.printf("PC: num initialized down %d up %d avg down %e up %e\n",
                averages.num_init_down,
                averages.num_init_up,
                averages.down_avg,
                averages.up_avg);

Also applies to: 1056-1056

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/pseudo_costs.cpp` around lines 835 - 874, The
variable averages (pseudo_cost_averages_t<i_t, f_t> averages) is only assigned
inside the reliable_threshold == 0 branch but later code can use it when one
side's pseudocosts remain uninitialized; call compute_averages() unconditionally
(e.g., assign averages = compute_averages() right after declaring averages or
before the if(reliable_threshold==0) block) so averages is always valid, and
keep the existing log.printf call inside the reliable_threshold == 0 branch so
logging behavior is unchanged; update references to averages accordingly
(symbols: averages, compute_averages(), reliable_threshold).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 1471-1474: The abs_gap is incorrectly computed as lower_bound -
lower_bound making it zero; update both inner search loop locations (the block
that sets lower_bound, upper_bound, rel_gap, abs_gap and the similar block
around lines 1597-1600) to compute abs_gap as the difference between upper and
lower bounds (e.g., abs_gap = upper_bound - lower_bound or fabs(upper_bound -
lower_bound)) while keeping rel_gap = user_relative_gap(original_lp_,
upper_bound, lower_bound); adjust the two occurrences that reference variables
lower_bound, upper_bound, rel_gap, abs_gap so abs_gap reflects the true gap.
- Around line 2491-2493: The absolute-gap check uses the stale pre-cut
root_relax_objective, so abs_gap is computed incorrectly and can miss early
termination; change the abs_gap computation to use the cut-improved
root_objective_ (i.e., compute abs_gap = upper_bound_ - root_objective_) so the
check that compares abs_gap against settings_.absolute_mip_gap_tol (and the
related rel_gap computed via user_relative_gap(original_lp_,
upper_bound_.load(), root_objective_)) reflects the tightened root bound; update
the abs_gap declaration near where rel_gap, original_lp_, upper_bound_.load(),
root_relax_objective and root_objective_ are used in branch_and_bound.cpp
accordingly.
- Around line 391-408: The root_reduced_cost_fixing function currently can run
concurrently with the async root LP solve and reads/mutates original_lp_ and
uses root_objective_/root_relax_soln_.z before they are finalized; guard this by
checking a root-relaxation-complete flag (e.g., root_relaxation_done_ or
similar) or otherwise ensure the root solve is finished before proceeding, and
only then snapshot original_lp_.lower/upper under mutex_original_lp_ and call
reduced_cost_fixing; in short, add a precondition that verifies root_objective_
and root_relax_soln_.z are valid (or wait for completion) and only then
lock/copy original_lp_ and invoke reduced_cost_fixing from
root_reduced_cost_fixing to eliminate the race on original_lp_ and uninitialized
root data.

In `@cpp/src/branch_and_bound/diving_heuristics.cpp`:
- Around line 154-160: The guided diving loop reads incumbent[j] without
checking whether an incumbent snapshot exists; if incumbent is empty this can
read past bounds and crash. Fix by guarding access to incumbent in the loop over
fractional (e.g., check incumbent.empty() or incumbent.size() <= j) inside the
function that contains fractional, solution, and rounding_direction_t logic, and
when no incumbent is available compute dir using a safe fallback (for example
decide by f_down vs f_up or treat distances as infinite so rounding uses DOWN/UP
deterministically); ensure the same guarded logic is used wherever guided diving
relies on incumbent to avoid out-of-range reads.

In `@cpp/src/branch_and_bound/diving_heuristics.hpp`:
- Around line 25-33: The header must directly include constants.hpp before any
use of branch_and_bound_mode_t to avoid include-order dependencies: add an
`#include` "constants.hpp" at the top of
cpp/src/branch_and_bound/diving_heuristics.hpp (before the template declarations
for pseudocost_diving and guided_diving that use branch_and_bound_mode_t) and
apply the same change to cpp/src/branch_and_bound/pseudo_costs.hpp so
rounding_direction_t and branch_and_bound_mode_t are defined regardless of
include order.

In `@cpp/src/branch_and_bound/mip_node.hpp`:
- Around line 153-164: The get_variable_bounds flow currently overwrites
start_lower/start_upper with branch bounds and can undo previously tightened
bounds; change the logic so you intersect (lower[i] = max(lower[i],
branch_var_lower), upper[i] = min(upper[i], branch_var_upper)) instead of
assigning, update bounds_changed accordingly, and only then test feasibility
(upper >= lower) and return false if infeasible; apply the same
intersection/fetch-and-test fix where the same pattern appears (the other block
around update_branched_variable_bounds usage, e.g., lines referenced in the
comment), and ensure functions update_branched_variable_bounds /
get_variable_bounds use the lower/upper vectors (and bounds_changed flags) as
inputs to merge rather than skipping when bounds_changed[branch_var] is already
true.

In `@cpp/src/branch_and_bound/pseudo_costs.cpp`:
- Around line 574-576: The vectors strong_branch_down and strong_branch_up are
being initialized to 0 which treats unvisited candidates as zero-cost
observations; instead initialize them to NaN (e.g.
std::numeric_limits<f_t>::quiet_NaN()) wherever they are created (including the
other occurrences referenced) so untouched entries are marked missing, and
ensure the update/aggregation code that currently checks only for NaN continues
to skip those entries (and that any counters like num_strong_branches_completed
only reflect actual updates). This preserves the existing NaN-skip logic and
prevents zero-initialized pseudocosts from skewing reliability branching and
objective estimates.

In `@cpp/src/dual_simplex/phase2.cpp`:
- Around line 174-205: The CHECK_CHANGE_IN_REDUCED_COST debug block calls
phase2::compute_reduced_cost_update with outdated parameters (it references lp,
basic_list, nonbasic_list) and the helper was moved/renamed; update the debug
call to use the new helper signature (the standalone compute_delta_z or
new_compute_reduced_cost_update function) and pass the current variables used by
the non-debug path (e.g., delta_y_dense, leaving_index, direction,
delta_z_mark_check, delta_z_indices_check, delta_z_check, work_estimate) instead
of lp/basic_list/nonbasic_list, or adapt the helper call to
compute_delta_z(delta_y_dense, leaving_index, direction, delta_z_mark_check,
delta_z_indices_check, delta_z_check, work_estimate) so the debug path compiles
and validates the same sparse update as the main code.

---

Outside diff comments:
In `@cpp/src/branch_and_bound/pseudo_costs.cpp`:
- Around line 835-874: The variable averages (pseudo_cost_averages_t<i_t, f_t>
averages) is only assigned inside the reliable_threshold == 0 branch but later
code can use it when one side's pseudocosts remain uninitialized; call
compute_averages() unconditionally (e.g., assign averages = compute_averages()
right after declaring averages or before the if(reliable_threshold==0) block) so
averages is always valid, and keep the existing log.printf call inside the
reliable_threshold == 0 branch so logging behavior is unchanged; update
references to averages accordingly (symbols: averages, compute_averages(),
reliable_threshold).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5683742e-2ed8-4385-ba08-a3f28add35cf

📥 Commits

Reviewing files that changed from the base of the PR and between da8e980 and 6ec1fe8.

📒 Files selected for processing (24)
  • cpp/src/branch_and_bound/CMakeLists.txt
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/branch_and_bound/branch_and_bound.hpp
  • cpp/src/branch_and_bound/branch_and_bound_worker.hpp
  • cpp/src/branch_and_bound/constants.hpp
  • cpp/src/branch_and_bound/deterministic_workers.hpp
  • cpp/src/branch_and_bound/diving_heuristics.cpp
  • cpp/src/branch_and_bound/diving_heuristics.hpp
  • cpp/src/branch_and_bound/mip_node.cpp
  • cpp/src/branch_and_bound/mip_node.hpp
  • cpp/src/branch_and_bound/pseudo_costs.cpp
  • cpp/src/branch_and_bound/pseudo_costs.hpp
  • cpp/src/branch_and_bound/reduced_cost_fixing.hpp
  • cpp/src/branch_and_bound/worker.hpp
  • cpp/src/branch_and_bound/worker_pool.hpp
  • cpp/src/dual_simplex/phase2.cpp
  • cpp/src/dual_simplex/phase2.hpp
  • cpp/src/dual_simplex/simplex_solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip_heuristics/diversity/lns/rins.cu
  • cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
  • cpp/src/mip_heuristics/solver.cu
  • cpp/src/utilities/omp_helpers.hpp
  • cpp/tests/mip/determinism_test.cu
💤 Files with no reviewable changes (3)
  • cpp/src/branch_and_bound/mip_node.cpp
  • cpp/src/branch_and_bound/CMakeLists.txt
  • cpp/src/branch_and_bound/branch_and_bound_worker.hpp

Comment on lines +391 to +408
void branch_and_bound_t<i_t, f_t>::root_reduced_cost_fixing(f_t upper_bound)
{
if (settings_.reduced_cost_strengthening < 3) { return; }

mutex_original_lp_.lock();
std::vector<f_t> lower = original_lp_.lower;
std::vector<f_t> upper = original_lp_.upper;
std::vector<bool> bounds_changed;
mutex_original_lp_.unlock();

auto [num_fixed, num_improved] = reduced_cost_fixing(root_relax_soln_.z,
var_types_,
settings_,
root_objective_,
upper_bound,
lower,
upper,
bounds_changed);
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 | 🔴 Critical

Guard root reduced-cost fixing until the root relaxation is finalized.

This helper can now run before the root LP is finished, but it depends on root_objective_/root_relax_soln_.z being valid and it mutates original_lp_.lower/upper while the async root solve is still using original_lp_. That is a race and can also apply fixing against uninitialized root data.

🛡️ Suggested guard
 void branch_and_bound_t<i_t, f_t>::root_reduced_cost_fixing(f_t upper_bound)
 {
-  if (settings_.reduced_cost_strengthening < 3) { return; }
+  if (settings_.reduced_cost_strengthening < 3 || solving_root_relaxation_.load() ||
+      !std::isfinite(root_objective_)) {
+    return;
+  }

As per coding guidelines, "Ensure race conditions are absent in multi-threaded server implementations; verify proper synchronization of shared state".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` around lines 391 - 408, The
root_reduced_cost_fixing function currently can run concurrently with the async
root LP solve and reads/mutates original_lp_ and uses
root_objective_/root_relax_soln_.z before they are finalized; guard this by
checking a root-relaxation-complete flag (e.g., root_relaxation_done_ or
similar) or otherwise ensure the root solve is finished before proceeding, and
only then snapshot original_lp_.lower/upper under mutex_original_lp_ and call
reduced_cost_fixing; in short, add a precondition that verifies root_objective_
and root_relax_soln_.z are valid (or wait for completion) and only then
lock/copy original_lp_ and invoke reduced_cost_fixing from
root_reduced_cost_fixing to eliminate the race on original_lp_ and uninitialized
root data.

Comment on lines +154 to +160
for (i_t j : fractional) {
f_t f_down = solution[j] - std::floor(solution[j]);
f_t f_up = std::ceil(solution[j]) - solution[j];
f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j]));
f_t up_dist = std::abs(std::ceil(solution[j]) - incumbent[j]);
rounding_direction_t dir =
down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP;
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 | 🔴 Critical

Handle the no-incumbent case in guided diving.

incumbent[j] is unguarded now. The deterministic path can broadcast an empty incumbent snapshot before the first feasible solution is found, so a guided dive can read past the end of the vector and crash.

🐛 Minimal hardening
-    f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j]));
-    f_t up_dist   = std::abs(std::ceil(solution[j]) - incumbent[j]);
-    rounding_direction_t dir =
-      down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP;
+    rounding_direction_t dir = rounding_direction_t::DOWN;
+    if (j < static_cast<i_t>(incumbent.size())) {
+      f_t down_dist = std::abs(incumbent[j] - std::floor(solution[j]));
+      f_t up_dist   = std::abs(std::ceil(solution[j]) - incumbent[j]);
+      dir = down_dist < up_dist + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP;
+    } else {
+      dir = f_down < f_up + eps ? rounding_direction_t::DOWN : rounding_direction_t::UP;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/diving_heuristics.cpp` around lines 154 - 160, The
guided diving loop reads incumbent[j] without checking whether an incumbent
snapshot exists; if incumbent is empty this can read past bounds and crash. Fix
by guarding access to incumbent in the loop over fractional (e.g., check
incumbent.empty() or incumbent.size() <= j) inside the function that contains
fractional, solution, and rounding_direction_t logic, and when no incumbent is
available compute dir using a safe fallback (for example decide by f_down vs
f_up or treat distances as infinite so rounding uses DOWN/UP deterministically);
ensure the same guarded logic is used wherever guided diving relies on incumbent
to avoid out-of-range reads.

Comment on lines +25 to +33
template <typename i_t, typename f_t, branch_and_bound_mode_t Mode>
branch_variable_t<i_t> pseudocost_diving(pseudo_costs_t<i_t, f_t, Mode>& pc,
const std::vector<i_t>& fractional,
const std::vector<f_t>& solution,
const std::vector<f_t>& root_solution,
logger_t& log);

template <typename i_t, typename f_t>
branch_variable_t<i_t> guided_diving(pseudo_costs_t<i_t, f_t>& pc,
template <typename i_t, typename f_t, branch_and_bound_mode_t Mode>
branch_variable_t<i_t> guided_diving(pseudo_costs_t<i_t, f_t, Mode>& pc,
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 | 🔴 Critical

Include constants.hpp directly before using branch_and_bound_mode_t.

These declarations now depend on branch_and_bound_mode_t, but this header still only includes pseudo_costs.hpp. cpp/src/branch_and_bound/pseudo_costs.hpp still uses rounding_direction_t without including constants.hpp, so standalone includes now depend on include order and can fail to compile.

🔧 Minimal fix in this header
-#include <branch_and_bound/pseudo_costs.hpp>
+#include <branch_and_bound/constants.hpp>
+#include <branch_and_bound/pseudo_costs.hpp>

cpp/src/branch_and_bound/pseudo_costs.hpp needs the same direct include.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
template <typename i_t, typename f_t, branch_and_bound_mode_t Mode>
branch_variable_t<i_t> pseudocost_diving(pseudo_costs_t<i_t, f_t, Mode>& pc,
const std::vector<i_t>& fractional,
const std::vector<f_t>& solution,
const std::vector<f_t>& root_solution,
logger_t& log);
template <typename i_t, typename f_t>
branch_variable_t<i_t> guided_diving(pseudo_costs_t<i_t, f_t>& pc,
template <typename i_t, typename f_t, branch_and_bound_mode_t Mode>
branch_variable_t<i_t> guided_diving(pseudo_costs_t<i_t, f_t, Mode>& pc,
`#include` <branch_and_bound/constants.hpp>
`#include` <branch_and_bound/pseudo_costs.hpp>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/diving_heuristics.hpp` around lines 25 - 33, The
header must directly include constants.hpp before any use of
branch_and_bound_mode_t to avoid include-order dependencies: add an `#include`
"constants.hpp" at the top of cpp/src/branch_and_bound/diving_heuristics.hpp
(before the template declarations for pseudocost_diving and guided_diving that
use branch_and_bound_mode_t) and apply the same change to
cpp/src/branch_and_bound/pseudo_costs.hpp so rounding_direction_t and
branch_and_bound_mode_t are defined regardless of include order.

Comment on lines +153 to +164
bool get_variable_bounds(const std::vector<f_t>& start_lower,
const std::vector<f_t>& start_upper,
std::vector<f_t>& lower,
std::vector<f_t>& upper,
std::vector<bool>& bounds_changed) const
{
update_branched_variable_bounds(lower, upper, bounds_changed);
lower = start_lower;
upper = start_upper;

mip_node_t<i_t, f_t>* parent_ptr = parent;
bool feasible =
update_branched_variable_bounds(start_lower, start_upper, lower, upper, bounds_changed);
if (!feasible) { return false; }
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 | 🔴 Critical

Intersect with the current bounds instead of overwriting them.

get_variable_bounds() now starts from start_lower / start_upper, but update_branched_variable_bounds() still replaces that interval with the node's stored [branch_var_lower, branch_var_upper] and skips entirely when bounds_changed[branch_var] is already true. After root reduced-cost fixing, an older node can therefore re-widen a strengthened bound or drop its own tighter branch bound (for example, start [0,7], stored child [6,10] → returned [6,10]). Merge with max/min and test feasibility after the merge.

🐛 Suggested fix
-    if (!bounds_changed[branch_var]) {
-      // Apply the bounds at the current node
-      lower[branch_var]          = branch_var_lower;
-      upper[branch_var]          = branch_var_upper;
-      bounds_changed[branch_var] = true;
-    }
-
-    return true;
+    lower[branch_var] = std::max(lower[branch_var], branch_var_lower);
+    upper[branch_var] = std::min(upper[branch_var], branch_var_upper);
+    bounds_changed[branch_var] = true;
+    return upper[branch_var] >= lower[branch_var];

As per coding guidelines, validate algorithm correctness in optimization logic: simplex pivots, branch-and-bound decisions, routing heuristics, and constraint/objective handling must produce correct results.

Also applies to: 191-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/mip_node.hpp` around lines 153 - 164, The
get_variable_bounds flow currently overwrites start_lower/start_upper with
branch bounds and can undo previously tightened bounds; change the logic so you
intersect (lower[i] = max(lower[i], branch_var_lower), upper[i] = min(upper[i],
branch_var_upper)) instead of assigning, update bounds_changed accordingly, and
only then test feasibility (upper >= lower) and return false if infeasible;
apply the same intersection/fetch-and-test fix where the same pattern appears
(the other block around update_branched_variable_bounds usage, e.g., lines
referenced in the comment), and ensure functions update_branched_variable_bounds
/ get_variable_bounds use the lower/upper vectors (and bounds_changed flags) as
inputs to merge rather than skipping when bounds_changed[branch_var] is already
true.

Comment on lines +574 to +576
std::vector<f_t> strong_branch_down(fractional.size(), 0);
std::vector<f_t> strong_branch_up(fractional.size(), 0);
omp_atomic_t<i_t> num_strong_branches_completed = 0;
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

Don't treat unvisited strong-branch candidates as zero-cost observations.

strong_branch_down/up now start at 0, but the update path only skips NaN. If strong branching stops early on the global time limit, every untouched candidate gets recorded as an initialized zero pseudocost, which skews later reliability branching and objective estimates.

🧭 Proposed fix
-  std::vector<f_t> strong_branch_down(fractional.size(), 0);
-  std::vector<f_t> strong_branch_up(fractional.size(), 0);
+  std::vector<f_t> strong_branch_down(
+    fractional.size(), std::numeric_limits<f_t>::quiet_NaN());
+  std::vector<f_t> strong_branch_up(
+    fractional.size(), std::numeric_limits<f_t>::quiet_NaN());

Also applies to: 720-721, 1098-1119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/pseudo_costs.cpp` around lines 574 - 576, The
vectors strong_branch_down and strong_branch_up are being initialized to 0 which
treats unvisited candidates as zero-cost observations; instead initialize them
to NaN (e.g. std::numeric_limits<f_t>::quiet_NaN()) wherever they are created
(including the other occurrences referenced) so untouched entries are marked
missing, and ensure the update/aggregation code that currently checks only for
NaN continues to skip those entries (and that any counters like
num_strong_branches_completed only reflect actual updates). This preserves the
existing NaN-skip logic and prevents zero-initialized pseudocosts from skewing
reliability branching and objective estimates.

Comment on lines +174 to +205
#ifdef CHECK_CHANGE_IN_REDUCED_COST
const i_t m = A_transpose.n;
const i_t n = A_transpose.m;
std::vector<f_t> delta_y_dense(m);
delta_y.to_dense(delta_y_dense);
std::vector<f_t> delta_z_check(n);
std::vector<i_t> delta_z_mark_check(n, 0);
std::vector<i_t> delta_z_indices_check;
phase2::compute_reduced_cost_update(lp,
basic_list,
nonbasic_list,
delta_y_dense,
leaving_index,
direction,
delta_z_mark_check,
delta_z_indices_check,
delta_z_check,
work_estimate);
f_t error_check = 0.0;
for (i_t k = 0; k < n; ++k) {
const f_t diff = std::abs(delta_z[k] - delta_z_check[k]);
if (diff > 1e-6) {
printf("delta_z error %d transpose %e no transpose %e diff %e\n",
k,
delta_z[k],
delta_z_check[k],
diff);
}
error_check = std::max(error_check, diff);
}
if (error_check > 1e-6) { printf("delta_z error %e\n", error_check); }
#endif
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 | 🔴 Critical

CHECK_CHANGE_IN_REDUCED_COST no longer builds.

This validation block still references lp, basic_list, and nonbasic_list, but compute_delta_z(...) no longer receives them, and the helper was moved out of namespace phase2. Enabling the check now turns the debug path into a compile error instead of validating the sparse update.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/dual_simplex/phase2.cpp` around lines 174 - 205, The
CHECK_CHANGE_IN_REDUCED_COST debug block calls
phase2::compute_reduced_cost_update with outdated parameters (it references lp,
basic_list, nonbasic_list) and the helper was moved/renamed; update the debug
call to use the new helper signature (the standalone compute_delta_z or
new_compute_reduced_cost_update function) and pass the current variables used by
the non-debug path (e.g., delta_y_dense, leaving_index, direction,
delta_z_mark_check, delta_z_indices_check, delta_z_check, work_estimate) instead
of lp/basic_list/nonbasic_list, or adapt the helper call to
compute_delta_z(delta_y_dense, leaving_index, direction, delta_z_mark_check,
delta_z_indices_check, delta_z_check, work_estimate) so the debug path compiles
and validates the same sparse update as the main code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not merge Do not merge if this flag is set improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants