diff --git a/CMakeLists.txt b/CMakeLists.txt index a696f17..938c156 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,36 +13,48 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) FetchContent_Declare(fmt GIT_REPOSITORY https://github.com/fmtlib/fmt) FetchContent_MakeAvailable(fmt) -set(WARNING_FLAGS "-Wall -Wextra -Wpedantic -Wuninitialized -Wshadow -Wnull-dereference -Winit-self -Wunused-macros -Wwrite-strings -Wextra-semi") - -set(SANITIZERS_FLAGS "-fno-omit-frame-pointer -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=undefined") -set(OPT_FLAGS "-O3 -flto=auto") +if (MSVC) + #set(WARNING_FLAGS "/W4 /WX") # Treating all warnings as errors + set(WARNING_FLAGS "/W4") # Highest warning level but not treating warnings as errors + set(SANITIZERS_FLAGS "") + set(OPT_FLAGS "/O2") +else() + set(WARNING_FLAGS "-Wall -Wextra -Wpedantic -Wuninitialized -Wshadow -Wnull-dereference -Winit-self -Wunused-macros -Wwrite-strings -Wextra-semi") + set(SANITIZERS_FLAGS "-fno-omit-frame-pointer -fsanitize=address -fsanitize-address-use-after-scope -fsanitize=undefined") + set(OPT_FLAGS "-O3 -flto=auto") +endif() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${SANITIZERS_FLAGS}") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${OPT_FLAGS}") -set(SOURCE src/main.cpp) +set(SOURCE src/main.cpp) include_directories(src) add_executable(accft ${SOURCE}) -set(LIBRARIES fmt::fmt pthread dl m) + +if (WIN32) + set(LIBRARIES fmt::fmt) +else() + set(LIBRARIES fmt::fmt pthread dl m) +endif() + target_link_libraries(accft PUBLIC ${LIBRARIES}) if (SKBUILD) -set(PYTHON_BINDINGS ON) + set(PYTHON_BINDINGS ON) endif() if (PYTHON_BINDINGS) -message(STATUS "PYTHON_BINDINGS: ${PYTHON_BINDINGS}") + message(STATUS "PYTHON_BINDINGS: ${PYTHON_BINDINGS}") - # PyBind11 - FetchContent_Declare(pybind11 GIT_REPOSITORY https://github.com/pybind/pybind11.git GIT_TAG v2.13.6) - FetchContent_MakeAvailable(pybind11) # pybind11, essential - set(CMAKE_POSITION_INDEPENDENT_CODE ON) # The code needs to be compiled as PIC - # to build the shared lib for python. - set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) - # Ensure fmt is compiled with -fPIC - set_target_properties(fmt PROPERTIES POSITION_INDEPENDENT_CODE ON) + # PyBind11 + FetchContent_Declare(pybind11 GIT_REPOSITORY https://github.com/pybind/pybind11.git GIT_TAG v2.13.6) + FetchContent_MakeAvailable(pybind11) # pybind11, essential + set(CMAKE_POSITION_INDEPENDENT_CODE ON) # The code needs to be compiled as PIC + # to build the shared lib for python. + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + # Ensure fmt is compiled with -fPIC + set_target_properties(fmt PROPERTIES POSITION_INDEPENDENT_CODE ON) pybind11_add_module(_bindings ./src/pycft/_bindings.cpp) target_link_libraries(_bindings PUBLIC fmt::fmt ${LIBRARIES}) @@ -50,7 +62,7 @@ message(STATUS "PYTHON_BINDINGS: ${PYTHON_BINDINGS}") target_compile_options( _bindings PRIVATE "$<$:-Wall>") target_compile_definitions(_bindings PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) - install(TARGETS _bindings DESTINATION ./src/pycft/) + install(TARGETS _bindings DESTINATION src/pycft/) endif() ######################################## @@ -58,7 +70,7 @@ endif() ######################################## option(UNIT_TESTS "Build unit tests." OFF) message(STATUS "UNIT_TESTS: ${UNIT_TESTS}") -if (UNIT_TESTS) +if (UNIT_TESTS) enable_testing() add_subdirectory(test) -endif() +endif() \ No newline at end of file diff --git a/README.py.md b/README.py.md index 7e6b9df..5cd6c2b 100644 --- a/README.py.md +++ b/README.py.md @@ -5,16 +5,22 @@ SPDX-License-Identifier: MIT # Python Bindings for the AC-CFT Set Cover Heuristic +Implementation of the Caprara, Fischetti, and Toth algorithm for the [Set Covering problem](https://en.wikipedia.org/wiki/Set_cover_problem). +The original code is written in C++ and can be found [here](https://github.com/c4v4/cft). +This Python-packages wraps the C++ code using [pybind11](https://github.com/pybind/pybind11) and provides a simple interface to solve set cover instances. +*Caprara, A., Fischetti, M., & Toth, P. (1999). A Heuristic Method for the Set Covering Problem. Operations Research, 47(5), 730–743. [doi:10.1287/opre.47.5.730](https://doi.org/10.1287/opre.47.5.730)* ## Install -We will publish the package on PyPI soon. For now, you can install the package by cloning the repository and running the following command in the root directory: +The package can be installed via pip: ```bash -pip install --verbose . +pip install pycft ``` +It should be precompiled for Windows, Linux, and MacOS. If not precompiled version is available, it should be able to compile itself if a C++-compiler is available on the system. + ## Usage To use the `SetCoverSolver`, first create an instance of the solver. You can then add sets with their respective costs and solve the set cover problem. The solver will find the optimal selection of sets that covers all elements at the minimum cost. diff --git a/setup.py b/setup.py index 304198f..e666862 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,9 @@ def readme(): setup( # https://scikit-build.readthedocs.io/en/latest/usage.html#setup-options name="pycft", - version="0.0.1", - author="Francesco Cavaliere and Dominik Krupke", - license="LICENSE", + version="1.0.0", + author="Luca Accorsi, Francesco Cavaliere, and Dominik Krupke", + license="MIT", description="Python-Bindings for the C++-based Set Cover Algorithm CFT.", long_description=readme(), long_description_content_type="text/markdown", diff --git a/src/algorithms/Refinement.hpp b/src/algorithms/Refinement.hpp index 7997b70..b439455 100644 --- a/src/algorithms/Refinement.hpp +++ b/src/algorithms/Refinement.hpp @@ -43,7 +43,7 @@ namespace local { namespace { ) { ridx_t const nrows = rsize(inst.rows); - fix_fraction = min(1.0_F, fix_fraction * env.alpha); + fix_fraction = std::min(1.0_F, fix_fraction * env.alpha); if (best_sol.cost < prev_cost) fix_fraction = env.min_fixing; prev_cost = best_sol.cost; @@ -67,7 +67,7 @@ namespace local { namespace { gap_contrib += best_lagr_mult[i] * (cov - 1.0_F) / cov; reduced_cost -= best_lagr_mult[i]; } - gap_contrib += max(reduced_cost, 0.0_F); + gap_contrib += std::max(reduced_cost, 0.0_F); gap_contributions.push_back({j, gap_contrib}); } cft::sort(gap_contributions, [](CidxAndCost c) { return c.cost; }); diff --git a/src/algorithms/ThreePhase.hpp b/src/algorithms/ThreePhase.hpp index 6f31983..3e6eec1 100644 --- a/src/algorithms/ThreePhase.hpp +++ b/src/algorithms/ThreePhase.hpp @@ -136,7 +136,7 @@ class ThreePhase { for (ridx_t i = 0_R; i < rsize(inst.rows); ++i) for (cidx_t j : inst.rows[i]) { real_t candidate = inst.costs[j] / as_real(inst.cols[j].size()); - lagr_mult[i] = cft::min(lagr_mult[i], candidate); + lagr_mult[i] = std::min(lagr_mult[i], candidate); } } @@ -153,16 +153,16 @@ class ThreePhase { static void _build_tentative_core_instance(Instance const& inst, // in InstAndMap& core_inst // out ) { - static constexpr cidx_t min_row_coverage = 5_C; + static constexpr size_t min_row_coverage = 5; ridx_t const nrows = rsize(inst.rows); clear_inst(core_inst.inst); core_inst.col_map.clear(); // Select the first n columns of each row (there might be duplicates) - core_inst.col_map.reserve(checked_cast(as_cidx(nrows) * min_row_coverage)); + core_inst.col_map.reserve(checked_cast(nrows) * min_row_coverage); for (auto const& row : inst.rows) - for (size_t n = 0; n < min(row.size(), min_row_coverage); ++n) { + for (size_t n = 0; n < std::min(row.size(), min_row_coverage); ++n) { cidx_t j = row[n]; // column covering row i core_inst.col_map.push_back(j); } diff --git a/src/core/CliArgs.hpp b/src/core/CliArgs.hpp index 112e02c..ebf5835 100644 --- a/src/core/CliArgs.hpp +++ b/src/core/CliArgs.hpp @@ -147,7 +147,7 @@ inline Environment parse_cli_args(int argc, char const** argv) { auto args = cft::make_span(argv, checked_cast(argc)); auto env = Environment{}; - auto asize = size(args); + auto asize = args.size(); for (size_t a = 1; a < asize; ++a) { auto arg = StringView(args[a]); if (CFT_FLAG_MATCH(arg, HELP)) diff --git a/src/core/cft.hpp b/src/core/cft.hpp index 234aad5..3239ab9 100644 --- a/src/core/cft.hpp +++ b/src/core/cft.hpp @@ -91,14 +91,14 @@ constexpr real_t operator""_F(long double f) { // container as cidx_t. It also allows to avoid narrowing conversion warnings. template inline cidx_t csize(Cont const& cont) { - return as_cidx(cft::size(cont)); + return as_cidx(cont.size()); } // Since ridx_t could be any integer type, rsize provide a (debug) checked way to get the size of a // container as ridx_t. It also allows to avoid narrowing conversion warnings. template inline ridx_t rsize(Cont const& cont) { - return as_ridx(cft::size(cont)); + return as_ridx(cont.size()); } // Simple pair of column index and cost of some sort diff --git a/src/core/parsing.hpp b/src/core/parsing.hpp index de38c82..628c8e3 100644 --- a/src/core/parsing.hpp +++ b/src/core/parsing.hpp @@ -289,7 +289,7 @@ inline FileData parse_inst_and_initsol(Environment const& env) { if (env.use_unit_costs) { fdata.inst.costs.assign(csize(fdata.inst.costs), 1.0_F); - fdata.init_sol.cost = as_real(size(fdata.init_sol.idxs)); + fdata.init_sol.cost = as_real(fdata.init_sol.idxs.size()); } print<1>(env, "CFT> Instance size: {} x {}.\n", rsize(fdata.inst.rows), csize(fdata.inst.cols)); diff --git a/src/fixing/ColFixing.hpp b/src/fixing/ColFixing.hpp index 3cbb99b..6844457 100644 --- a/src/fixing/ColFixing.hpp +++ b/src/fixing/ColFixing.hpp @@ -43,7 +43,7 @@ class ColFixing { _select_non_overlapping_cols(inst, lagr_mult, row_coverage, cols_to_fix, reduced_costs); cidx_t no_overlap_ncols = csize(cols_to_fix); - cidx_t fix_at_least = csize(cols_to_fix) + max(1_C, as_cidx(orig_nrows / 200_R)); + cidx_t fix_at_least = csize(cols_to_fix) + std::max(1_C, as_cidx(orig_nrows / 200_R)); greedy(inst, lagr_mult, reduced_costs, cols_to_fix, limits::max(), fix_at_least); fix_columns_and_compute_maps(cols_to_fix, inst, fixing, old2new); diff --git a/src/greedy/Greedy.hpp b/src/greedy/Greedy.hpp index 0143a82..0d956b6 100644 --- a/src/greedy/Greedy.hpp +++ b/src/greedy/Greedy.hpp @@ -66,7 +66,8 @@ class Greedy { // Get the column-fraction with best scores if (good_scores.empty()) { - auto good_size = min(as_cidx(nrows_to_cover), csize(inst.cols) - csize(sol)); + cidx_t good_size = csize(inst.cols) - csize(sol); + good_size = std::min(good_size, as_cidx(nrows_to_cover)); good_scores = select_good_scores(score_info, good_size); worst_good_score = good_scores.back().score; } diff --git a/src/subgradient/Subgradient.hpp b/src/subgradient/Subgradient.hpp index 395dd0d..f17a540 100644 --- a/src/subgradient/Subgradient.hpp +++ b/src/subgradient/Subgradient.hpp @@ -40,19 +40,19 @@ class Subgradient { real_t& step_size, // inout std::vector& best_lagr_mult // inout ) { - size_t const nrows = size(orig_inst.rows); + size_t const nrows = orig_inst.rows.size(); real_t const max_real_lb = cutoff - env.epsilon; assert(!orig_inst.cols.empty() && "Empty instance"); assert(!core.inst.cols.empty() && "Empty core instance"); - assert(nrows == size(core.inst.rows) && "Incompatible instances"); + assert(nrows == core.inst.rows.size() && "Incompatible instances"); auto timer = Chrono<>(); auto next_step_size = local::StepSizeManager(20, step_size); auto should_exit = local::ExitConditionManager(300); - auto should_price = local::PricingManager(10ULL, min(1000ULL, nrows / 3ULL)); - real_t best_core_lb = limits::min(); - auto best_real_lb = limits::min(); + auto should_price = local::PricingManager(10ULL, std::min(1000ULL, nrows / 3ULL)); + real_t best_core_lb = limits::min(); + auto best_real_lb = limits::min(); _reset_lower_bounds(lb_sol, best_core_lb); lagr_mult = best_lagr_mult; @@ -95,7 +95,7 @@ class Subgradient { best_core_lb, step_size); - best_real_lb = max(best_real_lb, real_lb); + best_real_lb = std::max(best_real_lb, real_lb); _reset_lower_bounds(lb_sol, best_core_lb); if (env.timer.elapsed() > env.time_limit) @@ -244,4 +244,4 @@ class Subgradient { } // namespace cft -#endif /* CFT_SRC_SUBGRADIENT_SUBGRADIENT_HPP */ +#endif /* CFT_SRC_SUBGRADIENT_SUBGRADIENT_HPP */ \ No newline at end of file diff --git a/src/subgradient/utils.hpp b/src/subgradient/utils.hpp index 402ad2e..f6eb05f 100644 --- a/src/subgradient/utils.hpp +++ b/src/subgradient/utils.hpp @@ -33,8 +33,8 @@ namespace cft { namespace local { namespace { // Computes the next step size. real_t operator()(size_t iter, real_t lower_bound) { - min_lower_bound = min(min_lower_bound, lower_bound); - max_lower_bound = max(max_lower_bound, lower_bound); + min_lower_bound = std::min(min_lower_bound, lower_bound); + max_lower_bound = std::max(max_lower_bound, lower_bound); if (iter == next_update_iter) { next_update_iter += period; real_t diff = (max_lower_bound - min_lower_bound) / abs(max_lower_bound); @@ -101,11 +101,11 @@ namespace cft { namespace local { namespace { void update(real_t core_lb, real_t real_lb, real_t ub) { real_t const delta = (core_lb - real_lb) / ub; if (delta <= 1e-6_F) - period = min(max_period_increment, 10 * period); + period = std::min(max_period_increment, 10 * period); else if (delta <= 0.02_F) - period = min(max_period_increment, 5 * period); + period = std::min(max_period_increment, 5 * period); else if (delta <= 0.2_F) - period = min(max_period_increment, 2 * period); + period = std::min(max_period_increment, 2 * period); else period = 10; diff --git a/src/utils/StringView.hpp b/src/utils/StringView.hpp index 8158d67..0be44b0 100644 --- a/src/utils/StringView.hpp +++ b/src/utils/StringView.hpp @@ -131,7 +131,7 @@ struct StringView { } int compare(StringView other) const { - size_t min_size = cft::min(size(), other.size()); + size_t min_size = std::min(size(), other.size()); for (size_t n = 0; n < min_size; ++n) { if ((*this)[n] < other[n]) return -1; diff --git a/src/utils/utility.hpp b/src/utils/utility.hpp index 9e742d4..9bfb449 100644 --- a/src/utils/utility.hpp +++ b/src/utils/utility.hpp @@ -75,48 +75,14 @@ T abs(T val) { return val < T{} ? -val : val; } -/// Multi-arg max. NOTE: to avoid ambiguity, return type is always the first argument type -template -constexpr T1 max(T1 v1, T2 v2) { - return v1 > checked_cast(v2) ? v1 : checked_cast(v2); -} - -template -T1 max(T1 v1, T2 v2, Ts... tail) { - T1 mtail = max(v2, tail...); - return (v1 >= mtail ? v1 : mtail); -} - -/// Multi-arg min. NOTE: to avoid ambiguity, return type is always the first argument type -template -constexpr T1 min(T1 v1, T2 v2) { - return v1 < checked_cast(v2) ? v1 : checked_cast(v2); -} - -template -T1 min(T1 v1, T2 v2, Ts... tail) { - T1 mtail = min(v2, tail...); - return v1 <= mtail ? v1 : mtail; -} - ///////////// RANGES STUFF ///////////// -template -size_t size(C const& container) { - return container.size(); -} - -template -constexpr size_t size(C const (& /*unused*/)[N]) { - return N; -} - template using container_iterator_t = decltype(std::begin(std::declval())); template using container_value_type_t = no_cvr>())>; template -using container_size_type_t = decltype(cft::size(std::declval())); +using container_size_type_t = decltype(std::declval().size()); // Condition test operations template @@ -138,9 +104,9 @@ bool all(T const& container, O op) { // Return minimum value of a non-empty range template container_value_type_t range_min(C const& container, K key = {}) { - assert(cft::size(container) > 0ULL); + assert(container.size() > 0ULL); auto min_elem = container[0]; - for (size_t i = 1ULL; i < cft::size(container); ++i) + for (size_t i = 1ULL; i < container.size(); ++i) if (key(container[i]) < key(min_elem)) min_elem = container[i]; return min_elem; @@ -150,7 +116,7 @@ container_value_type_t range_min(C const& container, K key = {}) { template void remove_if(C& container, Op op) { size_t w = 0; - for (size_t i = 0ULL; i < cft::size(container); ++i) + for (size_t i = 0ULL; i < container.size(); ++i) if (!op(container[i])) container[w++] = container[i]; container.resize(w); diff --git a/test/parsing_unittests.cpp b/test/parsing_unittests.cpp index c7a64f8..3e136d1 100644 --- a/test/parsing_unittests.cpp +++ b/test/parsing_unittests.cpp @@ -179,6 +179,10 @@ TEST_CASE("Parsing SCP instance") { CHECK(csize(fdata.inst.cols) == 1000_C); CHECK((428.99_F < fdata.init_sol.cost && fdata.init_sol.cost < 429.01_F)); CHECK(sol.idxs == fdata.init_sol.idxs); + + env.use_unit_costs = true; + auto unit_fdata = parse_inst_and_initsol(env); + CHECK(abs(unit_fdata.init_sol.cost - unit_fdata.init_sol.idxs.size()) <= 0.001_F); std::remove(env.initsol_path.c_str()); } @@ -211,6 +215,10 @@ TEST_CASE("Parsing RAIL instance") { CHECK(csize(fdata.inst.cols) == 63009_C); CHECK(abs(fdata.init_sol.cost - 174.0_F) <= 0.01_F); CHECK(sol.idxs == fdata.init_sol.idxs); + + env.use_unit_costs = true; + auto unit_fdata = parse_inst_and_initsol(env); + CHECK(abs(unit_fdata.init_sol.cost - unit_fdata.init_sol.idxs.size()) <= 0.001_F); std::remove(env.initsol_path.c_str()); // Add more assertions as needed @@ -243,6 +251,10 @@ TEST_CASE("Parsing CVRP instance") { CHECK(csize(fdata.inst.cols) == 127262_C); CHECK(abs(fdata.init_sol.cost - 95480.0_F) < 0.01_F); CHECK(sol.idxs == fdata.init_sol.idxs); + + env.use_unit_costs = true; + auto unit_fdata = parse_inst_and_initsol(env); + CHECK(abs(unit_fdata.init_sol.cost - unit_fdata.init_sol.idxs.size()) <= 0.001_F); std::remove(env.initsol_path.c_str()); } @@ -282,6 +294,10 @@ TEST_CASE("Parsing MPS instance") { CHECK(csize(fdata.inst.cols) == 2187_C); CHECK(abs(fdata.init_sol.cost - 194.0_F) < 0.01_F); CHECK(sol.idxs == fdata.init_sol.idxs); + + env.use_unit_costs = true; + auto unit_fdata = parse_inst_and_initsol(env); + CHECK(abs(unit_fdata.init_sol.cost - unit_fdata.init_sol.idxs.size()) <= 0.001_F); std::remove(env.initsol_path.c_str()); } diff --git a/test/redundancy_unittests.cpp b/test/redundancy_unittests.cpp index ab6838d..0523ced 100644 --- a/test/redundancy_unittests.cpp +++ b/test/redundancy_unittests.cpp @@ -20,7 +20,7 @@ TEST_CASE("enumeration_removal removes redundant columns using implicit enumerat if ((k - 1) % 10 == 0) inst = make_easy_inst(k, 1000_C); - cidx_t sol_size = roll_dice(rnd, 0_C, min(csize(inst.cols) - 1_C, 200_C)); + cidx_t sol_size = roll_dice(rnd, 0_C,std::min(csize(inst.cols) - 1_C, 200_C)); auto sol = Solution(); for (cidx_t n = 0_C; n < sol_size; ++n) { cidx_t j = roll_dice(rnd, 0_C, csize(inst.cols) - 1_C); diff --git a/test/utility_unittests.cpp b/test/utility_unittests.cpp index d7ca7b4..1186716 100644 --- a/test/utility_unittests.cpp +++ b/test/utility_unittests.cpp @@ -103,34 +103,6 @@ TEST_CASE("test_abs") { } } -TEST_CASE("test_max") { - SUBCASE("Integers") { - CHECK(max(1, 2) == 2); - CHECK(max(1, 2, 3) == 3); - CHECK(max(3, 2, 1) == 3); - } - - SUBCASE("Floating-point numbers") { - CHECK(max(1.0, 2.0) == 2.0); - CHECK(max(1.0, 2.0, 3.0) == 3.0); - CHECK(max(3.0, 2.0, 1.0) == 3.0); - } -} - -TEST_CASE("test_min") { - SUBCASE("Integers") { - CHECK(min(1, 2) == 1); - CHECK(min(1, 2, 3) == 1); - CHECK(min(3, 2, 1) == 1); - } - - SUBCASE("Floating-point numbers") { - CHECK(min(1.0, 2.0) == 1.0); - CHECK(min(1.0, 2.0, 3.0) == 1.0); - CHECK(min(3.0, 2.0, 1.0) == 1.0); - } -} - TEST_CASE("test_any") { SUBCASE("Empty container") { std::vector empty_container; @@ -175,23 +147,6 @@ TEST_CASE("test_all") { } } -TEST_CASE("test_size") { - SUBCASE("Empty container") { - std::vector empty_container; - CHECK(cft::size(empty_container) == 0); - } - - SUBCASE("Non-empty container") { - std::array container = {1, 2, 3, 4, 5}; - CHECK(cft::size(container) == 5); - } - - SUBCASE("Array") { - int arr[] = {1, 2, 3, 4, 5}; - CHECK(size(arr) == 5); - } -} - TEST_CASE("test_range_min") { SUBCASE("Container with positive values") { auto container = std::vector{1, 2, 3, 4, 5};