From 3eb84ff803b3527d070914ba958cef50c74076be Mon Sep 17 00:00:00 2001 From: Florian Fontan Date: Thu, 3 Sep 2026 07:11:36 +0200 Subject: [PATCH] Add SMS++ as a Lagrangian relaxation solver Adds a GENERALIZEDASSIGNMENTSOLVER_USE_SMSPP build option and wires SMS++ into both Lagrangian relaxation algorithms (assignment and knapsack relaxations) as an alternative solver for the multiplier update subproblem, alongside dlib and ConicBundle. Bumps mathoptsolverscmake and enables SMS++ in CI. Getting this working on all three CI platforms required several supporting fixes, most of them for issues that only surfaced on Windows: - macOS: SMS++'s vendored CMakeLists.txt calls a plain find_package(Boost) and links against Boost::boost. Our FetchContent'd Boost only defines per-component targets, and Boost::headers is just an (essentially empty) meta package rather than an aggregate of every component's headers, so Boost::boost is now defined linked against every fetched library instead, letting SMS++ find whichever Boost headers it needs (including several header-only libraries -- algorithm, any, bimap, function, functional, multi_array, preprocessor -- added to BOOST_INCLUDE_LIBRARIES since SMS++ #includes them directly). - Ubuntu/macOS: install Eigen3 and netCDF-cxx (libeigen3-dev/ libnetcdf-c++4-dev on Ubuntu, eigen/netcdf-cxx via brew on macOS), which SMS++'s CMakeLists.txt requires unconditionally but which aren't present by default on CI runners. - Ubuntu: run `apt-get update` before `apt-get install`, since the runner image's cached package list pointed at a bzip2/libbz2-dev version that had since been pruned from the security mirror. - Windows: install netCDF-cxx4 and BZip2 via vcpkg (using the x64-windows-static triplet, to match this project's static CRT), wired in via CMAKE_PREFIX_PATH rather than the full vcpkg toolchain file, which globally wraps add_library()/find_package() and breaks unrelated FetchContent'd dependencies. - Bumps mathoptsolverscmake to fontanf/mathoptsolverscmake#11, which fixes several Windows-only issues in the SMS++/BundleSolver integration: a vendored FindCoinUtils.cmake's unconditional BZip2 requirement, Eigen3 not being installed by default, a broken GETOPT_LIBRARY-NOTFOUND link, vendored netCDF(Cxx) find modules that only work with the full vcpkg toolchain file, and CMake's own FindZLIB.cmake not recognizing vcpkg's static-triplet library name ("zs.lib"). Verified with full local builds and passing CI on all three platforms (Ubuntu, macOS, Windows). --- .github/workflows/build.yml | 13 ++++++++-- CMakeLists.txt | 1 + extern/CMakeLists.txt | 33 ++++++++++++++++++++++-- src/algorithms/lagrangian_relaxation.cpp | 17 ++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8caf1e4..a252d89 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,16 +31,25 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: sudo apt-get -y install liblapack-dev libbz2-dev libnauty-dev + run: | + sudo apt-get update + sudo apt-get -y install liblapack-dev libbz2-dev libnauty-dev libeigen3-dev libnetcdf-c++4-dev if: matrix.os == 'ubuntu-latest' - name: Install CLP (MacOS) run: | brew install clp brew install cbc + brew install eigen + brew install netcdf-cxx if: ${{ (matrix.os == 'macos-13') || (matrix.os == 'macos-latest') }} + - name: Install netCDF-cxx4 and BZip2 (Windows) + run: | + vcpkg install netcdf-cxx4:x64-windows-static bzip2:x64-windows-static + Add-Content -Path $env:GITHUB_ENV -Value "CMAKE_TOOLCHAIN_ARG=-DCMAKE_PREFIX_PATH=$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows-static" + if: matrix.os == 'windows-latest' - name: Build run: | - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGENERALIZEDASSIGNMENTSOLVER_USE_CLP=ON -DGENERALIZEDASSIGNMENTSOLVER_USE_CBC=ON -DGENERALIZEDASSIGNMENTSOLVER_USE_HIGHS=ON -DGENERALIZEDASSIGNMENTSOLVER_USE_DLIB=ON + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGENERALIZEDASSIGNMENTSOLVER_USE_CLP=ON -DGENERALIZEDASSIGNMENTSOLVER_USE_CBC=ON -DGENERALIZEDASSIGNMENTSOLVER_USE_HIGHS=ON -DGENERALIZEDASSIGNMENTSOLVER_USE_DLIB=ON -DGENERALIZEDASSIGNMENTSOLVER_USE_SMSPP=ON ${{ env.CMAKE_TOOLCHAIN_ARG }} cmake --build build --config Release cmake --install build --config Release --prefix install - name: Run tests diff --git a/CMakeLists.txt b/CMakeLists.txt index 8858634..3437600 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ option(GENERALIZEDASSIGNMENTSOLVER_USE_CPLEX "Use CPLEX" OFF) option(GENERALIZEDASSIGNMENTSOLVER_USE_KNITRO "Use Knitro" OFF) option(GENERALIZEDASSIGNMENTSOLVER_USE_DLIB "Use dlib" OFF) option(GENERALIZEDASSIGNMENTSOLVER_USE_CONICBUNDLE "Use ConicBundle" OFF) +option(GENERALIZEDASSIGNMENTSOLVER_USE_SMSPP "Use SMS++" OFF) # Avoid FetchContent warning. cmake_policy(SET CMP0135 NEW) diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 0b8807e..abdc838 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -3,14 +3,40 @@ include(FetchContent) # Fetch boost. set(BOOST_INCLUDE_LIBRARIES thread filesystem system program_options dynamic_bitset) +if(GENERALIZEDASSIGNMENTSOLVER_USE_SMSPP) + # Header-only Boost libraries #include'd directly by SMS++/BundleSolver + # (e.g. SMS++'s AbstractBlock.cpp includes ). + # Boost's modular CMake build only exposes headers for libraries listed + # here, even though the boost-*.tar.xz release ships all of Boost's + # sources. + list(APPEND BOOST_INCLUDE_LIBRARIES algorithm any bimap function functional multi_array preprocessor) +endif() set(BOOST_ENABLE_CMAKE ON) include(FetchContent) FetchContent_Declare( Boost URL https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.xz - EXCLUDE_FROM_ALL) + EXCLUDE_FROM_ALL + OVERRIDE_FIND_PACKAGE) FetchContent_MakeAvailable(Boost) +# Boost fetched via FetchContent only defines per-component targets (e.g. +# Boost::filesystem), and Boost::headers is just an (essentially empty) meta +# package, NOT an aggregate of every other component's headers -- unlike a +# classic system-wide Boost install, where one include path exposes all of +# Boost regardless of which libraries are actually used. The "Boost::boost" +# compatibility target normally provided by a plain find_package(Boost) is +# not created either, so define it here, linked against every library we +# fetched, so dependencies (e.g. SMS++) that link against Boost::boost +# directly and #include whichever Boost headers they need can still find +# them all. +if(NOT TARGET Boost::boost) + add_library(Boost::boost INTERFACE IMPORTED) + foreach(_boost_lib IN LISTS BOOST_INCLUDE_LIBRARIES) + target_link_libraries(Boost::boost INTERFACE Boost::${_boost_lib}) + endforeach() +endif() + # Fetch fontanf/optimizationtools. FetchContent_Declare( optimizationtools @@ -42,10 +68,13 @@ endif() if(GENERALIZEDASSIGNMENTSOLVER_USE_CONICBUNDLE) set(MATHOPTSOLVERSCMAKE_USE_CONICBUNDLE ON) endif() +if(GENERALIZEDASSIGNMENTSOLVER_USE_SMSPP) + set(MATHOPTSOLVERSCMAKE_USE_SMSPP ON) +endif() FetchContent_Declare( mathoptsolverscmake GIT_REPOSITORY https://github.com/fontanf/mathoptsolverscmake.git - GIT_TAG c312e55d551edd57e98edfd24d4e2330abdcbbd2 + GIT_TAG 7788a867fdfdd853ad9c0df7f0cdf15411a0f071 #SOURCE_DIR "${PROJECT_SOURCE_DIR}/../mathoptsolverscmake/" EXCLUDE_FROM_ALL) FetchContent_MakeAvailable(mathoptsolverscmake) diff --git a/src/algorithms/lagrangian_relaxation.cpp b/src/algorithms/lagrangian_relaxation.cpp index c7d1590..dabfc04 100644 --- a/src/algorithms/lagrangian_relaxation.cpp +++ b/src/algorithms/lagrangian_relaxation.cpp @@ -11,6 +11,9 @@ #if CONICBUNDLE_FOUND #include "mathoptsolverscmake/mathopt_conicbundle.hpp" #endif +#if SMSPP_FOUND +#include "mathoptsolverscmake/mathopt_smspp.hpp" +#endif #include "knapsacksolver/instance_builder.hpp" #include "knapsacksolver/algorithms/dynamic_programming_primal_dual.hpp" @@ -150,6 +153,13 @@ const LagrangianRelaxationAssignmentOutput generalizedassignmentsolver::lagrangi output.multipliers = mathoptsolverscmake::get_solution(model, solver); } #endif +#if SMSPP_FOUND + if (parameters.solver == mathoptsolverscmake::SolverName::Smspp) { + mathoptsolverscmake::SmsppOutput smspp_output = mathoptsolverscmake::solve_smspp(model); + bcnlp_bound = smspp_output.objective_value; + output.multipliers = smspp_output.solution; + } +#endif // Fill output. algorithm_formatter.update_bound(std::ceil(bcnlp_bound - FFOT_TOL), ""); @@ -259,6 +269,13 @@ const LagrangianRelaxationKnapsackOutput generalizedassignmentsolver::lagrangian output.multipliers = mathoptsolverscmake::get_solution(model, solver); } #endif +#if SMSPP_FOUND + if (parameters.solver == mathoptsolverscmake::SolverName::Smspp) { + mathoptsolverscmake::SmsppOutput smspp_output = mathoptsolverscmake::solve_smspp(model); + bcnlp_bound = smspp_output.objective_value; + output.multipliers = smspp_output.solution; + } +#endif // Fill output. algorithm_formatter.update_bound(std::ceil(bcnlp_bound - FFOT_TOL), "");