diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e3b2e78 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +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) --target bmatching_cli app + + - 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 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/.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..506a72d --- /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" OFF) +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/README.md b/README.md index 08af21d..e545b93 100644 --- a/README.md +++ b/README.md @@ -1,272 +1,344 @@ # 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: +[![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) -**Engineering Hypergraph $b$-Matching Algorithms** +**Tame your hypergraphs -- fast, flexible b-matching at any scale.** -by Ernestine Großmann, Felix Joos, Henrik Reinstädtler and Christian Schulz +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). -The latest software can be found at https://github.com/HeiHGM/Bmatching +--- -## Available Algorithms +## Quick Start -### `greedy` +```sh +# Clone & build +git clone https://github.com/HeiHGM/Bmatching.git +cd Bmatching +./compile.sh -Greedly adding edges to a b matching. `ordering_method` can have the following values: +# Try it right away on bundled examples +./build/app/bmatching_cli --graph examples/small.hgr --algorithms greedy --capacity 1 -- `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. +# Weighted hypergraph with reductions +./build/app/bmatching_cli --graph examples/weighted.hgr --algorithms reductions,greedy,unfold --capacity 2 -### `ilp_exact` +# Compact output (weight, size, time only) +./build/app/bmatching_cli --graph examples/weighted.hgr --algorithms reductions,greedy,unfold --quiet +``` -Exactly solves a bmatching using gurobi with a `timeout` of seconds. You need to build with `--define gurobi=enabled` to use this. +--- -`timeout` needs to be specified in `double_params`. +## Install via Homebrew -### `ils` +```sh +brew install --HEAD HeiHGM/bmatching/bmatching +bmatching --graph examples/weighted.hgr --algorithms greedy --capacity 2 --quiet +``` -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`. +--- -### `local_improvement` +## Building from Source -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. +**Prerequisites:** CMake >= 3.20, a C++17 compiler, and ncurses dev headers. -### `presolved_ilp` +```sh +./compile.sh # Release build (default) +./compile.sh Debug # Debug build +``` -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. +Or manually: -### `reductions` & `unfold` +```sh +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j$(nproc) +``` -Reduces the graph with our reductions for b matching problem. At the end you should call `unfold` to obtain the unfolded solution. +All external dependencies (Abseil, Protobuf, GoogleTest, Easylogging++, wide-integer) are fetched automatically via CMake's FetchContent. -Optional setting (disable_hint: "true") to not set the hint to following steps that the solution is exact. +### Options -Optional setting (assume_sorted: "true") to use implementations that assume the edges/nodes to be sorted, so that certain operations get a better complexity. +| 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` | OFF | Enable easylogging++ log output | +| `BMATCHING_FREE_MEMORY_CHECK` | OFF | Enable free memory checking in runner | +| `BUILD_TESTING` | ON | Build unit tests | -### `scip` +Example with Gurobi and bSuitor enabled: -Solves the remainder of a graph (e.g. after applying reductions) exactly using ILP via SCIP. You have to specify a `timeout` (`double_params`). +```sh +cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBMATCHING_USE_GUROBI=ON \ + -DBMATCHING_USE_BSUITOR=ON +cmake --build build -j$(nproc) +``` -## External Algorithms +### Built Binary -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/ +After building, the CLI is available at: -You can enable building with bazel them with `bsuitor=enabled` and `karp_sipser=enabled`. -### bSuitor +``` +build/app/bmatching_cli +``` -**`name`**: `bsuitor` +--- -**Authors:** Khan et al. +## Command-Line Interface +```sh +bmatching_cli --graph --algorithms [options] +``` -### Karp-Sipser scaling +### Required Flags -Can only run on capacity 1. +| Flag | Description | +|------|-------------| +| `--graph ` | Path to the hypergraph file | +| `--algorithms ` | Comma-separated algorithm pipeline (e.g. `reductions,greedy,unfold`) | -**`name`** `kss`or `ksmd` +### Common Flags -**Authors:** Dufosse et al. +| 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 | Use newer ILS interface internally | +### Output Flags -**External options:** `kss`: KSS iterations in `scaling_iterations` int64_params. +| 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` | +--- -## Adding an algorithm +## Algorithms +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` -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. +Greedily adds edges to a b-matching using a chosen ordering strategy. +Available `--ordering_method` values: +| 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 | -## Compiling +```sh +# Greedy with weight-based ordering +bmatching_cli --graph input.hgr --algorithms greedy --ordering_method bweight --capacity 3 -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). +# Greedy with dynamic scaling (default ordering) +bmatching_cli --graph input.hgr --algorithms greedy --capacity 5 +``` -We extensivly use (text)proto as format for storing configs and results. The proto definition can be found in `app/app_io.proto`. +### `reductions` & `unfold` -### Gurobi macOS support +Reduces the graph size using b-matching reductions. Always pair with `unfold` afterwards to recover the full solution. -To use on a mac please uncomment `--cxxopt=-stdlib=libc++` in [`.bazelrc`](.bazelrc) if you manually compiled Gurobi with a different stdlib. +```sh +# Reductions + greedy on the remainder +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --capacity 5 -### Configurable settings +# With extra reduction rounds +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold \ + --max_runs 20 --reps 3 --capacity 1 +``` -You can enable gurobi with `gurobi=enabled`. +### `ils` -You can enable edge hashing used in Weighted Domination reduction with defining `hashing=enabled` while building `//app` or `//runner` targets, e.g.: +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). ```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. +# Greedy + ILS refinement +bmatching_cli --graph input.hgr --algorithms greedy,ils --max_tries 5000 --capacity 1 -Note on mac you have to comment `--cxxopt=-frecord-gcc-switches` out in .bazelrc -### openmp on mac +# Full pipeline: reductions + greedy + ILS + unfold +bmatching_cli --graph input.hgr --algorithms reductions,greedy,ils,unfold \ + --max_tries 10000 --capacity 3 -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. +# ILS in-place mode +bmatching_cli --graph input.hgr --algorithms greedy,ils \ + --max_tries 5000 --inplace --capacity 1 +``` -## Running one-off computations (`app`) +### `ilp_exact` -Build the `//app` target and supply a simple config via the `command_textproto` option: +Exactly solves the b-matching using Gurobi ILP. Requires `BMATCHING_USE_GUROBI=ON`. ```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" }' +# Exact solve with 5-minute timeout +bmatching_cli --graph input.hgr --algorithms ilp_exact --timeout 300 --capacity 1 + +# Reductions first, then exact solve on the remainder +bmatching_cli --graph input.hgr --algorithms reductions,ilp_exact,unfold \ + --timeout 300 --capacity 1 ``` -## Running an experiment -### Getting experiment data +### `presolved_ilp` -To get a collection of hypergraphs visit [Zenodo](https://doi.org/10.5281/zenodo.18225669). The storage of hypergraphs is organized as follows: +Solves the reduced (presolved) graph exactly using Gurobi ILP. Designed to run after `reductions`. Requires `BMATCHING_USE_GUROBI=ON`. -``` -├── graphs -│ └── walshaw -│ ├── collection.textproto -| ├── ... -│ ├── graph1.graph -│ ├── graph1.graph.hgr -│ ├── graph1.graph.weighted.hgr -│ └── graph1.graph.weighted.hgr.mtx -├── README.md -└── storage.textproto +```sh +bmatching_cli --graph input.hgr --algorithms reductions,presolved_ilp,unfold \ + --timeout 300 --capacity 1 ``` -- `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). +### `scip` -Example: +Solves the reduced graph exactly using the SCIP solver. Requires `BMATCHING_USE_SCIP=ON`. +```sh +bmatching_cli --graph input.hgr --algorithms reductions,scip,unfold \ + --timeout 120 --capacity 3 ``` -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" -} -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 +### `local_improvement` -To generate the an experiment use the `//runner:generate_experiment_config` target to generate an example. +Iteratively improves solution quality by solving small ILP subproblems around selected edges. Requires an a priori solution and either Gurobi or SCIP. -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 +# 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 + +# 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 ``` +### External Algorithms -This generates an `experiment.textproto` in the folder ``. You can modify this file with any text editor and edit the `run_configs`. +For comparison, the runner can link external solvers. Enable at build time: -### Running an experiment +| 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 | -Build the runner in opt settings: +--- -```sh -bazel build -c opt //runner -``` +## Output Examples -and run +### Default (text) ```sh -./bazel-bin/runner/runner --experiment_path +bmatching_cli --graph input.hgr --algorithms greedy --capacity 2 ``` -This will execute the tasks in `concurrent_processes` parallel processes as configured in `experiment.textproto` generated in the previous step. +Prints the full result as text to stdout. -### Analysing the results +### Quiet mode -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`. +```sh +bmatching_cli --graph input.hgr --algorithms reductions,greedy,unfold --quiet +``` ``` -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_" -} +weight: 42 +size: 15 +exact: false +time_ms: 1.234 ``` -To show this example use the `tools/plot/plot.py` by running: +### JSON to file ```sh -bazel run tools/plot -- //visualisation.textproto +bmatching_cli --graph input.hgr --algorithms greedy --output_format json --output result.json ``` -The plots are stored in `/vis/`. + +### Binary to file + +```sh +bmatching_cli --graph input.hgr --algorithms greedy --output_format binary --output result.pb +``` + +--- ## Logging -By default `HeiHGM::BMatching` does not log, you can enable it by adding `--define logging=enabled`. +Logging is disabled by default. To enable: set `BMATCHING_ENABLE_LOGGING=ON` at cmake configure time. -If build with logging enabled, you can define a verbosity level by supplying to HeiHGM::BMatching `--undefok=v --v=` +When enabled, control verbosity at runtime: -Verbosity levels: +```sh +bmatching_cli --graph input.hgr --algorithms greedy --undefok=v --v=8 +``` | Level | Functions | -| ----- | ----- | -| 8 | addToMatching, removeFromMatching | +|-------|-----------| +| 8 | `addToMatching`, `removeFromMatching` | -## Usage with `spack` +--- +## 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 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 + +cd /path/to/Bmatching spack env activate . spack install spack load -# build as used to be with spack=enabled +``` + +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} +} ``` diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..29805a3 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,41 @@ +# 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() + +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 + absl::strings + 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 new file mode 100644 index 0000000..7bfed6e --- /dev/null +++ b/app/algorithms/CMakeLists.txt @@ -0,0 +1,63 @@ +# 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() + +# 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 + 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/app/cli.cc b/app/cli.cc new file mode 100644 index 0000000..9ccbaf6 --- /dev/null +++ b/app/cli.cc @@ -0,0 +1,265 @@ +/** + * @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 "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" +#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, "Use newer ILS interface internally."); + +// --- 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; + +AlgorithmConfig buildConfig(const std::string &algo_name) { + AlgorithmConfig config; + config.set_algorithm_name(algo_name); + + if (algo_name == "greedy") { + config.mutable_string_params()->insert( + {"ordering_method", absl::GetFlag(FLAGS_ordering_method)}); + } else if (algo_name == "ils") { + config.mutable_int64_params()->insert( + {"max_tries", absl::GetFlag(FLAGS_max_tries)}); + if (absl::GetFlag(FLAGS_inplace)) { + config.mutable_string_params()->insert({"inplace", "true"}); + } + } else if (algo_name == "local_improvement") { + 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()->insert( + {"timeout", absl::GetFlag(FLAGS_timeout)}); + } else if (algo_name == "reductions") { + config.mutable_string_params()->insert({"assume_sorted", "true"}); + if (absl::GetFlag(FLAGS_disable_hint)) { + config.mutable_string_params()->insert({"disable_hint", "true"}); + } + 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()->insert({"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"); + + 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; + } + + 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/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/compile.sh b/compile.sh new file mode 100755 index 0000000..db2f7ff --- /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 bmatching_cli runner fork_runner generate_experiment_config binary_to_textproto 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/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 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/reproducibility/reproducibility.md b/reproducibility/reproducibility.md new file mode 100644 index 0000000..47f38d8 --- /dev/null +++ b/reproducibility/reproducibility.md @@ -0,0 +1,57 @@ +# 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 + +The C++ plotting tool is the maintained version. Run it via Bazel: + +```sh +bazel run -c opt tools/plot:plot_cc /visualisation.textproto +``` 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})