From e2d8adda064f3691045d558b6c7822a63b5feac2 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 16:26:05 +0200 Subject: [PATCH 01/28] logger_t now can support formatting strings based on `std::format` Signed-off-by: Nicolas L. Guidotti --- cpp/src/dual_simplex/logger.hpp | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index c35712fc26..eac0be56b3 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace cuopt::mathematical_optimization::simplex { @@ -84,6 +85,26 @@ class logger_t { } } + template + void print_format(std::format_string fmt, Args... args) + { + if (log) { + std::string msg = std::format(fmt, args...); + if (log_to_console) { +#ifdef CUOPT_LOG_ACTIVE_LEVEL + CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); +#else + std::printf("%s", msg.c_str()); + fflush(stdout); +#endif + } + if (log_to_file && log_file != nullptr) { + std::fprintf(log_file, "%s", msg.c_str()); + fflush(log_file); + } + } + } + void debug([[maybe_unused]] const char* fmt, ...) { if (log) { @@ -118,6 +139,26 @@ class logger_t { } } + template + void debug_format(std::format_string fmt, Args... args) + { + if (log) { + std::string msg = std::format(fmt, args...); + if (log_to_console) { +#ifdef CUOPT_LOG_ACTIVE_LEVEL + CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); +#else + std::printf("%s", msg.c_str()); + fflush(stdout); +#endif + } + if (log_to_file && log_file != nullptr) { + std::fprintf(log_file, "%s", msg.c_str()); + fflush(log_file); + } + } + } + bool log; bool log_to_console; std::string log_prefix; From 7b2d92b2d3e64fe63e652e5fc4ee13c2f366438b Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 16:29:30 +0200 Subject: [PATCH 02/28] moved search_tree object to a separated file. added a depth-first clean method. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + cpp/src/branch_and_bound/mip_node.hpp | 136 +----------------- cpp/src/branch_and_bound/search_tree.hpp | 129 +++++++++++++++++ 3 files changed, 133 insertions(+), 133 deletions(-) create mode 100644 cpp/src/branch_and_bound/search_tree.hpp diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 2d503fa4ad..5074128390 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index fc7b3a90e9..e5072c4d90 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -41,43 +41,8 @@ inline bool inactive_status(node_status_t status) template class mip_node_t { public: - ~mip_node_t() - { - // Iterative teardown to avoid stack overflow on deep trees. - // Detach all descendants breadth-first, then destroy them as leaves. - // vector::push_back can throw bad_alloc; the catch-all keeps the destructor - // exception-free. Under OOM, any not-yet-detached descendants are destroyed - // via the recursive unique_ptr chain in `children` as this frame unwinds. - try { - std::vector> nodes; - for (auto& c : children) { - if (c) { nodes.push_back(std::move(c)); } - } - // nodes.size() grows so that this loop only terminates when only leaves remain - for (size_t i = 0; i < nodes.size(); ++i) { - for (auto& c : nodes[i]->children) { - if (c) { nodes.push_back(std::move(c)); } - } - } - - // scope-exit ensure destruction of all detached leaves - } catch (const std::exception& e) { - // fprintf to stderr is allocation-free and cannot throw; using the - // project logger here would risk a secondary bad_alloc that would - // escape the destructor and re-introduce std::terminate. - std::fprintf(stderr, - "mip_node_t destructor: iterative teardown failed (%s); falling back to " - "recursive unique_ptr destruction.\n", - e.what()); - } catch (...) { - std::fprintf(stderr, - "mip_node_t destructor: iterative teardown failed (unknown exception); " - "falling back to recursive unique_ptr destruction.\n"); - } - } - - mip_node_t(mip_node_t&&) noexcept = default; - mip_node_t& operator=(mip_node_t&&) noexcept = default; + mip_node_t(mip_node_t&&) = default; + mip_node_t& operator=(mip_node_t&&) = default; mip_node_t() : status(node_status_t::PENDING), @@ -364,99 +329,4 @@ void remove_fathomed_nodes(std::vector*>& stack) } } -template -class search_tree_t { - public: - search_tree_t() : num_nodes(0) {} - - search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} - - void update(mip_node_t* node_ptr, node_status_t status) - { - std::lock_guard lock(mutex); - std::vector*> stack; - node_ptr->set_status(status, stack); - remove_fathomed_nodes(stack); - } - - void branch(mip_node_t* parent_node, - const i_t branch_var, - const f_t fractional_val, - const i_t integer_infeasible, - const std::vector& parent_vstatus, - const simplex::lp_problem_t& original_lp, - simplex::logger_t& log) - { - i_t id = num_nodes.fetch_add(2); - - auto down_child = std::make_unique>(original_lp, - parent_node, - ++id, - branch_var, - branch_direction_t::DOWN, - fractional_val, - integer_infeasible, - parent_vstatus); - graphviz_edge(log, - parent_node, - down_child.get(), - branch_var, - branch_direction_t::DOWN, - std::floor(fractional_val)); - - auto up_child = std::make_unique>(original_lp, - parent_node, - ++id, - branch_var, - branch_direction_t::UP, - fractional_val, - integer_infeasible, - parent_vstatus); - - graphviz_edge(log, - parent_node, - up_child.get(), - branch_var, - branch_direction_t::UP, - std::ceil(fractional_val)); - - assert(parent_vstatus.size() == original_lp.num_cols); - parent_node->add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree - } - - void graphviz_node(simplex::logger_t& log, - const mip_node_t* node_ptr, - const std::string label, - const f_t val) - { - if (write_graphviz) { - log.printf("Node%d [label=\"%s %.16e\"]\n", node_ptr->node_id, label.c_str(), val); - } - } - - void graphviz_edge(simplex::logger_t& log, - const mip_node_t* origin_ptr, - const mip_node_t* dest_ptr, - const i_t branch_var, - branch_direction_t branch_dir, - const f_t bound) - { - if (write_graphviz) { - log.printf("Node%d -> Node%d [label=\"x%d %s %e\"]\n", - origin_ptr->node_id, - dest_ptr->node_id, - branch_var, - branch_dir == branch_direction_t::DOWN ? "<=" : ">=", - bound); - } - } - - mip_node_t root; - omp_mutex_t mutex; - omp_atomic_t num_nodes; - - static constexpr bool write_graphviz = false; -}; - -} // namespace cuopt::mathematical_optimization::mip +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp new file mode 100644 index 0000000000..5057d44f05 --- /dev/null +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -0,0 +1,129 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +class search_tree_t { + public: + search_tree_t() : num_nodes(0) {} + + search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} + + ~search_tree_t() { clean(); } + + void update(mip_node_t* node_ptr, node_status_t status) + { + std::lock_guard lock(mutex); + std::vector*> stack; + node_ptr->set_status(status, stack); + remove_fathomed_nodes(stack); + } + + void branch(mip_node_t* parent_node, + const i_t branch_var, + const f_t fractional_val, + const i_t integer_infeasible, + const std::vector& parent_vstatus, + const lp_problem_t& original_lp, + logger_t& log) + { + i_t id = num_nodes.fetch_add(2); + + auto down_child = std::make_unique>(original_lp, + parent_node, + ++id, + branch_var, + branch_direction_t::DOWN, + fractional_val, + integer_infeasible, + parent_vstatus); + graphviz_edge(log, + parent_node, + down_child.get(), + branch_var, + branch_direction_t::DOWN, + std::floor(fractional_val)); + + auto up_child = std::make_unique>(original_lp, + parent_node, + ++id, + branch_var, + branch_direction_t::UP, + fractional_val, + integer_infeasible, + parent_vstatus); + + graphviz_edge(log, + parent_node, + up_child.get(), + branch_var, + branch_direction_t::UP, + std::ceil(fractional_val)); + + assert(parent_vstatus.size() == original_lp.num_cols); + parent_node->add_children(std::move(down_child), + std::move(up_child)); // child pointers moved into the tree + } + + static void graphviz_node(logger_t& log, + const mip_node_t* node_ptr, + const std::string label, + const f_t val) + { + if (write_graphviz) { + log.printf("Node%d [label=\"%s %.16e\"]\n", node_ptr->node_id, label.c_str(), val); + } + } + + static void graphviz_edge(logger_t& log, + const mip_node_t* origin_ptr, + const mip_node_t* dest_ptr, + const i_t branch_var, + branch_direction_t branch_dir, + const f_t bound) + { + if (write_graphviz) { + log.printf("Node%d -> Node%d [label=\"x%d %s %e\"]\n", + origin_ptr->node_id, + dest_ptr->node_id, + branch_var, + branch_dir == branch_direction_t::DOWN ? "<=" : ">=", + bound); + } + } + + // Clean the tree using a depth first scheme + void clean() + { + std::vector>> stack; + + if (root.children[0]) stack.push_back(std::move(root.children[0])); + if (root.children[1]) stack.push_back(std::move(root.children[1])); + + while (!stack.empty()) { + auto node = std::move(stack.back()); + std::cout << std::format("destroying node {}", node->node_id) << std::endl; + stack.pop_back(); + if (node->children[0]) stack.push_back(std::move(node->children[0])); + if (node->children[1]) stack.push_back(std::move(node->children[1])); + // Implicitly call destructor for `node` + } + } + + mip_node_t root; + omp_mutex_t mutex; + omp_atomic_t num_nodes; + + static constexpr bool write_graphviz = false; +}; + +} // namespace cuopt::linear_programming::dual_simplex From 948919189d61f0e109cd7a4fd4a012627fa9fc3a Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 19:28:27 +0200 Subject: [PATCH 03/28] track search progress using tree the weight metric. migrate the B&B logs to use the std::format variant. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 158 +++++++++++------- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 + cpp/src/branch_and_bound/search_tree.hpp | 27 ++- cpp/src/dual_simplex/logger.hpp | 9 +- 4 files changed, 123 insertions(+), 72 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 806f2850ce..55411ec055 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -225,22 +225,12 @@ f_t user_relative_gap(const lp_problem_t& lp, f_t obj_value, f_t lower return user_mip_gap; } -template -std::string user_mip_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) +template +std::string to_percentage(f_t value) { - const f_t user_mip_gap = user_relative_gap(lp, obj_value, lower_bound); - if (user_mip_gap == std::numeric_limits::infinity()) { - return " - "; - } else { - constexpr int BUFFER_LEN = 32; - char buffer[BUFFER_LEN]; - if (user_mip_gap > 1e-3) { - snprintf(buffer, BUFFER_LEN - 1, "%5.1f%%", user_mip_gap * 100); - } else { - snprintf(buffer, BUFFER_LEN - 1, "%5.2f%%", user_mip_gap * 100); - } - return std::string(buffer); - } + if (value == std::numeric_limits::infinity()) return "---"; + if (value > 1e-3) { return std::format("{:5.1f}%", value * 100); } + return std::format("{:5.2f}%", value * 100); } } // namespace @@ -331,29 +321,72 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) upper_bound_ = bound; } +template +void branch_and_bound_t::print_table_header() +{ + if (settings_.deterministic) { + settings_.log.print_format( + "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|{:^8}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap", + "Completion", + "Work", + "Time"); + } else { + settings_.log.print_format( + "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap", + "Completion", + "Time"); + } +} + template void branch_and_bound_t::report_heuristic(f_t obj) { if (is_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(original_lp_, obj, get_lower_bound()); - - settings_.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", + f_t lower_bound = get_lower_bound(); + f_t user_obj = compute_user_objective(original_lp_, obj); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + std::string user_gap_text = to_percentage(user_gap); + + settings_.log.print_format( + "H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15} {:>8.2f}", + "", // nodes explored + "", // nodes unexplored user_obj, user_lower, - user_gap.c_str(), + "", // integer infeasible + "", // depth + "", // iter/node + user_gap_text.c_str(), + "", // tree progress toc(exploration_stats_.start_time)); } else { if (solving_root_relaxation_.load()) { f_t user_obj = compute_user_objective(original_lp_, obj); - std::string user_gap = - user_mip_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); + f_t user_gap = user_relative_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); + std::string user_gap_text = to_percentage(user_gap); settings_.log.printf( "New solution from primal heuristics. Objective %+.6e. Gap %s. Time %.2f\n", user_obj, - user_gap.c_str(), + user_gap_text.c_str(), toc(exploration_stats_.start_time)); } else { settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", @@ -368,16 +401,20 @@ void branch_and_bound_t::report( char symbol, f_t obj, f_t lower_bound, i_t node_depth, i_t node_int_infeas, double work_time) { update_user_bound(lower_bound); - const i_t nodes_explored = exploration_stats_.nodes_explored; - const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - const f_t user_obj = compute_user_objective(original_lp_, obj); - const f_t user_lower = compute_user_objective(original_lp_, lower_bound); - const f_t iters = static_cast(exploration_stats_.total_lp_iters); - const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; - const std::string user_gap = user_mip_gap(original_lp_, obj, lower_bound); + const i_t nodes_explored = exploration_stats_.nodes_explored; + const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + const f_t user_obj = compute_user_objective(original_lp_, obj); + const f_t user_lower = compute_user_objective(original_lp_, lower_bound); + const f_t iters = static_cast(exploration_stats_.total_lp_iters); + const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; + f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + std::string user_gap_text = to_percentage(user_gap); + std::string tree_completion = to_percentage(search_tree_.progress.load()); + if (work_time >= 0) { - settings_.log.printf( - "%c %10d %10lu %+13.6e %+10.6e %6d %6d %7.1e %s %9.2f %9.2f\n", + settings_.log.print_format( + "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f} " + "{:>8.2f}", symbol, nodes_explored, nodes_unexplored, @@ -386,21 +423,24 @@ void branch_and_bound_t::report( node_int_infeas, node_depth, iter_node, - user_gap.c_str(), + user_gap_text.c_str(), + tree_completion.c_str(), work_time, toc(exploration_stats_.start_time)); } else { - settings_.log.printf("%c %10d %10lu %+13.6e %+10.6e %6d %6d %7.1e %s %9.2f\n", - symbol, - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_int_infeas, - node_depth, - iter_node, - user_gap.c_str(), - toc(exploration_stats_.start_time)); + settings_.log.print_format( + "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f}", + symbol, + nodes_explored, + nodes_unexplored, + user_obj, + user_lower, + node_int_infeas, + node_depth, + iter_node, + user_gap_text.c_str(), + tree_completion.c_str(), + toc(exploration_stats_.start_time)); } } @@ -2644,11 +2684,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut is_running_ = true; lower_bound_numerical_ = inf; - if (num_fractional != 0 && settings_.max_cut_passes > 0) { - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node | " - "Gap | Time |\n"); - } + if (num_fractional != 0 && settings_.max_cut_passes > 0) { print_table_header(); } cut_pool_t cut_pool(original_lp_.num_cols, settings_); cut_generation_t cut_generation(cut_pool, @@ -2943,16 +2979,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.diving_settings.coefficient_diving != 0) { calculate_variable_locks(original_lp_, var_up_locks_, var_down_locks_); } - - if (settings_.deterministic) { - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node " - "| Gap | Work | Time |\n"); - } else { - settings_.log.printf( - " | Explored | Unexplored | Objective | Bound | IntInf | Depth | Iter/Node " - "| Gap | Time |\n"); - } + print_table_header(); #pragma omp taskgroup { @@ -3464,9 +3491,10 @@ void branch_and_bound_t::deterministic_sync_callback() exploration_stats_.last_log = tic(); } - f_t obj = compute_user_objective(original_lp_, upper_bound); - f_t user_lower = compute_user_objective(original_lp_, lower_bound); - std::string gap_user = user_mip_gap(original_lp_, upper_bound, lower_bound); + f_t obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + f_t user_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + std::string user_gap_text = to_percentage(user_gap); std::string idle_workers; i_t idle_count = 0; @@ -3482,7 +3510,7 @@ void branch_and_bound_t::deterministic_sync_callback() exploration_stats_.nodes_unexplored, obj, user_lower, - gap_user.c_str(), + user_gap_text.c_str(), toc(exploration_stats_.start_time), state_hash, idle_workers.empty() ? "" : " ", diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 5074128390..4de69a36dc 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -262,6 +262,7 @@ class branch_and_bound_t { omp_atomic_t lower_bound_numerical_; std::function user_bound_callback_; + void print_table_header(); void report_heuristic(f_t obj); void report(char symbol, f_t obj, diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index 5057d44f05..e3a6ed9901 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -22,7 +22,16 @@ class search_tree_t { void update(mip_node_t* node_ptr, node_status_t status) { - std::lock_guard lock(mutex); + std::lock_guard lock(mutex); + + --num_open_nodes; + if (status == node_status_t::HAS_CHILDREN) { + ++num_inner_nodes; + } else { + ++num_final_nodes; + progress += std::pow(2, -node_ptr->depth); + } + std::vector*> stack; node_ptr->set_status(status, stack); remove_fathomed_nodes(stack); @@ -72,6 +81,7 @@ class search_tree_t { assert(parent_vstatus.size() == original_lp.num_cols); parent_node->add_children(std::move(down_child), std::move(up_child)); // child pointers moved into the tree + num_open_nodes += 2; } static void graphviz_node(logger_t& log, @@ -111,7 +121,6 @@ class search_tree_t { while (!stack.empty()) { auto node = std::move(stack.back()); - std::cout << std::format("destroying node {}", node->node_id) << std::endl; stack.pop_back(); if (node->children[0]) stack.push_back(std::move(node->children[0])); if (node->children[1]) stack.push_back(std::move(node->children[1])); @@ -121,7 +130,19 @@ class search_tree_t { mip_node_t root; omp_mutex_t mutex; - omp_atomic_t num_nodes; + omp_atomic_t num_nodes; + + // Number of nodes that still needs to be explored + omp_atomic_t num_open_nodes; + + // Number of integer feasible, infeasible or fathomed nodes + omp_atomic_t num_final_nodes; + + // Number of inner nodes + omp_atomic_t num_inner_nodes; + + // Track the solver progress based on how much the tree was explored + omp_atomic_t progress = 0.0; static constexpr bool write_graphviz = false; }; diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index eac0be56b3..9303095230 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -12,6 +12,7 @@ #endif #include +#include #include #include @@ -86,10 +87,10 @@ class logger_t { } template - void print_format(std::format_string fmt, Args... args) + void print_format(std::string_view fmt, Args&&... args) { if (log) { - std::string msg = std::format(fmt, args...); + const std::string msg = std::vformat(fmt, std::make_format_args(args...)); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); @@ -140,10 +141,10 @@ class logger_t { } template - void debug_format(std::format_string fmt, Args... args) + void debug_format(std::string_view fmt, Args&&... args) { if (log) { - std::string msg = std::format(fmt, args...); + std::string msg = std::vformat(fmt, std::make_format_args(args...)); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); From dad242a12ec7541a46ca4bbc18220adc812cd68a Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 15:44:44 +0200 Subject: [PATCH 04/28] simplified log lines construction in B&B Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 119 +++++++----------- 1 file changed, 46 insertions(+), 73 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 55411ec055..0f9a890f48 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -324,36 +324,21 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) template void branch_and_bound_t::print_table_header() { - if (settings_.deterministic) { - settings_.log.print_format( - "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|{:^8}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap", - "Completion", - "Work", - "Time"); - } else { - settings_.log.print_format( - "{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|{:^8}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap", - "Completion", - "Time"); - } + std::string header = + std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap", + "Completion"); + if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } + header += std::format("{:^8}|", "Time"); + settings_.log.printf("%s", header.c_str()); } template @@ -366,18 +351,22 @@ void branch_and_bound_t::report_heuristic(f_t obj) f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); std::string user_gap_text = to_percentage(user_gap); - settings_.log.print_format( - "H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15} {:>8.2f}", - "", // nodes explored - "", // nodes unexplored - user_obj, - user_lower, - "", // integer infeasible - "", // depth - "", // iter/node - user_gap_text.c_str(), - "", // tree progress - toc(exploration_stats_.start_time)); + std::string log_line = + std::format("H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", + "", // nodes explored + "", // nodes unexplored + user_obj, + user_lower, + "", // integer infeasible + "", // depth + "", // iter/node + user_gap_text, + "" // tree progress + ); + + if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } + log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); + settings_.log.printf("%s", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { f_t user_obj = compute_user_objective(original_lp_, obj); @@ -411,37 +400,21 @@ void branch_and_bound_t::report( std::string user_gap_text = to_percentage(user_gap); std::string tree_completion = to_percentage(search_tree_.progress.load()); - if (work_time >= 0) { - settings_.log.print_format( - "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f} " - "{:>8.2f}", - symbol, - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_int_infeas, - node_depth, - iter_node, - user_gap_text.c_str(), - tree_completion.c_str(), - work_time, - toc(exploration_stats_.start_time)); - } else { - settings_.log.print_format( - "{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15} {:>8.2f}", - symbol, - nodes_explored, - nodes_unexplored, - user_obj, - user_lower, - node_int_infeas, - node_depth, - iter_node, - user_gap_text.c_str(), - tree_completion.c_str(), - toc(exploration_stats_.start_time)); - } + std::string log_line = + std::format("{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", + symbol, + nodes_explored, + nodes_unexplored, + user_obj, + user_lower, + node_int_infeas, + node_depth, + iter_node, + user_gap_text, + tree_completion); + if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } + log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); + settings_.log.printf("%s", log_line.c_str()); } template From 3056ba2a6905a946ad1849fe01770e3b0d72cb5b Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 15:56:03 +0200 Subject: [PATCH 05/28] switched to std::format for compile-time checks Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 13 +++++++------ cpp/src/dual_simplex/logger.hpp | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 0f9a890f48..989b575db6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -372,15 +372,16 @@ void branch_and_bound_t::report_heuristic(f_t obj) f_t user_obj = compute_user_objective(original_lp_, obj); f_t user_gap = user_relative_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); std::string user_gap_text = to_percentage(user_gap); - settings_.log.printf( - "New solution from primal heuristics. Objective %+.6e. Gap %s. Time %.2f\n", + settings_.log.print_format( + "New solution from primal heuristics. Objective {:+.6e}. Gap {}. Time {:.2f}\n", user_obj, - user_gap_text.c_str(), + user_gap_text, 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)); + settings_.log.print_format( + "New solution from primal heuristics. Objective {:+.6e}. Time {:.2f}\n", + compute_user_objective(original_lp_, obj), + toc(exploration_stats_.start_time)); } } } diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 9303095230..cb10db6406 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -87,10 +87,10 @@ class logger_t { } template - void print_format(std::string_view fmt, Args&&... args) + void print_format(std::format_string fmt, Args&&... args) { if (log) { - const std::string msg = std::vformat(fmt, std::make_format_args(args...)); + const std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); @@ -141,10 +141,10 @@ class logger_t { } template - void debug_format(std::string_view fmt, Args&&... args) + void debug_format(std::format_string fmt, Args&&... args) { if (log) { - std::string msg = std::vformat(fmt, std::make_format_args(args...)); + std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); From 83d60b9deb83e1e73c216275ad0d8b4aa87b4d17 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 17:03:06 +0200 Subject: [PATCH 06/28] migrate node_id from int to uint64_t. bug fixes Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/bb_event.hpp | 8 ++-- cpp/src/branch_and_bound/branch_and_bound.cpp | 35 ++++++++--------- .../deterministic_workers.hpp | 7 +++- cpp/src/branch_and_bound/mip_node.hpp | 4 +- cpp/src/branch_and_bound/search_tree.hpp | 38 +++++++++++-------- cpp/src/dual_simplex/logger.hpp | 4 +- 6 files changed, 54 insertions(+), 42 deletions(-) diff --git a/cpp/src/branch_and_bound/bb_event.hpp b/cpp/src/branch_and_bound/bb_event.hpp index d07d52e4a0..1200fdb7a7 100644 --- a/cpp/src/branch_and_bound/bb_event.hpp +++ b/cpp/src/branch_and_bound/bb_event.hpp @@ -25,8 +25,8 @@ enum class bb_event_type_t : int8_t { template struct branched_payload_t { - i_t down_child_id; - i_t up_child_id; + uint64_t down_child_id; + uint64_t up_child_id; f_t node_lower_bound; i_t branch_var; f_t branch_value; @@ -75,8 +75,8 @@ struct bb_event_t { static bb_event_t make_branched(double work_unit_ts, int worker, i_t node, - i_t down_id, - i_t up_id, + uint64_t down_id, + uint64_t up_id, f_t lower_bound, i_t branch_var, f_t branch_val) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 989b575db6..869c213d09 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -335,10 +335,10 @@ void branch_and_bound_t::print_table_header() "Depth", "Iter/Node", "Gap", - "Completion"); + "Est. Progress"); if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } header += std::format("{:^8}|", "Time"); - settings_.log.printf("%s", header.c_str()); + settings_.log.printf("%s\n", header.c_str()); } template @@ -366,7 +366,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); - settings_.log.printf("%s", log_line.c_str()); + settings_.log.printf("%s\n", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { f_t user_obj = compute_user_objective(original_lp_, obj); @@ -415,7 +415,7 @@ void branch_and_bound_t::report( tree_completion); if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); - settings_.log.printf("%s", log_line.c_str()); + settings_.log.printf("%s\n", log_line.c_str()); } template @@ -1015,8 +1015,8 @@ struct nondeterministic_policy_t : tree_update_policy_t { { if (worker->search_strategy == search_strategy_t::BEST_FIRST) { fetch_min(bnb.lower_bound_numerical_, node->lower_bound); - log.printf("LP returned numerical issue on node %d. Best bound set to %+10.6e.\n", - node->node_id, + log.print_format("LP returned numerical issue on node {}. Best bound set to {:+10.6e}.\n", + node->node_id, compute_user_objective(bnb.original_lp_, bnb.lower_bound_numerical_.load())); } } @@ -1277,15 +1277,15 @@ std::pair branch_and_bound_t::updat #ifdef DEBUG_FRACTIONAL_FIXED for (i_t j : leaf_fractional) { if (leaf_problem.lower[j] == leaf_problem.upper[j]) { - printf( - "Node %d: Fixed variable %d has a fractional value %e. Lower %e upper %e. Variable " - "status %d\n", + settings_.log.print_format( + "Node {}: Fixed variable {} has a fractional value {:e}. Lower {:e} upper {:e}. " + "Variable status {}\n", node_ptr->node_id, j, leaf_solution.x[j], leaf_problem.lower[j], leaf_problem.upper[j], - node_ptr->vstatus[j]); + static_cast(node_ptr->vstatus[j])); } } #endif @@ -1503,10 +1503,10 @@ dual_status_t branch_and_bound_t::solve_node_lp( ss >> logname; lp_settings.log.set_log_file(logname, "a"); lp_settings.log.log_to_console = false; - lp_settings.log.printf( - "%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(), + lp_settings.log.print_format( + "{}current node: id = {}, depth = {}, branch var = {}, branch dir = {}, fractional val = " + "{:f}, variable lower bound = {:f}, variable upper bound = {:f}, branch vstatus = {}\n\n", + settings_.log.log_prefix, node_ptr->node_id, node_ptr->depth, node_ptr->branch_var, @@ -1514,7 +1514,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( node_ptr->fractional_val, node_ptr->branch_var_lower, node_ptr->branch_var_upper, - node_ptr->vstatus[node_ptr->branch_var]); + static_cast(node_ptr->vstatus[node_ptr->branch_var])); #endif bool feasible = worker->set_lp_variable_bounds(node_ptr, settings_); @@ -1546,7 +1546,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( worker->leaf_edge_norms); if (lp_status == dual_status_t::NUMERICAL) { - log.debug("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); + log.debug_format("Numerical issue node {}. Resolving from scratch.\n", node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker->leaf_problem, lp_start_time, @@ -3570,7 +3570,8 @@ node_status_t branch_and_bound_t::solve_node_deterministic( &worker.work_context); if (lp_status == dual_status_t::NUMERICAL) { - settings_.log.printf("Numerical issue node %d. Resolving from scratch.\n", node_ptr->node_id); + settings_.log.print_format("Numerical issue node {}. Resolving from scratch.\n", + node_ptr->node_id); lp_status_t second_status = solve_linear_program_with_advanced_basis(worker.leaf_problem, lp_start_time, lp_settings, diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index f7c960cbdd..d5a439abc7 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -210,8 +210,11 @@ class deterministic_bfs_worker_t events.add(std::move(event)); } - void record_branched( - mip_node_t* node, i_t down_child_id, i_t up_child_id, i_t branch_var, f_t branch_val) + void record_branched(mip_node_t* node, + uint64_t down_child_id, + uint64_t up_child_id, + i_t branch_var, + f_t branch_val) { record_event(bb_event_t::make_branched(this->clock, this->worker_id, diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index e5072c4d90..24be038a0e 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -220,7 +220,7 @@ class mip_node_t { if (current_node->children[0] == nullptr && current_node->children[1] == nullptr && current_node->depth < 10) { - printf("Node %d with no children at depth %d lower bound %e. status %d\n", + printf("Node %ld with no children at depth %d lower bound %e. status %d\n", current_node->node_id, current_node->depth, current_node->lower_bound, @@ -269,7 +269,7 @@ class mip_node_t { f_t lower_bound; f_t objective_estimate; i_t depth; - i_t node_id; + uint64_t node_id; i_t branch_var; branch_direction_t branch_dir; f_t branch_var_lower; diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index e3a6ed9901..0ec7c36f70 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -14,9 +14,9 @@ namespace cuopt::linear_programming::dual_simplex { template class search_tree_t { public: - search_tree_t() : num_nodes(0) {} + search_tree_t() = default; - search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} + search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } ~search_tree_t() { clean(); } @@ -29,7 +29,7 @@ class search_tree_t { ++num_inner_nodes; } else { ++num_final_nodes; - progress += std::pow(2, -node_ptr->depth); + progress += std::ldexp(f_t(1), -node_ptr->depth); } std::vector*> stack; @@ -45,7 +45,7 @@ class search_tree_t { const lp_problem_t& original_lp, logger_t& log) { - i_t id = num_nodes.fetch_add(2); + uint64_t id = num_nodes.fetch_add(2); auto down_child = std::make_unique>(original_lp, parent_node, @@ -90,7 +90,7 @@ class search_tree_t { const f_t val) { if (write_graphviz) { - log.printf("Node%d [label=\"%s %.16e\"]\n", node_ptr->node_id, label.c_str(), val); + log.print_format("Node{} [label=\"{} {:.16e}\"]\n", node_ptr->node_id, label, val); } } @@ -102,12 +102,12 @@ class search_tree_t { const f_t bound) { if (write_graphviz) { - log.printf("Node%d -> Node%d [label=\"x%d %s %e\"]\n", - origin_ptr->node_id, - dest_ptr->node_id, - branch_var, - branch_dir == branch_direction_t::DOWN ? "<=" : ">=", - bound); + log.print_format("Node{} -> Node{} [label=\"x{} {} {:e}\"]\n", + origin_ptr->node_id, + dest_ptr->node_id, + branch_var, + branch_dir == branch_direction_t::DOWN ? "<=" : ">=", + bound); } } @@ -126,23 +126,29 @@ class search_tree_t { if (node->children[1]) stack.push_back(std::move(node->children[1])); // Implicitly call destructor for `node` } + + num_nodes = 0; + num_open_nodes = 0; + num_final_nodes = 0; + num_inner_nodes = 0; + progress = 0; } mip_node_t root; omp_mutex_t mutex; - omp_atomic_t num_nodes; + omp_atomic_t num_nodes = 0; // Number of nodes that still needs to be explored - omp_atomic_t num_open_nodes; + omp_atomic_t num_open_nodes = 0; // Number of integer feasible, infeasible or fathomed nodes - omp_atomic_t num_final_nodes; + omp_atomic_t num_final_nodes = 0; // Number of inner nodes - omp_atomic_t num_inner_nodes; + omp_atomic_t num_inner_nodes = 0; // Track the solver progress based on how much the tree was explored - omp_atomic_t progress = 0.0; + omp_atomic_t progress = 0; static constexpr bool write_graphviz = false; }; diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index cb10db6406..f6186013ee 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -90,9 +90,10 @@ class logger_t { void print_format(std::format_string fmt, Args&&... args) { if (log) { - const std::string msg = std::format(fmt, std::forward(args)...); + std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL + if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); #else std::printf("%s", msg.c_str()); @@ -147,6 +148,7 @@ class logger_t { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL + if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); #else std::printf("%s", msg.c_str()); From 9cd175e68cd864d1c3a4d64b349c14261c1e785c Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 11:15:10 +0200 Subject: [PATCH 07/28] revert changes to node_id. added exception handling to the destructor. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/bb_event.hpp | 8 +++--- cpp/src/branch_and_bound/branch_and_bound.cpp | 22 +++++++-------- .../deterministic_workers.hpp | 7 ++--- cpp/src/branch_and_bound/search_tree.hpp | 27 +++++++++++++++---- cpp/src/dual_simplex/logger.hpp | 1 - 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/cpp/src/branch_and_bound/bb_event.hpp b/cpp/src/branch_and_bound/bb_event.hpp index 1200fdb7a7..d07d52e4a0 100644 --- a/cpp/src/branch_and_bound/bb_event.hpp +++ b/cpp/src/branch_and_bound/bb_event.hpp @@ -25,8 +25,8 @@ enum class bb_event_type_t : int8_t { template struct branched_payload_t { - uint64_t down_child_id; - uint64_t up_child_id; + i_t down_child_id; + i_t up_child_id; f_t node_lower_bound; i_t branch_var; f_t branch_value; @@ -75,8 +75,8 @@ struct bb_event_t { static bb_event_t make_branched(double work_unit_ts, int worker, i_t node, - uint64_t down_id, - uint64_t up_id, + i_t down_id, + i_t up_id, f_t lower_bound, i_t branch_var, f_t branch_val) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 869c213d09..cad03b67e5 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1015,8 +1015,8 @@ struct nondeterministic_policy_t : tree_update_policy_t { { if (worker->search_strategy == search_strategy_t::BEST_FIRST) { fetch_min(bnb.lower_bound_numerical_, node->lower_bound); - log.print_format("LP returned numerical issue on node {}. Best bound set to {:+10.6e}.\n", - node->node_id, + log.printf("LP returned numerical issue on node %d. Best bound set to %+10.6e.\n", + node->node_id, compute_user_objective(bnb.original_lp_, bnb.lower_bound_numerical_.load())); } } @@ -1277,15 +1277,15 @@ std::pair branch_and_bound_t::updat #ifdef DEBUG_FRACTIONAL_FIXED for (i_t j : leaf_fractional) { if (leaf_problem.lower[j] == leaf_problem.upper[j]) { - settings_.log.print_format( - "Node {}: Fixed variable {} has a fractional value {:e}. Lower {:e} upper {:e}. " - "Variable status {}\n", + printf( + "Node %d: Fixed variable %d has a fractional value %e. Lower %e upper %e. Variable " + "status %d\n", node_ptr->node_id, j, leaf_solution.x[j], leaf_problem.lower[j], leaf_problem.upper[j], - static_cast(node_ptr->vstatus[j])); + node_ptr->vstatus[j]); } } #endif @@ -1503,10 +1503,10 @@ dual_status_t branch_and_bound_t::solve_node_lp( ss >> logname; lp_settings.log.set_log_file(logname, "a"); lp_settings.log.log_to_console = false; - lp_settings.log.print_format( - "{}current node: id = {}, depth = {}, branch var = {}, branch dir = {}, fractional val = " - "{:f}, variable lower bound = {:f}, variable upper bound = {:f}, branch vstatus = {}\n\n", - settings_.log.log_prefix, + lp_settings.log.printf( + "%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, @@ -1514,7 +1514,7 @@ dual_status_t branch_and_bound_t::solve_node_lp( node_ptr->fractional_val, node_ptr->branch_var_lower, node_ptr->branch_var_upper, - static_cast(node_ptr->vstatus[node_ptr->branch_var])); + node_ptr->vstatus[node_ptr->branch_var]); #endif bool feasible = worker->set_lp_variable_bounds(node_ptr, settings_); diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index d5a439abc7..f7c960cbdd 100644 --- a/cpp/src/branch_and_bound/deterministic_workers.hpp +++ b/cpp/src/branch_and_bound/deterministic_workers.hpp @@ -210,11 +210,8 @@ class deterministic_bfs_worker_t events.add(std::move(event)); } - void record_branched(mip_node_t* node, - uint64_t down_child_id, - uint64_t up_child_id, - i_t branch_var, - f_t branch_val) + void record_branched( + mip_node_t* node, i_t down_child_id, i_t up_child_id, i_t branch_var, f_t branch_val) { record_event(bb_event_t::make_branched(this->clock, this->worker_id, diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index 0ec7c36f70..85d001e773 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -18,7 +18,24 @@ class search_tree_t { search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } - ~search_tree_t() { clean(); } + ~search_tree_t() + { + try { + clean(); // scope-exit ensure destruction of all detached leaves + } catch (const std::exception& e) { + // fprintf to stderr is allocation-free and cannot throw; using the + // project logger here would risk a secondary bad_alloc that would + // escape the destructor and re-introduce std::terminate. + std::fprintf(stderr, + "search_tree_t destructor: iterative teardown failed (%s); falling back to " + "recursive unique_ptr destruction.\n", + e.what()); + } catch (...) { + std::fprintf(stderr, + "search_tree_t destructor: iterative teardown failed (unknown exception); " + "falling back to recursive unique_ptr destruction.\n"); + } + } void update(mip_node_t* node_ptr, node_status_t status) { @@ -136,16 +153,16 @@ class search_tree_t { mip_node_t root; omp_mutex_t mutex; - omp_atomic_t num_nodes = 0; + omp_atomic_t num_nodes = 0; // Number of nodes that still needs to be explored - omp_atomic_t num_open_nodes = 0; + omp_atomic_t num_open_nodes = 0; // Number of integer feasible, infeasible or fathomed nodes - omp_atomic_t num_final_nodes = 0; + omp_atomic_t num_final_nodes = 0; // Number of inner nodes - omp_atomic_t num_inner_nodes = 0; + omp_atomic_t num_inner_nodes = 0; // Track the solver progress based on how much the tree was explored omp_atomic_t progress = 0; diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index f6186013ee..19e95b4d41 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -12,7 +12,6 @@ #endif #include -#include #include #include From 88299efb191c6b75253eb41207c5bd85b47e4b83 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 13:15:19 +0200 Subject: [PATCH 08/28] address coderabbit Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 4 ++-- cpp/src/branch_and_bound/mip_node.hpp | 2 +- cpp/src/branch_and_bound/search_tree.hpp | 10 +++++++++- cpp/src/dual_simplex/logger.hpp | 7 ++++--- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index cad03b67e5..b7c501e3d1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -352,7 +352,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("H {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", + std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", "", // nodes explored "", // nodes unexplored user_obj, @@ -402,7 +402,7 @@ void branch_and_bound_t::report( std::string tree_completion = to_percentage(search_tree_.progress.load()); std::string log_line = - std::format("{:^1} {:>12} {:>12} {:^19.6e} {:^15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", + std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", symbol, nodes_explored, nodes_unexplored, diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 24be038a0e..4e304e0008 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -269,7 +269,7 @@ class mip_node_t { f_t lower_bound; f_t objective_estimate; i_t depth; - uint64_t node_id; + i_t node_id; i_t branch_var; branch_direction_t branch_dir; f_t branch_var_lower; diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index 85d001e773..f61588e2ee 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -9,6 +9,14 @@ #include +#include + +#include +#include +#include +#include +#include + namespace cuopt::linear_programming::dual_simplex { template @@ -62,7 +70,7 @@ class search_tree_t { const lp_problem_t& original_lp, logger_t& log) { - uint64_t id = num_nodes.fetch_add(2); + i_t id = num_nodes.fetch_add(2); auto down_child = std::make_unique>(original_lp, parent_node, diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 19e95b4d41..a0033438a3 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace cuopt::mathematical_optimization::simplex { @@ -92,7 +93,7 @@ class logger_t { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL - if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } + std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); #else std::printf("%s", msg.c_str()); @@ -147,8 +148,8 @@ class logger_t { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL - if (msg.size() > 0 && msg.back() == '\n') { msg.back() = '\0'; } - CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg.c_str()); + std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; + CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg_view.c_str()); #else std::printf("%s", msg.c_str()); fflush(stdout); From c4595f9b518a514c738fcb249ff2d8e1a742b0d4 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 19:40:30 +0200 Subject: [PATCH 09/28] revert log changes Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 7 +++---- cpp/src/branch_and_bound/mip_node.hpp | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index b7c501e3d1..51127babef 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -378,10 +378,9 @@ void branch_and_bound_t::report_heuristic(f_t obj) user_gap_text, toc(exploration_stats_.start_time)); } else { - settings_.log.print_format( - "New solution from primal heuristics. Objective {:+.6e}. Time {:.2f}\n", - compute_user_objective(original_lp_, obj), - toc(exploration_stats_.start_time)); + settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", + compute_user_objective(original_lp_, obj), + toc(exploration_stats_.start_time)); } } } diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 4e304e0008..e5072c4d90 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -220,7 +220,7 @@ class mip_node_t { if (current_node->children[0] == nullptr && current_node->children[1] == nullptr && current_node->depth < 10) { - printf("Node %ld with no children at depth %d lower bound %e. status %d\n", + printf("Node %d with no children at depth %d lower bound %e. status %d\n", current_node->node_id, current_node->depth, current_node->lower_bound, From 43e87c214f1209ce40b659ecc847815668b94664 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:01:29 +0200 Subject: [PATCH 10/28] removed search progress from the logs. It is too inaccurate, especially with restarts. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 51127babef..ed4888833d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -324,18 +324,16 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) template void branch_and_bound_t::print_table_header() { - std::string header = - std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|{:^15}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap", - "Est. Progress"); + std::string header = std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap"); if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } header += std::format("{:^8}|", "Time"); settings_.log.printf("%s\n", header.c_str()); @@ -352,7 +350,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11} {:^15}", + std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11}", "", // nodes explored "", // nodes unexplored user_obj, @@ -360,9 +358,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) "", // integer infeasible "", // depth "", // iter/node - user_gap_text, - "" // tree progress - ); + user_gap_text); if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); @@ -401,7 +397,7 @@ void branch_and_bound_t::report( std::string tree_completion = to_percentage(search_tree_.progress.load()); std::string log_line = - std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11} {:^15}", + std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11}", symbol, nodes_explored, nodes_unexplored, @@ -410,8 +406,7 @@ void branch_and_bound_t::report( node_int_infeas, node_depth, iter_node, - user_gap_text, - tree_completion); + user_gap_text); if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); settings_.log.printf("%s\n", log_line.c_str()); From 5ffd98edbc7aa000fafa1e4308d6581c68a7375c Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:02:38 +0200 Subject: [PATCH 11/28] removed try-catch block in the destructor. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/search_tree.hpp | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp index f61588e2ee..6450cf30dc 100644 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ b/cpp/src/branch_and_bound/search_tree.hpp @@ -26,24 +26,7 @@ class search_tree_t { search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } - ~search_tree_t() - { - try { - clean(); // scope-exit ensure destruction of all detached leaves - } catch (const std::exception& e) { - // fprintf to stderr is allocation-free and cannot throw; using the - // project logger here would risk a secondary bad_alloc that would - // escape the destructor and re-introduce std::terminate. - std::fprintf(stderr, - "search_tree_t destructor: iterative teardown failed (%s); falling back to " - "recursive unique_ptr destruction.\n", - e.what()); - } catch (...) { - std::fprintf(stderr, - "search_tree_t destructor: iterative teardown failed (unknown exception); " - "falling back to recursive unique_ptr destruction.\n"); - } - } + ~search_tree_t() { clean(); } void update(mip_node_t* node_ptr, node_status_t status) { From 459d9f396ca994c9adabeaddde490e6e0da7525e Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:09:08 +0200 Subject: [PATCH 12/28] removed changes to the search tree Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.hpp | 1 - cpp/src/branch_and_bound/mip_node.hpp | 134 +++++++++++++- cpp/src/branch_and_bound/search_tree.hpp | 164 ------------------ 3 files changed, 132 insertions(+), 167 deletions(-) delete mode 100644 cpp/src/branch_and_bound/search_tree.hpp diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 4de69a36dc..18eb38de66 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index e5072c4d90..ecd5dd37f6 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -41,8 +41,43 @@ inline bool inactive_status(node_status_t status) template class mip_node_t { public: - mip_node_t(mip_node_t&&) = default; - mip_node_t& operator=(mip_node_t&&) = default; + ~mip_node_t() + { + // Iterative teardown to avoid stack overflow on deep trees. + // Detach all descendants breadth-first, then destroy them as leaves. + // vector::push_back can throw bad_alloc; the catch-all keeps the destructor + // exception-free. Under OOM, any not-yet-detached descendants are destroyed + // via the recursive unique_ptr chain in `children` as this frame unwinds. + try { + std::vector> nodes; + for (auto& c : children) { + if (c) { nodes.push_back(std::move(c)); } + } + // nodes.size() grows so that this loop only terminates when only leaves remain + for (size_t i = 0; i < nodes.size(); ++i) { + for (auto& c : nodes[i]->children) { + if (c) { nodes.push_back(std::move(c)); } + } + } + + // scope-exit ensure destruction of all detached leaves + } catch (const std::exception& e) { + // fprintf to stderr is allocation-free and cannot throw; using the + // project logger here would risk a secondary bad_alloc that would + // escape the destructor and re-introduce std::terminate. + std::fprintf(stderr, + "mip_node_t destructor: iterative teardown failed (%s); falling back to " + "recursive unique_ptr destruction.\n", + e.what()); + } catch (...) { + std::fprintf(stderr, + "mip_node_t destructor: iterative teardown failed (unknown exception); " + "falling back to recursive unique_ptr destruction.\n"); + } + } + + mip_node_t(mip_node_t&&) noexcept = default; + mip_node_t& operator=(mip_node_t&&) noexcept = default; mip_node_t() : status(node_status_t::PENDING), @@ -329,4 +364,99 @@ void remove_fathomed_nodes(std::vector*>& stack) } } +template +class search_tree_t { + public: + search_tree_t() : num_nodes(0) {} + + search_tree_t(mip_node_t&& node) : root(std::move(node)), num_nodes(0) {} + + void update(mip_node_t* node_ptr, node_status_t status) + { + std::lock_guard lock(mutex); + std::vector*> stack; + node_ptr->set_status(status, stack); + remove_fathomed_nodes(stack); + } + + void branch(mip_node_t* parent_node, + const i_t branch_var, + const f_t fractional_val, + const i_t integer_infeasible, + const std::vector& parent_vstatus, + const lp_problem_t& original_lp, + logger_t& log) + { + i_t id = num_nodes.fetch_add(2); + + auto down_child = std::make_unique>(original_lp, + parent_node, + ++id, + branch_var, + branch_direction_t::DOWN, + fractional_val, + integer_infeasible, + parent_vstatus); + graphviz_edge(log, + parent_node, + down_child.get(), + branch_var, + branch_direction_t::DOWN, + std::floor(fractional_val)); + + auto up_child = std::make_unique>(original_lp, + parent_node, + ++id, + branch_var, + branch_direction_t::UP, + fractional_val, + integer_infeasible, + parent_vstatus); + + graphviz_edge(log, + parent_node, + up_child.get(), + branch_var, + branch_direction_t::UP, + std::ceil(fractional_val)); + + assert(parent_vstatus.size() == original_lp.num_cols); + parent_node->add_children(std::move(down_child), + std::move(up_child)); // child pointers moved into the tree + } + + void graphviz_node(logger_t& log, + const mip_node_t* node_ptr, + const std::string label, + const f_t val) + { + if (write_graphviz) { + log.printf("Node%d [label=\"%s %.16e\"]\n", node_ptr->node_id, label.c_str(), val); + } + } + + void graphviz_edge(logger_t& log, + const mip_node_t* origin_ptr, + const mip_node_t* dest_ptr, + const i_t branch_var, + branch_direction_t branch_dir, + const f_t bound) + { + if (write_graphviz) { + log.printf("Node%d -> Node%d [label=\"x%d %s %e\"]\n", + origin_ptr->node_id, + dest_ptr->node_id, + branch_var, + branch_dir == branch_direction_t::DOWN ? "<=" : ">=", + bound); + } + } + + mip_node_t root; + omp_mutex_t mutex; + omp_atomic_t num_nodes; + + static constexpr bool write_graphviz = false; +}; + } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/search_tree.hpp b/cpp/src/branch_and_bound/search_tree.hpp deleted file mode 100644 index 6450cf30dc..0000000000 --- a/cpp/src/branch_and_bound/search_tree.hpp +++ /dev/null @@ -1,164 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#pragma once - -#include - -#include - -#include -#include -#include -#include -#include - -namespace cuopt::linear_programming::dual_simplex { - -template -class search_tree_t { - public: - search_tree_t() = default; - - search_tree_t(mip_node_t&& node) : search_tree_t() { root = std::move(node); } - - ~search_tree_t() { clean(); } - - void update(mip_node_t* node_ptr, node_status_t status) - { - std::lock_guard lock(mutex); - - --num_open_nodes; - if (status == node_status_t::HAS_CHILDREN) { - ++num_inner_nodes; - } else { - ++num_final_nodes; - progress += std::ldexp(f_t(1), -node_ptr->depth); - } - - std::vector*> stack; - node_ptr->set_status(status, stack); - remove_fathomed_nodes(stack); - } - - void branch(mip_node_t* parent_node, - const i_t branch_var, - const f_t fractional_val, - const i_t integer_infeasible, - const std::vector& parent_vstatus, - const lp_problem_t& original_lp, - logger_t& log) - { - i_t id = num_nodes.fetch_add(2); - - auto down_child = std::make_unique>(original_lp, - parent_node, - ++id, - branch_var, - branch_direction_t::DOWN, - fractional_val, - integer_infeasible, - parent_vstatus); - graphviz_edge(log, - parent_node, - down_child.get(), - branch_var, - branch_direction_t::DOWN, - std::floor(fractional_val)); - - auto up_child = std::make_unique>(original_lp, - parent_node, - ++id, - branch_var, - branch_direction_t::UP, - fractional_val, - integer_infeasible, - parent_vstatus); - - graphviz_edge(log, - parent_node, - up_child.get(), - branch_var, - branch_direction_t::UP, - std::ceil(fractional_val)); - - assert(parent_vstatus.size() == original_lp.num_cols); - parent_node->add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree - num_open_nodes += 2; - } - - static void graphviz_node(logger_t& log, - const mip_node_t* node_ptr, - const std::string label, - const f_t val) - { - if (write_graphviz) { - log.print_format("Node{} [label=\"{} {:.16e}\"]\n", node_ptr->node_id, label, val); - } - } - - static void graphviz_edge(logger_t& log, - const mip_node_t* origin_ptr, - const mip_node_t* dest_ptr, - const i_t branch_var, - branch_direction_t branch_dir, - const f_t bound) - { - if (write_graphviz) { - log.print_format("Node{} -> Node{} [label=\"x{} {} {:e}\"]\n", - origin_ptr->node_id, - dest_ptr->node_id, - branch_var, - branch_dir == branch_direction_t::DOWN ? "<=" : ">=", - bound); - } - } - - // Clean the tree using a depth first scheme - void clean() - { - std::vector>> stack; - - if (root.children[0]) stack.push_back(std::move(root.children[0])); - if (root.children[1]) stack.push_back(std::move(root.children[1])); - - while (!stack.empty()) { - auto node = std::move(stack.back()); - stack.pop_back(); - if (node->children[0]) stack.push_back(std::move(node->children[0])); - if (node->children[1]) stack.push_back(std::move(node->children[1])); - // Implicitly call destructor for `node` - } - - num_nodes = 0; - num_open_nodes = 0; - num_final_nodes = 0; - num_inner_nodes = 0; - progress = 0; - } - - mip_node_t root; - omp_mutex_t mutex; - omp_atomic_t num_nodes = 0; - - // Number of nodes that still needs to be explored - omp_atomic_t num_open_nodes = 0; - - // Number of integer feasible, infeasible or fathomed nodes - omp_atomic_t num_final_nodes = 0; - - // Number of inner nodes - omp_atomic_t num_inner_nodes = 0; - - // Track the solver progress based on how much the tree was explored - omp_atomic_t progress = 0; - - static constexpr bool write_graphviz = false; -}; - -} // namespace cuopt::linear_programming::dual_simplex From aaf3efdd404b6c875688c058a612847d96eea255 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:28:43 +0200 Subject: [PATCH 13/28] fixed compilation Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index ed4888833d..86c2d3797b 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -386,15 +386,14 @@ void branch_and_bound_t::report( char symbol, f_t obj, f_t lower_bound, i_t node_depth, i_t node_int_infeas, double work_time) { update_user_bound(lower_bound); - const i_t nodes_explored = exploration_stats_.nodes_explored; - const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; - const f_t user_obj = compute_user_objective(original_lp_, obj); - const f_t user_lower = compute_user_objective(original_lp_, lower_bound); - const f_t iters = static_cast(exploration_stats_.total_lp_iters); - const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; - f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); - std::string user_gap_text = to_percentage(user_gap); - std::string tree_completion = to_percentage(search_tree_.progress.load()); + const i_t nodes_explored = exploration_stats_.nodes_explored; + const i_t nodes_unexplored = exploration_stats_.nodes_unexplored; + const f_t user_obj = compute_user_objective(original_lp_, obj); + const f_t user_lower = compute_user_objective(original_lp_, lower_bound); + const f_t iters = static_cast(exploration_stats_.total_lp_iters); + const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; + f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + std::string user_gap_text = to_percentage(user_gap); std::string log_line = std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11}", From 333723a33f8ec329364624a6b8d7667350a08c6c Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 14:37:47 +0200 Subject: [PATCH 14/28] use constexpr constants to determine column width Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 86c2d3797b..fb4547c7ce 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -75,6 +75,19 @@ using simplex::vector_norm_inf; namespace { +// Column widths for the branch-and-bound progress table. +constexpr int SYMBOL_WIDTH = 1; +constexpr int EXPLORED_WIDTH = 12; +constexpr int UNEXPLORED_WIDTH = 12; +constexpr int OBJECTIVE_WIDTH = 19; +constexpr int BOUND_WIDTH = 15; +constexpr int INTEGER_INFEASIBLE_WIDTH = 8; +constexpr int DEPTH_WIDTH = 7; +constexpr int ITER_NODE_WIDTH = 11; +constexpr int GAP_WIDTH = 11; +constexpr int WORK_WIDTH = 8; +constexpr int TIME_WIDTH = 8; + template bool is_fractional(f_t x, variable_type_t var_type, f_t integer_tol) { @@ -324,18 +337,28 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) template void branch_and_bound_t::print_table_header() { - std::string header = std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|", - "", - "Explored", - "Unexplored", - "Objective", - "Bound", - "IntInf", - "Depth", - "Iter/Node", - "Gap"); - if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } - header += std::format("{:^8}|", "Time"); + std::string header = + std::format("{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|", + "", + SYMBOL_WIDTH, + "Explored", + EXPLORED_WIDTH, + "Unexplored", + UNEXPLORED_WIDTH, + "Objective", + OBJECTIVE_WIDTH, + "Bound", + BOUND_WIDTH, + "IntInf", + INTEGER_INFEASIBLE_WIDTH, + "Depth", + DEPTH_WIDTH, + "Iter/Node", + ITER_NODE_WIDTH, + "Gap", + GAP_WIDTH); + if (settings_.deterministic) { header += std::format("{:^{}}|", "Work", WORK_WIDTH); } + header += std::format("{:^{}}|", "Time", TIME_WIDTH); settings_.log.printf("%s\n", header.c_str()); } @@ -350,18 +373,28 @@ void branch_and_bound_t::report_heuristic(f_t obj) std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11}", + std::format("{:^{}} {:>{}} {:>{}} {:^+{}.6e} {:^+{}.6e} {:>{}} {:>{}} {:^{}} {:^{}}", + "H", + SYMBOL_WIDTH, "", // nodes explored + EXPLORED_WIDTH, "", // nodes unexplored + UNEXPLORED_WIDTH, user_obj, + OBJECTIVE_WIDTH, user_lower, + BOUND_WIDTH, "", // integer infeasible + INTEGER_INFEASIBLE_WIDTH, "", // depth + DEPTH_WIDTH, "", // iter/node - user_gap_text); + ITER_NODE_WIDTH, + user_gap_text, + GAP_WIDTH); - if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } - log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); + if (settings_.deterministic) { log_line += std::format("{:^{}}", "", WORK_WIDTH); } + log_line += std::format(" {:>{}.2f}", toc(exploration_stats_.start_time), TIME_WIDTH); settings_.log.printf("%s\n", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { @@ -396,18 +429,27 @@ void branch_and_bound_t::report( std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11}", + std::format("{:^{}} {:>{}} {:>{}} {:^+{}.6e} {:^+{}.6e} {:>{}} {:>{}} {:^{}.1e} {:^{}}", symbol, + SYMBOL_WIDTH, nodes_explored, + EXPLORED_WIDTH, nodes_unexplored, + UNEXPLORED_WIDTH, user_obj, + OBJECTIVE_WIDTH, user_lower, + BOUND_WIDTH, node_int_infeas, + INTEGER_INFEASIBLE_WIDTH, node_depth, + DEPTH_WIDTH, iter_node, - user_gap_text); - if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } - log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); + ITER_NODE_WIDTH, + user_gap_text, + GAP_WIDTH); + if (work_time >= 0) { log_line += std::format(" {:>{}.2f}", work_time, WORK_WIDTH); } + log_line += std::format(" {:>{}.2f}", toc(exploration_stats_.start_time), TIME_WIDTH); settings_.log.printf("%s\n", log_line.c_str()); } From dfe341634580fb5e8534ab399b137bc4eac88d06 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 17:36:49 +0200 Subject: [PATCH 15/28] now the MIP solver gives a summary at the end Signed-off-by: Nicolas L. Guidotti --- .../mip/solver_stats.hpp | 2 + cpp/src/mip_heuristics/solve.cu | 5 +- cpp/src/mip_heuristics/solver.cu | 5 +- cpp/src/mip_heuristics/solver_solution.cu | 55 ++++++++++++++----- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp b/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp index 3434c1a420..aeda7706b6 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp @@ -22,6 +22,7 @@ struct solver_stats_t { if (this == &other) { return *this; } total_solve_time = other.total_solve_time; presolve_time = other.presolve_time; + bnb_time = other.bnb_time; solution_bound.store(other.solution_bound.load(std::memory_order_relaxed), std::memory_order_relaxed); num_nodes = other.num_nodes; @@ -35,6 +36,7 @@ struct solver_stats_t { f_t total_solve_time = 0.; f_t presolve_time = 0.; + f_t bnb_time = 0.; std::atomic solution_bound; i_t num_nodes = 0; i_t num_simplex_iterations = 0; diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 891c7c3a8b..42205af42b 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -747,10 +747,7 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p } } - if (sol.get_termination_status() == mip_termination_status_t::FeasibleFound || - sol.get_termination_status() == mip_termination_status_t::Optimal) { - sol.log_detailed_summary(); - } + sol.log_detailed_summary(); if (settings.sol_file != "") { CUOPT_LOG_INFO("Writing solution to file %s", settings.sol_file.c_str()); diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 0ede00964a..5b7a9fc1c1 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -496,7 +496,10 @@ solution_t mip_solver_t::run_solver() if (!context.settings.heuristics_only) { #pragma omp task default(shared) priority(CUOPT_CRITICAL_TASK_PRIORITY) { - branch_and_bound_status = branch_and_bound->solve(branch_and_bound_solution); + auto t0 = std::chrono::system_clock::now(); + branch_and_bound_status = branch_and_bound->solve(branch_and_bound_solution); + std::chrono::duration elapsed = std::chrono::system_clock::now() - t0; + context.stats.bnb_time = elapsed.count(); } } diff --git a/cpp/src/mip_heuristics/solver_solution.cu b/cpp/src/mip_heuristics/solver_solution.cu index 7782260dd5..f20a30cc63 100644 --- a/cpp/src/mip_heuristics/solver_solution.cu +++ b/cpp/src/mip_heuristics/solver_solution.cu @@ -238,21 +238,46 @@ void mip_solution_t::log_summary() const template void mip_solution_t::log_detailed_summary() const { - CUOPT_LOG_INFO( - "Solution objective: %f , relative_mip_gap %f solution_bound %f presolve_time %f " - "total_solve_time %f " - "max constraint violation %f max int violation %f max var bounds violation %f " - "nodes %d simplex_iterations %d", - objective_, - mip_gap_, - stats_.get_solution_bound(), - stats_.presolve_time, - stats_.total_solve_time, - max_constraint_violation_, - max_int_violation_, - max_variable_bound_violation_, - stats_.num_nodes, - stats_.num_simplex_iterations); + std::string section_divider(50, '='); + std::string text = "\nSummary\n" + section_divider + "\n"; + std::string solution_status = get_termination_status_string(); + + f_t dual_bound = stats_.get_solution_bound(); + f_t obj = objective_; + f_t gap = mip_gap_; + + bool has_solution = termination_status_ == mip_termination_status_t::FeasibleFound || + termination_status_ == mip_termination_status_t::Optimal; + + if (!has_solution) { + obj = std::numeric_limits::infinity(); + gap = std::numeric_limits::infinity(); + dual_bound = std::numeric_limits::infinity(); + } + + text += std::format("{:<30}{:>15}\n", "Solution status:", solution_status); + text += std::format("{:<30}{:>+15.6e}\n", "Solution objective:", obj); + text += std::format("{:<30}{:>+15.6e}\n", "Dual bound:", dual_bound); + text += std::format("{:<30}{:>+15.6e}\n", "Relative MIP gap:", gap); + + if (has_solution) { + text += + std::format("{:<30}{:>+15.6e}\n", "Max constraint violation:", max_constraint_violation_); + text += std::format("{:<30}{:>+15.6e}\n", "Max integer violation:", max_int_violation_); + text += std::format( + "{:<30}{:>+15.6e}\n", "Max variable bound violation:", max_variable_bound_violation_); + } else { + text += std::format("{:<30}{:>15}\n", "Max constraint violation:", "N/A"); + text += std::format("{:<30}{:>15}\n", "Max integer violation:", "N/A"); + text += std::format("{:<30}{:>15}\n", "Max variable bound violation:", "N/A"); + } + + text += std::format("{:<30}{:>15}\n", "Nodes explored:", stats_.num_nodes); + text += std::format("{:<30}{:>15}\n", "Simplex iterations:", stats_.num_simplex_iterations); + text += std::format("{:<30}{:>15.2f}\n", "Presolve time:", stats_.presolve_time); + text += std::format("{:<30}{:>15.2f}\n", "B&B time:", stats_.bnb_time); + text += std::format("{:<30}{:>15.2f}\n", "Total solve time:", stats_.total_solve_time); + CUOPT_LOG_INFO("%s\n", text); } #if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT From 49701673a88246fcbcc0cc937e981a6c78558735 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 15:06:36 +0200 Subject: [PATCH 16/28] clean up the MIP logs Signed-off-by: Nicolas L. Guidotti --- .../mip/solver_stats.hpp | 2 - cpp/src/branch_and_bound/branch_and_bound.cpp | 40 +++++++++---------- cpp/src/branch_and_bound/symmetry.hpp | 2 + cpp/src/cuts/cuts.cpp | 6 ++- cpp/src/dual_simplex/logger.hpp | 1 + .../diversity/diversity_manager.cu | 2 +- .../presolve/third_party_presolve.cpp | 10 ++--- cpp/src/mip_heuristics/solver.cu | 7 +--- cpp/src/mip_heuristics/solver_solution.cu | 14 +++---- 9 files changed, 40 insertions(+), 44 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp b/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp index aeda7706b6..3434c1a420 100644 --- a/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp +++ b/cpp/include/cuopt/mathematical_optimization/mip/solver_stats.hpp @@ -22,7 +22,6 @@ struct solver_stats_t { if (this == &other) { return *this; } total_solve_time = other.total_solve_time; presolve_time = other.presolve_time; - bnb_time = other.bnb_time; solution_bound.store(other.solution_bound.load(std::memory_order_relaxed), std::memory_order_relaxed); num_nodes = other.num_nodes; @@ -36,7 +35,6 @@ struct solver_stats_t { f_t total_solve_time = 0.; f_t presolve_time = 0.; - f_t bnb_time = 0.; std::atomic solution_bound; i_t num_nodes = 0; i_t num_simplex_iterations = 0; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index fb4547c7ce..5b89c2552f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -791,30 +791,27 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& f_t gap_rel = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); bool is_maximization = original_lp_.obj_scale < 0.0; - settings_.log.printf("Explored %d nodes in %.2fs.\n", - exploration_stats_.nodes_explored, - toc(exploration_stats_.start_time)); + settings_.log.print_format("Explored {} nodes ({} simplex iterations) in {:.2f}s.", + exploration_stats_.nodes_explored.load(), + exploration_stats_.total_lp_iters.load(), + toc(exploration_stats_.start_time)); + if (exploration_stats_.orbital_fixing_nodes.load() > 0 || exploration_stats_.orbital_conflict_nodes.load() > 0) { - settings_.log.printf( - "Orbital fixing applied at %lld nodes, %lld total variable fixings, " - "%lld nodes with conflicting orbits\n", - (long long)exploration_stats_.orbital_fixing_nodes.load(), - (long long)exploration_stats_.orbital_fixings_applied.load(), - (long long)exploration_stats_.orbital_conflict_nodes.load()); + settings_.log.print_format( + "Orbital fixing applied at {} nodes, {} total variable fixings, " + "{} nodes with conflicting orbits\n", + exploration_stats_.orbital_fixing_nodes.load(), + exploration_stats_.orbital_fixings_applied.load(), + exploration_stats_.orbital_conflict_nodes.load()); } if (exploration_stats_.lexical_reduction_nodes.load() > 0) { - settings_.log.printf( - "Lexical reduction applied at %lld nodes, %lld total variable fixings, %lld nodes pruned\n", - (long long)exploration_stats_.lexical_reduction_nodes.load(), - (long long)exploration_stats_.lexical_reduction_fixings_applied.load(), - (long long)exploration_stats_.lexical_reduction_pruned_nodes.load()); + settings_.log.print_format( + "Lexical reduction applied at {} nodes, {} total variable fixings, {} nodes pruned\n", + exploration_stats_.lexical_reduction_nodes.load(), + exploration_stats_.lexical_reduction_fixings_applied.load(), + exploration_stats_.lexical_reduction_pruned_nodes.load()); } - settings_.log.printf("Absolute Gap %e Objective %.16e %s Bound %.16e\n", - gap, - obj, - is_maximization ? "Upper" : "Lower", - user_bound); if (gap <= settings_.absolute_mip_gap_tol || gap_rel <= settings_.relative_mip_gap_tol) { solver_status_ = mip_status_t::OPTIMAL; @@ -2525,8 +2522,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.nodes_explored = 0; original_lp_.A.to_compressed_row(Arow_); - settings_.log.printf("Reduced cost strengthening enabled: %d\n", - settings_.reduced_cost_strengthening); + settings_.log.debug("Reduced cost strengthening enabled: %d\n", + settings_.reduced_cost_strengthening); variable_bounds_t variable_bounds( original_lp_, settings_, var_types_, Arow_, new_slacks_); @@ -3021,6 +3018,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } // Implicit barrier for all tasks created within the group (RINS, B&B workers) is_running_ = false; + settings_.log.printf("\n"); // Compute final lower bound f_t lower_bound; diff --git a/cpp/src/branch_and_bound/symmetry.hpp b/cpp/src/branch_and_bound/symmetry.hpp index 45426a9fa8..1848b3dad8 100644 --- a/cpp/src/branch_and_bound/symmetry.hpp +++ b/cpp/src/branch_and_bound/symmetry.hpp @@ -684,6 +684,8 @@ std::unique_ptr> detect_symmetry( const simplex::simplex_solver_settings_t& settings, bool& has_symmetry) { + settings.log.printf("\nRunning symmetry detection...\n"); + has_symmetry = false; f_t start_time = simplex::tic(); diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 2cc041d81a..54c1918441 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -3610,6 +3610,8 @@ variable_bounds_t::variable_bounds_t(const lp_problem_t& lp, } f_t start_time = tic(); + settings.log.debug("Computing variable bounds..."); + std::vector num_integer_in_row(lp.num_rows, 0); // Construct the slack map @@ -3781,7 +3783,7 @@ variable_bounds_t::variable_bounds_t(const lp_problem_t& lp, } } upper_offsets[lp.num_cols] = upper_edges; - settings.log.printf("%d variable upper bounds in %.2f seconds\n", upper_edges, toc(start_time)); + settings.log.debug("%d variable upper bounds in %.2f seconds\n", upper_edges, toc(start_time)); // Now go through all continuous variables and use the activiites to get lower variable bounds i_t lower_edges = 0; @@ -3872,7 +3874,7 @@ variable_bounds_t::variable_bounds_t(const lp_problem_t& lp, } } lower_offsets[lp.num_cols] = lower_edges; - settings.log.printf("%d variable lower bounds in %.2f seconds\n", lower_edges, toc(start_time)); + settings.log.debug("%d variable lower bounds in %.2f seconds\n", lower_edges, toc(start_time)); } template diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index a0033438a3..4cec411882 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace cuopt::mathematical_optimization::simplex { diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index e90c96d8e4..50b8ccab02 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -223,7 +223,7 @@ template bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_timer) { raft::common::nvtx::range fun_scope("run_presolve"); - CUOPT_LOG_INFO("Starting cuOpt presolve"); + CUOPT_LOG_INFO("\nRunning cuOpt presolve"); timer_t presolve_timer(time_limit); auto term_crit = ls.constraint_prop.bounds_update.solve(*problem_ptr); diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index d842d2c9ef..de1a7fe282 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -683,12 +683,12 @@ third_party_presolve_result_t third_party_presolve_t::apply( papilo::Problem papilo_problem = build_papilo_problem(op_problem, category, maximize_); - CUOPT_LOG_INFO("Original problem: %d constraints, %d variables, %d nonzeros", - papilo_problem.getNRows(), - papilo_problem.getNCols(), - papilo_problem.getConstraintMatrix().getNnz()); + CUOPT_LOG_DEBUG("Original problem: %d constraints, %d variables, %d nonzeros", + papilo_problem.getNRows(), + papilo_problem.getNCols(), + papilo_problem.getConstraintMatrix().getNnz()); - CUOPT_LOG_INFO("Calling Papilo presolver (git hash %s)", PAPILO_GITHASH); + CUOPT_LOG_INFO("\nRunning Papilo presolve (git hash %s)", PAPILO_GITHASH); if (category == problem_category_t::MIP) { dual_postsolve = false; } papilo::Presolve papilo_presolver; set_presolve_methods(papilo_presolver, category, dual_postsolve); diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 5b7a9fc1c1..bac04cb053 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -183,7 +183,7 @@ void extract_probing_implied_bounds(const problem_t& op_problem, } } - CUOPT_LOG_INFO("Probing implied bounds: %d zero entries, %d one entries", zero_nnz, one_nnz); + CUOPT_LOG_INFO("\nProbing implied bounds: %d zero entries, %d one entries", zero_nnz, one_nnz); } template @@ -496,10 +496,7 @@ solution_t mip_solver_t::run_solver() if (!context.settings.heuristics_only) { #pragma omp task default(shared) priority(CUOPT_CRITICAL_TASK_PRIORITY) { - auto t0 = std::chrono::system_clock::now(); - branch_and_bound_status = branch_and_bound->solve(branch_and_bound_solution); - std::chrono::duration elapsed = std::chrono::system_clock::now() - t0; - context.stats.bnb_time = elapsed.count(); + branch_and_bound_status = branch_and_bound->solve(branch_and_bound_solution); } } diff --git a/cpp/src/mip_heuristics/solver_solution.cu b/cpp/src/mip_heuristics/solver_solution.cu index f20a30cc63..5d3a778160 100644 --- a/cpp/src/mip_heuristics/solver_solution.cu +++ b/cpp/src/mip_heuristics/solver_solution.cu @@ -239,7 +239,7 @@ template void mip_solution_t::log_detailed_summary() const { std::string section_divider(50, '='); - std::string text = "\nSummary\n" + section_divider + "\n"; + std::string text = "\n" + section_divider + "\n"; std::string solution_status = get_termination_status_string(); f_t dual_bound = stats_.get_solution_bound(); @@ -255,11 +255,13 @@ void mip_solution_t::log_detailed_summary() const dual_bound = std::numeric_limits::infinity(); } - text += std::format("{:<30}{:>15}\n", "Solution status:", solution_status); text += std::format("{:<30}{:>+15.6e}\n", "Solution objective:", obj); text += std::format("{:<30}{:>+15.6e}\n", "Dual bound:", dual_bound); + text += std::format("{:<30}{:>+15.6e}\n", "Absolute MIP gap:", std::abs(obj - dual_bound)); text += std::format("{:<30}{:>+15.6e}\n", "Relative MIP gap:", gap); +#ifdef PRINT_CONSTRAINT_VIOLATION + if (has_solution) { text += std::format("{:<30}{:>+15.6e}\n", "Max constraint violation:", max_constraint_violation_); @@ -271,12 +273,8 @@ void mip_solution_t::log_detailed_summary() const text += std::format("{:<30}{:>15}\n", "Max integer violation:", "N/A"); text += std::format("{:<30}{:>15}\n", "Max variable bound violation:", "N/A"); } - - text += std::format("{:<30}{:>15}\n", "Nodes explored:", stats_.num_nodes); - text += std::format("{:<30}{:>15}\n", "Simplex iterations:", stats_.num_simplex_iterations); - text += std::format("{:<30}{:>15.2f}\n", "Presolve time:", stats_.presolve_time); - text += std::format("{:<30}{:>15.2f}\n", "B&B time:", stats_.bnb_time); - text += std::format("{:<30}{:>15.2f}\n", "Total solve time:", stats_.total_solve_time); +#endif + text += std::format("{:<30}{:>15.2f}\n", "Solve time:", stats_.total_solve_time); CUOPT_LOG_INFO("%s\n", text); } From dfdb29e19f243d0600a9565e1af16f0c3d7dd3a4 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 15:15:08 +0200 Subject: [PATCH 17/28] revert column width to use constants Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 82 +++++-------------- 1 file changed, 20 insertions(+), 62 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 5b89c2552f..1291655280 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -75,19 +75,6 @@ using simplex::vector_norm_inf; namespace { -// Column widths for the branch-and-bound progress table. -constexpr int SYMBOL_WIDTH = 1; -constexpr int EXPLORED_WIDTH = 12; -constexpr int UNEXPLORED_WIDTH = 12; -constexpr int OBJECTIVE_WIDTH = 19; -constexpr int BOUND_WIDTH = 15; -constexpr int INTEGER_INFEASIBLE_WIDTH = 8; -constexpr int DEPTH_WIDTH = 7; -constexpr int ITER_NODE_WIDTH = 11; -constexpr int GAP_WIDTH = 11; -constexpr int WORK_WIDTH = 8; -constexpr int TIME_WIDTH = 8; - template bool is_fractional(f_t x, variable_type_t var_type, f_t integer_tol) { @@ -337,28 +324,18 @@ void branch_and_bound_t::set_initial_upper_bound(f_t bound) template void branch_and_bound_t::print_table_header() { - std::string header = - std::format("{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|{:^{}}|", - "", - SYMBOL_WIDTH, - "Explored", - EXPLORED_WIDTH, - "Unexplored", - UNEXPLORED_WIDTH, - "Objective", - OBJECTIVE_WIDTH, - "Bound", - BOUND_WIDTH, - "IntInf", - INTEGER_INFEASIBLE_WIDTH, - "Depth", - DEPTH_WIDTH, - "Iter/Node", - ITER_NODE_WIDTH, - "Gap", - GAP_WIDTH); - if (settings_.deterministic) { header += std::format("{:^{}}|", "Work", WORK_WIDTH); } - header += std::format("{:^{}}|", "Time", TIME_WIDTH); + std::string header = std::format("{:^1}|{:^12}|{:^12}|{:^19}|{:^15}|{:^8}|{:^7}|{:^11}|{:^11}|", + "", + "Explored", + "Unexplored", + "Objective", + "Bound", + "IntInf", + "Depth", + "Iter/Node", + "Gap"); + if (settings_.deterministic) { header += std::format("{:^8}|", "Work"); } + header += std::format("{:^8}|", "Time"); settings_.log.printf("%s\n", header.c_str()); } @@ -373,28 +350,18 @@ void branch_and_bound_t::report_heuristic(f_t obj) std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("{:^{}} {:>{}} {:>{}} {:^+{}.6e} {:^+{}.6e} {:>{}} {:>{}} {:^{}} {:^{}}", - "H", - SYMBOL_WIDTH, + std::format("H {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11} {:^11}", "", // nodes explored - EXPLORED_WIDTH, "", // nodes unexplored - UNEXPLORED_WIDTH, user_obj, - OBJECTIVE_WIDTH, user_lower, - BOUND_WIDTH, "", // integer infeasible - INTEGER_INFEASIBLE_WIDTH, "", // depth - DEPTH_WIDTH, "", // iter/node - ITER_NODE_WIDTH, - user_gap_text, - GAP_WIDTH); + user_gap_text); - if (settings_.deterministic) { log_line += std::format("{:^{}}", "", WORK_WIDTH); } - log_line += std::format(" {:>{}.2f}", toc(exploration_stats_.start_time), TIME_WIDTH); + if (settings_.deterministic) { log_line += std::format("{:^8}", ""); } + log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); settings_.log.printf("%s\n", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { @@ -429,27 +396,18 @@ void branch_and_bound_t::report( std::string user_gap_text = to_percentage(user_gap); std::string log_line = - std::format("{:^{}} {:>{}} {:>{}} {:^+{}.6e} {:^+{}.6e} {:>{}} {:>{}} {:^{}.1e} {:^{}}", + std::format("{:^1} {:>12} {:>12} {:^+19.6e} {:^+15.6e} {:>8} {:>7} {:^11.1e} {:^11}", symbol, - SYMBOL_WIDTH, nodes_explored, - EXPLORED_WIDTH, nodes_unexplored, - UNEXPLORED_WIDTH, user_obj, - OBJECTIVE_WIDTH, user_lower, - BOUND_WIDTH, node_int_infeas, - INTEGER_INFEASIBLE_WIDTH, node_depth, - DEPTH_WIDTH, iter_node, - ITER_NODE_WIDTH, - user_gap_text, - GAP_WIDTH); - if (work_time >= 0) { log_line += std::format(" {:>{}.2f}", work_time, WORK_WIDTH); } - log_line += std::format(" {:>{}.2f}", toc(exploration_stats_.start_time), TIME_WIDTH); + user_gap_text); + if (work_time >= 0) { log_line += std::format(" {:>8.2f}", work_time); } + log_line += std::format(" {:>8.2f}", toc(exploration_stats_.start_time)); settings_.log.printf("%s\n", log_line.c_str()); } From b2bafe626855f8a07e31ad97977e7179d0b64f20 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 15:51:46 +0200 Subject: [PATCH 18/28] fix string_view for format variants in the logger_t Signed-off-by: Nicolas L. Guidotti --- cpp/src/dual_simplex/logger.hpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 4cec411882..899efe148e 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -94,8 +94,10 @@ class logger_t { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL - std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; - CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg.c_str()); + std::string msg_no_newline = msg; + if (msg_no_newline.size() > 0 && msg.ends_with("\n")) { msg_no_newline.pop_back(); } + + CUOPT_LOG_INFO("%s%s", log_prefix.c_str(), msg_no_newline.c_str()); #else std::printf("%s", msg.c_str()); fflush(stdout); @@ -148,9 +150,10 @@ class logger_t { if (log) { std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { -#ifdef CUOPT_LOG_ACTIVE_LEVEL - std::string_view msg_view = msg.ends_with("\n") ? msg.substr(0, msg.size() - 1) : msg; - CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg_view.c_str()); +#ifdef CUOPT_LOG_DEBUG + std::string msg_no_newline = msg; + if (msg_no_newline.size() > 0 && msg.ends_with("\n")) { msg_no_newline.pop_back(); } + CUOPT_LOG_TRACE("%s%s", log_prefix.c_str(), msg_no_newline.c_str()); #else std::printf("%s", msg.c_str()); fflush(stdout); From 63940b67a7cc0d26972fc4f03f42210bab8f1b5f Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 16:00:35 +0200 Subject: [PATCH 19/28] address coderabbit Signed-off-by: Nicolas L. Guidotti --- cpp/src/dual_simplex/logger.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 899efe148e..8bd4efaeb9 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -91,7 +91,7 @@ class logger_t { void print_format(std::format_string fmt, Args&&... args) { if (log) { - std::string msg = std::format(fmt, std::forward(args)...); + std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_ACTIVE_LEVEL std::string msg_no_newline = msg; @@ -148,7 +148,7 @@ class logger_t { void debug_format(std::format_string fmt, Args&&... args) { if (log) { - std::string msg = std::format(fmt, std::forward(args)...); + std::string msg = std::format(fmt, std::forward(args)...); if (log_to_console) { #ifdef CUOPT_LOG_DEBUG std::string msg_no_newline = msg; From 78eaa87a7839837ffa300705ba0a1c08c34130a9 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 10 Jun 2026 13:01:02 +0200 Subject: [PATCH 20/28] update last log line Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 1 - cpp/src/mip_heuristics/solver_solution.cu | 65 +++++++++---------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 1291655280..b10e34cd31 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -795,7 +795,6 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& if (solver_status_ == mip_status_t::UNSET) { if (exploration_stats_.nodes_explored > 0 && exploration_stats_.nodes_unexplored == 0 && upper_bound_ == inf) { - settings_.log.printf("Integer infeasible.\n"); solver_status_ = mip_status_t::INFEASIBLE; if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); diff --git a/cpp/src/mip_heuristics/solver_solution.cu b/cpp/src/mip_heuristics/solver_solution.cu index 5d3a778160..d84b3af6b4 100644 --- a/cpp/src/mip_heuristics/solver_solution.cu +++ b/cpp/src/mip_heuristics/solver_solution.cu @@ -238,44 +238,39 @@ void mip_solution_t::log_summary() const template void mip_solution_t::log_detailed_summary() const { - std::string section_divider(50, '='); - std::string text = "\n" + section_divider + "\n"; - std::string solution_status = get_termination_status_string(); + switch (termination_status_) { + case mip_termination_status_t::Optimal: + case mip_termination_status_t::FeasibleFound: + CUOPT_LOG_INFO("%s\n", + std::format("Best objective {:+.6e}, best bound {:+.6e}, gap {:.2f}%.\n", + objective_, + stats_.get_solution_bound(), + mip_gap_ * 100) + .c_str()); + break; + + case mip_termination_status_t::Infeasible: + CUOPT_LOG_INFO("The problem is integer infeasible.\n"); + break; + + case mip_termination_status_t::TimeLimit: + CUOPT_LOG_INFO("No feasible solution was found within the time limit.\n"); + break; + + case mip_termination_status_t::WorkLimit: + CUOPT_LOG_INFO("No feasible solution was found within the work limit.\n"); + break; + + case mip_termination_status_t::Unbounded: CUOPT_LOG_INFO("The problem is unbounded.\n"); break; - f_t dual_bound = stats_.get_solution_bound(); - f_t obj = objective_; - f_t gap = mip_gap_; - - bool has_solution = termination_status_ == mip_termination_status_t::FeasibleFound || - termination_status_ == mip_termination_status_t::Optimal; - - if (!has_solution) { - obj = std::numeric_limits::infinity(); - gap = std::numeric_limits::infinity(); - dual_bound = std::numeric_limits::infinity(); - } + case mip_termination_status_t::UnboundedOrInfeasible: + CUOPT_LOG_INFO("The problem is unbounded or infeasible.\n"); + break; - text += std::format("{:<30}{:>+15.6e}\n", "Solution objective:", obj); - text += std::format("{:<30}{:>+15.6e}\n", "Dual bound:", dual_bound); - text += std::format("{:<30}{:>+15.6e}\n", "Absolute MIP gap:", std::abs(obj - dual_bound)); - text += std::format("{:<30}{:>+15.6e}\n", "Relative MIP gap:", gap); - -#ifdef PRINT_CONSTRAINT_VIOLATION - - if (has_solution) { - text += - std::format("{:<30}{:>+15.6e}\n", "Max constraint violation:", max_constraint_violation_); - text += std::format("{:<30}{:>+15.6e}\n", "Max integer violation:", max_int_violation_); - text += std::format( - "{:<30}{:>+15.6e}\n", "Max variable bound violation:", max_variable_bound_violation_); - } else { - text += std::format("{:<30}{:>15}\n", "Max constraint violation:", "N/A"); - text += std::format("{:<30}{:>15}\n", "Max integer violation:", "N/A"); - text += std::format("{:<30}{:>15}\n", "Max variable bound violation:", "N/A"); + case mip_termination_status_t::NoTermination: + CUOPT_LOG_INFO("Warning: The solver did not terminate successfully.\n"); + break; } -#endif - text += std::format("{:<30}{:>15.2f}\n", "Solve time:", stats_.total_solve_time); - CUOPT_LOG_INFO("%s\n", text); } #if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT From 38bd283505044ca76cc44b22a6bed3230f4e017b Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 10 Jun 2026 13:13:43 +0200 Subject: [PATCH 21/28] fixed log message when concurrent root solve is disabled Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index b10e34cd31..3b6c9b3af6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2554,6 +2554,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut nonbasic_list, root_vstatus_, edge_norms_); + if (root_status == lp_status_t::OPTIMAL) { + settings_.log.printf("\n"); + settings_.log.printf( + "Root relaxation solution found in %d iterations and %.2fs by Dual Simplex\n", + root_relax_soln_.iterations, + toc(exploration_stats_.start_time)); + settings_.log.printf("Root relaxation objective %+.8e\n", + compute_user_objective(original_lp_, root_relax_soln_.x)); + settings_.log.printf("\n"); + } + } else { settings_.log.printf("\nSolving LP root relaxation in concurrent mode\n"); root_status = solve_root_relaxation(lp_settings, From 5478c7fd78e5c03ae13c827c6f17f723d3606375 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 10 Jun 2026 14:56:39 +0200 Subject: [PATCH 22/28] fixes parsing for remote execution test Signed-off-by: Nicolas L. Guidotti --- .../cuopt/tests/linear_programming/test_cpu_only_execution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py index 4453d38bcc..07ae224ef9 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py @@ -120,9 +120,9 @@ def _parse_cli_output(output): result["status"] = "Optimal" continue - # MIP solution: "Solution objective: 2.000000 , ..." + # MIP solution: "Best objective 2.000000 , ..." m = re.match( - r"Solution objective:\s*([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)", + r"Best objective \s*([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)", stripped, ) if m: From e7426df916261c92f01ada44dbc1a03551a989aa Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 11 Jun 2026 09:59:45 +0200 Subject: [PATCH 23/28] Fix test_cli.sh to match renamed MIP summary log line The MIP log cleanup (PR #1402) renamed `log_detailed_summary()` output from "Solution objective: ..." to "Best objective ...", but test_cli.sh was not updated to match. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Nicolas L. Guidotti --- python/libcuopt/libcuopt/tests/test_cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/libcuopt/libcuopt/tests/test_cli.sh b/python/libcuopt/libcuopt/tests/test_cli.sh index 85f73fa58f..51c0397e74 100644 --- a/python/libcuopt/libcuopt/tests/test_cli.sh +++ b/python/libcuopt/libcuopt/tests/test_cli.sh @@ -30,4 +30,4 @@ cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/linear_programming/good-mps-1.lp.bz2 | gr # Add a for mixed integer programming test with options -cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/mip/sample.mps --mip-absolute-gap 0.01 --time-limit 10 | grep -q "Solution objective" || (echo "Expected solution objective not found" && exit 1) +cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/mip/sample.mps --mip-absolute-gap 0.01 --time-limit 10 | grep -q "Best objective" || (echo "Expected solution objective not found" && exit 1) From bbab9eea6cd6f52e2176b6262039296108699dcc Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 18 Jun 2026 18:18:21 +0200 Subject: [PATCH 24/28] update logs based on Miles comments Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 51 +++++-------------- cpp/src/mip_heuristics/solver_solution.cu | 4 +- 2 files changed, 15 insertions(+), 40 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3b6c9b3af6..25092a0575 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2032,11 +2032,6 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( std::vector& nonbasic_list, std::vector& edge_norms) { - f_t start_time = tic(); - f_t user_objective = 0; - i_t iter = 0; - std::string solver_name = ""; - lp_status_t root_status; // Launch a task for solving the root LP relaxation via dual simplex. @@ -2143,40 +2138,18 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( // Set the edge norms to a default value edge_norms.resize(original_lp_.num_cols, -1.0); set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms); - user_objective = root_crossover_soln_.user_objective; - iter = root_crossover_soln_.iterations; - solver_name = method_to_string(root_relax_solved_by); } else { // Wait for the dual simplex to finish (after telling PDLP/Barrier to stop) #pragma omp taskwait depend(in : root_status) - user_objective = root_relax_soln_.user_objective; - iter = root_relax_soln_.iterations; root_relax_solved_by = DualSimplex; - solver_name = "Dual Simplex"; } } else { // Wait for the dual simplex to finish (crossover do not produced a solution) #pragma omp taskwait depend(in : root_status) - user_objective = root_relax_soln_.user_objective; - iter = root_relax_soln_.iterations; root_relax_solved_by = DualSimplex; - solver_name = "Dual Simplex"; - } - - settings_.log.printf("\n"); - if (root_status == lp_status_t::OPTIMAL) { - settings_.log.printf("Root relaxation solution found in %d iterations and %.2fs by %s\n", - iter, - toc(start_time), - solver_name.c_str()); - settings_.log.printf("Root relaxation objective %+.8e\n", user_objective); - } else { - settings_.log.printf("Root relaxation returned: %s\n", - simplex::lp_status_to_string(root_status).c_str()); } - settings_.log.printf("\n"); is_root_solution_set = true; return root_status; @@ -2542,6 +2515,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lp_status_t root_status; solving_root_relaxation_ = true; + f_t root_relax_start_time = tic(); if (!enable_concurrent_lp_root_solve()) { // RINS/SUBMIP path settings_.log.printf("\nSolving LP root relaxation with dual simplex\n"); @@ -2554,16 +2528,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut nonbasic_list, root_vstatus_, edge_norms_); - if (root_status == lp_status_t::OPTIMAL) { - settings_.log.printf("\n"); - settings_.log.printf( - "Root relaxation solution found in %d iterations and %.2fs by Dual Simplex\n", - root_relax_soln_.iterations, - toc(exploration_stats_.start_time)); - settings_.log.printf("Root relaxation objective %+.8e\n", - compute_user_objective(original_lp_, root_relax_soln_.x)); - settings_.log.printf("\n"); - } } else { settings_.log.printf("\nSolving LP root relaxation in concurrent mode\n"); @@ -2577,7 +2541,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } solving_root_relaxation_ = false; exploration_stats_.total_lp_iters = root_relax_soln_.iterations; - exploration_stats_.total_lp_solve_time = toc(exploration_stats_.start_time); + f_t root_relax_elapsed_time = toc(root_relax_start_time); + exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); @@ -2618,6 +2583,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return solver_status_; } + assert(root_status == lp_status_t::OPTIMAL); + settings_.log.print_format( + "\nRoot relaxation solution found in {} iterations and {:.2f}s by {}\n", + root_relax_soln_.iterations, + root_relax_elapsed_time, + method_to_string(root_relax_solved_by)); + settings_.log.printf("Root relaxation objective %+.8e\n\n", root_relax_soln_.user_objective); + assert(root_vstatus_.size() == original_lp_.num_cols); set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); @@ -2812,6 +2785,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_.num_rows, original_lp_.num_cols, original_lp_.A.col_start[original_lp_.A.n]); + } else { + settings_.log.printf("\n"); } if (enable_root_cut_cpufj && cut_info.has_cuts()) { diff --git a/cpp/src/mip_heuristics/solver_solution.cu b/cpp/src/mip_heuristics/solver_solution.cu index d84b3af6b4..c0f30876c3 100644 --- a/cpp/src/mip_heuristics/solver_solution.cu +++ b/cpp/src/mip_heuristics/solver_solution.cu @@ -242,7 +242,7 @@ void mip_solution_t::log_detailed_summary() const case mip_termination_status_t::Optimal: case mip_termination_status_t::FeasibleFound: CUOPT_LOG_INFO("%s\n", - std::format("Best objective {:+.6e}, best bound {:+.6e}, gap {:.2f}%.\n", + std::format("Best objective {:.4g}, best bound {:.4g}, gap {:.2f}%.", objective_, stats_.get_solution_bound(), mip_gap_ * 100) @@ -250,7 +250,7 @@ void mip_solution_t::log_detailed_summary() const break; case mip_termination_status_t::Infeasible: - CUOPT_LOG_INFO("The problem is integer infeasible.\n"); + CUOPT_LOG_INFO("No integer feasible solution exists.\n"); break; case mip_termination_status_t::TimeLimit: From 05b929d69df15568c19418f1f7503c1e60130fe9 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 25 Jun 2026 15:30:27 +0200 Subject: [PATCH 25/28] increased log precision. changed relative_user_gap to use already compute values. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 67 +++++++++++-------- cpp/src/mip_heuristics/solver_solution.cu | 2 +- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 25092a0575..657077f2e5 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -213,14 +213,12 @@ f_t compute_user_abs_gap(const lp_problem_t& lp, f_t obj_value, f_t lo return gap; } -template -f_t user_relative_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) +template +f_t user_relative_gap(f_t user_obj, f_t user_lower_bound) { - f_t user_obj = compute_user_objective(lp, obj_value); - f_t user_lower_bound = compute_user_objective(lp, lower_bound); - f_t user_mip_gap = user_obj == 0.0 - ? (user_lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) - : compute_user_abs_gap(lp, obj_value, lower_bound) / std::abs(user_obj); + f_t user_mip_gap = user_obj == 0.0 + ? (user_lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) + : std::abs(user_obj - user_lower_bound) / std::abs(user_obj); if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } return user_mip_gap; } @@ -346,7 +344,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) f_t lower_bound = get_lower_bound(); f_t user_obj = compute_user_objective(original_lp_, obj); f_t user_lower = compute_user_objective(original_lp_, lower_bound); - f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + f_t user_gap = user_relative_gap(user_obj, user_lower); std::string user_gap_text = to_percentage(user_gap); std::string log_line = @@ -365,8 +363,9 @@ void branch_and_bound_t::report_heuristic(f_t obj) settings_.log.printf("%s\n", log_line.c_str()); } else { if (solving_root_relaxation_.load()) { - f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_gap = user_relative_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); + f_t user_obj = compute_user_objective(original_lp_, obj); + f_t user_lower = compute_user_objective(original_lp_, root_lp_current_lower_bound_.load()); + f_t user_gap = user_relative_gap(user_obj, user_lower); std::string user_gap_text = to_percentage(user_gap); settings_.log.print_format( "New solution from primal heuristics. Objective {:+.6e}. Gap {}. Time {:.2f}\n", @@ -392,7 +391,7 @@ void branch_and_bound_t::report( const f_t user_lower = compute_user_objective(original_lp_, lower_bound); const f_t iters = static_cast(exploration_stats_.total_lp_iters); const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; - f_t user_gap = user_relative_gap(original_lp_, obj, lower_bound); + f_t user_gap = user_relative_gap(user_obj, user_lower); std::string user_gap_text = to_percentage(user_gap); std::string log_line = @@ -743,10 +742,10 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& settings_.heuristic_preemption_callback(); } - f_t obj = compute_user_objective(original_lp_, upper_bound_.load()); + f_t user_obj = compute_user_objective(original_lp_, upper_bound_.load()); f_t user_bound = compute_user_objective(original_lp_, lower_bound); - f_t gap = std::abs(obj - user_bound); - f_t gap_rel = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); + f_t gap = std::abs(user_obj - user_bound); + f_t gap_rel = user_relative_gap(user_obj, user_bound); bool is_maximization = original_lp_.obj_scale < 0.0; settings_.log.print_format("Explored {} nodes ({} simplex iterations) in {:.2f}s.", @@ -1580,7 +1579,9 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, f_t lower_bound = get_lower_bound(); f_t upper_bound = upper_bound_; - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + f_t user_obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + f_t rel_gap = user_relative_gap(user_obj, user_lower); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && @@ -1712,7 +1713,9 @@ void branch_and_bound_t::plunge_with(bfs_worker_t* worker, lower_bound = get_lower_bound(); upper_bound = upper_bound_; - rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + user_obj = compute_user_objective(original_lp_, upper_bound); + user_lower = compute_user_objective(original_lp_, lower_bound); + rel_gap = user_relative_gap(user_obj, user_lower); abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); } @@ -1781,8 +1784,10 @@ template void branch_and_bound_t::best_first_search_with(bfs_worker_t* worker) { f_t lower_bound = get_lower_bound(); + f_t user_obj = compute_user_objective(original_lp_, upper_bound_.load()); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), lower_bound); - f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); + f_t rel_gap = user_relative_gap(user_obj, user_lower); f_t steal_chance = settings_.bnb_steal_chance >= 0 ? settings_.bnb_steal_chance : MIP_DEFAULT_STEAL_CHANCE; node_queue_t& node_queue = worker->node_queue; @@ -1843,8 +1848,10 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t plunge_with(worker, start_node); lower_bound = get_lower_bound(); + user_obj = compute_user_objective(original_lp_, upper_bound_.load()); + user_lower = compute_user_objective(original_lp_, lower_bound); abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), lower_bound); - rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); + rel_gap = user_relative_gap(user_obj, user_lower); if (abs_gap <= settings_.absolute_mip_gap_tol || rel_gap <= settings_.relative_mip_gap_tol) { node_concurrent_halt_ = 1; @@ -1897,7 +1904,9 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker) f_t lower_bound = get_lower_bound(); f_t upper_bound = upper_bound_; - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + f_t user_obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + f_t rel_gap = user_relative_gap(user_obj, user_lower); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && @@ -1953,7 +1962,9 @@ void branch_and_bound_t::dive_with(diving_worker_t* worker) lower_bound = get_lower_bound(); upper_bound = upper_bound_; - rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + user_obj = compute_user_objective(original_lp_, upper_bound); + user_lower = compute_user_objective(original_lp_, lower_bound); + rel_gap = user_relative_gap(user_obj, user_lower); abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); } @@ -2414,8 +2425,10 @@ auto branch_and_bound_t::do_cut_pass( f_t obj = upper_bound_.load(); report(' ', obj, root_objective_, 0, num_fractional); - f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); - f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), root_objective_); + f_t user_obj = compute_user_objective(original_lp_, upper_bound_.load()); + f_t user_lower = compute_user_objective(original_lp_, root_objective_); + f_t rel_gap = user_relative_gap(user_obj, user_lower); + f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), root_objective_); if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { if (num_fractional == 0) { set_solution_at_root(solution, cut_info); } set_final_solution(solution, root_objective_); @@ -3400,8 +3413,10 @@ void branch_and_bound_t::deterministic_sync_callback() f_t lower_bound = deterministic_compute_lower_bound(); f_t upper_bound = upper_bound_.load(); + f_t user_obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + f_t rel_gap = user_relative_gap(user_obj, user_lower); // Apply limit-based statuses first so a definitive answer (gap closure or tree exhaustion) // detected in the same callback can override them. Otherwise a long producer wait that @@ -3441,9 +3456,7 @@ void branch_and_bound_t::deterministic_sync_callback() exploration_stats_.last_log = tic(); } - f_t obj = compute_user_objective(original_lp_, upper_bound); - f_t user_lower = compute_user_objective(original_lp_, lower_bound); - f_t user_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); + f_t user_gap = user_relative_gap(user_obj, user_lower); std::string user_gap_text = to_percentage(user_gap); std::string idle_workers; @@ -3458,7 +3471,7 @@ void branch_and_bound_t::deterministic_sync_callback() deterministic_current_horizon_, exploration_stats_.nodes_explored, exploration_stats_.nodes_unexplored, - obj, + user_obj, user_lower, user_gap_text.c_str(), toc(exploration_stats_.start_time), diff --git a/cpp/src/mip_heuristics/solver_solution.cu b/cpp/src/mip_heuristics/solver_solution.cu index c0f30876c3..c0afaf9a33 100644 --- a/cpp/src/mip_heuristics/solver_solution.cu +++ b/cpp/src/mip_heuristics/solver_solution.cu @@ -242,7 +242,7 @@ void mip_solution_t::log_detailed_summary() const case mip_termination_status_t::Optimal: case mip_termination_status_t::FeasibleFound: CUOPT_LOG_INFO("%s\n", - std::format("Best objective {:.4g}, best bound {:.4g}, gap {:.2f}%.", + std::format("Best objective {:.6g}, best bound {:.6g}, gap {:.2f}%.", objective_, stats_.get_solution_bound(), mip_gap_ * 100) From 500b4e693d587d638c723a82416ff72b2e8be4e2 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 26 Jun 2026 10:45:24 +0200 Subject: [PATCH 26/28] addressing Chris feedback Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 10 +++++++--- cpp/src/cuts/cuts.cpp | 8 +++++--- cpp/src/mip_heuristics/solver_solution.cu | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 657077f2e5..8c3a264ddb 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -226,7 +226,7 @@ f_t user_relative_gap(f_t user_obj, f_t user_lower_bound) template std::string to_percentage(f_t value) { - if (value == std::numeric_limits::infinity()) return "---"; + if (value == std::numeric_limits::infinity()) return "-"; if (value > 1e-3) { return std::format("{:5.1f}%", value * 100); } return std::format("{:5.2f}%", value * 100); } @@ -2558,13 +2558,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; if (root_status == lp_status_t::INFEASIBLE) { - settings_.log.printf("MIP Infeasible\n"); + settings_.log.printf("The root LP relaxation is infeasible\n", + lp_status_to_string(root_status).c_str()); signal_extend_cliques_.store(true, std::memory_order_release); #pragma omp taskwait depend(in : *clique_signal) return mip_status_t::INFEASIBLE; } + if (root_status == lp_status_t::UNBOUNDED) { - settings_.log.printf("MIP Unbounded\n"); + settings_.log.printf("The root relaxation is unbounded\n", + lp_status_to_string(root_status).c_str()); if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } @@ -2572,6 +2575,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #pragma omp taskwait depend(in : *clique_signal) return mip_status_t::UNBOUNDED; } + if (root_status == lp_status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, -inf); diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 54c1918441..40efbe6185 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -3610,7 +3610,7 @@ variable_bounds_t::variable_bounds_t(const lp_problem_t& lp, } f_t start_time = tic(); - settings.log.debug("Computing variable bounds..."); + settings.log.printf("Computing variable bounds..."); std::vector num_integer_in_row(lp.num_rows, 0); @@ -3782,8 +3782,9 @@ variable_bounds_t::variable_bounds_t(const lp_problem_t& lp, } } } + upper_offsets[lp.num_cols] = upper_edges; - settings.log.debug("%d variable upper bounds in %.2f seconds\n", upper_edges, toc(start_time)); + settings.log.print_format("{} variable upper bounds in {:.2f}s\n", upper_edges, toc(start_time)); // Now go through all continuous variables and use the activiites to get lower variable bounds i_t lower_edges = 0; @@ -3873,8 +3874,9 @@ variable_bounds_t::variable_bounds_t(const lp_problem_t& lp, } } } + lower_offsets[lp.num_cols] = lower_edges; - settings.log.debug("%d variable lower bounds in %.2f seconds\n", lower_edges, toc(start_time)); + settings.log.print_format("{} variable lower bounds in {:.2f}s\n", lower_edges, toc(start_time)); } template diff --git a/cpp/src/mip_heuristics/solver_solution.cu b/cpp/src/mip_heuristics/solver_solution.cu index c0afaf9a33..5b59e9a0fa 100644 --- a/cpp/src/mip_heuristics/solver_solution.cu +++ b/cpp/src/mip_heuristics/solver_solution.cu @@ -242,7 +242,7 @@ void mip_solution_t::log_detailed_summary() const case mip_termination_status_t::Optimal: case mip_termination_status_t::FeasibleFound: CUOPT_LOG_INFO("%s\n", - std::format("Best objective {:.6g}, best bound {:.6g}, gap {:.2f}%.", + std::format("Best objective {:.6e}, best bound {:.6e}, gap {:.2f}%.", objective_, stats_.get_solution_bound(), mip_gap_ * 100) @@ -250,7 +250,7 @@ void mip_solution_t::log_detailed_summary() const break; case mip_termination_status_t::Infeasible: - CUOPT_LOG_INFO("No integer feasible solution exists.\n"); + CUOPT_LOG_INFO("Problem has no integer feasible solution.\n"); break; case mip_termination_status_t::TimeLimit: From 6e22eb24f2cbd69a7a5e4a382a831432e99be089 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 26 Jun 2026 11:05:24 +0200 Subject: [PATCH 27/28] improve log when the optimal solution is found in Papilo. Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 7 +++++-- cpp/src/mip_heuristics/presolve/third_party_presolve.cpp | 2 +- cpp/src/mip_heuristics/solve.cu | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 8c3a264ddb..a17bc6716c 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2558,7 +2558,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.total_lp_solve_time = root_relax_elapsed_time; if (root_status == lp_status_t::INFEASIBLE) { - settings_.log.printf("The root LP relaxation is infeasible\n", + settings_.log.printf("\nThe root LP relaxation is infeasible\n", lp_status_to_string(root_status).c_str()); signal_extend_cliques_.store(true, std::memory_order_release); #pragma omp taskwait depend(in : *clique_signal) @@ -2566,7 +2566,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::UNBOUNDED) { - settings_.log.printf("The root relaxation is unbounded\n", + settings_.log.printf("\nThe root relaxation is unbounded\n", lp_status_to_string(root_status).c_str()); if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); @@ -2577,6 +2577,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::TIME_LIMIT) { + settings_.log.printf("\n"); solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, -inf); signal_extend_cliques_.store(true, std::memory_order_release); @@ -2585,6 +2586,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::WORK_LIMIT) { + settings_.log.printf("\n"); solver_status_ = mip_status_t::WORK_LIMIT; set_final_solution(solution, -inf); signal_extend_cliques_.store(true, std::memory_order_release); @@ -2593,6 +2595,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (root_status == lp_status_t::NUMERICAL_ISSUES) { + settings_.log.printf("\n"); solver_status_ = mip_status_t::NUMERICAL; set_final_solution(solution, -inf); signal_extend_cliques_.store(true, std::memory_order_release); diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index de1a7fe282..08bd7779a0 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -735,7 +735,7 @@ third_party_presolve_result_t third_party_presolve_t::apply( // Check if presolve found the optimal solution (problem fully reduced) if (papilo_problem.getNRows() == 0 && papilo_problem.getNCols() == 0) { - CUOPT_LOG_INFO("Optimal solution found during presolve"); + status = third_party_presolve_status_t::OPTIMAL; } auto opt_problem = build_optimization_problem( diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 42205af42b..634c6a0dbd 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -149,7 +149,6 @@ mip_solution_t run_mip_solver( auto stats = solver_stats_t{}; stats.set_solution_bound(solution.get_user_objective()); // log the objective for scripts which need it - CUOPT_LOG_INFO("Best feasible: %f", solution.get_user_objective()); for (auto callback : settings.get_mip_callbacks()) { if (callback->get_type() == internals::base_solution_callback_type::GET_SOLUTION) { auto temp_sol(solution); @@ -593,6 +592,10 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p CUOPT_LOG_INFO("%d implied integers", presolve_result_opt->implied_integer_indices.size()); } CUOPT_LOG_INFO("Papilo presolve time: %.2f", presolve_time); + + if (result.status == detail::third_party_presolve_status_t::OPTIMAL) { + CUOPT_LOG_INFO("Optimal solution found during presolve."); + } } // Stop early GPU FJ now that Papilo presolve is complete From b3850eba25e19e32bbfe7570cfe7d407b1fe17c9 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 26 Jun 2026 18:42:55 +0200 Subject: [PATCH 28/28] fix merge changes Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/mip_node.hpp | 12 ++++++------ cpp/src/dual_simplex/logger.hpp | 1 - cpp/src/mip_heuristics/solve.cu | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index ecd5dd37f6..fc7b3a90e9 100644 --- a/cpp/src/branch_and_bound/mip_node.hpp +++ b/cpp/src/branch_and_bound/mip_node.hpp @@ -383,9 +383,9 @@ class search_tree_t { const i_t branch_var, const f_t fractional_val, const i_t integer_infeasible, - const std::vector& parent_vstatus, - const lp_problem_t& original_lp, - logger_t& log) + const std::vector& parent_vstatus, + const simplex::lp_problem_t& original_lp, + simplex::logger_t& log) { i_t id = num_nodes.fetch_add(2); @@ -425,7 +425,7 @@ class search_tree_t { std::move(up_child)); // child pointers moved into the tree } - void graphviz_node(logger_t& log, + void graphviz_node(simplex::logger_t& log, const mip_node_t* node_ptr, const std::string label, const f_t val) @@ -435,7 +435,7 @@ class search_tree_t { } } - void graphviz_edge(logger_t& log, + void graphviz_edge(simplex::logger_t& log, const mip_node_t* origin_ptr, const mip_node_t* dest_ptr, const i_t branch_var, @@ -459,4 +459,4 @@ class search_tree_t { static constexpr bool write_graphviz = false; }; -} // namespace cuopt::linear_programming::dual_simplex +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/dual_simplex/logger.hpp b/cpp/src/dual_simplex/logger.hpp index 8bd4efaeb9..d1688da2a0 100644 --- a/cpp/src/dual_simplex/logger.hpp +++ b/cpp/src/dual_simplex/logger.hpp @@ -17,7 +17,6 @@ #include #include #include -#include #include namespace cuopt::mathematical_optimization::simplex { diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 634c6a0dbd..a7cb4884e4 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -593,7 +593,7 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p } CUOPT_LOG_INFO("Papilo presolve time: %.2f", presolve_time); - if (result.status == detail::third_party_presolve_status_t::OPTIMAL) { + if (result.status == mip::third_party_presolve_status_t::OPTIMAL) { CUOPT_LOG_INFO("Optimal solution found during presolve."); } }