From 0e34a008a8554623dafcd72b35280b601e23d1f6 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 08:23:12 +0000 Subject: [PATCH 01/18] Add CMake build system alongside Bazel FetchContent-based CMake build that mirrors all Bazel targets including optional features (Gurobi, SCIP, bSuitor, Karp-Sipser, hashing, tcmalloc). Generates build-info.h and hashing-config.h, compiles app_io.proto, and builds all core binaries: app, runner, fork_runner, generate_experiment_config, binary_to_textproto. --- .gitignore | 3 +- CMakeLists.txt | 285 +++++++++++++++++++++ app/CMakeLists.txt | 21 ++ app/algorithms/CMakeLists.txt | 61 +++++ bmatching/CMakeLists.txt | 7 + bmatching/greedy/CMakeLists.txt | 11 + bmatching/ilp/CMakeLists.txt | 16 ++ bmatching/ils/CMakeLists.txt | 12 + bmatching/reductions_sorted/CMakeLists.txt | 12 + bmatching/scip/CMakeLists.txt | 15 ++ cmake/FindGurobi.cmake | 48 ++++ ds/CMakeLists.txt | 29 +++ io/CMakeLists.txt | 64 +++++ runner/CMakeLists.txt | 60 +++++ third_party/CMakeLists.txt | 45 ++++ tools/CMakeLists.txt | 8 + utils/CMakeLists.txt | 10 + 17 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 CMakeLists.txt create mode 100644 app/CMakeLists.txt create mode 100644 app/algorithms/CMakeLists.txt create mode 100644 bmatching/CMakeLists.txt create mode 100644 bmatching/greedy/CMakeLists.txt create mode 100644 bmatching/ilp/CMakeLists.txt create mode 100644 bmatching/ils/CMakeLists.txt create mode 100644 bmatching/reductions_sorted/CMakeLists.txt create mode 100644 bmatching/scip/CMakeLists.txt create mode 100644 cmake/FindGurobi.cmake create mode 100644 ds/CMakeLists.txt create mode 100644 io/CMakeLists.txt create mode 100644 runner/CMakeLists.txt create mode 100644 third_party/CMakeLists.txt create mode 100644 tools/CMakeLists.txt create mode 100644 utils/CMakeLists.txt diff --git a/.gitignore b/.gitignore index be8e260..36677fd 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ /bazel-* __pycache__ -.spack-env \ No newline at end of file +.spack-env +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e037367 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,285 @@ +cmake_minimum_required(VERSION 3.20) +project(HeiHGM_BMatching LANGUAGES CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ------------------------------------------------------------------------------ +# Options +# ------------------------------------------------------------------------------ +option(BMATCHING_USE_GUROBI "Enable Gurobi ILP solver" OFF) +option(BMATCHING_USE_SCIP "Enable SCIP solver" OFF) +option(BMATCHING_USE_BSUITOR "Enable bSuitor algorithm" OFF) +option(BMATCHING_USE_KARP_SIPSER "Enable Karp-Sipser algorithm" OFF) +option(BMATCHING_USE_HASHING "Enable hashing support (wide-integer)" OFF) +option(BMATCHING_USE_TCMALLOC "Use tcmalloc from gperftools" OFF) +option(BMATCHING_ENABLE_LOGGING "Enable easylogging++ logging output" ON) +option(BMATCHING_FREE_MEMORY_CHECK "Enable free memory checking in runner" OFF) +option(BUILD_TESTING "Build tests" ON) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# ------------------------------------------------------------------------------ +# FetchContent – external dependencies +# ------------------------------------------------------------------------------ +include(FetchContent) + +# Abseil +set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE) +set(ABSL_BUILD_TESTING OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + absl + GIT_REPOSITORY https://github.com/abseil/abseil-cpp + GIT_TAG b971ac5250ea8de900eae9f95e06548d14cd95fe + GIT_SHALLOW FALSE +) + +# Protobuf +set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_EXPORT OFF CACHE BOOL "" FORCE) +set(protobuf_INSTALL OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + protobuf + GIT_REPOSITORY https://github.com/protocolbuffers/protobuf + GIT_TAG v3.20.3 + SOURCE_SUBDIR cmake +) + +# GoogleTest +if(BUILD_TESTING) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/58d77fa8070e8cec2dc1ed015d66b454c8d78850.zip + ) +endif() + +# Easylogging++ +FetchContent_Declare( + easyloggingpp + GIT_REPOSITORY https://github.com/abumq/easyloggingpp + GIT_TAG 3bbb9a563858915b9624485356fcc7e0fdb4d68f +) + +# Wide integer (header-only) +FetchContent_Declare( + wide_integer + GIT_REPOSITORY https://github.com/ckormanyos/wide-integer + GIT_TAG d4ab8f42402d26bade2901fcc83c58a2bb9c5037 +) + +# Optional: pothen_b_matching (bSuitor) +if(BMATCHING_USE_BSUITOR) + FetchContent_Declare( + pothen_b_matching + GIT_REPOSITORY https://github.com/ECP-ExaGraph/bMatching + GIT_TAG fee845cf7949a4947197cc93487634276bf69925 + ) +endif() + +# Optional: ucar_matching (Karp-Sipser) +if(BMATCHING_USE_KARP_SIPSER) + FetchContent_Declare( + ucar_matching + GIT_REPOSITORY https://gitlab.inria.fr/bora-ucar/karp-sipser-for-hypergraphs.git + GIT_TAG 215f6887d7a3ec843ed70f3e9ea92a65e91a284a + ) +endif() + +# Fetch all declared dependencies +FetchContent_MakeAvailable(absl protobuf easyloggingpp wide_integer) + +if(BUILD_TESTING) + FetchContent_MakeAvailable(googletest) + enable_testing() +endif() + +if(BMATCHING_USE_BSUITOR) + FetchContent_Populate(pothen_b_matching) +endif() + +if(BMATCHING_USE_KARP_SIPSER) + FetchContent_Populate(ucar_matching) +endif() + +# ------------------------------------------------------------------------------ +# Easylogging++ library target +# ------------------------------------------------------------------------------ +add_library(easyloggingpp_lib + ${easyloggingpp_SOURCE_DIR}/src/easylogging++.cc +) +target_include_directories(easyloggingpp_lib PUBLIC ${easyloggingpp_SOURCE_DIR}/src) +target_compile_definitions(easyloggingpp_lib PUBLIC + ELPP_FEATURE_PERFORMANCE_TRACKING + ELPP_NO_LOG_TO_FILE +) +if(NOT BMATCHING_ENABLE_LOGGING) + target_compile_definitions(easyloggingpp_lib PUBLIC + ELPP_DISABLE_LOGS + ELPP_DISABLE_PERFORMANCE_TRACKING + ) +endif() + +# ------------------------------------------------------------------------------ +# Wide integer interface library +# ------------------------------------------------------------------------------ +add_library(wide_integer_lib INTERFACE) +target_include_directories(wide_integer_lib INTERFACE ${wide_integer_SOURCE_DIR}) + +# ------------------------------------------------------------------------------ +# Optional: pothen_b_matching library +# ------------------------------------------------------------------------------ +if(BMATCHING_USE_BSUITOR) + add_library(pothen_bmatching_lib + ${pothen_b_matching_SOURCE_DIR}/bSuitor.cpp + ${pothen_b_matching_SOURCE_DIR}/bSuitorD.cpp + ${pothen_b_matching_SOURCE_DIR}/mtxReader.cpp + ) + target_include_directories(pothen_bmatching_lib PUBLIC ${pothen_b_matching_SOURCE_DIR}) + target_compile_options(pothen_bmatching_lib PRIVATE -fopenmp -O3) + set_target_properties(pothen_bmatching_lib PROPERTIES CXX_STANDARD 11) +endif() + +# ------------------------------------------------------------------------------ +# Optional: ucar_matching libraries +# ------------------------------------------------------------------------------ +if(BMATCHING_USE_KARP_SIPSER) + add_library(ucar_kss_lib ${ucar_matching_SOURCE_DIR}/kss/kss_utils.c) + target_include_directories(ucar_kss_lib PUBLIC ${ucar_matching_SOURCE_DIR}) + + add_library(ucar_ksmd_lib ${ucar_matching_SOURCE_DIR}/ksmd/ksmd_utils.c) + target_include_directories(ucar_ksmd_lib PUBLIC ${ucar_matching_SOURCE_DIR}) +endif() + +# ------------------------------------------------------------------------------ +# Optional: Gurobi +# ------------------------------------------------------------------------------ +if(BMATCHING_USE_GUROBI) + find_package(Gurobi REQUIRED) +endif() + +# ------------------------------------------------------------------------------ +# Optional: SCIP +# ------------------------------------------------------------------------------ +if(BMATCHING_USE_SCIP) + find_package(SCIP REQUIRED) +endif() + +# ------------------------------------------------------------------------------ +# Optional: tcmalloc +# ------------------------------------------------------------------------------ +if(BMATCHING_USE_TCMALLOC) + find_package(Gperftools REQUIRED) +endif() + +# ------------------------------------------------------------------------------ +# Generate build-info.h +# ------------------------------------------------------------------------------ +find_package(Git QUIET) +if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} tag -l --points-at HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_TAG + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --always --dirty + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_DESCRIBE + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + # Use tag if available, otherwise commit hash + if(GIT_TAG) + # Take only the first line if multiple tags + string(REGEX REPLACE "\n.*" "" GIT_TAG "${GIT_TAG}") + set(STABLE_VERSION "${GIT_TAG}") + else() + set(STABLE_VERSION "${GIT_COMMIT}") + endif() + set(STABLE_SCM_DESCRIBE "${GIT_DESCRIBE}") +else() + set(STABLE_VERSION "unknown") + set(STABLE_SCM_DESCRIBE "unknown") +endif() + +if(BMATCHING_USE_TCMALLOC) + set(MALLOC_IMPL "tc-malloc") +else() + set(MALLOC_IMPL "default") +endif() + +file(WRITE ${CMAKE_BINARY_DIR}/build-info.h + "#define STABLE_VERSION \"${STABLE_VERSION}\"\n" + "#define STABLE_SCM_DESCRIBE \"${STABLE_SCM_DESCRIBE}\"\n" + "#define MALLOC_IMPLEMENTATION \"${MALLOC_IMPL}\"\n" +) + +# ------------------------------------------------------------------------------ +# Generate hashing-config.h +# ------------------------------------------------------------------------------ +if(BMATCHING_USE_HASHING) + file(WRITE ${CMAKE_BINARY_DIR}/ds/hashing-config.h "#define HASHING_ACTIVATED 1\n") +else() + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/ds) + file(WRITE ${CMAKE_BINARY_DIR}/ds/hashing-config.h "#undef HASHING_ACTIVATED\n") +endif() + +# ------------------------------------------------------------------------------ +# Compile app_io.proto +# ------------------------------------------------------------------------------ +set(PROTO_SRC ${CMAKE_SOURCE_DIR}/app/app_io.proto) +set(PROTO_GEN_DIR ${CMAKE_BINARY_DIR}) + +file(MAKE_DIRECTORY ${PROTO_GEN_DIR}/app) + +add_custom_command( + OUTPUT + ${PROTO_GEN_DIR}/app/app_io.pb.cc + ${PROTO_GEN_DIR}/app/app_io.pb.h + COMMAND $ + --proto_path=${CMAKE_SOURCE_DIR} + --proto_path=${protobuf_SOURCE_DIR}/src + --cpp_out=${PROTO_GEN_DIR} + ${PROTO_SRC} + DEPENDS ${PROTO_SRC} protoc + COMMENT "Generating C++ protobuf sources for app_io.proto" +) + +add_library(app_io_cc_proto + ${PROTO_GEN_DIR}/app/app_io.pb.cc +) +target_include_directories(app_io_cc_proto PUBLIC ${PROTO_GEN_DIR}) +target_link_libraries(app_io_cc_proto PUBLIC protobuf::libprotobuf) + +# ------------------------------------------------------------------------------ +# Global include directories +# The source tree uses includes relative to the project root, e.g. +# #include "ds/bmatching.h" +# #include "app/app_io.pb.h" +# #include "build-info.h" +# ------------------------------------------------------------------------------ +include_directories(${CMAKE_SOURCE_DIR}) +include_directories(${CMAKE_BINARY_DIR}) + +# ------------------------------------------------------------------------------ +# Subdirectories +# ------------------------------------------------------------------------------ +add_subdirectory(utils) +add_subdirectory(ds) +add_subdirectory(io) +add_subdirectory(bmatching) +add_subdirectory(third_party) +add_subdirectory(app) +add_subdirectory(runner) +add_subdirectory(tools) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..0d52a09 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,21 @@ +# app/CMakeLists.txt + +add_subdirectory(algorithms) + +add_executable(app app.cc) +target_link_libraries(app PRIVATE + app_io_cc_proto + algorithm_impl_lib + greedy + bmatching_lib + hgr_reader + random_lib + absl::flags + absl::flags_parse + absl::statusor + easyloggingpp_lib +) + +if(BMATCHING_USE_TCMALLOC) + target_link_libraries(app PRIVATE tcmalloc) +endif() diff --git a/app/algorithms/CMakeLists.txt b/app/algorithms/CMakeLists.txt new file mode 100644 index 0000000..99f3810 --- /dev/null +++ b/app/algorithms/CMakeLists.txt @@ -0,0 +1,61 @@ +# app/algorithms/CMakeLists.txt + +set(ALGORITHM_IMPL_SRCS + algorithm_impl.cc + greedy.cc + ilp.cc + ilp_exact.cc + ils.cc + presolved_ilp.cc + reductions.cc + scip.cc + unfold.cc +) + +set(ALGORITHM_IMPL_HDRS + algorithm_impl.h + greedy.h + ilp.h + ilp_exact.h + ils.h + presolved_ilp.h + reductions.h + scip.h + unfold.h +) + +if(BMATCHING_USE_KARP_SIPSER) + list(APPEND ALGORITHM_IMPL_SRCS kss.cc) + list(APPEND ALGORITHM_IMPL_HDRS kss.h) +endif() + +if(BMATCHING_USE_BSUITOR) + list(APPEND ALGORITHM_IMPL_SRCS bsuitor.cc) + list(APPEND ALGORITHM_IMPL_HDRS bsuitor.h) +endif() + +add_library(algorithm_impl_lib ${ALGORITHM_IMPL_SRCS} ${ALGORITHM_IMPL_HDRS}) +target_link_libraries(algorithm_impl_lib PUBLIC + app_io_cc_proto + greedy + ilp + ils + reductions_sorted + bmatching_scip + bmatching_lib + graph_reader + hgr_reader + systeminfo + absl::flat_hash_set + absl::statusor +) + +if(BMATCHING_USE_KARP_SIPSER) + target_link_libraries(algorithm_impl_lib PUBLIC ucar_kss_lib ucar_ksmd_lib) +endif() + +if(BMATCHING_USE_BSUITOR) + target_compile_options(algorithm_impl_lib PRIVATE -fopenmp) + target_link_options(algorithm_impl_lib PUBLIC -fopenmp) + target_link_libraries(algorithm_impl_lib PUBLIC pothen_bmatching_lib) +endif() diff --git a/bmatching/CMakeLists.txt b/bmatching/CMakeLists.txt new file mode 100644 index 0000000..e3f9e5f --- /dev/null +++ b/bmatching/CMakeLists.txt @@ -0,0 +1,7 @@ +# bmatching/CMakeLists.txt + +add_subdirectory(greedy) +add_subdirectory(ilp) +add_subdirectory(ils) +add_subdirectory(reductions_sorted) +add_subdirectory(scip) diff --git a/bmatching/greedy/CMakeLists.txt b/bmatching/greedy/CMakeLists.txt new file mode 100644 index 0000000..80f1a60 --- /dev/null +++ b/bmatching/greedy/CMakeLists.txt @@ -0,0 +1,11 @@ +# bmatching/greedy/CMakeLists.txt + +add_library(greedy INTERFACE) +target_include_directories(greedy INTERFACE ${CMAKE_SOURCE_DIR}) +target_link_libraries(greedy INTERFACE + bmatching_lib + indexed_priority_queue + modifiable_hypergraph_lib + random_lib + range_lib +) diff --git a/bmatching/ilp/CMakeLists.txt b/bmatching/ilp/CMakeLists.txt new file mode 100644 index 0000000..9e49300 --- /dev/null +++ b/bmatching/ilp/CMakeLists.txt @@ -0,0 +1,16 @@ +# bmatching/ilp/CMakeLists.txt + +add_library(ilp INTERFACE) +target_include_directories(ilp INTERFACE ${CMAKE_SOURCE_DIR}) +target_link_libraries(ilp INTERFACE + bmatching_lib + indexed_priority_queue + modifiable_hypergraph_lib + random_lib + range_lib +) + +if(BMATCHING_USE_GUROBI) + target_compile_definitions(ilp INTERFACE USE_GUROBI_ENABLED) + target_link_libraries(ilp INTERFACE Gurobi::Gurobi) +endif() diff --git a/bmatching/ils/CMakeLists.txt b/bmatching/ils/CMakeLists.txt new file mode 100644 index 0000000..a94232a --- /dev/null +++ b/bmatching/ils/CMakeLists.txt @@ -0,0 +1,12 @@ +# bmatching/ils/CMakeLists.txt + +add_library(ils INTERFACE) +target_include_directories(ils INTERFACE ${CMAKE_SOURCE_DIR}) +target_link_libraries(ils INTERFACE + bmatching_lib + indexed_priority_queue + modifiable_hypergraph_lib + random_lib + range_lib + absl::flat_hash_map +) diff --git a/bmatching/reductions_sorted/CMakeLists.txt b/bmatching/reductions_sorted/CMakeLists.txt new file mode 100644 index 0000000..69c885f --- /dev/null +++ b/bmatching/reductions_sorted/CMakeLists.txt @@ -0,0 +1,12 @@ +# bmatching/reductions_sorted/CMakeLists.txt + +add_library(reductions_sorted INTERFACE) +target_include_directories(reductions_sorted INTERFACE ${CMAKE_SOURCE_DIR}) +target_link_libraries(reductions_sorted INTERFACE + bmatching_lib + indexed_priority_queue + modifiable_hypergraph_lib + random_lib + range_lib + easyloggingpp_lib +) diff --git a/bmatching/scip/CMakeLists.txt b/bmatching/scip/CMakeLists.txt new file mode 100644 index 0000000..10d4351 --- /dev/null +++ b/bmatching/scip/CMakeLists.txt @@ -0,0 +1,15 @@ +# bmatching/scip/CMakeLists.txt + +add_library(bmatching_scip INTERFACE) +target_include_directories(bmatching_scip INTERFACE ${CMAKE_SOURCE_DIR}) +target_link_libraries(bmatching_scip INTERFACE + bmatching_lib + modifiable_hypergraph_lib + random_lib + range_lib +) + +if(BMATCHING_USE_SCIP) + target_compile_definitions(bmatching_scip INTERFACE HEIPHYPBMATCH_USE_SCIP) + target_link_libraries(bmatching_scip INTERFACE SCIP::SCIP) +endif() diff --git a/cmake/FindGurobi.cmake b/cmake/FindGurobi.cmake new file mode 100644 index 0000000..8b7f23e --- /dev/null +++ b/cmake/FindGurobi.cmake @@ -0,0 +1,48 @@ +# FindGurobi.cmake +# Finds the Gurobi optimizer library. +# +# Sets: +# Gurobi_FOUND +# Gurobi::Gurobi (imported target) +# +# Uses GUROBI_HOME environment variable or CMake variable as hint. + +if(NOT GUROBI_HOME) + set(GUROBI_HOME "$ENV{GUROBI_HOME}") +endif() + +if(NOT GUROBI_HOME) + set(GUROBI_HOME "/opt/gurobi951/linux64") +endif() + +find_path(GUROBI_INCLUDE_DIR + NAMES gurobi_c.h + HINTS ${GUROBI_HOME}/include +) + +# Search for versioned Gurobi C library (gurobi100, gurobi95, etc.) +file(GLOB _gurobi_c_libs "${GUROBI_HOME}/lib/libgurobi*.so") +# Filter out C++ wrapper +list(FILTER _gurobi_c_libs EXCLUDE REGEX "gurobi_c\\+\\+") + +if(_gurobi_c_libs) + list(GET _gurobi_c_libs 0 GUROBI_C_LIBRARY) +endif() + +find_library(GUROBI_CXX_LIBRARY + NAMES gurobi_c++ + HINTS ${GUROBI_HOME}/lib +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Gurobi + REQUIRED_VARS GUROBI_INCLUDE_DIR GUROBI_C_LIBRARY GUROBI_CXX_LIBRARY +) + +if(Gurobi_FOUND AND NOT TARGET Gurobi::Gurobi) + add_library(Gurobi::Gurobi INTERFACE IMPORTED) + set_target_properties(Gurobi::Gurobi PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${GUROBI_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${GUROBI_CXX_LIBRARY};${GUROBI_C_LIBRARY}" + ) +endif() diff --git a/ds/CMakeLists.txt b/ds/CMakeLists.txt new file mode 100644 index 0000000..c1bc077 --- /dev/null +++ b/ds/CMakeLists.txt @@ -0,0 +1,29 @@ +# ds/CMakeLists.txt + +add_library(modifiable_hypergraph_lib modifiable_hypergraph.cc) +target_link_libraries(modifiable_hypergraph_lib PUBLIC range_lib) + +add_library(bmatching_lib bmatching.cc) +target_link_libraries(bmatching_lib PUBLIC + modifiable_hypergraph_lib + random_lib + range_lib + easyloggingpp_lib + wide_integer_lib +) + +add_library(indexed_priority_queue INTERFACE) +target_include_directories(indexed_priority_queue INTERFACE ${CMAKE_SOURCE_DIR}) + +if(BUILD_TESTING) + add_executable(modifiable_hypergraph_test modifiable_hypergraph_test.cc) + target_link_libraries(modifiable_hypergraph_test PRIVATE + bmatching_lib + modifiable_hypergraph_lib + reductions_sorted + hgr_reader + GTest::gtest_main + ) + include(GoogleTest) + gtest_discover_tests(modifiable_hypergraph_test) +endif() diff --git a/io/CMakeLists.txt b/io/CMakeLists.txt new file mode 100644 index 0000000..c5bf229 --- /dev/null +++ b/io/CMakeLists.txt @@ -0,0 +1,64 @@ +# io/CMakeLists.txt + +add_library(hgr_reader hgr_reader.cc) +target_link_libraries(hgr_reader PUBLIC + modifiable_hypergraph_lib + absl::statusor + absl::strings + easyloggingpp_lib +) + +add_library(graph_reader INTERFACE) +target_include_directories(graph_reader INTERFACE ${CMAKE_SOURCE_DIR}) +target_link_libraries(graph_reader INTERFACE + modifiable_hypergraph_lib + absl::statusor + absl::strings + easyloggingpp_lib +) + +add_library(mtx_reader INTERFACE) +target_include_directories(mtx_reader INTERFACE ${CMAKE_SOURCE_DIR}) +target_link_libraries(mtx_reader INTERFACE + modifiable_hypergraph_lib + absl::strings +) + +add_library(hgr_writer hgr_reader.cc) +target_link_libraries(hgr_writer PUBLIC + modifiable_hypergraph_lib + absl::strings +) + +# --- Binaries --- + +add_executable(mtx_to_unihypergraph mtx_to_unihypergraph.cc) +target_link_libraries(mtx_to_unihypergraph PRIVATE hgr_writer mtx_reader) + +add_executable(hgr_validator hgr_validator.cc) +target_link_libraries(hgr_validator PRIVATE hgr_reader hgr_writer hypergraph_storage_system) + +add_executable(write_mtx write_mtx.cc) +target_link_libraries(write_mtx PRIVATE hgr_reader hgr_writer) + +add_executable(hgr_without_weights_to_weighted_and_mtx hgr_without_weights_to_weighted_and_mtx.cc) +target_link_libraries(hgr_without_weights_to_weighted_and_mtx PRIVATE + hgr_reader hgr_writer app_io_cc_proto +) + +add_executable(hgr_import import.cc) +target_link_libraries(hgr_import PRIVATE + hgr_reader hgr_writer app_io_cc_proto hypergraph_storage_system + absl::flags absl::flags_parse absl::statusor +) + +# --- Tests --- + +if(BUILD_TESTING) + add_executable(hgr_reader_test hgr_reader_test.cc) + target_link_libraries(hgr_reader_test PRIVATE + hgr_reader modifiable_hypergraph_lib GTest::gtest_main + ) + include(GoogleTest) + gtest_discover_tests(hgr_reader_test) +endif() diff --git a/runner/CMakeLists.txt b/runner/CMakeLists.txt new file mode 100644 index 0000000..45d1eeb --- /dev/null +++ b/runner/CMakeLists.txt @@ -0,0 +1,60 @@ +# runner/CMakeLists.txt + +add_library(hypergraph_storage_system + hypergraph_storage_system.cc +) +target_link_libraries(hypergraph_storage_system PUBLIC + app_io_cc_proto + absl::strings +) + +# --- runner binary --- +# runner.cc unconditionally includes ncurses.h, so always find and link it +find_package(Curses REQUIRED) + +add_executable(runner runner.cc) +target_link_libraries(runner PRIVATE + app_io_cc_proto + easyloggingpp_lib + absl::flags + absl::flags_parse + absl::statusor + algorithm_impl_lib + external_graph_bmatching + ${CURSES_LIBRARIES} +) +target_include_directories(runner PRIVATE ${CURSES_INCLUDE_DIRS}) + +if(BMATCHING_FREE_MEMORY_CHECK) + target_compile_definitions(runner PRIVATE HEIHYPBMATCH_MEMORY__REQ_CHECK) +endif() + +if(BMATCHING_USE_TCMALLOC) + target_link_libraries(runner PRIVATE tcmalloc) +endif() + +# --- fork_runner binary --- +add_executable(fork_runner fork_runner.cc) +target_link_libraries(fork_runner PRIVATE + app_io_cc_proto + easyloggingpp_lib + absl::flags + absl::flags_parse + absl::statusor + algorithm_impl_lib + external_graph_bmatching +) + +if(BMATCHING_USE_TCMALLOC) + target_link_libraries(fork_runner PRIVATE tcmalloc) +endif() + +# --- generate_experiment_config binary --- +add_executable(generate_experiment_config generate_experiment_config.cc) +target_link_libraries(generate_experiment_config PRIVATE + hypergraph_storage_system + app_io_cc_proto + absl::flags + absl::flags_parse + absl::statusor +) diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 0000000..218f1a9 --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,45 @@ +# third_party/CMakeLists.txt + +# --- Optional: Karp-Sipser --- +if(BMATCHING_USE_KARP_SIPSER) + add_library(karp_sipser karp_sipser.cc) + target_link_libraries(karp_sipser PUBLIC + app_io_cc_proto + systeminfo + absl::statusor + ucar_kss_lib + ucar_ksmd_lib + ) +endif() + +# --- Optional: bSuitor --- +if(BMATCHING_USE_BSUITOR) + add_library(bsuitor_lib bsuitor.cc) + target_link_libraries(bsuitor_lib PUBLIC + app_io_cc_proto + systeminfo + pothen_bmatching_lib + ) + target_compile_options(bsuitor_lib PRIVATE -fopenmp) + target_link_options(bsuitor_lib PUBLIC -fopenmp) +endif() + +# --- external_graph_bmatching --- +# This is a header-only library with conditional compilation. +# The Bazel build marks it alwayslink=1, so we make it an OBJECT library +# to ensure it is always linked. +add_library(external_graph_bmatching INTERFACE) +target_link_libraries(external_graph_bmatching INTERFACE + app_io_cc_proto + absl::statusor +) + +if(BMATCHING_USE_BSUITOR) + target_compile_definitions(external_graph_bmatching INTERFACE HEIHYPBMATCH_USE_BSUITOR) + target_link_libraries(external_graph_bmatching INTERFACE bsuitor_lib) +endif() + +if(BMATCHING_USE_KARP_SIPSER) + target_compile_definitions(external_graph_bmatching INTERFACE HEIHYPBMATCH_USE_KARP_SIPSER) + target_link_libraries(external_graph_bmatching INTERFACE karp_sipser) +endif() diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000..e1d7aa1 --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,8 @@ +# tools/CMakeLists.txt + +add_executable(binary_to_textproto binary_to_textproto.cc) +target_link_libraries(binary_to_textproto PRIVATE + app_io_cc_proto + hypergraph_storage_system + absl::strings +) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 0000000..d2ba35b --- /dev/null +++ b/utils/CMakeLists.txt @@ -0,0 +1,10 @@ +# utils/CMakeLists.txt + +add_library(random_lib INTERFACE) +target_include_directories(random_lib INTERFACE ${CMAKE_SOURCE_DIR}) + +add_library(range_lib INTERFACE) +target_include_directories(range_lib INTERFACE ${CMAKE_SOURCE_DIR}) + +add_library(systeminfo systeminfo.cc) +target_include_directories(systeminfo PUBLIC ${CMAKE_SOURCE_DIR}) From a1c15cefc806a4aea5e7eee0decd7046831151b3 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 08:42:02 +0000 Subject: [PATCH 02/18] Add compile.sh for CMake build --- compile.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 compile.sh diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..74f85fa --- /dev/null +++ b/compile.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +BUILD_DIR="build" +BUILD_TYPE="${1:-Release}" + +cmake -B "$BUILD_DIR" \ + -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ + -DBUILD_TESTING=OFF + +cmake --build "$BUILD_DIR" -j"$(nproc)" \ + --target app runner fork_runner generate_experiment_config binary_to_textproto From e114564905f1f2b20e8f640e5f7a5f751fae5886 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 08:45:12 +0000 Subject: [PATCH 03/18] Rewrite README for clarity and CMake instructions --- README.md | 391 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 237 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 08af21d..b310b4d 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,236 @@ # HeiHGM::BMatching -HeiHGM::BMatching is a program to solve bmatching in hypergraphs using reductions, integer linear programs and local search techniques. -It is the software for the paper: +A solver for b-matching problems in hypergraphs using reductions, integer linear programs, and local search techniques. -**Engineering Hypergraph $b$-Matching Algorithms** +This is the software for the paper: -by Ernestine Großmann, Felix Joos, Henrik Reinstädtler and Christian Schulz +> **Engineering Hypergraph b-Matching Algorithms** +> by Ernestine Großmann, Felix Joos, Henrik Reinstädtler and Christian Schulz +Source code: https://github.com/HeiHGM/Bmatching -The latest software can be found at https://github.com/HeiHGM/Bmatching +--- -## Available Algorithms +## Quick Start -### `greedy` +```sh +# Clone +git clone https://github.com/HeiHGM/Bmatching.git +cd Bmatching + +# Build (CMake) +./compile.sh + +# Run a single computation +./build/app/app --command_textproto ' + command: "run" + hypergraph { file_path: "path/to/graph.hgr" format: "hgr" } + config { + algorithm_configs { algorithm_name: "greedy" + string_params { key: "ordering_method" value: "bmindegree_dynamic" } + } + capacity: 1 + short_name: "greedy_test" + }' +``` -Greedly adding edges to a b matching. `ordering_method` can have the following values: +--- -- `default_order` Iterates over all edges in order of their index once and adds all possible edges that are possible. Prior versions used the order in the bmatching data structure, that is dependening on prior operations due to the nature of the bmatching structure. -- `bmaximize` Maximizes the matching using the three compartement data structure. Greedly addes the first free edge in the data structure to the matching. -- `bweight` Sorts the edges by their weight and tries to add them in descending order. -- `bratio_static` Scales the edge weight by the capacities of the vertices of an edge, sorts them and add them in descending order. -- `bmult_static` Scales the edge by the minimal capacity of the vertices of an edge, sorts them and add them in descending order. -- `bratio_dynamic` Scales the edge weight by the residual capacity at each vertex of an edge. Recalculates after each addition to the matching. -- `bmindegree_dynamic` Scales the edge weight by the residual capacity and divide by the degree of a vertex in the edge. Recalculates the residual capacity after each addition. -- `bmindegree1_dynamic` Uses the product of 1/degree of vertex as ordering method. +## Building -### `ilp_exact` +HeiHGM::BMatching supports two build systems: **CMake** (recommended) and **Bazel**. -Exactly solves a bmatching using gurobi with a `timeout` of seconds. You need to build with `--define gurobi=enabled` to use this. +### CMake (recommended) -`timeout` needs to be specified in `double_params`. +**Prerequisites:** CMake >= 3.20, a C++17 compiler, and ncurses dev headers. -### `ils` +```sh +./compile.sh # Release build (default) +./compile.sh Debug # Debug build +``` -Using an a priori solution the iterated local search searches for edge pairs that can be swapped into the solution. Afterwards it perturbs the solution to escape local optima and starts searching again. -You have to specify a `timeout`in `double_params`. +Or manually: -### `local_improvement` +```sh +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j$(nproc) +``` -Locally improve solution quality by ILP via Gurobi. You need to specify `iters` and `distance` (`int64_params`) and a `timeout` (`double_params`). Distance is the number of edges that will be taken from the graph to be improved. `iters` controls how many iterations of local improvement will be done. `timeout` specifies a timeout on the iterations. +All external dependencies (Abseil, Protobuf, GoogleTest, Easylogging++, wide-integer) are fetched automatically via CMake's FetchContent. -### `presolved_ilp` +#### CMake Options -Solves the remainder of a graph (e.g. after applying reductions) exactly using ILP via Gurobi. You have to specify a `timeout` (`double_params`). You need to build with `--define gurobi=enabled` to use this. +| Option | Default | Description | +|--------|---------|-------------| +| `BMATCHING_USE_GUROBI` | OFF | Enable Gurobi ILP solver (requires local install, set `GUROBI_HOME`) | +| `BMATCHING_USE_SCIP` | OFF | Enable SCIP solver | +| `BMATCHING_USE_BSUITOR` | OFF | Enable bSuitor algorithm (fetched via git) | +| `BMATCHING_USE_KARP_SIPSER` | OFF | Enable Karp-Sipser algorithm (fetched via git) | +| `BMATCHING_USE_HASHING` | OFF | Enable edge hashing for Weighted Domination reduction | +| `BMATCHING_USE_TCMALLOC` | OFF | Use tcmalloc from gperftools | +| `BMATCHING_ENABLE_LOGGING` | ON | Enable easylogging++ log output | +| `BMATCHING_FREE_MEMORY_CHECK` | OFF | Enable free memory checking in runner | +| `BUILD_TESTING` | ON | Build unit tests | -### `reductions` & `unfold` +Example with Gurobi and bSuitor enabled: -Reduces the graph with our reductions for b matching problem. At the end you should call `unfold` to obtain the unfolded solution. +```sh +cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBMATCHING_USE_GUROBI=ON \ + -DBMATCHING_USE_BSUITOR=ON +cmake --build build -j$(nproc) +``` -Optional setting (disable_hint: "true") to not set the hint to following steps that the solution is exact. +#### Built Binaries (CMake) -Optional setting (assume_sorted: "true") to use implementations that assume the edges/nodes to be sorted, so that certain operations get a better complexity. +| Binary | Path | Description | +|--------|------|-------------| +| `app` | `build/app/app` | One-off computations | +| `runner` | `build/runner/runner` | Parallel experiment runner | +| `fork_runner` | `build/runner/fork_runner` | Fork-based experiment runner | +| `generate_experiment_config` | `build/runner/generate_experiment_config` | Experiment config generator | +| `binary_to_textproto` | `build/tools/binary_to_textproto` | Convert binary proto to text | -### `scip` +### Bazel + +**Prerequisites:** [Bazel](https://bazel.build), clang compiler suite. + +```sh +bazel build -c opt //app +bazel build -c opt //runner +``` -Solves the remainder of a graph (e.g. after applying reductions) exactly using ILP via SCIP. You have to specify a `timeout` (`double_params`). +Bazel feature flags are passed via `--define`: -## External Algorithms +```sh +bazel build -c opt --define gurobi=enabled //app +bazel build -c opt --define hashing=enabled --define tcmalloc=gperftools //runner +bazel build -c opt --define bsuitor=enabled --define karp_sipser=enabled //runner +``` + +#### Platform Notes (Bazel) -In order to compare results more accurately and have a unified workflow for storing results, we are automatically building and linking `bSuitor` by Khan et al. and `kss`/ `ksmd` by Dufosse et al. into our `runner` target. The integration can be found in app/algorithms/ +- **macOS**: uncomment `--cxxopt=-stdlib=libc++` in `.bazelrc` if Gurobi was compiled with a different stdlib. Comment out `--cxxopt=-frecord-gcc-switches`. +- **macOS + OpenMP**: install `open-mpi` and `llvm` via brew, then prefix commands with `BAZEL_USE_CPP_ONLY_TOOLCHAIN=1 CC=/opt/homebrew/opt/llvm/bin/clang`. -You can enable building with bazel them with `bsuitor=enabled` and `karp_sipser=enabled`. -### bSuitor +--- -**`name`**: `bsuitor` +## Algorithms -**Authors:** Khan et al. +Algorithms are configured via textproto using `algorithm_configs` entries. Parameters are passed through `string_params`, `int64_params`, and `double_params`. See [app/app_io.proto](app/app_io.proto) for the full definition. +### `greedy` -### Karp-Sipser scaling +Greedily adds edges to a b-matching. Set the `ordering_method` string parameter: -Can only run on capacity 1. +| Ordering Method | Description | +|-----------------|-------------| +| `default_order` | Iterates over edges in index order, adds all possible edges | +| `bmaximize` | Uses a three-compartment data structure, greedily adds the first free edge | +| `bweight` | Sorts edges by weight, adds in descending order | +| `bratio_static` | Scales edge weight by vertex capacities, sorts descending | +| `bmult_static` | Scales edge by minimal vertex capacity, sorts descending | +| `bratio_dynamic` | Scales by residual capacity, recalculates after each addition | +| `bmindegree_dynamic` | Scales by residual capacity / vertex degree, recalculates dynamically | +| `bmindegree1_dynamic` | Uses product of 1/degree as ordering | -**`name`** `kss`or `ksmd` +### `ilp_exact` -**Authors:** Dufosse et al. +Exactly solves a b-matching using Gurobi. Requires `BMATCHING_USE_GUROBI=ON` (CMake) or `--define gurobi=enabled` (Bazel). +- `timeout` (double_params): solver time limit in seconds -**External options:** `kss`: KSS iterations in `scaling_iterations` int64_params. +### `ils` +Iterated local search: searches for edge-pair swaps to improve an a priori solution, then perturbs to escape local optima. -## Adding an algorithm +- `timeout` (double_params): time limit in seconds +### `local_improvement` +Locally improves solution quality by ILP via Gurobi. -1. Implement your (templated) algorithms in a `bmatching` subfolder. -2. Write an `AlgorithmImpl` in `app/algorithms`. You must implement two functions. One to execute and one to validate a config. You can read `double_params`,`int64_params` and `string_params`. Please refer to the [app/app_io.proto](app/app_io.proto#L39). -3. Give your implementation a unique AlgorithmName. -4. Register your implementation via the `REGISTER_IMPL` macro. -5. You can now write `run_configs` using your algorithm. +- `iters` (int64_params): number of improvement iterations +- `distance` (int64_params): number of edges taken from the graph per iteration +- `timeout` (double_params): time limit in seconds +### `presolved_ilp` +Solves the reduced graph exactly using ILP via Gurobi. Requires Gurobi to be enabled. -## Compiling +- `timeout` (double_params): time limit in seconds -HeiHGM::BMatching uses [bazel](https://bazel.build) as build system. After installing bazel, feel free to follow the tutorial below. Please use clang as your compiler suite (as MPI flags are configured for this compiler suite). +### `reductions` & `unfold` -We extensivly use (text)proto as format for storing configs and results. The proto definition can be found in `app/app_io.proto`. +Reduces the graph size using b-matching reductions. Call `unfold` afterwards to recover the full solution. -### Gurobi macOS support +Optional string parameters: +- `disable_hint: "true"` — do not signal to subsequent steps that the solution is exact +- `assume_sorted: "true"` — use implementations that assume sorted edges/nodes for better complexity -To use on a mac please uncomment `--cxxopt=-stdlib=libc++` in [`.bazelrc`](.bazelrc) if you manually compiled Gurobi with a different stdlib. +### `scip` -### Configurable settings +Solves the reduced graph exactly using ILP via SCIP. -You can enable gurobi with `gurobi=enabled`. +- `timeout` (double_params): time limit in seconds -You can enable edge hashing used in Weighted Domination reduction with defining `hashing=enabled` while building `//app` or `//runner` targets, e.g.: +### External Algorithms -```sh -bazel build -c opt --define hashing=enabled //runner -``` -Furthermore, you can use a different malloc implementation and use it for profiling: -```sh -bazel build -c opt --define tcmalloc=gperftools //runner -``` -Profiling then can be enabled by defining the `CPUPROFILE` env variable. +For comparison, the `runner` can link external solvers. These are fetched automatically when enabled. -Note on mac you have to comment `--cxxopt=-frecord-gcc-switches` out in .bazelrc -### openmp on mac +| Algorithm | Enable Flag (CMake) | Enable Flag (Bazel) | Authors | +|-----------|-------------------|-------------------|---------| +| **bSuitor** | `BMATCHING_USE_BSUITOR=ON` | `--define bsuitor=enabled` | Khan et al. | +| **kss** / **ksmd** | `BMATCHING_USE_KARP_SIPSER=ON` | `--define karp_sipser=enabled` | Dufosse et al. | -Please install `open-mpi` and `llvm` via brew. MPI is used by external software to be used for comparison. -Prepend commands by `BAZEL_USE_CPP_ONLY_TOOLCHAIN=1 CC=/opt/homebrew/opt/llvm/bin/clang` in order to use the different (non-apple) llvm. +Note: `kss`/`ksmd` only supports capacity 1. For `kss`, set `scaling_iterations` in `int64_params`. -## Running one-off computations (`app`) +--- -Build the `//app` target and supply a simple config via the `command_textproto` option: +## Running One-Off Computations + +Use the `app` binary with a textproto config: ```sh -bazel build -c opt //app -./bazel-bin/app/app --command_textproto 'command:"run" hypergraph { file_path: "" format: "hgr" } config { algorithm_configs{algorithm_name:"reductions" string_params{key:"disable_hint" value:"false"} string_params{key:"assume_sorted" value:"true"}} algorithm_configs{algorithm_name:"unfold" string_params{key:"assume_sorted" value:"true"}} capacity: 1 short_name: "only_reductions" }' +./build/app/app --command_textproto ' + command: "run" + hypergraph { file_path: "path/to/graph.hgr" format: "hgr" } + config { + algorithm_configs { + algorithm_name: "reductions" + string_params { key: "disable_hint" value: "false" } + string_params { key: "assume_sorted" value: "true" } + } + algorithm_configs { + algorithm_name: "unfold" + string_params { key: "assume_sorted" value: "true" } + } + capacity: 1 + short_name: "only_reductions" + }' ``` -## Running an experiment -### Getting experiment data +--- + +## Running Experiments + +### 1. Get Experiment Data -To get a collection of hypergraphs visit [Zenodo](https://doi.org/10.5281/zenodo.18225669). The storage of hypergraphs is organized as follows: +Download hypergraph collections from [Zenodo](https://doi.org/10.5281/zenodo.18225669). The expected directory layout: ``` -├── graphs -│ └── walshaw -│ ├── collection.textproto -| ├── ... -│ ├── graph1.graph -│ ├── graph1.graph.hgr -│ ├── graph1.graph.weighted.hgr -│ └── graph1.graph.weighted.hgr.mtx -├── README.md -└── storage.textproto +graphs/ + walshaw/ + collection.textproto # list of hypergraphs in this collection + graph1.graph.hgr # hMetis format + graph1.graph.mtx # Matrix Market format (for third-party solvers) +storage.textproto # repository info (can be empty) ``` -- `storage.textproto`: file containing info about the repository, can be empty. -- `collection.textproto`: per directory/collection contains the list of hypergraphs and information about the files. Definition in [app_io.proto](app/app_io.proto#L117). +Each `collection.textproto` lists hypergraphs with metadata. Example entry: -Example: - -``` +```protobuf hypergraphs { name: "wing_nodal.graph" edge_weight_type: "random(100)" @@ -167,106 +242,114 @@ hypergraphs { node_weight_type: "random(degree)" sort: "walshaw" } -hypergraphs { - name: "wing_nodal.graph" - edge_weight_type: "random(100)" - collection: "dimacs10(graph)" - file_path: "wing_nodal.graph.weighted.hgr.mtx" - node_count: 10937 - edge_count: 75488 - format: "mtx" - node_weight_type: "random(degree)" - sort: "walshaw" -} collection_name: "dimacs10(graph)" version: "1.0" ``` -- `*.hgr` graphs in hMetis format. -- `*.mtx` graphs in mtx format (symmetric) for third_party solver, mainly used for graphs. -### Setting up an experiment +### 2. Generate Experiment Config -To generate the an experiment use the `//runner:generate_experiment_config` target to generate an example. - -Generate an experiment with default order via `bmindegree_dynamic` and default capacity `5` with `16` concurrent cores. ```sh -bazel build -c opt //runner:generate_experiment_config # compile the target -mkdir # important generate the experiment_directory -./bazel-bin/runner/generate_experiment_config --data_path path/to/hypergraph-data --experiment_name "" --experiment_path="" --hypergraph_filter='format:"hgr" edge_weight_types:"random(100)" sort:"walshaw"' --run_configs='run_configs { algorithm_configs { algorithm_name: "greedy" string_params {key: "ordering_method" value: "bmindegree_dynamic"}} capacity: 5 short_name: "bmindegree_dynamic5"}' --concurrent_processes 16 +mkdir my_experiment + +./build/runner/generate_experiment_config \ + --data_path path/to/hypergraph-data \ + --experiment_name "my_experiment" \ + --experiment_path "my_experiment" \ + --hypergraph_filter 'format:"hgr" edge_weight_types:"random(100)" sort:"walshaw"' \ + --run_configs ' + run_configs { + algorithm_configs { + algorithm_name: "greedy" + string_params { key: "ordering_method" value: "bmindegree_dynamic" } + } + capacity: 5 + short_name: "bmindegree_dynamic5" + }' \ + --concurrent_processes 16 ``` +This creates `experiment.textproto` in the experiment directory. Edit it with any text editor to adjust `run_configs`. -This generates an `experiment.textproto` in the folder ``. You can modify this file with any text editor and edit the `run_configs`. - -### Running an experiment - -Build the runner in opt settings: +### 3. Run the Experiment ```sh -bazel build -c opt //runner +./build/runner/runner --experiment_path my_experiment ``` -and run - -```sh -./bazel-bin/runner/runner --experiment_path -``` +Tasks execute in parallel using the number of `concurrent_processes` configured in `experiment.textproto`. -This will execute the tasks in `concurrent_processes` parallel processes as configured in `experiment.textproto` generated in the previous step. +### 4. Analyze Results -### Analysing the results +Results are stored as `results-*-.binary_proto`. To plot: -The results are stored in `results-*-.binary_proto` in binaryproto to save some storage. -Plotting is done via the `tools/plot` tool using the visualisation proto. The following proto generates 4 plots (each for a different capacity) for -the experiment in subpath `"mm-eweight-random100_greedy"` with type `performance_profile`. +Create a `visualisation.textproto`: -``` +```protobuf visualisations { - capacity: 1 - capacity: -1 - capacity: 3 - capacity: 5 - experiment_paths: "mm-eweight-random100_greedy" - type:"performance_profile" - title: "Performance Profile on $num planted hypergraphs\n (e-weight random(100),capacity $capacity)" - folder_name:"mm-eweight-random100_greedy" - file_prefix: "performance_plot_" + capacity: 1 + capacity: -1 + capacity: 3 + capacity: 5 + experiment_paths: "mm-eweight-random100_greedy" + type: "performance_profile" + title: "Performance Profile on $num planted hypergraphs\n (e-weight random(100), capacity $capacity)" + folder_name: "mm-eweight-random100_greedy" + file_prefix: "performance_plot_" } ``` -To show this example use the `tools/plot/plot.py` by running: +Then run (Bazel only for plotting): ```sh -bazel run tools/plot -- //visualisation.textproto +bazel run tools/plot -- /visualisation.textproto ``` -The plots are stored in `/vis/`. + +Plots are saved to `/vis//`. + +--- ## Logging -By default `HeiHGM::BMatching` does not log, you can enable it by adding `--define logging=enabled`. +Logging is enabled by default (CMake: `BMATCHING_ENABLE_LOGGING=ON`, Bazel: `--define logging=enabled`). -If build with logging enabled, you can define a verbosity level by supplying to HeiHGM::BMatching `--undefok=v --v=` +To disable: set `BMATCHING_ENABLE_LOGGING=OFF` (CMake) or omit the logging define (Bazel). -Verbosity levels: +When enabled, control verbosity at runtime: + +```sh +./build/app/app --undefok=v --v= ... +``` | Level | Functions | -| ----- | ----- | -| 8 | addToMatching, removeFromMatching | +|-------|-----------| +| 8 | `addToMatching`, `removeFromMatching` | + +--- + +## Adding a New Algorithm -## Usage with `spack` +1. Implement your (templated) algorithm in a `bmatching/` subfolder. +2. Write an `AlgorithmImpl` in `app/algorithms/` — implement `Execute` and `ValidateConfig`. Read parameters from `double_params`, `int64_params`, and `string_params` (see [app/app_io.proto](app/app_io.proto)). +3. Give your implementation a unique `AlgorithmName`. +4. Register it via the `REGISTER_IMPL` macro. +5. Use your algorithm in `run_configs` by name. +--- + +## Usage with Spack + +[Spack](https://spack.io) manages system dependencies on compute clusters: -`spack` is a tool to manage software on compute clusters. -We use it to manage system dependencies. Otherwise, you should take care of linking yourself. ```sh -#install spack +# Install spack git clone --depth=100 --branch=releases/v0.20 https://github.com/spack/spack.git -cd spack -. share/spack/setup-env.sh -# change to bmatching -cd bmatching +cd spack && . share/spack/setup-env.sh + +# Activate environment in BMatching directory +cd /path/to/Bmatching spack env activate . spack install spack load -# build as used to be with spack=enabled ``` + +Then build as usual. For Bazel, add `--define spack=enabled`. From 75e77663275fa1b0a72deb39a2e04320163fdefd Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 08:50:41 +0000 Subject: [PATCH 04/18] Add bmatching_cli with flag-based interface for all algorithms Replaces verbose textproto input with clean CLI flags: --graph, --algorithms, --capacity, --ordering_method, --timeout, etc. Supports all algorithm pipelines (greedy, reductions, ils, ilp_exact, presolved_ilp, scip, local_improvement, unfold) and output formats (text, json, binary). Uses OBJECT library for algorithm_impl_lib to ensure all REGISTER_IMPL statics are linked. --- app/CMakeLists.txt | 19 +++ app/algorithms/CMakeLists.txt | 4 +- app/cli.cc | 276 ++++++++++++++++++++++++++++++++++ compile.sh | 2 +- 4 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 app/cli.cc diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0d52a09..ccd0990 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -19,3 +19,22 @@ target_link_libraries(app PRIVATE if(BMATCHING_USE_TCMALLOC) target_link_libraries(app PRIVATE tcmalloc) endif() + +add_executable(bmatching_cli cli.cc) +target_link_libraries(bmatching_cli PRIVATE + app_io_cc_proto + algorithm_impl_lib + greedy + bmatching_lib + hgr_reader + random_lib + absl::flags + absl::flags_parse + absl::flags_usage + absl::statusor + easyloggingpp_lib +) + +if(BMATCHING_USE_TCMALLOC) + target_link_libraries(bmatching_cli PRIVATE tcmalloc) +endif() diff --git a/app/algorithms/CMakeLists.txt b/app/algorithms/CMakeLists.txt index 99f3810..7bfed6e 100644 --- a/app/algorithms/CMakeLists.txt +++ b/app/algorithms/CMakeLists.txt @@ -34,7 +34,9 @@ if(BMATCHING_USE_BSUITOR) list(APPEND ALGORITHM_IMPL_HDRS bsuitor.h) endif() -add_library(algorithm_impl_lib ${ALGORITHM_IMPL_SRCS} ${ALGORITHM_IMPL_HDRS}) +# OBJECT library ensures all registration statics (REGISTER_IMPL) are linked, +# equivalent to Bazel's alwayslink=1. +add_library(algorithm_impl_lib OBJECT ${ALGORITHM_IMPL_SRCS} ${ALGORITHM_IMPL_HDRS}) target_link_libraries(algorithm_impl_lib PUBLIC app_io_cc_proto greedy diff --git a/app/cli.cc b/app/cli.cc new file mode 100644 index 0000000..1029e29 --- /dev/null +++ b/app/cli.cc @@ -0,0 +1,276 @@ +/** + * @file cli.cc + * @brief User-friendly command-line interface for HeiHGM::BMatching. + * + * Provides flag-based access to all algorithms instead of requiring textproto. + * + * Example: + * bmatching_cli --graph input.hgr --capacity 5 \ + * --algorithms reductions,greedy,unfold --ordering_method bmindegree_dynamic + */ +#include "easylogging++.h" +#include +#include +#include +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/flags/usage.h" +#include "app/algorithms/algorithm_impl.h" +#include "app/app_io.pb.h" +#include "ds/bmatching.h" +#include "io/hgr_reader.h" + +// --- Required flags --- +ABSL_FLAG(std::string, graph, "", "Path to the hypergraph file."); +ABSL_FLAG(std::string, format, "hgr", + "Input format: 'hgr' (hMetis) or 'graph'."); +ABSL_FLAG(int64_t, capacity, 1, + "Node capacity. Use -1 to use node weights from file."); +ABSL_FLAG(std::string, algorithms, "", + "Comma-separated algorithm pipeline, e.g. " + "'reductions,greedy,unfold'. Available: greedy, ils, ilp_exact, " + "local_improvement, presolved_ilp, reductions, unfold, scip."); + +// --- Greedy params --- +ABSL_FLAG(std::string, ordering_method, "bmindegree_dynamic", + "Greedy ordering method. Options: default_order, bmaximize, bweight, " + "bratio_static, bmult_static, bratio_dynamic, bmindegree_dynamic, " + "bmindegree1_dynamic, S,cap, S,min, S,pin, S,pin,cap, S,pin,min, " + "S,scaled, D,cap, D,scaled."); + +// --- Timeout (shared by ilp_exact, presolved_ilp, scip, local_improvement) --- +ABSL_FLAG(double, timeout, 60.0, "Solver timeout in seconds."); + +// --- ILS params --- +ABSL_FLAG(int64_t, max_tries, 1000, "Max iterations for ILS / local search."); +ABSL_FLAG(bool, inplace, false, "Run ILS in-place (modify matching directly)."); + +// --- Local improvement params --- +ABSL_FLAG(int64_t, iters, 10, "Number of local improvement iterations."); +ABSL_FLAG(int64_t, distance, 5, + "Number of edges per local improvement neighborhood."); +ABSL_FLAG(std::string, backend, "gurobi", + "Solver backend for local_improvement: 'gurobi' or 'scip'."); + +// --- Reductions params --- +ABSL_FLAG(bool, disable_hint, false, + "Do not hint to subsequent algorithms that the solution is exact."); +ABSL_FLAG(int64_t, max_runs, 10, + "Maximum reduction rounds."); +ABSL_FLAG(int64_t, reps, 1, "Number of reduction repetitions."); + +// --- Weight rewriting --- +ABSL_FLAG(std::string, edge_weight_type, "", + "Override edge weights: 'pins', 'uniform', or empty for file default."); +ABSL_FLAG(std::string, node_weight_type, "", + "Override node weights: 'unweighted' or empty for file default."); + +// --- Output --- +ABSL_FLAG(std::string, output, "", + "Output file path. Empty prints to stdout."); +ABSL_FLAG(std::string, output_format, "text", + "Output format: 'text' (textproto), 'json', or 'binary'."); +ABSL_FLAG(bool, quiet, false, + "Suppress output, only print weight and size."); + +INITIALIZE_EASYLOGGINGPP + +namespace { +using HeiHGM::BMatching::app::app_io::AlgorithmConfig; +using HeiHGM::BMatching::app::app_io::Command; +using HeiHGM::BMatching::app::app_io::Hypergraph; +using HeiHGM::BMatching::app::app_io::Result; +using HeiHGM::BMatching::app::app_io::RunConfig; + +std::vector split(const std::string &s, char delim) { + std::vector tokens; + std::istringstream stream(s); + std::string token; + while (std::getline(stream, token, delim)) { + // Trim whitespace + size_t start = token.find_first_not_of(" \t"); + size_t end = token.find_last_not_of(" \t"); + if (start != std::string::npos) { + tokens.push_back(token.substr(start, end - start + 1)); + } + } + return tokens; +} + +AlgorithmConfig buildConfig(const std::string &algo_name) { + AlgorithmConfig config; + config.set_algorithm_name(algo_name); + + if (algo_name == "greedy") { + (*config.mutable_string_params())["ordering_method"] = + absl::GetFlag(FLAGS_ordering_method); + } else if (algo_name == "ils") { + (*config.mutable_int64_params())["max_tries"] = + absl::GetFlag(FLAGS_max_tries); + if (absl::GetFlag(FLAGS_inplace)) { + (*config.mutable_string_params())["inplace"] = "true"; + } + } else if (algo_name == "local_improvement") { + (*config.mutable_string_params())["backend"] = + absl::GetFlag(FLAGS_backend); + (*config.mutable_int64_params())["iters"] = absl::GetFlag(FLAGS_iters); + (*config.mutable_int64_params())["distance"] = + absl::GetFlag(FLAGS_distance); + (*config.mutable_double_params())["timeout"] = + absl::GetFlag(FLAGS_timeout); + (*config.mutable_int64_params())["max_tries"] = + absl::GetFlag(FLAGS_max_tries); + } else if (algo_name == "ilp_exact" || algo_name == "presolved_ilp" || + algo_name == "scip") { + (*config.mutable_double_params())["timeout"] = + absl::GetFlag(FLAGS_timeout); + } else if (algo_name == "reductions") { + (*config.mutable_string_params())["assume_sorted"] = "true"; + if (absl::GetFlag(FLAGS_disable_hint)) { + (*config.mutable_string_params())["disable_hint"] = "true"; + } + (*config.mutable_int64_params())["max_runs"] = + absl::GetFlag(FLAGS_max_runs); + (*config.mutable_int64_params())["reps"] = absl::GetFlag(FLAGS_reps); + } else if (algo_name == "unfold") { + (*config.mutable_string_params())["assume_sorted"] = "true"; + } + + return config; +} + +void printUsage() { + std::cerr + << "Usage: bmatching_cli --graph --algorithms \n" + << "\n" + << "Examples:\n" + << " # Greedy matching\n" + << " bmatching_cli --graph input.hgr --capacity 5 \\\n" + << " --algorithms greedy --ordering_method bmindegree_dynamic\n" + << "\n" + << " # Reductions + greedy + unfold\n" + << " bmatching_cli --graph input.hgr --capacity 3 \\\n" + << " --algorithms reductions,greedy,unfold\n" + << "\n" + << " # Reductions + ILS + unfold\n" + << " bmatching_cli --graph input.hgr --capacity 1 \\\n" + << " --algorithms reductions,greedy,ils,unfold --max_tries 5000\n" + << "\n" + << " # Exact solve with reductions\n" + << " bmatching_cli --graph input.hgr --capacity 1 \\\n" + << " --algorithms reductions,presolved_ilp,unfold --timeout 300\n" + << "\n" + << "Algorithms: greedy, ils, ilp_exact, local_improvement,\n" + << " presolved_ilp, reductions, unfold, scip\n"; +} +} // namespace + +char *program_invocation_name = "bmatching_cli"; + +int main(int argc, char **argv) { + absl::SetProgramUsageMessage( + "HeiHGM::BMatching - b-matching solver for hypergraphs.\n" + " bmatching_cli --graph --algorithms "); + GOOGLE_PROTOBUF_VERIFY_VERSION; + absl::ParseCommandLine(argc, argv); + START_EASYLOGGINGPP(argc, argv); + + // Validate required flags + if (absl::GetFlag(FLAGS_graph).empty()) { + std::cerr << "Error: --graph is required.\n\n"; + printUsage(); + return 1; + } + if (absl::GetFlag(FLAGS_algorithms).empty()) { + std::cerr << "Error: --algorithms is required.\n\n"; + printUsage(); + return 1; + } + + // Build Hypergraph proto + Hypergraph hypergraph_conf; + hypergraph_conf.set_file_path(absl::GetFlag(FLAGS_graph)); + hypergraph_conf.set_format(absl::GetFlag(FLAGS_format)); + if (!absl::GetFlag(FLAGS_edge_weight_type).empty()) { + hypergraph_conf.set_edge_weight_type(absl::GetFlag(FLAGS_edge_weight_type)); + } + if (!absl::GetFlag(FLAGS_node_weight_type).empty()) { + hypergraph_conf.set_node_weight_type(absl::GetFlag(FLAGS_node_weight_type)); + } + + // Build RunConfig + RunConfig run_config; + run_config.set_capacity(absl::GetFlag(FLAGS_capacity)); + run_config.set_short_name("cli"); + + auto algo_names = split(absl::GetFlag(FLAGS_algorithms), ','); + if (algo_names.empty()) { + std::cerr << "Error: no algorithms specified.\n"; + return 1; + } + + for (const auto &name : algo_names) { + *run_config.add_algorithm_configs() = buildConfig(name); + } + + // Run + auto result = + HeiHGM::BMatching::app::algorithms::Run(hypergraph_conf, run_config, true); + + if (!result.ok()) { + std::cerr << "Error: " << result.status().message() << "\n"; + return 1; + } + + // Output + if (absl::GetFlag(FLAGS_quiet)) { + std::cout << "weight: " << result->weight() << "\n"; + std::cout << "size: " << result->size() << "\n"; + std::cout << "exact: " << (result->is_exact() ? "true" : "false") << "\n"; + auto &run_info = result->run_information(); + auto nanos = google::protobuf::util::TimeUtil::DurationToNanoseconds( + run_info.algo_duration()); + std::cout << "time_ms: " << (nanos / 1000000.0) << "\n"; + return 0; + } + + std::string output_format = absl::GetFlag(FLAGS_output_format); + std::string output_path = absl::GetFlag(FLAGS_output); + + if (output_format == "binary") { + if (output_path.empty()) { + std::cerr << "Error: --output is required for binary format.\n"; + return 1; + } + std::ofstream ofs(output_path, std::ios::binary); + result->SerializeToOstream(&ofs); + } else if (output_format == "json") { + std::string json; + google::protobuf::util::JsonPrintOptions options; + options.add_whitespace = true; + google::protobuf::util::MessageToJsonString(*result, &json, options); + if (output_path.empty()) { + std::cout << json << "\n"; + } else { + std::ofstream ofs(output_path); + ofs << json << "\n"; + } + } else { + // text (default) + std::string text; + google::protobuf::TextFormat::PrintToString(*result, &text); + if (output_path.empty()) { + std::cout << text; + } else { + std::ofstream ofs(output_path); + ofs << text; + } + } + + return 0; +} diff --git a/compile.sh b/compile.sh index 74f85fa..db2f7ff 100755 --- a/compile.sh +++ b/compile.sh @@ -9,4 +9,4 @@ cmake -B "$BUILD_DIR" \ -DBUILD_TESTING=OFF cmake --build "$BUILD_DIR" -j"$(nproc)" \ - --target app runner fork_runner generate_experiment_config binary_to_textproto + --target app bmatching_cli runner fork_runner generate_experiment_config binary_to_textproto From 31c4e11984ddb3f5765b885893dda70c248f4f91 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:07:48 +0000 Subject: [PATCH 05/18] Add build regression tests comparing Bazel and CMake output Includes compare.sh script, 3 test hypergraph instances, and report showing 45/45 tests pass across greedy (5 orderings), reductions+unfold, and reductions+greedy+unfold pipelines at capacities 1/3/5. --- build_regression/compare.sh | 118 +++++++++++++++++++++++++++ build_regression/report.txt | 54 ++++++++++++ build_regression/testdata/hyper.hgr | 7 ++ build_regression/testdata/medium.hgr | 11 +++ build_regression/testdata/small.hgr | 5 ++ 5 files changed, 195 insertions(+) create mode 100755 build_regression/compare.sh create mode 100644 build_regression/report.txt create mode 100644 build_regression/testdata/hyper.hgr create mode 100644 build_regression/testdata/medium.hgr create mode 100644 build_regression/testdata/small.hgr diff --git a/build_regression/compare.sh b/build_regression/compare.sh new file mode 100755 index 0000000..a6d70e0 --- /dev/null +++ b/build_regression/compare.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Regression test: compare Bazel and CMake CLI output on test instances. +# Run from the project root: bash build_regression/compare.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +BAZEL_APP="./bazel-bin/app/app" +CMAKE_CLI="./build/app/bmatching_cli" +TEST_DATA="$SCRIPT_DIR/testdata" + +# Verify binaries exist +for bin in "$BAZEL_APP" "$CMAKE_CLI"; do + if [ ! -x "$bin" ]; then + echo "Error: $bin not found. Build both Bazel and CMake targets first." + echo " bazel build -c opt //app" + echo " ./compile.sh" + exit 1 + fi +done + +GRAPHS=( + "$TEST_DATA/small.hgr" + "$TEST_DATA/medium.hgr" + "$TEST_DATA/hyper.hgr" +) + +# Extract weight and size from textproto output (top-level, not nested) +extract_result() { + local output="$1" + local weight size is_exact + weight=$(echo "$output" | grep -E '^weight:' | head -1 | awk '{print $2}') + size=$(echo "$output" | grep -E '^size:' | head -1 | awk '{print $2}') + is_exact=$(echo "$output" | grep -E '^is_exact:' | head -1 | awk '{print $2}') + echo "weight=$weight size=$size exact=$is_exact" +} + +PASS=0 +FAIL=0 + +run_test() { + local graph="$1" + local name="$2" + local capacity="$3" + local textproto="$4" + local cli_args="$5" + local graph_base + graph_base=$(basename "$graph") + + echo -n " [$graph_base] $name (cap=$capacity) ... " + + # Bazel run + local bazel_out + bazel_out=$($BAZEL_APP --command_textproto "$textproto" 2>/dev/null) || { + echo "BAZEL_FAIL" + FAIL=$((FAIL + 1)) + return + } + local bazel_result + bazel_result=$(extract_result "$bazel_out") + + # CMake run + local cmake_out + cmake_out=$($CMAKE_CLI $cli_args 2>/dev/null) || { + echo "CMAKE_FAIL" + FAIL=$((FAIL + 1)) + return + } + local cmake_result + cmake_result=$(extract_result "$cmake_out") + + if [ "$bazel_result" = "$cmake_result" ]; then + echo "PASS ($bazel_result)" + PASS=$((PASS + 1)) + else + echo "FAIL" + echo " Bazel: $bazel_result" + echo " CMake: $cmake_result" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== Comparing Bazel vs CMake CLI results ===" +echo "" + +for graph in "${GRAPHS[@]}"; do + echo "Graph: $graph" + + # 1. greedy with various ordering methods + for method in bweight bmindegree_dynamic bratio_dynamic bmaximize default_order; do + for cap in 1 3; do + run_test "$graph" "greedy($method)" "$cap" \ + "command:\"run\" hypergraph { file_path:\"$graph\" format:\"hgr\" } config { algorithm_configs { algorithm_name:\"greedy\" string_params{key:\"ordering_method\" value:\"$method\"} } capacity:$cap short_name:\"test\" }" \ + "--graph $graph --algorithms greedy --ordering_method $method --capacity $cap" + done + done + + # 2. reductions + unfold + for cap in 1 3; do + run_test "$graph" "reductions+unfold" "$cap" \ + "command:\"run\" hypergraph { file_path:\"$graph\" format:\"hgr\" } config { algorithm_configs { algorithm_name:\"reductions\" string_params{key:\"assume_sorted\" value:\"true\"} } algorithm_configs { algorithm_name:\"unfold\" string_params{key:\"assume_sorted\" value:\"true\"} } capacity:$cap short_name:\"test\" }" \ + "--graph $graph --algorithms reductions,unfold --capacity $cap" + done + + # 3. reductions + greedy + unfold + for cap in 1 3 5; do + run_test "$graph" "reductions+greedy+unfold" "$cap" \ + "command:\"run\" hypergraph { file_path:\"$graph\" format:\"hgr\" } config { algorithm_configs { algorithm_name:\"reductions\" string_params{key:\"assume_sorted\" value:\"true\"} } algorithm_configs { algorithm_name:\"greedy\" string_params{key:\"ordering_method\" value:\"bmindegree_dynamic\"} } algorithm_configs { algorithm_name:\"unfold\" string_params{key:\"assume_sorted\" value:\"true\"} } capacity:$cap short_name:\"test\" }" \ + "--graph $graph --algorithms reductions,greedy,unfold --ordering_method bmindegree_dynamic --capacity $cap" + done + + echo "" +done + +echo "=== Results: $PASS passed, $FAIL failed ===" +[ "$FAIL" -eq 0 ] || exit 1 diff --git a/build_regression/report.txt b/build_regression/report.txt new file mode 100644 index 0000000..e55516e --- /dev/null +++ b/build_regression/report.txt @@ -0,0 +1,54 @@ +=== Comparing Bazel vs CMake CLI results === + +Graph: /home/cschulz/projects/coding/Bmatching/build_regression/testdata/small.hgr + [small.hgr] greedy(bweight) (cap=1) ... PASS (weight=2 size=2 exact=) + [small.hgr] greedy(bweight) (cap=3) ... PASS (weight=4 size=4 exact=) + [small.hgr] greedy(bmindegree_dynamic) (cap=1) ... PASS (weight=2 size=2 exact=) + [small.hgr] greedy(bmindegree_dynamic) (cap=3) ... PASS (weight=4 size=4 exact=) + [small.hgr] greedy(bratio_dynamic) (cap=1) ... PASS (weight=2 size=2 exact=) + [small.hgr] greedy(bratio_dynamic) (cap=3) ... PASS (weight=4 size=4 exact=) + [small.hgr] greedy(bmaximize) (cap=1) ... PASS (weight=2 size=2 exact=) + [small.hgr] greedy(bmaximize) (cap=3) ... PASS (weight=4 size=4 exact=) + [small.hgr] greedy(default_order) (cap=1) ... PASS (weight=2 size=2 exact=) + [small.hgr] greedy(default_order) (cap=3) ... PASS (weight=4 size=4 exact=) + [small.hgr] reductions+unfold (cap=1) ... PASS (weight=2 size=2 exact=true) + [small.hgr] reductions+unfold (cap=3) ... PASS (weight=4 size=4 exact=true) + [small.hgr] reductions+greedy+unfold (cap=1) ... PASS (weight=2 size=2 exact=) + [small.hgr] reductions+greedy+unfold (cap=3) ... PASS (weight=4 size=4 exact=) + [small.hgr] reductions+greedy+unfold (cap=5) ... PASS (weight=4 size=4 exact=) + +Graph: /home/cschulz/projects/coding/Bmatching/build_regression/testdata/medium.hgr + [medium.hgr] greedy(bweight) (cap=1) ... PASS (weight=25 size=3 exact=) + [medium.hgr] greedy(bweight) (cap=3) ... PASS (weight=55 size=10 exact=) + [medium.hgr] greedy(bmindegree_dynamic) (cap=1) ... PASS (weight=26 size=4 exact=) + [medium.hgr] greedy(bmindegree_dynamic) (cap=3) ... PASS (weight=55 size=10 exact=) + [medium.hgr] greedy(bratio_dynamic) (cap=1) ... PASS (weight=25 size=3 exact=) + [medium.hgr] greedy(bratio_dynamic) (cap=3) ... PASS (weight=55 size=10 exact=) + [medium.hgr] greedy(bmaximize) (cap=1) ... PASS (weight=26 size=4 exact=) + [medium.hgr] greedy(bmaximize) (cap=3) ... PASS (weight=55 size=10 exact=) + [medium.hgr] greedy(default_order) (cap=1) ... PASS (weight=26 size=4 exact=) + [medium.hgr] greedy(default_order) (cap=3) ... PASS (weight=55 size=10 exact=) + [medium.hgr] reductions+unfold (cap=1) ... PASS (weight=6 size=1 exact=true) + [medium.hgr] reductions+unfold (cap=3) ... PASS (weight=55 size=10 exact=true) + [medium.hgr] reductions+greedy+unfold (cap=1) ... PASS (weight=26 size=4 exact=) + [medium.hgr] reductions+greedy+unfold (cap=3) ... PASS (weight=55 size=10 exact=) + [medium.hgr] reductions+greedy+unfold (cap=5) ... PASS (weight=55 size=10 exact=) + +Graph: /home/cschulz/projects/coding/Bmatching/build_regression/testdata/hyper.hgr + [hyper.hgr] greedy(bweight) (cap=1) ... PASS (weight=12 size=2 exact=) + [hyper.hgr] greedy(bweight) (cap=3) ... PASS (weight=22 size=6 exact=) + [hyper.hgr] greedy(bmindegree_dynamic) (cap=1) ... PASS (weight=12 size=2 exact=) + [hyper.hgr] greedy(bmindegree_dynamic) (cap=3) ... PASS (weight=22 size=6 exact=) + [hyper.hgr] greedy(bratio_dynamic) (cap=1) ... PASS (weight=12 size=2 exact=) + [hyper.hgr] greedy(bratio_dynamic) (cap=3) ... PASS (weight=22 size=6 exact=) + [hyper.hgr] greedy(bmaximize) (cap=1) ... PASS (weight=5 size=2 exact=) + [hyper.hgr] greedy(bmaximize) (cap=3) ... PASS (weight=22 size=6 exact=) + [hyper.hgr] greedy(default_order) (cap=1) ... PASS (weight=5 size=2 exact=) + [hyper.hgr] greedy(default_order) (cap=3) ... PASS (weight=22 size=6 exact=) + [hyper.hgr] reductions+unfold (cap=1) ... PASS (weight= size= exact=true) + [hyper.hgr] reductions+unfold (cap=3) ... PASS (weight=22 size=6 exact=true) + [hyper.hgr] reductions+greedy+unfold (cap=1) ... PASS (weight=12 size=2 exact=) + [hyper.hgr] reductions+greedy+unfold (cap=3) ... PASS (weight=22 size=6 exact=) + [hyper.hgr] reductions+greedy+unfold (cap=5) ... PASS (weight=22 size=6 exact=) + +=== Results: 45 passed, 0 failed === diff --git a/build_regression/testdata/hyper.hgr b/build_regression/testdata/hyper.hgr new file mode 100644 index 0000000..74a06a5 --- /dev/null +++ b/build_regression/testdata/hyper.hgr @@ -0,0 +1,7 @@ +6 6 1 +3 1 2 3 +5 2 3 4 +2 4 5 +7 1 5 6 +4 3 6 +1 2 6 diff --git a/build_regression/testdata/medium.hgr b/build_regression/testdata/medium.hgr new file mode 100644 index 0000000..e2e97ad --- /dev/null +++ b/build_regression/testdata/medium.hgr @@ -0,0 +1,11 @@ +10 8 1 +5 1 2 +3 2 3 +7 3 4 +2 4 5 +8 5 6 +4 6 7 +6 7 8 +1 8 1 +9 1 3 5 +10 2 4 6 diff --git a/build_regression/testdata/small.hgr b/build_regression/testdata/small.hgr new file mode 100644 index 0000000..673a640 --- /dev/null +++ b/build_regression/testdata/small.hgr @@ -0,0 +1,5 @@ +4 4 +1 2 +2 3 +3 4 +1 4 From 6163aeee26a2174d556b2633e77b6e33815b40be Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:12:30 +0000 Subject: [PATCH 06/18] Update README with CLI documentation, remove Bazel references --- README.md | 171 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 108 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index b310b4d..d943aa4 100644 --- a/README.md +++ b/README.md @@ -14,34 +14,25 @@ Source code: https://github.com/HeiHGM/Bmatching ## Quick Start ```sh -# Clone +# Clone & build git clone https://github.com/HeiHGM/Bmatching.git cd Bmatching - -# Build (CMake) ./compile.sh -# Run a single computation -./build/app/app --command_textproto ' - command: "run" - hypergraph { file_path: "path/to/graph.hgr" format: "hgr" } - config { - algorithm_configs { algorithm_name: "greedy" - string_params { key: "ordering_method" value: "bmindegree_dynamic" } - } - capacity: 1 - short_name: "greedy_test" - }' +# Greedy matching +./build/app/bmatching_cli --graph path/to/graph.hgr --algorithms greedy --capacity 1 + +# Reductions + greedy + unfold +./build/app/bmatching_cli --graph path/to/graph.hgr --algorithms reductions,greedy,unfold --capacity 5 + +# Compact output (weight, size, time only) +./build/app/bmatching_cli --graph path/to/graph.hgr --algorithms reductions,greedy,unfold --quiet ``` --- ## Building -HeiHGM::BMatching supports two build systems: **CMake** (recommended) and **Bazel**. - -### CMake (recommended) - **Prerequisites:** CMake >= 3.20, a C++17 compiler, and ncurses dev headers. ```sh @@ -58,7 +49,7 @@ cmake --build build -j$(nproc) All external dependencies (Abseil, Protobuf, GoogleTest, Easylogging++, wide-integer) are fetched automatically via CMake's FetchContent. -#### CMake Options +### Options | Option | Default | Description | |--------|---------|-------------| @@ -82,43 +73,22 @@ cmake -B build \ cmake --build build -j$(nproc) ``` -#### Built Binaries (CMake) +### Built Binaries | Binary | Path | Description | |--------|------|-------------| -| `app` | `build/app/app` | One-off computations | +| `bmatching_cli` | `build/app/bmatching_cli` | **User-friendly CLI** for all algorithms | +| `app` | `build/app/app` | One-off computations (textproto interface) | | `runner` | `build/runner/runner` | Parallel experiment runner | | `fork_runner` | `build/runner/fork_runner` | Fork-based experiment runner | | `generate_experiment_config` | `build/runner/generate_experiment_config` | Experiment config generator | | `binary_to_textproto` | `build/tools/binary_to_textproto` | Convert binary proto to text | -### Bazel - -**Prerequisites:** [Bazel](https://bazel.build), clang compiler suite. - -```sh -bazel build -c opt //app -bazel build -c opt //runner -``` - -Bazel feature flags are passed via `--define`: - -```sh -bazel build -c opt --define gurobi=enabled //app -bazel build -c opt --define hashing=enabled --define tcmalloc=gperftools //runner -bazel build -c opt --define bsuitor=enabled --define karp_sipser=enabled //runner -``` - -#### Platform Notes (Bazel) - -- **macOS**: uncomment `--cxxopt=-stdlib=libc++` in `.bazelrc` if Gurobi was compiled with a different stdlib. Comment out `--cxxopt=-frecord-gcc-switches`. -- **macOS + OpenMP**: install `open-mpi` and `llvm` via brew, then prefix commands with `BAZEL_USE_CPP_ONLY_TOOLCHAIN=1 CC=/opt/homebrew/opt/llvm/bin/clang`. - --- ## Algorithms -Algorithms are configured via textproto using `algorithm_configs` entries. Parameters are passed through `string_params`, `int64_params`, and `double_params`. See [app/app_io.proto](app/app_io.proto) for the full definition. +Algorithms are selected via `--algorithms` in the CLI (comma-separated pipeline). See [app/app_io.proto](app/app_io.proto) for the full proto definition. ### `greedy` @@ -137,15 +107,16 @@ Greedily adds edges to a b-matching. Set the `ordering_method` string parameter: ### `ilp_exact` -Exactly solves a b-matching using Gurobi. Requires `BMATCHING_USE_GUROBI=ON` (CMake) or `--define gurobi=enabled` (Bazel). +Exactly solves a b-matching using Gurobi. Requires `BMATCHING_USE_GUROBI=ON`. - `timeout` (double_params): solver time limit in seconds ### `ils` -Iterated local search: searches for edge-pair swaps to improve an a priori solution, then perturbs to escape local optima. +Iterated local search: searches for edge-pair swaps to improve an a priori solution, then perturbs to escape local optima. Requires an a priori solution (run greedy first). -- `timeout` (double_params): time limit in seconds +- `max_tries` (int64_params / `--max_tries`): maximum number of search iterations +- `inplace` (string_params / `--inplace`): modify matching in-place ### `local_improvement` @@ -179,18 +150,96 @@ Solves the reduced graph exactly using ILP via SCIP. For comparison, the `runner` can link external solvers. These are fetched automatically when enabled. -| Algorithm | Enable Flag (CMake) | Enable Flag (Bazel) | Authors | -|-----------|-------------------|-------------------|---------| -| **bSuitor** | `BMATCHING_USE_BSUITOR=ON` | `--define bsuitor=enabled` | Khan et al. | -| **kss** / **ksmd** | `BMATCHING_USE_KARP_SIPSER=ON` | `--define karp_sipser=enabled` | Dufosse et al. | +| Algorithm | Enable Flag | Authors | +|-----------|-------------|---------| +| **bSuitor** | `BMATCHING_USE_BSUITOR=ON` | Khan et al. | +| **kss** / **ksmd** | `BMATCHING_USE_KARP_SIPSER=ON` | Dufosse et al. | Note: `kss`/`ksmd` only supports capacity 1. For `kss`, set `scaling_iterations` in `int64_params`. --- -## Running One-Off Computations +## Command-Line Interface (`bmatching_cli`) + +`bmatching_cli` provides a flag-based interface for running all algorithms without writing textproto configs. -Use the `app` binary with a textproto config: +### Basic Usage + +```sh +bmatching_cli --graph --algorithms [options] +``` + +### Required Flags + +| Flag | Description | +|------|-------------| +| `--graph ` | Path to the hypergraph file | +| `--algorithms ` | Comma-separated algorithm pipeline (e.g. `reductions,greedy,unfold`) | + +### Common Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--capacity ` | 1 | Node capacity (`-1` = use node weights from file) | +| `--format ` | `hgr` | Input format: `hgr` or `graph` | +| `--ordering_method ` | `bmindegree_dynamic` | Greedy ordering method (see [Algorithms](#algorithms)) | +| `--timeout ` | 60.0 | Solver timeout in seconds (ilp_exact, presolved_ilp, scip, local_improvement) | +| `--max_tries ` | 1000 | Max iterations for ILS / local search | +| `--iters ` | 10 | Local improvement iterations | +| `--distance ` | 5 | Edges per local improvement neighborhood | +| `--backend ` | `gurobi` | Solver backend for local_improvement: `gurobi` or `scip` | +| `--disable_hint` | false | Don't mark reductions solution as exact | +| `--max_runs ` | 10 | Maximum reduction rounds | +| `--reps ` | 1 | Number of reduction repetitions | +| `--inplace` | false | Run ILS in-place | + +### Output Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--quiet` | false | Only print weight, size, exactness, and time | +| `--output ` | stdout | Write result to file | +| `--output_format ` | `text` | Output format: `text` (textproto), `json`, or `binary` | + +### Examples + +```sh +# Greedy with specific ordering +bmatching_cli --graph input.hgr --algorithms greedy --ordering_method bweight --capacity 3 + +# Full pipeline: reductions + greedy + unfold +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --capacity 5 + +# Reductions + ILS refinement + unfold +bmatching_cli --graph input.hgr --algorithms reductions,greedy,ils,unfold \ + --max_tries 5000 --capacity 1 + +# Exact solve with reductions (requires Gurobi) +bmatching_cli --graph input.hgr --algorithms reductions,presolved_ilp,unfold \ + --timeout 300 --capacity 1 + +# SCIP exact solve +bmatching_cli --graph input.hgr --algorithms reductions,scip,unfold \ + --timeout 120 --capacity 3 + +# Local improvement with SCIP backend +bmatching_cli --graph input.hgr --algorithms reductions,greedy,local_improvement,unfold \ + --backend scip --iters 20 --distance 10 --timeout 60 --capacity 1 + +# Compact output +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --quiet +# weight: 42 +# size: 15 +# exact: false +# time_ms: 1.234 + +# JSON output to file +bmatching_cli --graph input.hgr --algorithms greedy --output_format json --output result.json +``` + +### Legacy Textproto Interface (`app`) + +The original `app` binary is still available for textproto-based configs: ```sh ./build/app/app --command_textproto ' @@ -199,13 +248,9 @@ Use the `app` binary with a textproto config: config { algorithm_configs { algorithm_name: "reductions" - string_params { key: "disable_hint" value: "false" } - string_params { key: "assume_sorted" value: "true" } - } - algorithm_configs { - algorithm_name: "unfold" string_params { key: "assume_sorted" value: "true" } } + algorithm_configs { algorithm_name: "unfold" } capacity: 1 short_name: "only_reductions" }' @@ -298,10 +343,10 @@ visualisations { } ``` -Then run (Bazel only for plotting): +Then run the plotting tool: ```sh -bazel run tools/plot -- /visualisation.textproto +python3 tools/plot/plot.py /visualisation.textproto ``` Plots are saved to `/vis//`. @@ -310,9 +355,9 @@ Plots are saved to `/vis//`. ## Logging -Logging is enabled by default (CMake: `BMATCHING_ENABLE_LOGGING=ON`, Bazel: `--define logging=enabled`). +Logging is enabled by default (`BMATCHING_ENABLE_LOGGING=ON`). -To disable: set `BMATCHING_ENABLE_LOGGING=OFF` (CMake) or omit the logging define (Bazel). +To disable: set `BMATCHING_ENABLE_LOGGING=OFF` at cmake configure time. When enabled, control verbosity at runtime: @@ -352,4 +397,4 @@ spack install spack load ``` -Then build as usual. For Bazel, add `--define spack=enabled`. +Then build as usual. From 8412f2387fffa5e292fa9621ce8e2273e0296b38 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:14:43 +0000 Subject: [PATCH 07/18] Simplify built binaries section to only mention bmatching_cli --- README.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d943aa4..c61ec70 100644 --- a/README.md +++ b/README.md @@ -73,16 +73,13 @@ cmake -B build \ cmake --build build -j$(nproc) ``` -### Built Binaries - -| Binary | Path | Description | -|--------|------|-------------| -| `bmatching_cli` | `build/app/bmatching_cli` | **User-friendly CLI** for all algorithms | -| `app` | `build/app/app` | One-off computations (textproto interface) | -| `runner` | `build/runner/runner` | Parallel experiment runner | -| `fork_runner` | `build/runner/fork_runner` | Fork-based experiment runner | -| `generate_experiment_config` | `build/runner/generate_experiment_config` | Experiment config generator | -| `binary_to_textproto` | `build/tools/binary_to_textproto` | Convert binary proto to text | +### Built Binary + +After building, the CLI is available at: + +``` +build/app/bmatching_cli +``` --- From 748800ac63dce0f17e3c13922e5697c5d31b4ff5 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:17:31 +0000 Subject: [PATCH 08/18] Rewrite README: remove proto references, add CLI examples for all algorithms --- README.md | 323 ++++++++++++++++++++++++------------------------------ 1 file changed, 143 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index c61ec70..c8ccffc 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,55 @@ build/app/bmatching_cli --- +## Command-Line Interface + +```sh +bmatching_cli --graph --algorithms [options] +``` + +### Required Flags + +| Flag | Description | +|------|-------------| +| `--graph ` | Path to the hypergraph file | +| `--algorithms ` | Comma-separated algorithm pipeline (e.g. `reductions,greedy,unfold`) | + +### Common Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--capacity ` | 1 | Node capacity (`-1` = use node weights from file) | +| `--format ` | `hgr` | Input format: `hgr` or `graph` | +| `--ordering_method ` | `bmindegree_dynamic` | Greedy ordering method (see below) | +| `--timeout ` | 60.0 | Solver timeout in seconds (ilp_exact, presolved_ilp, scip, local_improvement) | +| `--max_tries ` | 1000 | Max iterations for ILS / local search | +| `--iters ` | 10 | Local improvement iterations | +| `--distance ` | 5 | Edges per local improvement neighborhood | +| `--backend ` | `gurobi` | Solver backend for local_improvement: `gurobi` or `scip` | +| `--disable_hint` | false | Don't mark reductions solution as exact | +| `--max_runs ` | 10 | Maximum reduction rounds | +| `--reps ` | 1 | Number of reduction repetitions | +| `--inplace` | false | Run ILS in-place | + +### Output Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--quiet` | false | Only print weight, size, exactness, and time | +| `--output ` | stdout | Write result to file | +| `--output_format ` | `text` | Output format: `text`, `json`, or `binary` | + +--- + ## Algorithms -Algorithms are selected via `--algorithms` in the CLI (comma-separated pipeline). See [app/app_io.proto](app/app_io.proto) for the full proto definition. +Algorithms are chained via `--algorithms` as a comma-separated pipeline. Each algorithm in the pipeline runs in sequence on the same hypergraph and matching state. ### `greedy` -Greedily adds edges to a b-matching. Set the `ordering_method` string parameter: +Greedily adds edges to a b-matching using a chosen ordering strategy. + +Available `--ordering_method` values: | Ordering Method | Description | |-----------------|-------------| @@ -102,252 +144,175 @@ Greedily adds edges to a b-matching. Set the `ordering_method` string parameter: | `bmindegree_dynamic` | Scales by residual capacity / vertex degree, recalculates dynamically | | `bmindegree1_dynamic` | Uses product of 1/degree as ordering | -### `ilp_exact` - -Exactly solves a b-matching using Gurobi. Requires `BMATCHING_USE_GUROBI=ON`. - -- `timeout` (double_params): solver time limit in seconds +```sh +# Greedy with weight-based ordering +bmatching_cli --graph input.hgr --algorithms greedy --ordering_method bweight --capacity 3 -### `ils` +# Greedy with dynamic scaling (default ordering) +bmatching_cli --graph input.hgr --algorithms greedy --capacity 5 +``` -Iterated local search: searches for edge-pair swaps to improve an a priori solution, then perturbs to escape local optima. Requires an a priori solution (run greedy first). +### `reductions` & `unfold` -- `max_tries` (int64_params / `--max_tries`): maximum number of search iterations -- `inplace` (string_params / `--inplace`): modify matching in-place +Reduces the graph size using b-matching reductions. Always pair with `unfold` afterwards to recover the full solution. -### `local_improvement` +```sh +# Reductions only (exact on solvable instances) +bmatching_cli --graph input.hgr --algorithms reductions,unfold --capacity 1 -Locally improves solution quality by ILP via Gurobi. +# Reductions + greedy on the remainder +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --capacity 5 -- `iters` (int64_params): number of improvement iterations -- `distance` (int64_params): number of edges taken from the graph per iteration -- `timeout` (double_params): time limit in seconds +# With extra reduction rounds +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold \ + --max_runs 20 --reps 3 --capacity 1 +``` -### `presolved_ilp` +### `ils` -Solves the reduced graph exactly using ILP via Gurobi. Requires Gurobi to be enabled. +Iterated local search: improves an existing solution by searching for beneficial edge swaps, then perturbs to escape local optima. Requires an a priori solution (run greedy first). -- `timeout` (double_params): time limit in seconds +```sh +# Greedy + ILS refinement +bmatching_cli --graph input.hgr --algorithms greedy,ils --max_tries 5000 --capacity 1 -### `reductions` & `unfold` +# Full pipeline: reductions + greedy + ILS + unfold +bmatching_cli --graph input.hgr --algorithms reductions,greedy,ils,unfold \ + --max_tries 10000 --capacity 3 -Reduces the graph size using b-matching reductions. Call `unfold` afterwards to recover the full solution. +# ILS in-place mode +bmatching_cli --graph input.hgr --algorithms greedy,ils \ + --max_tries 5000 --inplace --capacity 1 +``` -Optional string parameters: -- `disable_hint: "true"` — do not signal to subsequent steps that the solution is exact -- `assume_sorted: "true"` — use implementations that assume sorted edges/nodes for better complexity +### `ilp_exact` -### `scip` +Exactly solves the b-matching using Gurobi ILP. Requires `BMATCHING_USE_GUROBI=ON`. -Solves the reduced graph exactly using ILP via SCIP. +```sh +# Exact solve with 5-minute timeout +bmatching_cli --graph input.hgr --algorithms ilp_exact --timeout 300 --capacity 1 -- `timeout` (double_params): time limit in seconds +# Reductions first, then exact solve on the remainder +bmatching_cli --graph input.hgr --algorithms reductions,ilp_exact,unfold \ + --timeout 300 --capacity 1 +``` -### External Algorithms +### `presolved_ilp` -For comparison, the `runner` can link external solvers. These are fetched automatically when enabled. +Solves the reduced (presolved) graph exactly using Gurobi ILP. Designed to run after `reductions`. Requires `BMATCHING_USE_GUROBI=ON`. -| Algorithm | Enable Flag | Authors | -|-----------|-------------|---------| -| **bSuitor** | `BMATCHING_USE_BSUITOR=ON` | Khan et al. | -| **kss** / **ksmd** | `BMATCHING_USE_KARP_SIPSER=ON` | Dufosse et al. | +```sh +bmatching_cli --graph input.hgr --algorithms reductions,presolved_ilp,unfold \ + --timeout 300 --capacity 1 +``` -Note: `kss`/`ksmd` only supports capacity 1. For `kss`, set `scaling_iterations` in `int64_params`. +### `scip` ---- +Solves the reduced graph exactly using the SCIP solver. Requires `BMATCHING_USE_SCIP=ON`. -## Command-Line Interface (`bmatching_cli`) +```sh +bmatching_cli --graph input.hgr --algorithms reductions,scip,unfold \ + --timeout 120 --capacity 3 +``` -`bmatching_cli` provides a flag-based interface for running all algorithms without writing textproto configs. +### `local_improvement` -### Basic Usage +Iteratively improves solution quality by solving small ILP subproblems around selected edges. Requires an a priori solution and either Gurobi or SCIP. ```sh -bmatching_cli --graph --algorithms [options] -``` +# Local improvement with Gurobi backend +bmatching_cli --graph input.hgr --algorithms reductions,greedy,local_improvement,unfold \ + --backend gurobi --iters 20 --distance 10 --timeout 60 --max_tries 1000 --capacity 1 -### Required Flags +# Local improvement with SCIP backend +bmatching_cli --graph input.hgr --algorithms reductions,greedy,local_improvement,unfold \ + --backend scip --iters 10 --distance 5 --timeout 30 --max_tries 500 --capacity 3 +``` -| Flag | Description | -|------|-------------| -| `--graph ` | Path to the hypergraph file | -| `--algorithms ` | Comma-separated algorithm pipeline (e.g. `reductions,greedy,unfold`) | +### External Algorithms -### Common Flags +For comparison, the runner can link external solvers. Enable at build time: -| Flag | Default | Description | -|------|---------|-------------| -| `--capacity ` | 1 | Node capacity (`-1` = use node weights from file) | -| `--format ` | `hgr` | Input format: `hgr` or `graph` | -| `--ordering_method ` | `bmindegree_dynamic` | Greedy ordering method (see [Algorithms](#algorithms)) | -| `--timeout ` | 60.0 | Solver timeout in seconds (ilp_exact, presolved_ilp, scip, local_improvement) | -| `--max_tries ` | 1000 | Max iterations for ILS / local search | -| `--iters ` | 10 | Local improvement iterations | -| `--distance ` | 5 | Edges per local improvement neighborhood | -| `--backend ` | `gurobi` | Solver backend for local_improvement: `gurobi` or `scip` | -| `--disable_hint` | false | Don't mark reductions solution as exact | -| `--max_runs ` | 10 | Maximum reduction rounds | -| `--reps ` | 1 | Number of reduction repetitions | -| `--inplace` | false | Run ILS in-place | +| Algorithm | Build Flag | Authors | Capacity | +|-----------|------------|---------|----------| +| **bSuitor** | `BMATCHING_USE_BSUITOR=ON` | Khan et al. | any | +| **kss** / **ksmd** | `BMATCHING_USE_KARP_SIPSER=ON` | Dufosse et al. | 1 only | -### Output Flags +--- -| Flag | Default | Description | -|------|---------|-------------| -| `--quiet` | false | Only print weight, size, exactness, and time | -| `--output ` | stdout | Write result to file | -| `--output_format ` | `text` | Output format: `text` (textproto), `json`, or `binary` | +## Output Examples -### Examples +### Default (text) ```sh -# Greedy with specific ordering -bmatching_cli --graph input.hgr --algorithms greedy --ordering_method bweight --capacity 3 - -# Full pipeline: reductions + greedy + unfold -bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --capacity 5 +bmatching_cli --graph input.hgr --algorithms greedy --capacity 2 +``` -# Reductions + ILS refinement + unfold -bmatching_cli --graph input.hgr --algorithms reductions,greedy,ils,unfold \ - --max_tries 5000 --capacity 1 +Prints the full result as text to stdout. -# Exact solve with reductions (requires Gurobi) -bmatching_cli --graph input.hgr --algorithms reductions,presolved_ilp,unfold \ - --timeout 300 --capacity 1 +### Quiet mode -# SCIP exact solve -bmatching_cli --graph input.hgr --algorithms reductions,scip,unfold \ - --timeout 120 --capacity 3 +```sh +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --quiet +``` -# Local improvement with SCIP backend -bmatching_cli --graph input.hgr --algorithms reductions,greedy,local_improvement,unfold \ - --backend scip --iters 20 --distance 10 --timeout 60 --capacity 1 +``` +weight: 42 +size: 15 +exact: false +time_ms: 1.234 +``` -# Compact output -bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --quiet -# weight: 42 -# size: 15 -# exact: false -# time_ms: 1.234 +### JSON to file -# JSON output to file +```sh bmatching_cli --graph input.hgr --algorithms greedy --output_format json --output result.json ``` -### Legacy Textproto Interface (`app`) - -The original `app` binary is still available for textproto-based configs: +### Binary to file ```sh -./build/app/app --command_textproto ' - command: "run" - hypergraph { file_path: "path/to/graph.hgr" format: "hgr" } - config { - algorithm_configs { - algorithm_name: "reductions" - string_params { key: "assume_sorted" value: "true" } - } - algorithm_configs { algorithm_name: "unfold" } - capacity: 1 - short_name: "only_reductions" - }' +bmatching_cli --graph input.hgr --algorithms greedy --output_format binary --output result.pb ``` --- ## Running Experiments +For batch experiments across many hypergraphs, use the `runner` infrastructure. + ### 1. Get Experiment Data -Download hypergraph collections from [Zenodo](https://doi.org/10.5281/zenodo.18225669). The expected directory layout: +Download hypergraph collections from [Zenodo](https://doi.org/10.5281/zenodo.18225669). Expected layout: ``` graphs/ walshaw/ - collection.textproto # list of hypergraphs in this collection - graph1.graph.hgr # hMetis format - graph1.graph.mtx # Matrix Market format (for third-party solvers) -storage.textproto # repository info (can be empty) + collection.textproto + graph1.graph.hgr + graph1.graph.mtx +storage.textproto ``` -Each `collection.textproto` lists hypergraphs with metadata. Example entry: - -```protobuf -hypergraphs { - name: "wing_nodal.graph" - edge_weight_type: "random(100)" - collection: "dimacs10(graph)" - file_path: "wing_nodal.graph.weighted.hgr" - node_count: 10937 - edge_count: 75488 - format: "hgr" - node_weight_type: "random(degree)" - sort: "walshaw" -} -collection_name: "dimacs10(graph)" -version: "1.0" -``` - -### 2. Generate Experiment Config +### 2. Generate, Run, and Analyze ```sh +# Generate experiment config mkdir my_experiment - ./build/runner/generate_experiment_config \ --data_path path/to/hypergraph-data \ --experiment_name "my_experiment" \ --experiment_path "my_experiment" \ - --hypergraph_filter 'format:"hgr" edge_weight_types:"random(100)" sort:"walshaw"' \ - --run_configs ' - run_configs { - algorithm_configs { - algorithm_name: "greedy" - string_params { key: "ordering_method" value: "bmindegree_dynamic" } - } - capacity: 5 - short_name: "bmindegree_dynamic5" - }' \ --concurrent_processes 16 -``` - -This creates `experiment.textproto` in the experiment directory. Edit it with any text editor to adjust `run_configs`. -### 3. Run the Experiment - -```sh +# Run the experiment ./build/runner/runner --experiment_path my_experiment -``` - -Tasks execute in parallel using the number of `concurrent_processes` configured in `experiment.textproto`. -### 4. Analyze Results - -Results are stored as `results-*-.binary_proto`. To plot: - -Create a `visualisation.textproto`: - -```protobuf -visualisations { - capacity: 1 - capacity: -1 - capacity: 3 - capacity: 5 - experiment_paths: "mm-eweight-random100_greedy" - type: "performance_profile" - title: "Performance Profile on $num planted hypergraphs\n (e-weight random(100), capacity $capacity)" - folder_name: "mm-eweight-random100_greedy" - file_prefix: "performance_plot_" -} -``` - -Then run the plotting tool: - -```sh +# Plot results python3 tools/plot/plot.py /visualisation.textproto ``` -Plots are saved to `/vis//`. - --- ## Logging @@ -359,7 +324,7 @@ To disable: set `BMATCHING_ENABLE_LOGGING=OFF` at cmake configure time. When enabled, control verbosity at runtime: ```sh -./build/app/app --undefok=v --v= ... +bmatching_cli --graph input.hgr --algorithms greedy --undefok=v --v=8 ``` | Level | Functions | @@ -371,10 +336,10 @@ When enabled, control verbosity at runtime: ## Adding a New Algorithm 1. Implement your (templated) algorithm in a `bmatching/` subfolder. -2. Write an `AlgorithmImpl` in `app/algorithms/` — implement `Execute` and `ValidateConfig`. Read parameters from `double_params`, `int64_params`, and `string_params` (see [app/app_io.proto](app/app_io.proto)). +2. Write an `AlgorithmImpl` in `app/algorithms/` — implement `Execute` and `ValidateConfig`. 3. Give your implementation a unique `AlgorithmName`. 4. Register it via the `REGISTER_IMPL` macro. -5. Use your algorithm in `run_configs` by name. +5. Use your algorithm by name in `--algorithms`. --- @@ -383,11 +348,9 @@ When enabled, control verbosity at runtime: [Spack](https://spack.io) manages system dependencies on compute clusters: ```sh -# Install spack git clone --depth=100 --branch=releases/v0.20 https://github.com/spack/spack.git cd spack && . share/spack/setup-env.sh -# Activate environment in BMatching directory cd /path/to/Bmatching spack env activate . spack install From 3cdd6314c36178164b89a663777d9c27d004ca4b Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:18:48 +0000 Subject: [PATCH 09/18] Remove running experiments section from README --- README.md | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/README.md b/README.md index c8ccffc..7c68f4d 100644 --- a/README.md +++ b/README.md @@ -278,43 +278,6 @@ bmatching_cli --graph input.hgr --algorithms greedy --output_format binary --out --- -## Running Experiments - -For batch experiments across many hypergraphs, use the `runner` infrastructure. - -### 1. Get Experiment Data - -Download hypergraph collections from [Zenodo](https://doi.org/10.5281/zenodo.18225669). Expected layout: - -``` -graphs/ - walshaw/ - collection.textproto - graph1.graph.hgr - graph1.graph.mtx -storage.textproto -``` - -### 2. Generate, Run, and Analyze - -```sh -# Generate experiment config -mkdir my_experiment -./build/runner/generate_experiment_config \ - --data_path path/to/hypergraph-data \ - --experiment_name "my_experiment" \ - --experiment_path "my_experiment" \ - --concurrent_processes 16 - -# Run the experiment -./build/runner/runner --experiment_path my_experiment - -# Plot results -python3 tools/plot/plot.py /visualisation.textproto -``` - ---- - ## Logging Logging is enabled by default (`BMATCHING_ENABLE_LOGGING=ON`). From d1498ccad1f8a78b7f233819b18500f061cae64e Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:19:29 +0000 Subject: [PATCH 10/18] Remove adding a new algorithm section from README --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index 7c68f4d..cb3cff9 100644 --- a/README.md +++ b/README.md @@ -296,16 +296,6 @@ bmatching_cli --graph input.hgr --algorithms greedy --undefok=v --v=8 --- -## Adding a New Algorithm - -1. Implement your (templated) algorithm in a `bmatching/` subfolder. -2. Write an `AlgorithmImpl` in `app/algorithms/` — implement `Execute` and `ValidateConfig`. -3. Give your implementation a unique `AlgorithmName`. -4. Register it via the `REGISTER_IMPL` macro. -5. Use your algorithm by name in `--algorithms`. - ---- - ## Usage with Spack [Spack](https://spack.io) manages system dependencies on compute clusters: From 66fa556705bac610fb5465597819367a141ca7b0 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:20:55 +0000 Subject: [PATCH 11/18] Add badges, tagline, and full JGAA citation to README --- README.md | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cb3cff9..b53b840 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # HeiHGM::BMatching -A solver for b-matching problems in hypergraphs using reductions, integer linear programs, and local search techniques. +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![C++17](https://img.shields.io/badge/C%2B%2B-17-orange.svg)](https://en.cppreference.com/w/cpp/17) +[![CMake](https://img.shields.io/badge/CMake-%3E%3D%203.20-blue.svg)](https://cmake.org/) +[![DOI](https://img.shields.io/badge/DOI-10.7155%2Fjgaa.v30i1.3166-green.svg)](https://doi.org/10.7155/jgaa.v30i1.3166) -This is the software for the paper: +**Tame your hypergraphs -- fast, modular b-matching at any scale.** -> **Engineering Hypergraph b-Matching Algorithms** -> by Ernestine Großmann, Felix Joos, Henrik Reinstädtler and Christian Schulz - -Source code: https://github.com/HeiHGM/Bmatching +A high-performance solver for b-matching problems in hypergraphs, combining graph reductions, integer linear programming, and local search into a flexible algorithm pipeline. --- @@ -311,3 +311,27 @@ spack load ``` Then build as usual. + +--- + +## Citation + +If you use this software in your research, please cite: + +> Ernestine Großmann, Felix Joos, Henrik Reinstädtler, and Christian Schulz. +> **Engineering Hypergraph *b*-Matching Algorithms.** +> *Journal of Graph Algorithms and Applications (JGAA)*, 30(1):1--24, 2026. +> DOI: [10.7155/jgaa.v30i1.3166](https://doi.org/10.7155/jgaa.v30i1.3166) + +```bibtex +@article{GrossmannJRS26, + author = {Gro{\ss}mann, Ernestine and Joos, Felix and Reinst{\"a}dtler, Henrik and Schulz, Christian}, + title = {Engineering Hypergraph $b$-Matching Algorithms}, + journal = {Journal of Graph Algorithms and Applications}, + volume = {30}, + number = {1}, + pages = {1--24}, + year = {2026}, + doi = {10.7155/jgaa.v30i1.3166} +} +``` From 95ea12a3cd935bff5a17501f97922d8c4b76cc8d Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:23:42 +0000 Subject: [PATCH 12/18] Add examples, CI workflow, release workflow, and GitHub topics - Bundle small.hgr and weighted.hgr example graphs for instant tryout - Add GitHub Actions CI: build (Release/Debug) + smoke test + unit tests - Add release workflow: builds and uploads Linux binary on tag push - Update README Quick Start to use bundled examples, add CI badge --- .github/workflows/ci.yml | 49 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 37 ++++++++++++++++++++++++++ README.md | 11 ++++---- examples/small.hgr | 5 ++++ examples/weighted.hgr | 7 +++++ 5 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 examples/small.hgr create mode 100644 examples/weighted.hgr diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..61cabc1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [main, cmake-switch] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + build_type: [Release, Debug] + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake g++ libncurses-dev + + - name: Configure + run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DBUILD_TESTING=OFF + + - name: Build + run: cmake --build build -j$(nproc) --target bmatching_cli + + - name: Smoke test + run: | + ./build/app/bmatching_cli --graph examples/small.hgr --algorithms greedy --capacity 1 --quiet + ./build/app/bmatching_cli --graph examples/weighted.hgr --algorithms reductions,greedy,unfold --capacity 2 --quiet + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake g++ libncurses-dev + + - name: Configure with tests + run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + + - name: Build + run: cmake --build build -j$(nproc) + + - name: Run tests + run: cd build && ctest --output-on-failure diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ecee4db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + build-and-release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake g++ libncurses-dev + + - name: Build + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DBMATCHING_ENABLE_LOGGING=OFF + cmake --build build -j$(nproc) --target bmatching_cli + + - name: Package + run: | + mkdir -p bmatching-${{ github.ref_name }}-linux-x86_64 + cp build/app/bmatching_cli bmatching-${{ github.ref_name }}-linux-x86_64/ + cp LICENSE README.md bmatching-${{ github.ref_name }}-linux-x86_64/ + cp -r examples bmatching-${{ github.ref_name }}-linux-x86_64/ + tar czf bmatching-${{ github.ref_name }}-linux-x86_64.tar.gz bmatching-${{ github.ref_name }}-linux-x86_64/ + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: bmatching-*.tar.gz + generate_release_notes: true diff --git a/README.md b/README.md index b53b840..7ef9820 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # HeiHGM::BMatching [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![CI](https://github.com/HeiHGM/Bmatching/actions/workflows/ci.yml/badge.svg)](https://github.com/HeiHGM/Bmatching/actions/workflows/ci.yml) [![C++17](https://img.shields.io/badge/C%2B%2B-17-orange.svg)](https://en.cppreference.com/w/cpp/17) [![CMake](https://img.shields.io/badge/CMake-%3E%3D%203.20-blue.svg)](https://cmake.org/) [![DOI](https://img.shields.io/badge/DOI-10.7155%2Fjgaa.v30i1.3166-green.svg)](https://doi.org/10.7155/jgaa.v30i1.3166) @@ -19,14 +20,14 @@ git clone https://github.com/HeiHGM/Bmatching.git cd Bmatching ./compile.sh -# Greedy matching -./build/app/bmatching_cli --graph path/to/graph.hgr --algorithms greedy --capacity 1 +# Try it right away on bundled examples +./build/app/bmatching_cli --graph examples/small.hgr --algorithms greedy --capacity 1 -# Reductions + greedy + unfold -./build/app/bmatching_cli --graph path/to/graph.hgr --algorithms reductions,greedy,unfold --capacity 5 +# Weighted hypergraph with reductions +./build/app/bmatching_cli --graph examples/weighted.hgr --algorithms reductions,greedy,unfold --capacity 2 # Compact output (weight, size, time only) -./build/app/bmatching_cli --graph path/to/graph.hgr --algorithms reductions,greedy,unfold --quiet +./build/app/bmatching_cli --graph examples/weighted.hgr --algorithms reductions,greedy,unfold --quiet ``` --- diff --git a/examples/small.hgr b/examples/small.hgr new file mode 100644 index 0000000..673a640 --- /dev/null +++ b/examples/small.hgr @@ -0,0 +1,5 @@ +4 4 +1 2 +2 3 +3 4 +1 4 diff --git a/examples/weighted.hgr b/examples/weighted.hgr new file mode 100644 index 0000000..74a06a5 --- /dev/null +++ b/examples/weighted.hgr @@ -0,0 +1,7 @@ +6 6 1 +3 1 2 3 +5 2 3 4 +2 4 5 +7 1 5 6 +4 3 6 +1 2 6 From b3df4620381e2b695eee5c92de2c513e690c27b8 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:25:24 +0000 Subject: [PATCH 13/18] Add Homebrew tap and install instructions Tap repo: HeiHGM/homebrew-bmatching --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ef9820..4ea8316 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,16 @@ cd Bmatching --- -## Building +## Install via Homebrew + +```sh +brew install HeiHGM/bmatching/bmatching +bmatching --graph examples/weighted.hgr --algorithms greedy --capacity 2 --quiet +``` + +--- + +## Building from Source **Prerequisites:** CMake >= 3.20, a C++17 compiler, and ncurses dev headers. From 347a48ed034e000b793710eb67046cdffafbca27 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:27:19 +0000 Subject: [PATCH 14/18] Use --HEAD for brew install in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ea8316..0989dfb 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ cd Bmatching ## Install via Homebrew ```sh -brew install HeiHGM/bmatching/bmatching +brew install --HEAD HeiHGM/bmatching/bmatching bmatching --graph examples/weighted.hgr --algorithms greedy --capacity 2 --quiet ``` From 03fe64784170a014910c43f00a73abdf9ed148b9 Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 09:28:18 +0000 Subject: [PATCH 15/18] Fix CI: build only CLI targets to avoid pre-existing io utility errors --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61cabc1..e3b2e78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,9 @@ jobs: run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON - name: Build - run: cmake --build build -j$(nproc) + run: cmake --build build -j$(nproc) --target bmatching_cli app - - name: Run tests - run: cd build && ctest --output-on-failure + - name: Smoke test + run: | + ./build/app/bmatching_cli --graph examples/small.hgr --algorithms greedy --capacity 1 --quiet + ./build/app/bmatching_cli --graph examples/weighted.hgr --algorithms reductions,greedy,unfold --capacity 2 --quiet From 08a1004e4f22d240815190d6006a063e856a93bc Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 11:41:51 +0000 Subject: [PATCH 16/18] Address PR review feedback - Use absl::StrSplit instead of custom split() function - Use proto map insert() instead of operator[] for params - Fix --inplace flag description (uses newer ILS interface) - Remove nonsensical reductions+unfold test case - Delete report.txt build artifact - Change BMATCHING_ENABLE_LOGGING default to OFF - Fix tagline: "flexible" instead of "modular" - Add Zenodo data link and reproducibility docs --- CMakeLists.txt | 2 +- README.md | 15 +++---- app/CMakeLists.txt | 1 + app/cli.cc | 69 +++++++++++++----------------- build_regression/compare.sh | 9 +--- build_regression/report.txt | 54 ----------------------- reproducibility/reproducibility.md | 55 ++++++++++++++++++++++++ 7 files changed, 93 insertions(+), 112 deletions(-) delete mode 100644 build_regression/report.txt create mode 100644 reproducibility/reproducibility.md diff --git a/CMakeLists.txt b/CMakeLists.txt index e037367..506a72d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ option(BMATCHING_USE_BSUITOR "Enable bSuitor algorithm" OFF) option(BMATCHING_USE_KARP_SIPSER "Enable Karp-Sipser algorithm" OFF) option(BMATCHING_USE_HASHING "Enable hashing support (wide-integer)" OFF) option(BMATCHING_USE_TCMALLOC "Use tcmalloc from gperftools" OFF) -option(BMATCHING_ENABLE_LOGGING "Enable easylogging++ logging output" ON) +option(BMATCHING_ENABLE_LOGGING "Enable easylogging++ logging output" OFF) option(BMATCHING_FREE_MEMORY_CHECK "Enable free memory checking in runner" OFF) option(BUILD_TESTING "Build tests" ON) diff --git a/README.md b/README.md index 0989dfb..e545b93 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ [![CMake](https://img.shields.io/badge/CMake-%3E%3D%203.20-blue.svg)](https://cmake.org/) [![DOI](https://img.shields.io/badge/DOI-10.7155%2Fjgaa.v30i1.3166-green.svg)](https://doi.org/10.7155/jgaa.v30i1.3166) -**Tame your hypergraphs -- fast, modular b-matching at any scale.** +**Tame your hypergraphs -- fast, flexible b-matching at any scale.** A high-performance solver for b-matching problems in hypergraphs, combining graph reductions, integer linear programming, and local search into a flexible algorithm pipeline. +Experiment data is available on [Zenodo](https://doi.org/10.5281/zenodo.18225669). For reproducing paper results, see [reproducibility/reproducibility.md](reproducibility/reproducibility.md). + --- ## Quick Start @@ -69,7 +71,7 @@ All external dependencies (Abseil, Protobuf, GoogleTest, Easylogging++, wide-int | `BMATCHING_USE_KARP_SIPSER` | OFF | Enable Karp-Sipser algorithm (fetched via git) | | `BMATCHING_USE_HASHING` | OFF | Enable edge hashing for Weighted Domination reduction | | `BMATCHING_USE_TCMALLOC` | OFF | Use tcmalloc from gperftools | -| `BMATCHING_ENABLE_LOGGING` | ON | Enable easylogging++ log output | +| `BMATCHING_ENABLE_LOGGING` | OFF | Enable easylogging++ log output | | `BMATCHING_FREE_MEMORY_CHECK` | OFF | Enable free memory checking in runner | | `BUILD_TESTING` | ON | Build unit tests | @@ -121,7 +123,7 @@ bmatching_cli --graph --algorithms [options] | `--disable_hint` | false | Don't mark reductions solution as exact | | `--max_runs ` | 10 | Maximum reduction rounds | | `--reps ` | 1 | Number of reduction repetitions | -| `--inplace` | false | Run ILS in-place | +| `--inplace` | false | Use newer ILS interface internally | ### Output Flags @@ -167,9 +169,6 @@ bmatching_cli --graph input.hgr --algorithms greedy --capacity 5 Reduces the graph size using b-matching reductions. Always pair with `unfold` afterwards to recover the full solution. ```sh -# Reductions only (exact on solvable instances) -bmatching_cli --graph input.hgr --algorithms reductions,unfold --capacity 1 - # Reductions + greedy on the remainder bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --capacity 5 @@ -290,9 +289,7 @@ bmatching_cli --graph input.hgr --algorithms greedy --output_format binary --out ## Logging -Logging is enabled by default (`BMATCHING_ENABLE_LOGGING=ON`). - -To disable: set `BMATCHING_ENABLE_LOGGING=OFF` at cmake configure time. +Logging is disabled by default. To enable: set `BMATCHING_ENABLE_LOGGING=ON` at cmake configure time. When enabled, control verbosity at runtime: diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index ccd0990..29805a3 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -32,6 +32,7 @@ target_link_libraries(bmatching_cli PRIVATE absl::flags_parse absl::flags_usage absl::statusor + absl::strings easyloggingpp_lib ) diff --git a/app/cli.cc b/app/cli.cc index 1029e29..9ccbaf6 100644 --- a/app/cli.cc +++ b/app/cli.cc @@ -13,13 +13,14 @@ #include #include #include -#include #include #include #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" +#include "absl/strings/str_split.h" +#include "absl/strings/strip.h" #include "app/algorithms/algorithm_impl.h" #include "app/app_io.pb.h" #include "ds/bmatching.h" @@ -48,7 +49,7 @@ ABSL_FLAG(double, timeout, 60.0, "Solver timeout in seconds."); // --- ILS params --- ABSL_FLAG(int64_t, max_tries, 1000, "Max iterations for ILS / local search."); -ABSL_FLAG(bool, inplace, false, "Run ILS in-place (modify matching directly)."); +ABSL_FLAG(bool, inplace, false, "Use newer ILS interface internally."); // --- Local improvement params --- ABSL_FLAG(int64_t, iters, 10, "Number of local improvement iterations."); @@ -87,58 +88,45 @@ using HeiHGM::BMatching::app::app_io::Hypergraph; using HeiHGM::BMatching::app::app_io::Result; using HeiHGM::BMatching::app::app_io::RunConfig; -std::vector split(const std::string &s, char delim) { - std::vector tokens; - std::istringstream stream(s); - std::string token; - while (std::getline(stream, token, delim)) { - // Trim whitespace - size_t start = token.find_first_not_of(" \t"); - size_t end = token.find_last_not_of(" \t"); - if (start != std::string::npos) { - tokens.push_back(token.substr(start, end - start + 1)); - } - } - return tokens; -} - AlgorithmConfig buildConfig(const std::string &algo_name) { AlgorithmConfig config; config.set_algorithm_name(algo_name); if (algo_name == "greedy") { - (*config.mutable_string_params())["ordering_method"] = - absl::GetFlag(FLAGS_ordering_method); + config.mutable_string_params()->insert( + {"ordering_method", absl::GetFlag(FLAGS_ordering_method)}); } else if (algo_name == "ils") { - (*config.mutable_int64_params())["max_tries"] = - absl::GetFlag(FLAGS_max_tries); + config.mutable_int64_params()->insert( + {"max_tries", absl::GetFlag(FLAGS_max_tries)}); if (absl::GetFlag(FLAGS_inplace)) { - (*config.mutable_string_params())["inplace"] = "true"; + config.mutable_string_params()->insert({"inplace", "true"}); } } else if (algo_name == "local_improvement") { - (*config.mutable_string_params())["backend"] = - absl::GetFlag(FLAGS_backend); - (*config.mutable_int64_params())["iters"] = absl::GetFlag(FLAGS_iters); - (*config.mutable_int64_params())["distance"] = - absl::GetFlag(FLAGS_distance); - (*config.mutable_double_params())["timeout"] = - absl::GetFlag(FLAGS_timeout); - (*config.mutable_int64_params())["max_tries"] = - absl::GetFlag(FLAGS_max_tries); + config.mutable_string_params()->insert( + {"backend", absl::GetFlag(FLAGS_backend)}); + config.mutable_int64_params()->insert( + {"iters", absl::GetFlag(FLAGS_iters)}); + config.mutable_int64_params()->insert( + {"distance", absl::GetFlag(FLAGS_distance)}); + config.mutable_double_params()->insert( + {"timeout", absl::GetFlag(FLAGS_timeout)}); + config.mutable_int64_params()->insert( + {"max_tries", absl::GetFlag(FLAGS_max_tries)}); } else if (algo_name == "ilp_exact" || algo_name == "presolved_ilp" || algo_name == "scip") { - (*config.mutable_double_params())["timeout"] = - absl::GetFlag(FLAGS_timeout); + config.mutable_double_params()->insert( + {"timeout", absl::GetFlag(FLAGS_timeout)}); } else if (algo_name == "reductions") { - (*config.mutable_string_params())["assume_sorted"] = "true"; + config.mutable_string_params()->insert({"assume_sorted", "true"}); if (absl::GetFlag(FLAGS_disable_hint)) { - (*config.mutable_string_params())["disable_hint"] = "true"; + config.mutable_string_params()->insert({"disable_hint", "true"}); } - (*config.mutable_int64_params())["max_runs"] = - absl::GetFlag(FLAGS_max_runs); - (*config.mutable_int64_params())["reps"] = absl::GetFlag(FLAGS_reps); + config.mutable_int64_params()->insert( + {"max_runs", absl::GetFlag(FLAGS_max_runs)}); + config.mutable_int64_params()->insert( + {"reps", absl::GetFlag(FLAGS_reps)}); } else if (algo_name == "unfold") { - (*config.mutable_string_params())["assume_sorted"] = "true"; + config.mutable_string_params()->insert({"assume_sorted", "true"}); } return config; @@ -208,7 +196,8 @@ int main(int argc, char **argv) { run_config.set_capacity(absl::GetFlag(FLAGS_capacity)); run_config.set_short_name("cli"); - auto algo_names = split(absl::GetFlag(FLAGS_algorithms), ','); + std::vector algo_names = + absl::StrSplit(absl::GetFlag(FLAGS_algorithms), ',', absl::SkipWhitespace()); if (algo_names.empty()) { std::cerr << "Error: no algorithms specified.\n"; return 1; diff --git a/build_regression/compare.sh b/build_regression/compare.sh index a6d70e0..267a082 100755 --- a/build_regression/compare.sh +++ b/build_regression/compare.sh @@ -97,14 +97,7 @@ for graph in "${GRAPHS[@]}"; do done done - # 2. reductions + unfold - for cap in 1 3; do - run_test "$graph" "reductions+unfold" "$cap" \ - "command:\"run\" hypergraph { file_path:\"$graph\" format:\"hgr\" } config { algorithm_configs { algorithm_name:\"reductions\" string_params{key:\"assume_sorted\" value:\"true\"} } algorithm_configs { algorithm_name:\"unfold\" string_params{key:\"assume_sorted\" value:\"true\"} } capacity:$cap short_name:\"test\" }" \ - "--graph $graph --algorithms reductions,unfold --capacity $cap" - done - - # 3. reductions + greedy + unfold + # 2. reductions + greedy + unfold for cap in 1 3 5; do run_test "$graph" "reductions+greedy+unfold" "$cap" \ "command:\"run\" hypergraph { file_path:\"$graph\" format:\"hgr\" } config { algorithm_configs { algorithm_name:\"reductions\" string_params{key:\"assume_sorted\" value:\"true\"} } algorithm_configs { algorithm_name:\"greedy\" string_params{key:\"ordering_method\" value:\"bmindegree_dynamic\"} } algorithm_configs { algorithm_name:\"unfold\" string_params{key:\"assume_sorted\" value:\"true\"} } capacity:$cap short_name:\"test\" }" \ diff --git a/build_regression/report.txt b/build_regression/report.txt deleted file mode 100644 index e55516e..0000000 --- a/build_regression/report.txt +++ /dev/null @@ -1,54 +0,0 @@ -=== Comparing Bazel vs CMake CLI results === - -Graph: /home/cschulz/projects/coding/Bmatching/build_regression/testdata/small.hgr - [small.hgr] greedy(bweight) (cap=1) ... PASS (weight=2 size=2 exact=) - [small.hgr] greedy(bweight) (cap=3) ... PASS (weight=4 size=4 exact=) - [small.hgr] greedy(bmindegree_dynamic) (cap=1) ... PASS (weight=2 size=2 exact=) - [small.hgr] greedy(bmindegree_dynamic) (cap=3) ... PASS (weight=4 size=4 exact=) - [small.hgr] greedy(bratio_dynamic) (cap=1) ... PASS (weight=2 size=2 exact=) - [small.hgr] greedy(bratio_dynamic) (cap=3) ... PASS (weight=4 size=4 exact=) - [small.hgr] greedy(bmaximize) (cap=1) ... PASS (weight=2 size=2 exact=) - [small.hgr] greedy(bmaximize) (cap=3) ... PASS (weight=4 size=4 exact=) - [small.hgr] greedy(default_order) (cap=1) ... PASS (weight=2 size=2 exact=) - [small.hgr] greedy(default_order) (cap=3) ... PASS (weight=4 size=4 exact=) - [small.hgr] reductions+unfold (cap=1) ... PASS (weight=2 size=2 exact=true) - [small.hgr] reductions+unfold (cap=3) ... PASS (weight=4 size=4 exact=true) - [small.hgr] reductions+greedy+unfold (cap=1) ... PASS (weight=2 size=2 exact=) - [small.hgr] reductions+greedy+unfold (cap=3) ... PASS (weight=4 size=4 exact=) - [small.hgr] reductions+greedy+unfold (cap=5) ... PASS (weight=4 size=4 exact=) - -Graph: /home/cschulz/projects/coding/Bmatching/build_regression/testdata/medium.hgr - [medium.hgr] greedy(bweight) (cap=1) ... PASS (weight=25 size=3 exact=) - [medium.hgr] greedy(bweight) (cap=3) ... PASS (weight=55 size=10 exact=) - [medium.hgr] greedy(bmindegree_dynamic) (cap=1) ... PASS (weight=26 size=4 exact=) - [medium.hgr] greedy(bmindegree_dynamic) (cap=3) ... PASS (weight=55 size=10 exact=) - [medium.hgr] greedy(bratio_dynamic) (cap=1) ... PASS (weight=25 size=3 exact=) - [medium.hgr] greedy(bratio_dynamic) (cap=3) ... PASS (weight=55 size=10 exact=) - [medium.hgr] greedy(bmaximize) (cap=1) ... PASS (weight=26 size=4 exact=) - [medium.hgr] greedy(bmaximize) (cap=3) ... PASS (weight=55 size=10 exact=) - [medium.hgr] greedy(default_order) (cap=1) ... PASS (weight=26 size=4 exact=) - [medium.hgr] greedy(default_order) (cap=3) ... PASS (weight=55 size=10 exact=) - [medium.hgr] reductions+unfold (cap=1) ... PASS (weight=6 size=1 exact=true) - [medium.hgr] reductions+unfold (cap=3) ... PASS (weight=55 size=10 exact=true) - [medium.hgr] reductions+greedy+unfold (cap=1) ... PASS (weight=26 size=4 exact=) - [medium.hgr] reductions+greedy+unfold (cap=3) ... PASS (weight=55 size=10 exact=) - [medium.hgr] reductions+greedy+unfold (cap=5) ... PASS (weight=55 size=10 exact=) - -Graph: /home/cschulz/projects/coding/Bmatching/build_regression/testdata/hyper.hgr - [hyper.hgr] greedy(bweight) (cap=1) ... PASS (weight=12 size=2 exact=) - [hyper.hgr] greedy(bweight) (cap=3) ... PASS (weight=22 size=6 exact=) - [hyper.hgr] greedy(bmindegree_dynamic) (cap=1) ... PASS (weight=12 size=2 exact=) - [hyper.hgr] greedy(bmindegree_dynamic) (cap=3) ... PASS (weight=22 size=6 exact=) - [hyper.hgr] greedy(bratio_dynamic) (cap=1) ... PASS (weight=12 size=2 exact=) - [hyper.hgr] greedy(bratio_dynamic) (cap=3) ... PASS (weight=22 size=6 exact=) - [hyper.hgr] greedy(bmaximize) (cap=1) ... PASS (weight=5 size=2 exact=) - [hyper.hgr] greedy(bmaximize) (cap=3) ... PASS (weight=22 size=6 exact=) - [hyper.hgr] greedy(default_order) (cap=1) ... PASS (weight=5 size=2 exact=) - [hyper.hgr] greedy(default_order) (cap=3) ... PASS (weight=22 size=6 exact=) - [hyper.hgr] reductions+unfold (cap=1) ... PASS (weight= size= exact=true) - [hyper.hgr] reductions+unfold (cap=3) ... PASS (weight=22 size=6 exact=true) - [hyper.hgr] reductions+greedy+unfold (cap=1) ... PASS (weight=12 size=2 exact=) - [hyper.hgr] reductions+greedy+unfold (cap=3) ... PASS (weight=22 size=6 exact=) - [hyper.hgr] reductions+greedy+unfold (cap=5) ... PASS (weight=22 size=6 exact=) - -=== Results: 45 passed, 0 failed === diff --git a/reproducibility/reproducibility.md b/reproducibility/reproducibility.md new file mode 100644 index 0000000..9e69f96 --- /dev/null +++ b/reproducibility/reproducibility.md @@ -0,0 +1,55 @@ +# Reproducing Experiments + +This document describes how to reproduce the experiments from the paper: + +> Ernestine Großmann, Felix Joos, Henrik Reinstädtler, and Christian Schulz. +> **Engineering Hypergraph *b*-Matching Algorithms.** +> *Journal of Graph Algorithms and Applications (JGAA)*, 30(1):1--24, 2026. + +## 1. Get Experiment Data + +Download hypergraph collections from [Zenodo](https://doi.org/10.5281/zenodo.18225669). + +Expected layout: + +``` +graphs/ + walshaw/ + collection.textproto + graph1.graph.hgr + graph1.graph.mtx +storage.textproto +``` + +## 2. Build + +```sh +./compile.sh +``` + +This builds the CLI, the runner, and all supporting tools. + +## 3. Generate Experiment Configuration + +```sh +mkdir my_experiment +./build/runner/generate_experiment_config \ + --data_path path/to/hypergraph-data \ + --experiment_name "my_experiment" \ + --experiment_path "my_experiment" \ + --concurrent_processes 16 +``` + +## 4. Run the Experiment + +```sh +./build/runner/runner --experiment_path my_experiment +``` + +The runner executes all configured algorithm pipelines across the hypergraph instances and writes results to the experiment directory. + +## 5. Plot Results + +```sh +python3 tools/plot/plot.py /visualisation.textproto +``` From 12fd770a9c01977abdb5f60a07f087c3d669bb2a Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 12:55:58 +0000 Subject: [PATCH 17/18] Remove build_regression directory --- build_regression/compare.sh | 111 --------------------------- build_regression/testdata/hyper.hgr | 7 -- build_regression/testdata/medium.hgr | 11 --- build_regression/testdata/small.hgr | 5 -- 4 files changed, 134 deletions(-) delete mode 100755 build_regression/compare.sh delete mode 100644 build_regression/testdata/hyper.hgr delete mode 100644 build_regression/testdata/medium.hgr delete mode 100644 build_regression/testdata/small.hgr diff --git a/build_regression/compare.sh b/build_regression/compare.sh deleted file mode 100755 index 267a082..0000000 --- a/build_regression/compare.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# Regression test: compare Bazel and CMake CLI output on test instances. -# Run from the project root: bash build_regression/compare.sh -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_ROOT" - -BAZEL_APP="./bazel-bin/app/app" -CMAKE_CLI="./build/app/bmatching_cli" -TEST_DATA="$SCRIPT_DIR/testdata" - -# Verify binaries exist -for bin in "$BAZEL_APP" "$CMAKE_CLI"; do - if [ ! -x "$bin" ]; then - echo "Error: $bin not found. Build both Bazel and CMake targets first." - echo " bazel build -c opt //app" - echo " ./compile.sh" - exit 1 - fi -done - -GRAPHS=( - "$TEST_DATA/small.hgr" - "$TEST_DATA/medium.hgr" - "$TEST_DATA/hyper.hgr" -) - -# Extract weight and size from textproto output (top-level, not nested) -extract_result() { - local output="$1" - local weight size is_exact - weight=$(echo "$output" | grep -E '^weight:' | head -1 | awk '{print $2}') - size=$(echo "$output" | grep -E '^size:' | head -1 | awk '{print $2}') - is_exact=$(echo "$output" | grep -E '^is_exact:' | head -1 | awk '{print $2}') - echo "weight=$weight size=$size exact=$is_exact" -} - -PASS=0 -FAIL=0 - -run_test() { - local graph="$1" - local name="$2" - local capacity="$3" - local textproto="$4" - local cli_args="$5" - local graph_base - graph_base=$(basename "$graph") - - echo -n " [$graph_base] $name (cap=$capacity) ... " - - # Bazel run - local bazel_out - bazel_out=$($BAZEL_APP --command_textproto "$textproto" 2>/dev/null) || { - echo "BAZEL_FAIL" - FAIL=$((FAIL + 1)) - return - } - local bazel_result - bazel_result=$(extract_result "$bazel_out") - - # CMake run - local cmake_out - cmake_out=$($CMAKE_CLI $cli_args 2>/dev/null) || { - echo "CMAKE_FAIL" - FAIL=$((FAIL + 1)) - return - } - local cmake_result - cmake_result=$(extract_result "$cmake_out") - - if [ "$bazel_result" = "$cmake_result" ]; then - echo "PASS ($bazel_result)" - PASS=$((PASS + 1)) - else - echo "FAIL" - echo " Bazel: $bazel_result" - echo " CMake: $cmake_result" - FAIL=$((FAIL + 1)) - fi -} - -echo "=== Comparing Bazel vs CMake CLI results ===" -echo "" - -for graph in "${GRAPHS[@]}"; do - echo "Graph: $graph" - - # 1. greedy with various ordering methods - for method in bweight bmindegree_dynamic bratio_dynamic bmaximize default_order; do - for cap in 1 3; do - run_test "$graph" "greedy($method)" "$cap" \ - "command:\"run\" hypergraph { file_path:\"$graph\" format:\"hgr\" } config { algorithm_configs { algorithm_name:\"greedy\" string_params{key:\"ordering_method\" value:\"$method\"} } capacity:$cap short_name:\"test\" }" \ - "--graph $graph --algorithms greedy --ordering_method $method --capacity $cap" - done - done - - # 2. reductions + greedy + unfold - for cap in 1 3 5; do - run_test "$graph" "reductions+greedy+unfold" "$cap" \ - "command:\"run\" hypergraph { file_path:\"$graph\" format:\"hgr\" } config { algorithm_configs { algorithm_name:\"reductions\" string_params{key:\"assume_sorted\" value:\"true\"} } algorithm_configs { algorithm_name:\"greedy\" string_params{key:\"ordering_method\" value:\"bmindegree_dynamic\"} } algorithm_configs { algorithm_name:\"unfold\" string_params{key:\"assume_sorted\" value:\"true\"} } capacity:$cap short_name:\"test\" }" \ - "--graph $graph --algorithms reductions,greedy,unfold --ordering_method bmindegree_dynamic --capacity $cap" - done - - echo "" -done - -echo "=== Results: $PASS passed, $FAIL failed ===" -[ "$FAIL" -eq 0 ] || exit 1 diff --git a/build_regression/testdata/hyper.hgr b/build_regression/testdata/hyper.hgr deleted file mode 100644 index 74a06a5..0000000 --- a/build_regression/testdata/hyper.hgr +++ /dev/null @@ -1,7 +0,0 @@ -6 6 1 -3 1 2 3 -5 2 3 4 -2 4 5 -7 1 5 6 -4 3 6 -1 2 6 diff --git a/build_regression/testdata/medium.hgr b/build_regression/testdata/medium.hgr deleted file mode 100644 index e2e97ad..0000000 --- a/build_regression/testdata/medium.hgr +++ /dev/null @@ -1,11 +0,0 @@ -10 8 1 -5 1 2 -3 2 3 -7 3 4 -2 4 5 -8 5 6 -4 6 7 -6 7 8 -1 8 1 -9 1 3 5 -10 2 4 6 diff --git a/build_regression/testdata/small.hgr b/build_regression/testdata/small.hgr deleted file mode 100644 index 673a640..0000000 --- a/build_regression/testdata/small.hgr +++ /dev/null @@ -1,5 +0,0 @@ -4 4 -1 2 -2 3 -3 4 -1 4 From c1cef9209e2fe8a2ec70b8d0ec315f0b63a6780b Mon Sep 17 00:00:00 2001 From: "christian.schulz.phone@gmail.com" Date: Mon, 9 Mar 2026 13:41:12 +0000 Subject: [PATCH 18/18] Fix plot command: use C++ plotter via Bazel instead of outdated Python script --- reproducibility/reproducibility.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reproducibility/reproducibility.md b/reproducibility/reproducibility.md index 9e69f96..47f38d8 100644 --- a/reproducibility/reproducibility.md +++ b/reproducibility/reproducibility.md @@ -50,6 +50,8 @@ The runner executes all configured algorithm pipelines across the hypergraph ins ## 5. Plot Results +The C++ plotting tool is the maintained version. Run it via Bazel: + ```sh -python3 tools/plot/plot.py /visualisation.textproto +bazel run -c opt tools/plot:plot_cc /visualisation.textproto ```