Skip to content
166 changes: 71 additions & 95 deletions cpp/src/dual_simplex/branch_and_bound.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand Down Expand Up @@ -183,21 +183,21 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound)
{
const f_t user_mip_gap = relative_gap(obj_value, lower_bound);
if (user_mip_gap == std::numeric_limits<f_t>::infinity()) {
return " - ";
return " - ";
} else {
constexpr int BUFFER_LEN = 32;
char buffer[BUFFER_LEN];
snprintf(buffer, BUFFER_LEN - 1, "%4.1f%%", user_mip_gap * 100);
snprintf(buffer, BUFFER_LEN - 1, "%5.1f%%", user_mip_gap * 100);
return std::string(buffer);
}
}

inline const char* feasible_solution_symbol(thread_type_t type)
{
switch (type) {
case thread_type_t::EXPLORATION: return "B";
case thread_type_t::DIVING: return "D";
default: return "U";
case thread_type_t::EXPLORATION: return "B ";
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Why not return a single char here and do the spacing in the format string itself?

case thread_type_t::DIVING: return "D ";
default: return "U ";
}
}

Expand Down Expand Up @@ -265,6 +265,51 @@ i_t branch_and_bound_t<i_t, f_t>::get_heap_size()
return size;
}

template <typename i_t, typename f_t>
void branch_and_bound_t<i_t, f_t>::report_heuristic(f_t obj)
{
if (solver_status_ == mip_exploration_status_t::RUNNING) {
f_t user_obj = compute_user_objective(original_lp_, obj);
f_t user_lower = compute_user_objective(original_lp_, get_lower_bound());
std::string user_gap = user_mip_gap<f_t>(user_obj, user_lower);

settings_.log.printf(
"H %+13.6e %+10.6e %s %9.2f\n",
user_obj,
user_lower,
user_gap.c_str(),
toc(exploration_stats_.start_time));
} else {
settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n",
compute_user_objective(original_lp_, obj),
toc(exploration_stats_.start_time));
}
}

template <typename i_t, typename f_t>
void branch_and_bound_t<i_t, f_t>::report(std::string symbol,
f_t obj,
f_t lower_bound,
i_t node_depth)
{
i_t nodes_explored = exploration_stats_.nodes_explored;
i_t nodes_unexplored = exploration_stats_.nodes_unexplored;
f_t user_obj = compute_user_objective(original_lp_, obj);
f_t user_lower = compute_user_objective(original_lp_, lower_bound);
f_t iter_node = exploration_stats_.total_lp_iters / nodes_explored;
std::string user_gap = user_mip_gap<f_t>(user_obj, user_lower);
settings_.log.printf("%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n",
symbol.c_str(),
nodes_explored,
nodes_unexplored,
user_obj,
user_lower,
node_depth,
iter_node,
user_gap.c_str(),
toc(exploration_stats_.start_time));
}

template <typename i_t, typename f_t>
void branch_and_bound_t<i_t, f_t>::set_new_solution(const std::vector<f_t>& solution)
{
Expand Down Expand Up @@ -303,25 +348,7 @@ void branch_and_bound_t<i_t, f_t>::set_new_solution(const std::vector<f_t>& solu
}
mutex_upper_.unlock();

if (is_feasible) {
if (solver_status_ == mip_exploration_status_t::RUNNING) {
f_t user_obj = compute_user_objective(original_lp_, obj);
f_t user_lower = compute_user_objective(original_lp_, get_lower_bound());
std::string gap = user_mip_gap<f_t>(user_obj, user_lower);

settings_.log.printf(
"H %+13.6e %+10.6e %s %9.2f\n",
user_obj,
user_lower,
gap.c_str(),
toc(exploration_stats_.start_time));
} else {
settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n",
compute_user_objective(original_lp_, obj),
toc(exploration_stats_.start_time));
}
}

