From a7f51212f8f506398abc26ef9a02b52decb68b19 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 16:26:05 +0200 Subject: [PATCH 01/23] 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 f813086708..9f92561532 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::linear_programming::dual_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 27f2973ab40710f8843fa0a0c8a4e560d603a3cd Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 16:29:30 +0200 Subject: [PATCH 02/23] 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 | 134 +----------------- cpp/src/branch_and_bound/search_tree.hpp | 129 +++++++++++++++++ 3 files changed, 132 insertions(+), 132 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 0b32b2ece5..4c7df73bb4 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 2c0968ffce..ad53aaabb9 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 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 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 501220b61fe44ba7f4aa238ede71559ac68cad18 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 27 May 2026 19:28:27 +0200 Subject: [PATCH 03/23] 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 0222ad6fe9..2db23764b9 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -195,22 +195,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); } #ifdef SHOW_DIVING_TYPE @@ -326,29 +316,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", @@ -363,16 +396,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, @@ -381,21 +418,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)); } } @@ -2597,11 +2637,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, @@ -2864,16 +2900,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 { @@ -3385,9 +3412,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; @@ -3403,7 +3431,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 4c7df73bb4..77d02ccfb1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -264,6 +264,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 9f92561532..53c3cef9db 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 9672194b7f94736f6bf343ceff9b64cc596f5180 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 15:44:44 +0200 Subject: [PATCH 04/23] 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 2db23764b9..6aa75dc392 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -319,36 +319,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 @@ -361,18 +346,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); @@ -406,37 +395,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 83672994bf34826fa9214e7277228ad9d06ac261 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 15:56:03 +0200 Subject: [PATCH 05/23] 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 6aa75dc392..13cca92ece 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -367,15 +367,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 53c3cef9db..93f4230e91 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 b865ae95937bf2c46faf173af5fc6330c389d9ee Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 29 May 2026 17:03:06 +0200 Subject: [PATCH 06/23] 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 b5ef80a167..20b014fb70 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 13cca92ece..45c95d7bf1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -330,10 +330,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 @@ -361,7 +361,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); @@ -410,7 +410,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 @@ -999,8 +999,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())); } } @@ -1254,15 +1254,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 @@ -1480,10 +1480,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, @@ -1491,7 +1491,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_); @@ -1523,7 +1523,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, @@ -3491,7 +3491,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 fb1c0f450f..835c81f388 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 ad53aaabb9..56dbf3f603 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 93f4230e91..2ed8045ec4 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 6d7f0f5c63eccaf7f615bd9e57416547ec59f6cd Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 11:15:10 +0200 Subject: [PATCH 07/23] 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 | 24 ++++++++--------- .../deterministic_workers.hpp | 7 ++--- cpp/src/branch_and_bound/search_tree.hpp | 27 +++++++++++++++---- cpp/src/dual_simplex/logger.hpp | 1 - 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/cpp/src/branch_and_bound/bb_event.hpp b/cpp/src/branch_and_bound/bb_event.hpp index 20b014fb70..b5ef80a167 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 45c95d7bf1..2e2070b9c6 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -999,8 +999,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())); } } @@ -1254,15 +1254,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 @@ -1480,10 +1480,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, @@ -1491,7 +1491,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_); @@ -1523,7 +1523,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( worker->leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.debug_format("Numerical issue node {}. Resolving from scratch.\n", node_ptr->node_id); + log.debug("Numerical issue node %d. 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, diff --git a/cpp/src/branch_and_bound/deterministic_workers.hpp b/cpp/src/branch_and_bound/deterministic_workers.hpp index 835c81f388..fb1c0f450f 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 2ed8045ec4..f203ee1efb 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 e5ea4992110d0d8c7f44c65fcea62b03ab35f127 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 13:15:19 +0200 Subject: [PATCH 08/23] address coderabbit Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 +++--- 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, 17 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 2e2070b9c6..ed66e681af 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -347,7 +347,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, @@ -397,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} {:^15}", symbol, nodes_explored, nodes_unexplored, @@ -1523,7 +1523,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, diff --git a/cpp/src/branch_and_bound/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 56dbf3f603..8fd27d24f2 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 f203ee1efb..3f384e1952 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::linear_programming::dual_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 da1015f2f57d2b57f2e042942f28d28d635bd7ab Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 1 Jun 2026 19:40:30 +0200 Subject: [PATCH 09/23] revert log changes Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 12 +++++------- cpp/src/branch_and_bound/mip_node.hpp | 2 +- 2 files changed, 6 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 ed66e681af..9f2434be43 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -373,10 +373,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)); } } } @@ -1523,7 +1522,7 @@ dual::status_t branch_and_bound_t::solve_node_lp( worker->leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - log.debug_format("Numerical issue node {}. Resolving from scratch.\n", node_ptr->node_id); + log.debug("Numerical issue node %d. 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, @@ -3491,8 +3490,7 @@ node_status_t branch_and_bound_t::solve_node_deterministic( &worker.work_context); if (lp_status == dual::status_t::NUMERICAL) { - settings_.log.print_format("Numerical issue node {}. Resolving from scratch.\n", - node_ptr->node_id); + settings_.log.printf("Numerical issue node %d. 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/mip_node.hpp b/cpp/src/branch_and_bound/mip_node.hpp index 8fd27d24f2..ad53aaabb9 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 ffdc469f891289db9225caa727c90a44ac4d87f2 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:01:29 +0200 Subject: [PATCH 10/23] 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 9f2434be43..5af526d1f7 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -319,18 +319,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()); @@ -347,7 +345,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, @@ -355,9 +353,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)); @@ -396,7 +392,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, @@ -405,8 +401,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 e47c61f32174455b54a634b306b131e17a723a9f Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:02:38 +0200 Subject: [PATCH 11/23] 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 5eb9730f21c540a872e70e7ade882c72c4dfac5a Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:09:08 +0200 Subject: [PATCH 12/23] 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 77d02ccfb1..15a6030dbc 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 ad53aaabb9..2c0968ffce 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 739aab2b1410f7f5f2b9585733ec6ea41ed1be08 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:28:43 +0200 Subject: [PATCH 13/23] 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 5af526d1f7..c45f1a730c 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -381,15 +381,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 3b9f99c179e0570b42ba094a16b2b44b059899ce Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 14:37:47 +0200 Subject: [PATCH 14/23] 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 c45f1a730c..c76e30b647 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,6 +45,19 @@ namespace cuopt::linear_programming::dual_simplex { 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) { @@ -319,18 +332,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()); } @@ -345,18 +368,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()) { @@ -391,18 +424,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 ec577e621a694e9bf4e498ca043edf3ef08cc9f6 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 17:36:49 +0200 Subject: [PATCH 15/23] now the MIP solver gives a summary at the end Signed-off-by: Nicolas L. Guidotti --- .../linear_programming/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/linear_programming/mip/solver_stats.hpp b/cpp/include/cuopt/linear_programming/mip/solver_stats.hpp index a546354a65..8167ab54e5 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_stats.hpp +++ b/cpp/include/cuopt/linear_programming/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 2b64a7c681..004016dc70 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -735,10 +735,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 c25ade0c05..d9d920b3f2 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -490,7 +490,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 8f6f8de05f..e8ed5b1cb2 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 2e9059ed0cfbbead369210a2641ec47193912cb1 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 15:06:36 +0200 Subject: [PATCH 16/23] clean up the MIP logs Signed-off-by: Nicolas L. Guidotti --- .../linear_programming/mip/solver_stats.hpp | 3 -- cpp/src/branch_and_bound/branch_and_bound.cpp | 43 +++++++++---------- 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, 41 insertions(+), 47 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_stats.hpp b/cpp/include/cuopt/linear_programming/mip/solver_stats.hpp index 8167ab54e5..0eb0ce41af 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_stats.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_stats.hpp @@ -9,7 +9,6 @@ #include #include namespace cuopt::linear_programming { - template struct solver_stats_t { // Direction-neutral placeholder; solver_context initializes based on maximize/minimize. @@ -22,7 +21,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 +34,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 c76e30b647..eb9bb67b38 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -786,30 +786,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; @@ -2181,8 +2178,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( solver_name.c_str()); settings_.log.printf("Root relaxation objective %+.8e\n", user_objective); } else { - settings_.log.printf("Root relaxation returned: %s\n", - lp_status_to_string(root_status).c_str()); + settings_.log.debug_format("Root relaxation returned: {}", lp_status_to_string(root_status)); } settings_.log.printf("\n"); @@ -2483,8 +2479,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_); @@ -2942,6 +2938,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 bcce5ece2a..359895e579 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_solver_settings_t& settings, bool& has_symmetry) { + settings.log.printf("\nRunning symmetry detection...\n"); + has_symmetry = false; f_t start_time = tic(); diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 64d58db681..79dbf39153 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -3571,6 +3571,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 @@ -3742,7 +3744,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; @@ -3833,7 +3835,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 3f384e1952..b62159cb1c 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::linear_programming::dual_simplex { diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 7b038d6fa6..4b0e5168ec 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 d94cf5aa67..ef8df93e6d 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -682,12 +682,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 d9d920b3f2..aa84a874ed 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -181,7 +181,7 @@ void extract_probing_implied_bounds( } } - 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 @@ -490,10 +490,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 e8ed5b1cb2..3145ec1e54 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 4b3a6b6312cbb4c013bff42f8fcad01c7f60e879 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 15:15:08 +0200 Subject: [PATCH 17/23] 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 eb9bb67b38..608cbdf27b 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -45,19 +45,6 @@ namespace cuopt::linear_programming::dual_simplex { 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) { @@ -332,28 +319,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()); } @@ -368,28 +345,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()) { @@ -424,27 +391,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 e7523fdcce0caf70a81b06562eae034f602a3edb Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 15:51:46 +0200 Subject: [PATCH 18/23] 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 b62159cb1c..28431ffa72 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 ae178fb42f31bdf08a68a994e2a3789803daa16d Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 9 Jun 2026 16:00:35 +0200 Subject: [PATCH 19/23] 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 28431ffa72..530c81d459 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 9e4e2a330bef3dd6aec6e26f2d204eb3b0b9af0c Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 10 Jun 2026 13:01:02 +0200 Subject: [PATCH 20/23] 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 608cbdf27b..2e75e9dd24 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -790,7 +790,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 3145ec1e54..b3920a4936 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 1d30f7f5e4e277ff980d6f3933fea82f640186ca Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 10 Jun 2026 13:13:43 +0200 Subject: [PATCH 21/23] 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 2e75e9dd24..7e672ebb83 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2511,6 +2511,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 3fbee382e9c098e5a83e87407eab2b41d35d948f Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Wed, 10 Jun 2026 14:56:39 +0200 Subject: [PATCH 22/23] 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 1e992c08b0d6c66a68bbc14f7a4fa24a58615d91 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 11 Jun 2026 09:59:45 +0200 Subject: [PATCH 23/23] 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)