diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a91d9f..6ca4833 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,12 +11,13 @@ option(SETCOVERINGSOLVER_USE_CLP "Use Clp" OFF) option(SETCOVERINGSOLVER_USE_CBC "Use Cbc" OFF) option(SETCOVERINGSOLVER_USE_HIGHS "Use HiGHS" OFF) option(SETCOVERINGSOLVER_USE_XPRESS "Use FICO Xpress" OFF) +option(SETCOVERINGSOLVER_USE_ORTOOLS "Use OR-Tools" OFF) # Avoid FetchContent warning. cmake_policy(SET CMP0135 NEW) -# Require C++14. -set(CMAKE_CXX_STANDARD 14) +# Require C++17. +set(CMAKE_CXX_STANDARD 17) # Enable output of compile commands during generation. set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -24,6 +25,9 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Set MSVC_RUNTIME_LIBRARY. set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +# Update RPATH. +set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib") + # Add sub-directories. add_subdirectory(extern) add_subdirectory(src) diff --git a/README.md b/README.md index c94264f..18d730d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Some parts of the code have been contributed by [Guillaume Verger](https://githu - Mixed-integer linear program `--algorithm milp --solver highs` +- CP-SAT solved with OR-Tools `--algorithm cp-sat-ortools` + - Row weighting local search (unicost only) `--algorithm local-search-row-weighting` - Large neighborhood search `--algorithm large-neighborhood-search --maximum-number-of-iterations 100000 --maximum-number-of-iterations-without-improvement 10000` @@ -32,6 +34,23 @@ cmake --build build --config Release --parallel cmake --install build --config Release --prefix install ``` +To use OR-Tools +* Download and extract OR-Tools distribution binaries from the official website: https://developers.google.com/optimization/install/cpp +* Define an `ORTOOLSDIR` environment variable as the path to the extracted folder: +```shell +export ORTOOLSDIR="/home/$USER/or-tools_x86_64_Ubuntu-24.04_cpp_v9.10.4067" +``` +* Compile this project with option `-DSETCOVERINGSOLVER_USE_ORTOOLS=ON`: +```shell +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DSETCOVERINGSOLVER_USE_ORTOOLS=ON +cmake --build build --config Release --parallel +cmake --install build --config Release --prefix install +``` +* Update the `LD_LIBRARY_PATH` environment variable: +```shell +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$ORTOOLSDIR/lib" +``` + Download data: ```shell python3 scripts/download_data.py diff --git a/include/setcoveringsolver/algorithms/cp_sat_ortools.hpp b/include/setcoveringsolver/algorithms/cp_sat_ortools.hpp new file mode 100644 index 0000000..c4df187 --- /dev/null +++ b/include/setcoveringsolver/algorithms/cp_sat_ortools.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "setcoveringsolver/algorithm.hpp" + +namespace setcoveringsolver +{ + +struct CpSatOrtoolsParameters: Parameters +{ + /** Initial solution. */ + const Solution* initial_solution = NULL; +}; + +const Output cp_sat_ortools( + const Instance& instance, + const CpSatOrtoolsParameters& parameters = {}); + +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8ce9bca..0ddc8a1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries(SetCoveringSolver_set_covering PUBLIC OptimizationTools::graph Threads::Threads) add_library(SetCoveringSolver::set_covering ALIAS SetCoveringSolver_set_covering) +install(TARGETS SetCoveringSolver_set_covering) add_subdirectory(algorithms) @@ -31,6 +32,10 @@ if(SETCOVERINGSOLVER_BUILD_MAIN) SetCoveringSolver_trivial_bound SetCoveringSolver_clique_cover_bound Boost::program_options) + if(SETCOVERINGSOLVER_USE_ORTOOLS) + target_link_libraries(SetCoveringSolver_main PUBLIC + SetCoveringSolver_cp_sat_ortools) + endif() set_target_properties(SetCoveringSolver_main PROPERTIES OUTPUT_NAME "setcoveringsolver") install(TARGETS SetCoveringSolver_main) endif() diff --git a/src/algorithms/CMakeLists.txt b/src/algorithms/CMakeLists.txt index a7c016a..8c09276 100644 --- a/src/algorithms/CMakeLists.txt +++ b/src/algorithms/CMakeLists.txt @@ -26,6 +26,22 @@ target_link_libraries(SetCoveringSolver_milp PUBLIC MathOptSolversCMake::milp) add_library(SetCoveringSolver::milp ALIAS SetCoveringSolver_milp) +if(SETCOVERINGSOLVER_USE_ORTOOLS) + list(APPEND CMAKE_PREFIX_PATH "$ENV{ORTOOLSDIR}") + find_package(ortools CONFIG REQUIRED) + add_library(SetCoveringSolver_cp_sat_ortools) + target_sources(SetCoveringSolver_cp_sat_ortools PRIVATE + cp_sat_ortools.cpp) + target_include_directories(SetCoveringSolver_cp_sat_ortools PUBLIC + ${PROJECT_SOURCE_DIR}/include) + target_compile_definitions(SetCoveringSolver_cp_sat_ortools PUBLIC + ORTOOLS_FOUND=1) + target_link_libraries(SetCoveringSolver_cp_sat_ortools PUBLIC + SetCoveringSolver_set_covering + ortools::ortools) + add_library(SetCoveringSolver::cp_sat_ortools ALIAS SetCoveringSolver_cp_sat_ortools) +endif() + add_library(SetCoveringSolver_local_search_row_weighting) target_sources(SetCoveringSolver_local_search_row_weighting PRIVATE local_search_row_weighting.cpp) diff --git a/src/algorithms/cp_sat_ortools.cpp b/src/algorithms/cp_sat_ortools.cpp new file mode 100644 index 0000000..b1fb4e4 --- /dev/null +++ b/src/algorithms/cp_sat_ortools.cpp @@ -0,0 +1,74 @@ +#include "setcoveringsolver/algorithms/cp_sat_ortools.hpp" + +#include "setcoveringsolver/algorithm_formatter.hpp" + +#include "ortools/sat/cp_model.h" + +using namespace setcoveringsolver; + +const Output setcoveringsolver::cp_sat_ortools( + const Instance& instance, + const CpSatOrtoolsParameters& parameters) +{ + Output output(instance); + AlgorithmFormatter algorithm_formatter(parameters, output); + algorithm_formatter.start("CP-SAT (OR-Tools)"); + + // Reduction. + if (parameters.reduction_parameters.reduce) + return solve_reduced_instance(cp_sat_ortools, instance, parameters, algorithm_formatter, output); + + algorithm_formatter.print_header(); + + operations_research::sat::CpModelBuilder cp; + + // Variables. + std::vector x; + x.reserve(instance.number_of_sets()); + for (SetId set_id = 0; set_id < instance.number_of_sets(); ++set_id) + x.push_back(cp.NewBoolVar()); + + // Objective: minimize total weight + operations_research::sat::LinearExpr objective; + for (SetId set_id = 0; set_id < instance.number_of_sets(); ++set_id) { + const Set& set = instance.set(set_id); + objective += set.cost * x[set_id]; + } + cp.Minimize(objective); + + // Constraints: each element must be covered. + for (ElementId element_id = 0; + element_id < instance.number_of_elements(); + ++element_id) { + const Element& element = instance.element(element_id); + operations_research::sat::LinearExpr expr; + for (SetId set_id: element.sets) + expr += x[set_id]; + cp.AddGreaterOrEqual(expr, 1); + } + + operations_research::sat::SatParameters cp_parameters; + + // Write solver output to file. + // TODO + //cp_parameters.set_log_search_progress(true); + + // Set time limit. + cp_parameters.set_max_time_in_seconds(parameters.timer.remaining_time()); + + // Solve + operations_research::sat::CpSolverResponse resp = operations_research::sat::SolveWithParameters(cp.Build(), cp_parameters); + if (resp.status() == operations_research::sat::CpSolverStatus::OPTIMAL + || resp.status() == operations_research::sat::CpSolverStatus::FEASIBLE) { + Solution solution(instance); + for (SetId set_id = 0; set_id < instance.number_of_sets(); ++set_id) + if (SolutionIntegerValue(resp, x[set_id]) > 0) + solution.add(set_id); + algorithm_formatter.update_solution(solution, ""); + if (resp.status() == operations_research::sat::CpSolverStatus::OPTIMAL) + algorithm_formatter.update_bound(solution.cost(), ""); + } + + algorithm_formatter.end(); + return output; +} diff --git a/src/main.cpp b/src/main.cpp index d469c7d..385796d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,9 @@ #include "setcoveringsolver/solution.hpp" #include "setcoveringsolver/algorithms/greedy.hpp" #include "setcoveringsolver/algorithms/milp.hpp" +#if ORTOOLS_FOUND +#include "setcoveringsolver/algorithms/cp_sat_ortools.hpp" +#endif #include "setcoveringsolver/algorithms/local_search_row_weighting.hpp" #include "setcoveringsolver/algorithms/large_neighborhood_search.hpp" #include "setcoveringsolver/algorithms/trivial_bound.hpp" @@ -129,6 +132,12 @@ Output run( XPRSfree(); #endif return output; +#if ORTOOLS_FOUND + } else if (algorithm == "cp-sat-ortools") { + CpSatOrtoolsParameters parameters; + read_args(parameters, vm); + return cp_sat_ortools(instance, parameters); +#endif } else if (algorithm == "local-search-row-weighting") { LocalSearchRowWeightingParameters parameters; read_args(parameters, vm);