if (is_feasible) { report_heuristic(obj); }
if (attempt_repair) {
mutex_repair_.lock();
repair_queue_.push_back(crushed_solution);
Expand Down Expand Up @@ -417,17 +444,7 @@ void branch_and_bound_t<i_t, f_t>::repair_heuristic_solutions()
if (repaired_obj < upper_bound_) {
upper_bound_ = repaired_obj;
incumbent_.set_incumbent_solution(repaired_obj, repaired_solution);

f_t obj = compute_user_objective(original_lp_, repaired_obj);
f_t lower = compute_user_objective(original_lp_, get_lower_bound());
std::string user_gap = user_mip_gap<f_t>(obj, lower);

settings_.log.printf(
"H %+13.6e %+10.6e %s %9.2f\n",
obj,
lower,
user_gap.c_str(),
toc(exploration_stats_.start_time));
report_heuristic(repaired_obj);

if (settings_.solution_callback != nullptr) {
std::vector<f_t> original_x;
Expand Down Expand Up @@ -523,29 +540,13 @@ void branch_and_bound_t<i_t, f_t>::add_feasible_solution(f_t leaf_objective,
i_t leaf_depth,
thread_type_t thread_type)
{
bool send_solution = false;
i_t nodes_explored = exploration_stats_.nodes_explored;
i_t nodes_unexplored = exploration_stats_.nodes_unexplored;
bool send_solution = false;

mutex_upper_.lock();
if (leaf_objective < upper_bound_) {
incumbent_.set_incumbent_solution(leaf_objective, leaf_solution);
upper_bound_ = leaf_objective;
f_t lower_bound = get_lower_bound();
f_t obj = compute_user_objective(original_lp_, upper_bound_);
f_t lower = compute_user_objective(original_lp_, lower_bound);
settings_.log.printf(
"%s%10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n",
feasible_solution_symbol(thread_type),
nodes_explored,
nodes_unexplored,
obj,
lower,
leaf_depth,
nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0,
user_mip_gap<f_t>(obj, lower).c_str(),
toc(exploration_stats_.start_time));

upper_bound_ = leaf_objective;
report(feasible_solution_symbol(thread_type), leaf_objective, get_lower_bound(), leaf_depth);
send_solution = true;
}

Expand Down Expand Up @@ -611,18 +612,20 @@ node_solve_info_t branch_and_bound_t<i_t, f_t>::solve_node(
ss << "simplex-" << std::this_thread::get_id() << ".log";
std::string logname;
ss >> logname;
lp_settings.set_log_filename(logname);
lp_settings.log.enable_log_to_file("a+");
lp_settings.log.set_log_file(logname, "a");
lp_settings.log.log_to_console = false;
lp_settings.log.printf(
"%s node id = %d, branch var = %d, fractional val = %f, variable lower bound = %f, variable "
"upper bound = %f\n",
"%scurrent node: id = %d, depth = %d, branch var = %d, branch dir = %s, fractional val = "
"%f, variable lower bound = %f, variable upper bound = %f, branch vstatus = %d\n\n",
settings_.log.log_prefix.c_str(),
node_ptr->node_id,
node_ptr->depth,
node_ptr->branch_var,
node_ptr->branch_dir == rounding_direction_t::DOWN ? "DOWN" : "UP",
node_ptr->fractional_val,
node_ptr->branch_var_lower,
node_ptr->branch_var_upper);
node_ptr->branch_var_upper,
node_ptr->vstatus[node_ptr->branch_var]);
#endif

// Reset the bound_changed markers
Expand Down Expand Up @@ -685,6 +688,10 @@ node_solve_info_t branch_and_bound_t<i_t, f_t>::solve_node(
}
}

#ifdef LOG_NODE_SIMPLEX
lp_settings.log.printf("\nLP status: %d\n\n", lp_status);
#endif

if (lp_status == dual::status_t::DUAL_UNBOUNDED) {
// Node was infeasible. Do not branch
node_ptr->lower_bound = inf;
Expand Down Expand Up @@ -805,23 +812,7 @@ void branch_and_bound_t<i_t, f_t>::exploration_ramp_up(mip_node_t<i_t, f_t>* nod
bool should_report = should_report_.exchange(false);

if (should_report) {
f_t obj = compute_user_objective(original_lp_, upper_bound);
f_t user_lower = compute_user_objective(original_lp_, root_objective_);
std::string gap_user = user_mip_gap<f_t>(obj, user_lower);
i_t nodes_explored = exploration_stats_.nodes_explored;
i_t nodes_unexplored = exploration_stats_.nodes_unexplored;

settings_.log.printf(
" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n",
nodes_explored,
nodes_unexplored,
obj,
user_lower,
node->depth,
nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0,
gap_user.c_str(),
now);

report(" ", upper_bound, root_objective_, node->depth);
exploration_stats_.nodes_since_last_log = 0;
exploration_stats_.last_log = tic();
should_report_ = true;
Expand Down Expand Up @@ -936,22 +927,7 @@ void branch_and_bound_t<i_t, f_t>::explore_subtree(i_t task_id,
abs_gap < 10 * settings_.absolute_mip_gap_tol) &&
time_since_last_log >= 1) ||
(time_since_last_log > 30) || now > settings_.time_limit) {
f_t obj = compute_user_objective(original_lp_, upper_bound);
f_t user_lower = compute_user_objective(original_lp_, get_lower_bound());
std::string gap_user = user_mip_gap<f_t>(obj, user_lower);
i_t nodes_explored = exploration_stats_.nodes_explored;
i_t nodes_unexplored = exploration_stats_.nodes_unexplored;

settings_.log.printf(
" %10d %10lu %+13.6e %+10.6e %6d %7.1e %s %9.2f\n",
nodes_explored,
nodes_unexplored,
obj,
user_lower,
node_ptr->depth,
nodes_explored > 0 ? exploration_stats_.total_lp_iters / nodes_explored : 0,
gap_user.c_str(),
now);
report(" ", upper_bound, get_lower_bound(), node_ptr->depth);
exploration_stats_.last_log = tic();
exploration_stats_.nodes_since_last_log = 0;
}
Expand Down Expand Up @@ -1447,10 +1423,10 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
settings_.num_diving_threads);

settings_.log.printf(
" | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap "
" | Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap "
"| Time |\n");

exploration_stats_.nodes_explored = 0;
exploration_stats_.nodes_explored = 1;
exploration_stats_.nodes_unexplored = 2;
exploration_stats_.nodes_since_last_log = 0;
exploration_stats_.last_log = tic();
Expand Down
5 changes: 4 additions & 1 deletion cpp/src/dual_simplex/branch_and_bound.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand Down Expand Up @@ -199,6 +199,9 @@ class branch_and_bound_t {
// its blocks the progression of the lower bound.
omp_atomic_t<f_t> lower_bound_ceiling_;

void report_heuristic(f_t obj);
void report(std::string symbol, f_t obj, f_t lower_bound, i_t node_depth);

// Set the final solution.
mip_status_t set_final_solution(mip_solution_t<i_t, f_t>& solution, f_t lower_bound);

Expand Down
12 changes: 7 additions & 5 deletions cpp/src/dual_simplex/logger.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand Down Expand Up @@ -30,22 +30,24 @@ class logger_t {
{
}

void enable_log_to_file(std::string mode = "w")
void enable_log_to_file(const char* mode = "w")
{
if (log_file != nullptr) { std::fclose(log_file); }
log_file = std::fopen(log_filename.c_str(), mode.c_str());
log_file = std::fopen(log_filename.c_str(), mode);
log_to_file = true;
}

void set_log_file(const std::string& filename)
void set_log_file(const std::string& filename, const char* mode = "w")
{
log_filename = filename;
enable_log_to_file();
enable_log_to_file(mode);
}

void close_log_file()
{
if (log_file != nullptr) { std::fclose(log_file); }
log_file = nullptr;
log_to_file = false;
}

void printf(const char* fmt, ...)
Expand Down
Loading