From 56fde74a114f0662e41d8bc5e5e9dad537968320 Mon Sep 17 00:00:00 2001 From: Mizux Seiha Date: Tue, 4 Mar 2025 21:06:53 +0100 Subject: [PATCH] move set_cover --- cmake/cpp.cmake | 2 + cmake/python.cmake | 10 +- ortools/algorithms/BUILD.bazel | 164 +---- ortools/algorithms/CMakeLists.txt | 1 - ortools/algorithms/python/BUILD.bazel | 27 - ortools/algorithms/python/CMakeLists.txt | 23 - ortools/python/setup.py.in | 5 +- ortools/sat/2d_try_edge_propagator.cc | 6 +- ortools/set_cover/BUILD.bazel | 287 +++++++++ ortools/set_cover/CMakeLists.txt | 51 ++ ortools/set_cover/assignment.cc | 127 ++++ ortools/set_cover/assignment.h | 105 ++++ ortools/set_cover/assignment_test.cc | 140 +++++ ortools/set_cover/base_types.h | 70 +++ ortools/set_cover/capacity.proto | 75 +++ ortools/set_cover/capacity_invariant.cc | 123 ++++ ortools/set_cover/capacity_invariant.h | 96 +++ ortools/set_cover/capacity_invariant_test.cc | 55 ++ ortools/set_cover/capacity_model.cc | 127 ++++ ortools/set_cover/capacity_model.h | 155 +++++ ortools/set_cover/capacity_model_test.cc | 135 +++++ ortools/set_cover/python/BUILD.bazel | 44 ++ ortools/set_cover/python/CMakeLists.txt | 42 ++ ortools/set_cover/python/set_cover.cc | 570 ++++++++++++++++++ ortools/set_cover/python/set_cover_test.py | 228 +++++++ ortools/set_cover/samples/CMakeLists.txt | 44 ++ .../set_cover/samples/code_samples_cc_test.sh | 26 + .../set_cover/samples/code_samples_py_test.sh | 26 + .../samples/set_cover.cc | 6 +- .../samples/set_cover.py | 2 +- .../{algorithms => set_cover}/set_cover.proto | 6 +- .../set_cover_heuristics.cc | 65 +- .../set_cover_heuristics.h | 10 +- .../set_cover_invariant.cc | 14 +- .../set_cover_invariant.h | 22 +- .../set_cover_lagrangian.cc | 10 +- .../set_cover_lagrangian.h | 12 +- .../set_cover_mip.cc | 7 +- .../{algorithms => set_cover}/set_cover_mip.h | 10 +- .../set_cover_model.cc | 59 +- .../set_cover_model.h | 96 ++- .../set_cover_reader.cc | 110 +++- .../set_cover_reader.h | 8 +- .../set_cover_solve.cc | 9 +- .../set_cover_test.cc | 11 +- 45 files changed, 2801 insertions(+), 420 deletions(-) create mode 100644 ortools/set_cover/BUILD.bazel create mode 100644 ortools/set_cover/CMakeLists.txt create mode 100644 ortools/set_cover/assignment.cc create mode 100644 ortools/set_cover/assignment.h create mode 100644 ortools/set_cover/assignment_test.cc create mode 100644 ortools/set_cover/base_types.h create mode 100644 ortools/set_cover/capacity.proto create mode 100644 ortools/set_cover/capacity_invariant.cc create mode 100644 ortools/set_cover/capacity_invariant.h create mode 100644 ortools/set_cover/capacity_invariant_test.cc create mode 100644 ortools/set_cover/capacity_model.cc create mode 100644 ortools/set_cover/capacity_model.h create mode 100644 ortools/set_cover/capacity_model_test.cc create mode 100644 ortools/set_cover/python/BUILD.bazel create mode 100644 ortools/set_cover/python/CMakeLists.txt create mode 100644 ortools/set_cover/python/set_cover.cc create mode 100644 ortools/set_cover/python/set_cover_test.py create mode 100644 ortools/set_cover/samples/CMakeLists.txt create mode 100755 ortools/set_cover/samples/code_samples_cc_test.sh create mode 100755 ortools/set_cover/samples/code_samples_py_test.sh rename ortools/{algorithms => set_cover}/samples/set_cover.cc (92%) rename ortools/{algorithms => set_cover}/samples/set_cover.py (97%) rename ortools/{algorithms => set_cover}/set_cover.proto (94%) rename ortools/{algorithms => set_cover}/set_cover_heuristics.cc (94%) rename ortools/{algorithms => set_cover}/set_cover_heuristics.h (98%) rename ortools/{algorithms => set_cover}/set_cover_invariant.cc (97%) rename ortools/{algorithms => set_cover}/set_cover_invariant.h (93%) rename ortools/{algorithms => set_cover}/set_cover_lagrangian.cc (98%) rename ortools/{algorithms => set_cover}/set_cover_lagrangian.h (95%) rename ortools/{algorithms => set_cover}/set_cover_mip.cc (96%) rename ortools/{algorithms => set_cover}/set_cover_mip.h (90%) rename ortools/{algorithms => set_cover}/set_cover_model.cc (91%) rename ortools/{algorithms => set_cover}/set_cover_model.h (83%) rename ortools/{algorithms => set_cover}/set_cover_reader.cc (76%) rename ortools/{algorithms => set_cover}/set_cover_reader.h (96%) rename ortools/{algorithms => set_cover}/set_cover_solve.cc (97%) rename ortools/{algorithms => set_cover}/set_cover_test.cc (98%) diff --git a/cmake/cpp.cmake b/cmake/cpp.cmake index 523a339286..8d653225ad 100644 --- a/cmake/cpp.cmake +++ b/cmake/cpp.cmake @@ -412,6 +412,7 @@ file(GLOB_RECURSE OR_TOOLS_PROTO_FILES RELATIVE ${PROJECT_SOURCE_DIR} "ortools/packing/*.proto" "ortools/sat/*.proto" "ortools/scheduling/*.proto" + "ortools/set_cover/*.proto" "ortools/util/*.proto" ) if(USE_PDLP OR BUILD_MATH_OPT) @@ -524,6 +525,7 @@ foreach(SUBPROJECT IN ITEMS lp_data packing scheduling + set_cover port util) add_subdirectory(ortools/${SUBPROJECT}) diff --git a/cmake/python.cmake b/cmake/python.cmake index f89fe3ff77..2d18f821df 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -150,6 +150,7 @@ file(GLOB_RECURSE OR_TOOLS_PROTO_PY_FILES RELATIVE ${PROJECT_SOURCE_DIR} "ortools/packing/*.proto" "ortools/sat/*.proto" "ortools/scheduling/*.proto" + "ortools/set_cover/*.proto" "ortools/util/*.proto" ) list(REMOVE_ITEM OR_TOOLS_PROTO_PY_FILES "ortools/constraint_solver/demon_profiler.proto") @@ -288,6 +289,7 @@ foreach(SUBPROJECT IN ITEMS constraint_solver sat scheduling + set_cover util) add_subdirectory(ortools/${SUBPROJECT}/python) endforeach() @@ -345,6 +347,8 @@ file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/sat/python/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/sat/colab/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/scheduling/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/scheduling/python/__init__.py CONTENT "") +file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/set_cover/__init__.py CONTENT "") +file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/set_cover/python/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/util/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/util/python/__init__.py CONTENT "") @@ -645,6 +649,8 @@ add_custom_command( $ ${PYTHON_PROJECT}/sat/python COMMAND ${CMAKE_COMMAND} -E copy $ ${PYTHON_PROJECT}/scheduling/python + COMMAND ${CMAKE_COMMAND} -E copy + $ ${PYTHON_PROJECT}/set_cover/python COMMAND ${CMAKE_COMMAND} -E copy $ ${PYTHON_PROJECT}/util/python COMMAND ${CMAKE_COMMAND} -E touch ${PROJECT_BINARY_DIR}/python/pybind11_timestamp @@ -653,7 +659,6 @@ add_custom_command( DEPENDS init_pybind11 knapsack_solver_pybind11 - set_cover_pybind11 linear_sum_assignment_pybind11 max_flow_pybind11 min_cost_flow_pybind11 @@ -664,6 +669,7 @@ add_custom_command( $ cp_model_helper_pybind11 rcpsp_pybind11 + set_cover_pybind11 sorted_interval_list_pybind11 WORKING_DIRECTORY python COMMAND_EXPAND_LISTS) @@ -689,7 +695,6 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E remove -f stub_timestamp COMMAND ${stubgen_EXECUTABLE} -p ortools.init.python.init --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.algorithms.python.knapsack_solver --output . - COMMAND ${stubgen_EXECUTABLE} -p ortools.algorithms.python.set_cover --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.linear_sum_assignment --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.max_flow --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.min_cost_flow --output . @@ -701,6 +706,7 @@ add_custom_command( COMMAND ${stubgen_EXECUTABLE} -p ortools.pdlp.python.pdlp --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.sat.python.cp_model_helper --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.scheduling.python.rcpsp --output . + COMMAND ${stubgen_EXECUTABLE} -p ortools.set_cover.python.set_cover --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.util.python.sorted_interval_list --output . COMMAND ${CMAKE_COMMAND} -E touch ${PROJECT_BINARY_DIR}/python/stub_timestamp MAIN_DEPENDENCY diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index 186c2eccb4..eb0ea9b59e 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -12,10 +12,7 @@ # limitations under the License. load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") -load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") -load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@rules_cc//cc:defs.bzl", "cc_library") -load("@rules_python//python:proto.bzl", "py_proto_library") package(default_visibility = ["//visibility:public"]) @@ -68,6 +65,7 @@ cc_library( hdrs = ["binary_search.h"], deps = [ "//ortools/base", + "@com_google_absl//absl/base:log_severity", "@com_google_absl//absl/functional:function_ref", "@com_google_absl//absl/log:check", "@com_google_absl//absl/numeric:int128", @@ -166,10 +164,9 @@ cc_test( srcs = ["hungarian_test.cc"], deps = [ ":hungarian", - "//ortools/base", "//ortools/base:gmock_main", "//ortools/base:map_util", - "//ortools/base:types", + "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/random:distributions", "@com_google_absl//absl/types:span", @@ -250,163 +247,6 @@ cc_test( # query matching library. -# Weighted set covering library. - -proto_library( - name = "set_cover_proto", - srcs = ["set_cover.proto"], - deps = [ - "//ortools/util:int128_proto", - ], -) - -cc_proto_library( - name = "set_cover_cc_proto", - deps = [":set_cover_proto"], -) - -py_proto_library( - name = "set_cover_py_pb2", - deps = [":set_cover_proto"], -) - -cc_library( - name = "set_cover_lagrangian", - srcs = ["set_cover_lagrangian.cc"], - hdrs = ["set_cover_lagrangian.h"], - deps = [ - ":adjustable_k_ary_heap", - ":set_cover_invariant", - ":set_cover_model", - "//ortools/base:threadpool", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/synchronization", - ], -) - -cc_library( - name = "set_cover_model", - srcs = ["set_cover_model.cc"], - hdrs = ["set_cover_model.h"], - deps = [ - ":radix_sort", - ":set_cover_cc_proto", - "//ortools/base:intops", - "//ortools/base:strong_vector", - "@com_google_absl//absl/log", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/numeric:bits", - "@com_google_absl//absl/random", - "@com_google_absl//absl/random:distributions", - "@com_google_absl//absl/strings", - "@com_google_absl//absl/strings:str_format", - "@com_google_absl//absl/types:span", - ], -) - -cc_library( - name = "set_cover_invariant", - srcs = ["set_cover_invariant.cc"], - hdrs = ["set_cover_invariant.h"], - deps = [ - ":set_cover_cc_proto", - ":set_cover_model", - "//ortools/base", - "//ortools/base:mathutil", - "@com_google_absl//absl/log", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/types:span", - ], -) - -cc_library( - name = "set_cover_heuristics", - srcs = ["set_cover_heuristics.cc"], - hdrs = ["set_cover_heuristics.h"], - deps = [ - ":adjustable_k_ary_heap", - ":set_cover_invariant", - ":set_cover_model", - "//ortools/base", - "@com_google_absl//absl/base", - "@com_google_absl//absl/log", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/numeric:bits", - "@com_google_absl//absl/random:distributions", - "@com_google_absl//absl/types:span", - ], -) - -cc_library( - name = "set_cover_mip", - srcs = ["set_cover_mip.cc"], - hdrs = ["set_cover_mip.h"], - deps = [ - ":set_cover_invariant", - ":set_cover_model", - "//ortools/linear_solver", - "//ortools/lp_data:base", - "@com_google_absl//absl/log", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/types:span", - ], -) - -cc_library( - name = "set_cover_reader", - srcs = ["set_cover_reader.cc"], - hdrs = ["set_cover_reader.h"], - deps = [ - ":set_cover_cc_proto", - ":set_cover_model", - "//ortools/base:file", - "//ortools/util:filelineiter", - "@com_google_absl//absl/log", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/strings", - "@com_google_absl//absl/strings:str_format", - "@com_google_absl//absl/strings:string_view", - ], -) - -cc_binary( - name = "set_cover_solve", - srcs = ["set_cover_solve.cc"], - deps = [ - ":set_cover_heuristics", - ":set_cover_invariant", - ":set_cover_model", - ":set_cover_reader", - "//ortools/base", - "//ortools/base:timer", - "@com_google_absl//absl/flags:flag", - "@com_google_absl//absl/log", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/strings", - "@com_google_absl//absl/time", - ], -) - -cc_test( - name = "set_cover_test", - size = "medium", - timeout = "eternal", - srcs = ["set_cover_test.cc"], - deps = [ - ":set_cover_cc_proto", - ":set_cover_heuristics", - ":set_cover_invariant", - ":set_cover_mip", - ":set_cover_model", - "//ortools/base:gmock_main", - "//ortools/base:parse_text_proto", - "@com_google_absl//absl/log", - "@com_google_absl//absl/log:check", - "@com_google_absl//absl/strings", - "@com_google_benchmark//:benchmark", - ], -) - # Graph automorphism libraries. cc_library( name = "dense_doubly_linked_list", diff --git a/ortools/algorithms/CMakeLists.txt b/ortools/algorithms/CMakeLists.txt index 5ec8582202..9fa03e5279 100644 --- a/ortools/algorithms/CMakeLists.txt +++ b/ortools/algorithms/CMakeLists.txt @@ -36,7 +36,6 @@ target_link_libraries(${NAME} PRIVATE if(BUILD_TESTING) file(GLOB _TEST_SRCS "*_test.cc") list(FILTER _TEST_SRCS EXCLUDE REGEX ".*_stress_test.cc") - list(FILTER _TEST_SRCS EXCLUDE REGEX "set_cover_test.cc") foreach(_FULL_FILE_NAME IN LISTS _TEST_SRCS) get_filename_component(_NAME ${_FULL_FILE_NAME} NAME_WE) get_filename_component(_FILE_NAME ${_FULL_FILE_NAME} NAME) diff --git a/ortools/algorithms/python/BUILD.bazel b/ortools/algorithms/python/BUILD.bazel index 7b50163e0b..753bc93934 100644 --- a/ortools/algorithms/python/BUILD.bazel +++ b/ortools/algorithms/python/BUILD.bazel @@ -78,30 +78,3 @@ py_test( requirement("absl-py"), ], ) - -# set_cover -pybind_extension( - name = "set_cover", - srcs = ["set_cover.cc"], - visibility = ["//visibility:public"], - deps = [ - "//ortools/algorithms:set_cover_cc_proto", - "//ortools/algorithms:set_cover_heuristics", - "//ortools/algorithms:set_cover_invariant", - "//ortools/algorithms:set_cover_model", - "//ortools/algorithms:set_cover_reader", - "@com_google_absl//absl/strings", - "@pybind11_protobuf//pybind11_protobuf:native_proto_caster", - ], -) - -py_test( - name = "set_cover_test", - srcs = ["set_cover_test.py"], - python_version = "PY3", - deps = [ - ":set_cover", - "//ortools/algorithms:set_cover_py_pb2", - requirement("absl-py"), - ], -) diff --git a/ortools/algorithms/python/CMakeLists.txt b/ortools/algorithms/python/CMakeLists.txt index 46904dd991..87e29473d1 100644 --- a/ortools/algorithms/python/CMakeLists.txt +++ b/ortools/algorithms/python/CMakeLists.txt @@ -31,29 +31,6 @@ endif() target_link_libraries(knapsack_solver_pybind11 PRIVATE ${PROJECT_NAMESPACE}::ortools) add_library(${PROJECT_NAMESPACE}::knapsack_solver_pybind11 ALIAS knapsack_solver_pybind11) -# set_cover -pybind11_add_module(set_cover_pybind11 MODULE set_cover.cc) -set_target_properties(set_cover_pybind11 PROPERTIES - LIBRARY_OUTPUT_NAME "set_cover") - -# note: macOS is APPLE and also UNIX ! -if(APPLE) - set_target_properties(set_cover_pybind11 PROPERTIES - SUFFIX ".so" - INSTALL_RPATH "@loader_path;@loader_path/../../../${PYTHON_PROJECT}/.libs" - ) -elseif(UNIX) - set_target_properties(set_cover_pybind11 PROPERTIES - INSTALL_RPATH "$ORIGIN:$ORIGIN/../../../${PYTHON_PROJECT}/.libs" - ) -endif() - -target_link_libraries(set_cover_pybind11 PRIVATE - ${PROJECT_NAMESPACE}::ortools - pybind11_native_proto_caster -) -add_library(${PROJECT_NAMESPACE}::set_cover_pybind11 ALIAS set_cover_pybind11) - if(BUILD_TESTING) file(GLOB PYTHON_SRCS "*_test.py") foreach(FILE_NAME IN LISTS PYTHON_SRCS) diff --git a/ortools/python/setup.py.in b/ortools/python/setup.py.in index 370ec8fd2a..6db47dd42d 100644 --- a/ortools/python/setup.py.in +++ b/ortools/python/setup.py.in @@ -59,7 +59,6 @@ setup( ], '@PYTHON_PROJECT@.algorithms.python':[ '$', - '$', '*.pyi' ], '@PYTHON_PROJECT@.bop':['*.pyi'], @@ -113,6 +112,10 @@ setup( '$', '*.pyi' ], + '@PYTHON_PROJECT@.set_cover.python':[ + '$', + '*.pyi' + ], '@PYTHON_PROJECT@.util.python':[ '$', '*.pyi' diff --git a/ortools/sat/2d_try_edge_propagator.cc b/ortools/sat/2d_try_edge_propagator.cc index e5a49ba49d..f625a011c8 100644 --- a/ortools/sat/2d_try_edge_propagator.cc +++ b/ortools/sat/2d_try_edge_propagator.cc @@ -21,9 +21,6 @@ #include "absl/algorithm/container.h" #include "absl/log/check.h" -#include "ortools/algorithms/set_cover_heuristics.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/logging.h" #include "ortools/base/stl_util.h" #include "ortools/sat/diffn_util.h" @@ -33,6 +30,9 @@ #include "ortools/sat/no_overlap_2d_helper.h" #include "ortools/sat/synchronization.h" #include "ortools/sat/util.h" +#include "ortools/set_cover/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { namespace sat { diff --git a/ortools/set_cover/BUILD.bazel b/ortools/set_cover/BUILD.bazel new file mode 100644 index 0000000000..94b753170f --- /dev/null +++ b/ortools/set_cover/BUILD.bazel @@ -0,0 +1,287 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library") +load("@rules_cc//cc:defs.bzl", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +# Description: +# A solver for weighted set covering with side constraints + +proto_library( + name = "set_cover_proto", + srcs = ["set_cover.proto"], + deps = [ + "//ortools/util:int128_proto", + ], +) + +cc_proto_library( + name = "set_cover_cc_proto", + deps = [":set_cover_proto"], +) + +py_proto_library( + name = "set_cover_py_pb2", + deps = [":set_cover_proto"], +) + +cc_library( + name = "base_types", + hdrs = ["base_types.h"], + deps = [ + "//ortools/base:intops", + "//ortools/base:strong_vector", + ], +) + +cc_library( + name = "set_cover_lagrangian", + srcs = ["set_cover_lagrangian.cc"], + hdrs = ["set_cover_lagrangian.h"], + deps = [ + ":base_types", + ":set_cover_invariant", + ":set_cover_model", + "//ortools/algorithms:adjustable_k_ary_heap", + "//ortools/base:threadpool", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/synchronization", + ], +) + +cc_library( + name = "set_cover_model", + srcs = ["set_cover_model.cc"], + hdrs = ["set_cover_model.h"], + deps = [ + ":base_types", + ":set_cover_cc_proto", + "//ortools/algorithms:radix_sort", + "//ortools/base:intops", + "//ortools/base:strong_vector", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/numeric:bits", + "@com_google_absl//absl/random", + "@com_google_absl//absl/random:distributions", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/types:span", + ], +) + +cc_library( + name = "set_cover_invariant", + srcs = ["set_cover_invariant.cc"], + hdrs = ["set_cover_invariant.h"], + deps = [ + ":base_types", + ":set_cover_cc_proto", + ":set_cover_model", + "//ortools/base", + "//ortools/base:mathutil", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/types:span", + ], +) + +cc_library( + name = "set_cover_heuristics", + srcs = ["set_cover_heuristics.cc"], + hdrs = ["set_cover_heuristics.h"], + deps = [ + ":base_types", + ":set_cover_invariant", + ":set_cover_model", + "//ortools/algorithms:adjustable_k_ary_heap", + "//ortools/base", + "@com_google_absl//absl/base", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/numeric:bits", + "@com_google_absl//absl/random:distributions", + "@com_google_absl//absl/types:span", + ], +) + +cc_library( + name = "set_cover_mip", + srcs = ["set_cover_mip.cc"], + hdrs = ["set_cover_mip.h"], + deps = [ + ":base_types", + ":set_cover_invariant", + ":set_cover_model", + "//ortools/linear_solver", + "//ortools/lp_data:base", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/types:span", + ], +) + +cc_library( + name = "set_cover_reader", + srcs = ["set_cover_reader.cc"], + hdrs = ["set_cover_reader.h"], + deps = [ + ":base_types", + ":set_cover_cc_proto", + ":set_cover_model", + "//ortools/base:file", + "//ortools/util:filelineiter", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_library( + name = "assignment", + srcs = ["assignment.cc"], + hdrs = ["assignment.h"], + deps = [ + ":base_types", + ":capacity_invariant", + ":set_cover_invariant", + ":set_cover_model", + "//ortools/base:mathutil", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + ], +) + +cc_test( + name = "assignment_test", + srcs = ["assignment_test.cc"], + deps = [ + ":assignment", + ":set_cover_invariant", + ":set_cover_model", + "//ortools/base:gmock_main", + "@com_google_absl//absl/log:check", + ], +) + +cc_binary( + name = "set_cover_solve", + srcs = ["set_cover_solve.cc"], + deps = [ + ":base_types", + ":set_cover_heuristics", + ":set_cover_invariant", + ":set_cover_model", + ":set_cover_reader", + "//ortools/base", + "//ortools/base:timer", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + ], +) + +cc_test( + name = "set_cover_test", + size = "medium", + timeout = "eternal", + srcs = ["set_cover_test.cc"], + deps = [ + ":base_types", + ":set_cover_cc_proto", + ":set_cover_heuristics", + ":set_cover_invariant", + ":set_cover_mip", + ":set_cover_model", + "//ortools/base:gmock_main", + "//ortools/base:parse_text_proto", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", + "@com_google_benchmark//:benchmark", + ], +) + +# Side constraint: capacity. + +proto_library( + name = "capacity_proto", + srcs = ["capacity.proto"], +) + +cc_proto_library( + name = "capacity_cc_proto", + deps = [":capacity_proto"], +) + +py_proto_library( + name = "capacity_py_pb2", + deps = [":capacity_proto"], +) + +cc_library( + name = "capacity_model", + srcs = ["capacity_model.cc"], + hdrs = ["capacity_model.h"], + deps = [ + ":base_types", + ":capacity_cc_proto", + ":set_cover_model", + "//ortools/base:intops", + "//ortools/base:strong_vector", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + ], +) + +cc_test( + name = "capacity_model_test", + srcs = ["capacity_model_test.cc"], + deps = [ + ":capacity_model", + "//ortools/base:gmock_main", + ], +) + +cc_library( + name = "capacity_invariant", + srcs = ["capacity_invariant.cc"], + hdrs = ["capacity_invariant.h"], + deps = [ + ":base_types", + ":capacity_model", + ":set_cover_model", + "//ortools/util:saturated_arithmetic", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + ], +) + +cc_test( + name = "capacity_invariant_test", + srcs = ["capacity_invariant_test.cc"], + deps = [ + ":capacity_invariant", + ":capacity_model", + ":set_cover_model", + "//ortools/base:gmock_main", + ], +) diff --git a/ortools/set_cover/CMakeLists.txt b/ortools/set_cover/CMakeLists.txt new file mode 100644 index 0000000000..74879b33c5 --- /dev/null +++ b/ortools/set_cover/CMakeLists.txt @@ -0,0 +1,51 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +file(GLOB _SRCS "*.h" "*.cc") +list(FILTER _SRCS EXCLUDE REGEX ".*/.*_test.cc") +list(FILTER _SRCS EXCLUDE REGEX ".*/set_cover_solve.cc") + +set(NAME ${PROJECT_NAME}_set_cover) + +# Will be merge in libortools.so +#add_library(${NAME} STATIC ${_SRCS}) +add_library(${NAME} OBJECT ${_SRCS}) +set_target_properties(${NAME} PROPERTIES + POSITION_INDEPENDENT_CODE ON + ) +target_include_directories(${NAME} PRIVATE + ${PROJECT_SOURCE_DIR} + ${PROJECT_BINARY_DIR}) +target_link_libraries(${NAME} PRIVATE + protobuf::libprotobuf + ${PROJECT_NAMESPACE}::ortools_proto) +#add_library(${PROJECT_NAMESPACE}::set_cover ALIAS ${NAME}) + +if(BUILD_TESTING) + file(GLOB _TEST_SRCS "*_test.cc") + foreach(_FULL_FILE_NAME IN LISTS _TEST_SRCS) + get_filename_component(_NAME ${_FULL_FILE_NAME} NAME_WE) + get_filename_component(_FILE_NAME ${_FULL_FILE_NAME} NAME) + ortools_cxx_test( + NAME + set_cover_${_NAME} + SOURCES + ${_FILE_NAME} + LINK_LIBRARIES + benchmark::benchmark + GTest::gtest + GTest::gtest_main + GTest::gmock + ) + endforeach() +endif() diff --git a/ortools/set_cover/assignment.cc b/ortools/set_cover/assignment.cc new file mode 100644 index 0000000000..541b6db419 --- /dev/null +++ b/ortools/set_cover/assignment.cc @@ -0,0 +1,127 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/set_cover/assignment.h" + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "ortools/base/mathutil.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/capacity_invariant.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { + +void SetCoverAssignment::Clear() { + cost_ = Cost(0.0); + values_.assign(model_.subset_costs().size(), false); + DCHECK_EQ(values_.size(), model_.subset_costs().size()) + << "The cost vector (length: " << model_.subset_costs().size() + << ") is inconsistent with the assignment (length: " << values_.size() + << ")"; +} + +void SetCoverAssignment::AttachInvariant(SetCoverInvariant* i) { + CHECK(constraint_ == nullptr); + constraint_ = i; +} + +void SetCoverAssignment::AttachInvariant(CapacityInvariant* i) { + CHECK(constraint_ != nullptr); + side_constraints_.push_back(i); + // TODO(user): call i->SetAssignment or similar so that each and every + // constraint uses the same solution storage. +} + +void SetCoverAssignment::SetValue( + SubsetIndex subset, bool is_selected, + SetCoverInvariant::ConsistencyLevel set_cover_consistency) { + DVLOG(1) << "[Assignment] Subset " << subset << " becoming " << is_selected + << "; used to be " << values_[subset]; + + DCHECK(CheckConsistency()); + if (values_[subset] == is_selected) return; + + values_[subset] = is_selected; + if (is_selected) { + cost_ += model_.subset_costs()[subset]; + if (constraint_) { + constraint_->Select(subset, set_cover_consistency); + } + for (CapacityInvariant* const capacity_constraint : side_constraints_) { + capacity_constraint->Select(subset); + } + } else { + cost_ -= model_.subset_costs()[subset]; + if (constraint_) { + constraint_->Deselect(subset, set_cover_consistency); + } + for (CapacityInvariant* const capacity_constraint : side_constraints_) { + capacity_constraint->Deselect(subset); + } + } + DCHECK(CheckConsistency()); +} + +SetCoverSolutionResponse SetCoverAssignment::ExportSolutionAsProto() const { + SetCoverSolutionResponse message; + message.set_num_subsets(values_.size()); + message.set_cost(cost_); + for (SubsetIndex subset(0); + subset < SubsetIndex(model_.subset_costs().size()); ++subset) { + if (values_[subset]) { + message.add_subset(subset.value()); + } + } + return message; +} + +void SetCoverAssignment::LoadAssignment(const SubsetBoolVector& solution) { + DCHECK_EQ(solution.size(), values_.size()); + values_ = solution; + cost_ = ComputeCost(values_); +} + +void SetCoverAssignment::ImportSolutionFromProto( + const SetCoverSolutionResponse& message) { + values_.resize(SubsetIndex(message.num_subsets()), false); + cost_ = Cost(0.0); + for (auto s : message.subset()) { + SubsetIndex subset(s); + values_[subset] = true; + cost_ += model_.subset_costs()[subset]; + } + CHECK(MathUtil::AlmostEquals(message.cost(), cost_)); + DCHECK(CheckConsistency()); +} + +bool SetCoverAssignment::CheckConsistency() const { + Cost cst = ComputeCost(values_); + CHECK(MathUtil::AlmostEquals(cost_, cst)); + return true; +} + +Cost SetCoverAssignment::ComputeCost(const SubsetBoolVector& choices) const { + Cost cst = 0.0; + SubsetIndex subset(0); + for (const bool b : choices) { + if (b) { + cst += model_.subset_costs()[subset]; + } + ++subset; + } + return cst; +} + +} // namespace operations_research diff --git a/ortools/set_cover/assignment.h b/ortools/set_cover/assignment.h new file mode 100644 index 0000000000..9c78b97d57 --- /dev/null +++ b/ortools/set_cover/assignment.h @@ -0,0 +1,105 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_SET_COVER_ASSIGNMENT_H_ +#define OR_TOOLS_SET_COVER_ASSIGNMENT_H_ + +#include + +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/capacity_invariant.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { + +// `SetCoverAssignment` stores a possibly partial, possibly infeasible solution +// to a `SetCoverModel`. It only stores a solution and no metadata, +// so that it can be shared efficiently among constraints. +// +// This class is equivalent to an `Assignment` object in the CP/routing solver. +// (//ortools/routing). +class SetCoverAssignment { + public: + // Constructs an empty set covering assignment. + // + // The model size or costs must not change after the invariant was built. + // The caller must guarantee that the model outlives the assignment without + // changing its costs. + explicit SetCoverAssignment(const SetCoverModel& m) + : model_(m), constraint_(nullptr), side_constraints_({}) { + Clear(); + } + + // Clears the current assignment. + void Clear(); + + // Adds a constraint to the problem. At least one set-covering constraint is + // required; use side constraints as required (no set-covering constraint can + // be a side constraint). + void AttachInvariant(SetCoverInvariant* i); + void AttachInvariant(CapacityInvariant* i); + + // Returns the cost of current solution. + Cost cost() const { return cost_; } + + // Returns the subset assignment vector. + const SubsetBoolVector& assignment() const { return values_; } + + // Sets the subset's assignment to the given bool. + void SetValue(SubsetIndex subset, bool is_selected, + SetCoverInvariant::ConsistencyLevel set_cover_consistency); + + // Returns the current solution as a proto. + SetCoverSolutionResponse ExportSolutionAsProto() const; + + // Loads the solution and recomputes the data in the invariant. + // + // The given assignment must fit the model of this assignment. + void LoadAssignment(const SubsetBoolVector& solution); + + // Imports the solution from a proto. + // + // The given assignment must fit the model of this assignment. + void ImportSolutionFromProto(const SetCoverSolutionResponse& message); + + // Checks the consistency of the solution (between the selected subsets and + // the solution cost). + bool CheckConsistency() const; + + private: + // Computes the cost for the given choices. + Cost ComputeCost(const SubsetBoolVector& choices) const; + + // The weighted set covering model on which the solver is run. + const SetCoverModel& model_; + + // Current cost of the assignment. + Cost cost_; + + // Current assignment. Takes |S| bits. + SubsetBoolVector values_; + + // Constraints that this assignment must respect. The constraints are checked + // every time the assignment changes (with the methods `Flip`, `Select`, and + // `Deselect`). + // + // For now, the only side constraints are capacity constraints. + SetCoverInvariant* constraint_; + // TODO(user): merge the several constraints into one invariant. + std::vector side_constraints_; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_SET_COVER_ASSIGNMENT_H_ diff --git a/ortools/set_cover/assignment_test.cc b/ortools/set_cover/assignment_test.cc new file mode 100644 index 0000000000..92300df92b --- /dev/null +++ b/ortools/set_cover/assignment_test.cc @@ -0,0 +1,140 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/set_cover/assignment.h" + +#include "absl/log/check.h" +#include "gtest/gtest.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { +namespace { + +SetCoverModel MakeBasicModel() { + // 3 elements, 4 subsets (all of unitary cost). + // Optimal cost: 2 (subsets #0 and #1). + SetCoverModel model; + model.AddEmptySubset(1); + model.AddElementToLastSubset(0); + model.AddEmptySubset(1); + model.AddElementToLastSubset(1); + model.AddElementToLastSubset(2); + model.AddEmptySubset(1); + model.AddElementToLastSubset(1); + model.AddEmptySubset(1); + model.AddElementToLastSubset(2); + CHECK(model.ComputeFeasibility()); + return model; +} + +TEST(SetCoverAssignment, EmbryonicModelHasZeroCost) { + SetCoverModel model; + model.AddEmptySubset(1); + model.AddElementToLastSubset(0); + SetCoverAssignment assignment(model); + + EXPECT_TRUE(assignment.CheckConsistency()); + EXPECT_EQ(assignment.cost(), 0.0); + EXPECT_EQ(assignment.assignment(), SubsetBoolVector(1, false)); +} + +TEST(SetCoverAssignment, BasicModelHasCost) { + SetCoverModel model = MakeBasicModel(); + ASSERT_EQ(model.num_subsets(), 4); + ASSERT_EQ(model.num_elements(), 3); + SetCoverAssignment assignment(model); + + EXPECT_TRUE(assignment.CheckConsistency()); + EXPECT_EQ(assignment.cost(), 0.0); + EXPECT_EQ(assignment.assignment(), SubsetBoolVector(4, false)); + + assignment.SetValue(SubsetIndex(0), true, + SetCoverInvariant::ConsistencyLevel::kInconsistent); + assignment.SetValue(SubsetIndex(1), true, + SetCoverInvariant::ConsistencyLevel::kInconsistent); + + EXPECT_TRUE(assignment.CheckConsistency()); + EXPECT_EQ(assignment.cost(), 2.0); + EXPECT_EQ(assignment.assignment(), + SubsetBoolVector({true, true, false, false})); + + assignment.SetValue(SubsetIndex(1), false, + SetCoverInvariant::ConsistencyLevel::kInconsistent); + + EXPECT_TRUE(assignment.CheckConsistency()); + EXPECT_EQ(assignment.cost(), 1.0); + EXPECT_EQ(assignment.assignment(), + SubsetBoolVector({true, false, false, false})); +} + +TEST(SetCoverAssignment, BasicModelWorksWithSetCoverInvariant) { + // Changes to the solution imply changes in the invariant. + SetCoverModel model = MakeBasicModel(); + ASSERT_EQ(model.num_subsets(), 4); + ASSERT_EQ(model.num_elements(), 3); + SetCoverAssignment assignment(model); + + SetCoverInvariant inv(&model); + assignment.AttachInvariant(&inv); + + ASSERT_EQ(assignment.assignment(), SubsetBoolVector(4, false)); + EXPECT_EQ(inv.num_uncovered_elements(), 3); + + assignment.SetValue(SubsetIndex(0), true, + SetCoverInvariant::ConsistencyLevel::kRedundancy); + assignment.SetValue(SubsetIndex(1), true, + SetCoverInvariant::ConsistencyLevel::kRedundancy); + + EXPECT_EQ(inv.num_uncovered_elements(), 0); +} + +TEST(SetCoverAssignment, ImportFromVector) { + SetCoverModel model = MakeBasicModel(); + ASSERT_EQ(model.num_subsets(), 4); + ASSERT_EQ(model.num_elements(), 3); + const SubsetBoolVector reference_assignment = {true, false, false, false}; + + SetCoverAssignment assignment(model); + assignment.LoadAssignment(reference_assignment); + EXPECT_TRUE(assignment.CheckConsistency()); + EXPECT_EQ(assignment.cost(), 1.0); + EXPECT_EQ(assignment.assignment(), reference_assignment); +} + +TEST(SetCoverAssignment, ExportImportAllPullTogetherAsATeam) { + SetCoverModel model = MakeBasicModel(); + ASSERT_EQ(model.num_subsets(), 4); + ASSERT_EQ(model.num_elements(), 3); + + SetCoverAssignment assignment_1(model); + assignment_1.SetValue(SubsetIndex(0), true, + SetCoverInvariant::ConsistencyLevel::kInconsistent); + assignment_1.SetValue(SubsetIndex(1), true, + SetCoverInvariant::ConsistencyLevel::kInconsistent); + ASSERT_EQ(assignment_1.cost(), 2.0); + ASSERT_EQ(assignment_1.assignment(), + SubsetBoolVector({true, true, false, false})); + ASSERT_TRUE(assignment_1.CheckConsistency()); + + SetCoverSolutionResponse message = assignment_1.ExportSolutionAsProto(); + SetCoverAssignment assignment_2(model); + assignment_2.ImportSolutionFromProto(message); + + EXPECT_EQ(assignment_1.cost(), assignment_2.cost()); + EXPECT_EQ(assignment_1.assignment(), assignment_2.assignment()); + EXPECT_TRUE(assignment_2.CheckConsistency()); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/set_cover/base_types.h b/ortools/set_cover/base_types.h new file mode 100644 index 0000000000..222d0725fb --- /dev/null +++ b/ortools/set_cover/base_types.h @@ -0,0 +1,70 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_SET_COVER_BASE_TYPES_H_ +#define OR_TOOLS_SET_COVER_BASE_TYPES_H_ + +#include + +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" + +namespace operations_research { + +// Basic non-strict type for cost. The speed penalty for using double is ~2%. +using Cost = double; + +// Base non-strict integer type for counting elements and subsets. +// Using ints makes it possible to represent problems with more than 2 billion +// (2e9) elements and subsets. If need arises one day, BaseInt can be split +// into SubsetBaseInt and ElementBaseInt. +// Quick testing has shown a slowdown of about 20-25% when using int64_t. +using BaseInt = int32_t; + +// We make heavy use of strong typing to avoid obvious mistakes. +// Subset index. +DEFINE_STRONG_INT_TYPE(SubsetIndex, BaseInt); + +// Element index. +DEFINE_STRONG_INT_TYPE(ElementIndex, BaseInt); + +// Position in a vector. The vector may either represent a column, i.e. a +// subset with all its elements, or a row, i,e. the list of subsets which +// contain a given element. +DEFINE_STRONG_INT_TYPE(ColumnEntryIndex, BaseInt); +DEFINE_STRONG_INT_TYPE(RowEntryIndex, BaseInt); + +using SubsetRange = util_intops::StrongIntRange; +using ElementRange = util_intops::StrongIntRange; +using ColumnEntryRange = util_intops::StrongIntRange; + +using SubsetCostVector = util_intops::StrongVector; +using ElementCostVector = util_intops::StrongVector; + +using SparseColumn = util_intops::StrongVector; +using SparseRow = util_intops::StrongVector; + +using ElementToIntVector = util_intops::StrongVector; +using SubsetToIntVector = util_intops::StrongVector; + +// Views of the sparse vectors. These need not be aligned as it's their contents +// that need to be aligned. +using SparseColumnView = util_intops::StrongVector; +using SparseRowView = util_intops::StrongVector; + +using SubsetBoolVector = util_intops::StrongVector; +using ElementBoolVector = util_intops::StrongVector; + +} // namespace operations_research + +#endif // OR_TOOLS_SET_COVER_BASE_TYPES_H_ diff --git a/ortools/set_cover/capacity.proto b/ortools/set_cover/capacity.proto new file mode 100644 index 0000000000..df8db6dbad --- /dev/null +++ b/ortools/set_cover/capacity.proto @@ -0,0 +1,75 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package operations_research; + +option java_package = "com.google.ortools.setcover"; +option java_multiple_files = true; + +// Represents a capacity constraint to be used in conjunction with a +// SetCoverProto. This constraint only considers one dimension. +// +// Such a capacity constraint mathematically looks like: +// min_capacity <= \sum_{e in elements} weight_e * x_e <= max_capacity +// where either `min_capacity` or `max_capacity` can be omitted. `x_e` indicates +// for a given solution `x` whether the element `e` is selected and counts for +// this capacity constraint (`x_e == 1`) or not (`x_e == 0`). The weights are +// given in `capacity_term`, each of them being a reference to an element being +// present in a subset (in set-covering parlance) and its weight. +// +// For instance, this constraint can be used together with a set-covering +// problem where parcels (element) must be covered by trucks (subsets) while +// respecting truck capacities (this object). Each element can be covered by a +// given set of trucks (set-covering problem); if an element is taken within a +// truck, it uses some capacity for this truck (such as weight). +// +// In particular, this representation does not imply that a given element must +// have the same weight in all the capacity constraints of a set-covering +// problem (e.g., the same parcel might have different weights depending on +// which truck is being considered). +message CapacityConstraintProto { + message CapacityTerm { + // The subset this weight corresponds to (index of the subset in the + // `subset` repeated field in `SetCoverProto`). + int64 subset = 1; + + message ElementWeightPair { + // The element this weight corresponds to (value of `element` in + // `SetCoverProto.Subset`). + int64 element = 1; + + // The weight of the element. + int64 weight = 2; + } + + repeated ElementWeightPair element_weights = 2; + } + + // The list of terms in the constraint. + // + // The list is supposed to be in canonical form, which means it is sorted + // first by increasing subset index then increasing element index. + // No duplicate term is allowed (two terms for the same element in the same + // subset). + repeated CapacityTerm capacity_term = 1; + + // The minimum amount of resource that must be consumed. At least one of + // `min_capacity` and `max_capacity` must be present. + int64 min_capacity = 2; + + // The maximum amount of resource that can be consumed. At least one of + // `min_capacity` and `max_capacity` must be present. + int64 max_capacity = 3; +} diff --git a/ortools/set_cover/capacity_invariant.cc b/ortools/set_cover/capacity_invariant.cc new file mode 100644 index 0000000000..976dfac713 --- /dev/null +++ b/ortools/set_cover/capacity_invariant.cc @@ -0,0 +1,123 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/set_cover/capacity_invariant.h" + +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/capacity_model.h" +#include "ortools/set_cover/set_cover_model.h" +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research { + +void CapacityInvariant::Clear() { + current_slack_ = 0.0; + is_selected_.assign(set_cover_model_->num_subsets(), false); +} + +namespace { +// Returns true if the addition of `q` to `sum` overflows, for q >= 0. +bool PositiveAddOverflows(CapacityWeight sum, CapacityWeight q) { + DCHECK_GE(q, 0); + return sum > std::numeric_limits::max() - q; +} + +// Returns true if the addition of `q` to `sum` overflows, for q <= 0. +bool NegativeAddOverflows(CapacityWeight sum, CapacityWeight q) { + DCHECK_LE(q, 0); + return sum < std::numeric_limits::min() - q; +} +} // namespace + +CapacityWeight CapacityInvariant::ComputeSlackChange( + const SubsetIndex subset) const { + CapacityWeight slack_change = 0; + for (CapacityTermIndex term : model_->TermRange()) { + if (model_->GetTermSubsetIndex(term) == subset) { + // Hypothesis: GetTermSubsetIndex(term) is an element of the subset. + // This information is stored in a SetCoverModel instance. + const CapacityWeight term_weight = model_->GetTermCapacityWeight(term); + // Make sure that the slack change will not overflow. + CHECK(!PositiveAddOverflows(slack_change, term_weight)); + slack_change += term_weight; + } + } + return slack_change; +} + +bool CapacityInvariant::SlackChangeFitsConstraint( + CapacityWeight slack_change) const { + CHECK(!AddOverflows(current_slack_, slack_change)); + const CapacityWeight new_slack = current_slack_ + slack_change; + return new_slack >= model_->GetMinimumCapacity() && + new_slack <= model_->GetMaximumCapacity(); +} + +bool CapacityInvariant::Select(SubsetIndex subset) { + DVLOG(1) << "[Capacity constraint] Selecting subset " << subset; + DCHECK(!is_selected_[subset]); + + const CapacityWeight slack_change = ComputeSlackChange(subset); + if (!SlackChangeFitsConstraint(slack_change)) { + DVLOG(1) << "[Capacity constraint] Selecting subset " << subset + << ": infeasible"; + return false; + } + CHECK(!PositiveAddOverflows(current_slack_, slack_change)); + is_selected_[subset] = true; + current_slack_ += slack_change; + DVLOG(1) << "[Capacity constraint] New slack: " << current_slack_; + return true; +} + +bool CapacityInvariant::CanSelect(SubsetIndex subset) const { + DVLOG(1) << "[Capacity constraint] Can select subset " << subset << "?"; + DCHECK(!is_selected_[subset]); + + const CapacityWeight slack_change = ComputeSlackChange(subset); + DVLOG(1) << "[Capacity constraint] New slack if selecting: " + << current_slack_ + slack_change; + return SlackChangeFitsConstraint(slack_change); +} + +bool CapacityInvariant::Deselect(SubsetIndex subset) { + DVLOG(1) << "[Capacity constraint] Deselecting subset " << subset; + DCHECK(is_selected_[subset]); + + const CapacityWeight slack_change = -ComputeSlackChange(subset); + if (!SlackChangeFitsConstraint(slack_change)) { + DVLOG(1) << "[Capacity constraint] Deselecting subset " << subset + << ": infeasible"; + return false; + } + CHECK(!NegativeAddOverflows(current_slack_, slack_change)); + is_selected_[subset] = false; + current_slack_ += slack_change; + DVLOG(1) << "[Capacity constraint] New slack: " << current_slack_; + return true; +} + +bool CapacityInvariant::CanDeselect(SubsetIndex subset) const { + DVLOG(1) << "[Capacity constraint] Can deselect subset " << subset << "?"; + DCHECK(is_selected_[subset]); + + const CapacityWeight slack_change = -ComputeSlackChange(subset); + DVLOG(1) << "[Capacity constraint] New slack if deselecting: " + << current_slack_ + slack_change; + return SlackChangeFitsConstraint(slack_change); +} +} // namespace operations_research diff --git a/ortools/set_cover/capacity_invariant.h b/ortools/set_cover/capacity_invariant.h new file mode 100644 index 0000000000..3b29a2c86f --- /dev/null +++ b/ortools/set_cover/capacity_invariant.h @@ -0,0 +1,96 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_SET_COVER_CAPACITY_INVARIANT_H_ +#define OR_TOOLS_SET_COVER_CAPACITY_INVARIANT_H_ + +#include "absl/log/check.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/capacity_model.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { +class CapacityInvariant { + public: + // Constructs an empty capacity invariant state. + // The model may not change after the invariant was built. + explicit CapacityInvariant(CapacityModel* m, SetCoverModel* sc) + : model_(m), set_cover_model_(sc) { + DCHECK(model_->ComputeFeasibility()); + Clear(); + } + + // Clears the invariant. + void Clear(); + + // Returns `true` when the constraint is not violated by selecting all of the + // items in the subset and incrementally updates the invariant. Otherwise, + // returns `false` and does not change the object. (If the subset is already + // selected, the behavior is undefined.) + bool Select(SubsetIndex subset); + + // Returns `true` when the constraint would not be violated by selecting all + // of the items in the subset. Otherwise, returns `false`. The object never + // changes. (If the subset is already selected, the behavior is undefined.) + bool CanSelect(SubsetIndex subset) const; + + // Returns `true` when the constraint is not violated by unselecting all of + // the items in the subset and incrementally updates the invariant. Otherwise, + // returns `false` and does not change the object. (If the subset is already + // not selected, the behavior is undefined.) + bool Deselect(SubsetIndex subset); + + // Returns `true` when the constraint would not be violated by unselecting all + // of the items in the subset. Otherwise, returns `false`. The object never + // changes. (If the subset is already not selected, the behavior is + // undefined.) + bool CanDeselect(SubsetIndex subset) const; + + // TODO(user): implement the functions where you only select/deselect an + // item of a subset (instead of all items at once). The behavior gets much + // more interesting: if two subsets cover one item and the two item-subset + // combinations are terms in this capacity constraint, only one of them counts + // towards the capacity. + // + // The solver is not yet ready for this move: you need to + // decide which subset covers a given item, instead of ensuring that an item + // is covered by at least one subset. Currently, we could aggregate the terms + // per subset to make the code much faster when (de)selecting at the cost of + // increased initialization times. + + private: + // The capacity-constraint model on which the invariant runs. + CapacityModel* model_; + + // The set-cover model on which the invariant runs. + SetCoverModel* set_cover_model_; + + // Current slack of the constraint. + CapacityWeight current_slack_; + + // Current solution assignment. + // TODO(user): reuse the assignment of a SetCoverInvariant. + SubsetBoolVector is_selected_; + + // Determines the change in slack when (de)selecting the given subset. + // The returned value is nonnegative; add it to the slack when selecting + // and subtract it when deselecting. + CapacityWeight ComputeSlackChange(SubsetIndex subset) const; + + // Determines whether the given slack change violates the constraint + // (`false`) or not (`true`). + bool SlackChangeFitsConstraint(CapacityWeight slack_change) const; +}; +} // namespace operations_research + +#endif // OR_TOOLS_SET_COVER_CAPACITY_INVARIANT_H_ diff --git a/ortools/set_cover/capacity_invariant_test.cc b/ortools/set_cover/capacity_invariant_test.cc new file mode 100644 index 0000000000..d93fd3aee8 --- /dev/null +++ b/ortools/set_cover/capacity_invariant_test.cc @@ -0,0 +1,55 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/set_cover/capacity_invariant.h" + +#include "gtest/gtest.h" +#include "ortools/set_cover/capacity_model.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { +namespace { + +TEST(CapacityModel, ChecksConstraintViolation) { + // Compatibility constraint: choose either of the two subsets. + SetCoverModel sc; + sc.AddEmptySubset(1.0); + sc.AddElementToLastSubset(ElementIndex(0)); + sc.AddEmptySubset(1.0); + sc.AddElementToLastSubset(ElementIndex(0)); + CapacityModel m(0.0, 1.0); + m.AddTerm(SubsetIndex(0), ElementIndex(0), CapacityWeight(1.0)); + m.AddTerm(SubsetIndex(1), ElementIndex(0), CapacityWeight(1.0)); + EXPECT_TRUE(m.ComputeFeasibility()); + + CapacityInvariant cinv(&m, &sc); + // Current assignment: [false, false]. Current activation: 0. + EXPECT_TRUE(cinv.CanSelect(SubsetIndex(0))); // All moves are possible. + EXPECT_TRUE(cinv.CanSelect(SubsetIndex(1))); + + EXPECT_TRUE(cinv.Select(SubsetIndex(0))); + EXPECT_TRUE(cinv.CanDeselect(SubsetIndex(0))); // Undoing: still valid. + EXPECT_FALSE(cinv.CanSelect(SubsetIndex(1))); // Impossible move. + EXPECT_FALSE(cinv.Select(SubsetIndex(1))); + // Current assignment: [true, false]. Current activation: 1. + + EXPECT_TRUE(cinv.Deselect(SubsetIndex(0))); + EXPECT_TRUE(cinv.CanSelect(SubsetIndex(0))); // Undoing: still valid. + EXPECT_TRUE(cinv.CanSelect(SubsetIndex(1))); // Valid when 0 not selected. + EXPECT_TRUE(cinv.Select(SubsetIndex(1))); + EXPECT_FALSE(cinv.CanSelect(SubsetIndex(0))); // Impossible move. + EXPECT_TRUE(cinv.CanDeselect(SubsetIndex(1))); // Undoing: still valid. +} + +} // namespace +} // namespace operations_research diff --git a/ortools/set_cover/capacity_model.cc b/ortools/set_cover/capacity_model.cc new file mode 100644 index 0000000000..7880aef83a --- /dev/null +++ b/ortools/set_cover/capacity_model.cc @@ -0,0 +1,127 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/set_cover/capacity_model.h" + +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { +void CapacityModel::AddTerm(SubsetIndex subset, ElementIndex element, + CapacityWeight weight) { + subsets_.push_back(subset); + elements_.push_back(element); + weights_.push_back(weight); + + CHECK_EQ(elements_.size(), subsets_.size()); + CHECK_EQ(elements_.size(), weights_.size()); +} + +void CapacityModel::SetMinimumCapacity(CapacityWeight min_capacity) { + CHECK_NE(min_capacity, std::numeric_limits::max()); + min_capacity_ = min_capacity; +} + +void CapacityModel::SetMaximumCapacity(CapacityWeight max_capacity) { + CHECK_NE(max_capacity, std::numeric_limits::min()); + max_capacity_ = max_capacity; +} + +bool CapacityModel::ComputeFeasibility() const { + if (weights_.empty()) { + // A sum of zero terms is zero. + return min_capacity_ <= 0.0 && max_capacity_ >= 0.0; + } + + // Compute the minimum and maximum constraint activations. + CapacityWeight min_activation = 0.0; + CapacityWeight max_activation = 0.0; + for (const CapacityWeight weight : weights_) { + if (weight < 0.0) { + min_activation += weight; + } else { + max_activation += weight; + } + } + + DVLOG(1) << "[Capacity constraint] Activation bounds: [" << min_activation + << ", " << max_activation << "]"; + DVLOG(1) << "[Capacity constraint] Capacity bounds: [" << min_capacity_ + << ", " << max_capacity_ << "]"; + return min_activation <= max_capacity_ && max_activation >= min_capacity_; +} + +std::vector CapacityModel::CanonicalIndexing() { + std::vector idx(num_terms()); + std::iota(idx.begin(), idx.end(), CapacityTermIndex(0)); + // TODO(user): use RadixSort when it's available. The implementation in + // radix_sort.h does not support a lambda for comparing. + std::sort(idx.begin(), idx.end(), + [&](CapacityTermIndex lhs, CapacityTermIndex rhs) -> bool { + return subsets_[lhs] < subsets_[rhs] + ? true + : elements_[lhs] < elements_[rhs]; + }); + return idx; +} + +CapacityConstraintProto CapacityModel::ExportModelAsProto() { + CapacityConstraintProto proto; + proto.set_min_capacity(min_capacity_); + proto.set_max_capacity(max_capacity_); + + CapacityConstraintProto::CapacityTerm* current_term = nullptr; + + for (CapacityTermIndex i : CanonicalIndexing()) { + if (current_term == nullptr || + current_term->subset() != subsets_[i].value()) { + current_term = proto.add_capacity_term(); + current_term->set_subset(subsets_[i].value()); + } + DCHECK(current_term != nullptr); + + CapacityConstraintProto::CapacityTerm::ElementWeightPair* pair = + current_term->add_element_weights(); + pair->set_element(elements_[i].value()); + pair->set_weight(weights_[i]); + } + + return proto; +} + +void CapacityModel::ImportModelFromProto(const CapacityConstraintProto& proto) { + elements_.clear(); + subsets_.clear(); + weights_.clear(); + + SetMinimumCapacity(proto.min_capacity()); + SetMaximumCapacity(proto.max_capacity()); + + ReserveNumTerms(proto.capacity_term_size()); + for (const CapacityConstraintProto::CapacityTerm& term : + proto.capacity_term()) { + for (const CapacityConstraintProto::CapacityTerm::ElementWeightPair& pair : + term.element_weights()) { + AddTerm(SubsetIndex(term.subset()), ElementIndex(pair.element()), + pair.weight()); + } + } +} +} // namespace operations_research diff --git a/ortools/set_cover/capacity_model.h b/ortools/set_cover/capacity_model.h new file mode 100644 index 0000000000..e985ae9c3f --- /dev/null +++ b/ortools/set_cover/capacity_model.h @@ -0,0 +1,155 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_SET_COVER_CAPACITY_MODEL_H_ +#define OR_TOOLS_SET_COVER_CAPACITY_MODEL_H_ + +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/capacity.pb.h" +#include "ortools/set_cover/set_cover_model.h" + +// Representation class for the capacity side-constraint for a weighted +// set-covering problem. +// +// This constraint restricts the selection of elements within subsets that +// respect the constraint. Such a constraint can mix elements in any subset. +// +// Using the same mixed-integer-programming formulation as `set_cover_model.h`, +// this class corresponds to the following constraint: +// min_capacity <= \sum_{e in elements} weight_e * x_e <= max_capacity + +namespace operations_research { +// Basic type for weights. For now, the same as `Cost` for the set covering. +using CapacityWeight = int64_t; + +// Term index in a capacity constraint. +DEFINE_STRONG_INT_TYPE(CapacityTermIndex, BaseInt); + +// The terms are represented as three aligned vectors: the element, the subset, +// and the weight. Each vector is indexed by the term. +using CapacityElements = + util_intops::StrongVector; +using CapacitySubsets = + util_intops::StrongVector; +using CapacityWeights = + util_intops::StrongVector; + +// Main class for describing a single capacity constraint in the context of a +// set-covering problem. +class CapacityModel { + public: + // Builds an empty capacity constraint. + // + // Use either WithMinimumWeight or WithMaximumWeight to set only one of the + // two bounds. + CapacityModel(CapacityWeight min, CapacityWeight max) + : elements_(), + subsets_(), + weights_(), + min_capacity_(min), + max_capacity_(max) { + // At least one bound must be set. Otherwise, the constraint is vacuous. + CHECK(min_capacity_ != std::numeric_limits::min() || + max_capacity_ != std::numeric_limits::max()); + } + + static CapacityModel WithMinimumWeight(CapacityWeight min) { + return CapacityModel(min, std::numeric_limits::max()); + } + + static CapacityModel WithMaximumWeight(CapacityWeight max) { + return CapacityModel(std::numeric_limits::min(), max); + } + + // Returns the current number of terms in the constraint. + BaseInt num_terms() const { return elements_.size(); } + + // Returns the range of terms. + util_intops::StrongIntRange TermRange() const { + return util_intops::StrongIntRange( + CapacityTermIndex(num_terms())); + } + + // Adds a new term to the constraint. + void AddTerm(SubsetIndex subset, ElementIndex element, CapacityWeight weight); + + // Returns the element, subset, or capacity of the given term. + ElementIndex GetTermElementIndex(CapacityTermIndex term) const { + return elements_[term]; + } + SubsetIndex GetTermSubsetIndex(CapacityTermIndex term) const { + return subsets_[term]; + } + CapacityWeight GetTermCapacityWeight(CapacityTermIndex term) const { + return weights_[term]; + } + + // Sets the lower/upper bounds for the constraint. + // This will CHECK-fail if a capacity is a NaN. + void SetMinimumCapacity(CapacityWeight min_capacity); + void SetMaximumCapacity(CapacityWeight max_capacity); + + // Returns the lower/upper bounds for the constraint. + CapacityWeight GetMinimumCapacity() const { return min_capacity_; } + CapacityWeight GetMaximumCapacity() const { return max_capacity_; } + + // Returns true if the constraint is feasible, i.e. there is at least one + // assignment that satisfies the constraint. + bool ComputeFeasibility() const; + + // Reserves num_terms terms in the model. + void ReserveNumTerms(BaseInt num_terms) { + ReserveNumTerms(CapacityTermIndex(num_terms)); + } + + void ReserveNumTerms(CapacityTermIndex num_terms) { + subsets_.reserve(num_terms); + elements_.reserve(num_terms); + weights_.reserve(num_terms); + } + + // Returns the model as a CapacityConstraintProto. + // + // The function is not const because the terms need to be sorted for the + // representation as a protobuf to be canonical. + CapacityConstraintProto ExportModelAsProto(); + + // Imports the model from a CapacityConstraintProto. + void ImportModelFromProto(const CapacityConstraintProto& proto); + + private: + // The terms in the constraint. + CapacityElements elements_; + CapacitySubsets subsets_; + CapacityWeights weights_; + + // The bounds of the constraint. Both are always active at the same time. + // An inactive constraint corresponds to a capacity set to ±∞. + CapacityWeight min_capacity_; + CapacityWeight max_capacity_; + + // Returns a canonical indexing of the constraint, i.e. reading the terms in + // this order yields the order that is explained in the proto. + std::vector CanonicalIndexing(); +}; +} // namespace operations_research + +#endif // OR_TOOLS_SET_COVER_CAPACITY_MODEL_H_ diff --git a/ortools/set_cover/capacity_model_test.cc b/ortools/set_cover/capacity_model_test.cc new file mode 100644 index 0000000000..cf0f6ebdb0 --- /dev/null +++ b/ortools/set_cover/capacity_model_test.cc @@ -0,0 +1,135 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/set_cover/capacity_model.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" + +namespace operations_research { +namespace { + +using ::testing::EqualsProto; + +TEST(CapacityModel, ConstructorRequiresOneBound) { + EXPECT_DEATH(CapacityModel(std::numeric_limits::min(), + std::numeric_limits::max()), + "min"); +} + +TEST(CapacityModel, WithMinimumWeightRequiresNonVacuousMinimum) { + EXPECT_DEATH(CapacityModel::WithMinimumWeight( + std::numeric_limits::min()), + "min"); +} + +TEST(CapacityModel, WithMaximumWeightRequiresNonVacuousMaximum) { + EXPECT_DEATH(CapacityModel::WithMaximumWeight( + std::numeric_limits::max()), + "min"); +} + +TEST(CapacityModel, SetMinimumCapacityRejectsPlusInfinity) { + CapacityModel m(0, 1); + EXPECT_DEATH(m.SetMinimumCapacity(std::numeric_limits::max()), + "max"); +} + +TEST(CapacityModel, SetMaximumCapacityRejectsMinusInfinity) { + CapacityModel m(0, 1); + EXPECT_DEATH(m.SetMaximumCapacity(std::numeric_limits::min()), + "min"); +} + +TEST(CapacityModel, ComputeFeasibilityWithNoTerms) { + CapacityModel m(0, 1); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-1); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-2); + m.SetMaximumCapacity(-1); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +TEST(CapacityModel, ComputeFeasibilityWithOnlyPositiveWeights) { + CapacityModel m(0, 1); + m.AddTerm(SubsetIndex(0), ElementIndex(0), 1); + m.AddTerm(SubsetIndex(0), ElementIndex(1), 2); + m.AddTerm(SubsetIndex(0), ElementIndex(2), 3); + // Activation bounds: [0, 6]. + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-1); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(-1); + EXPECT_FALSE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(7); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(7); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +TEST(CapacityModel, ComputeFeasibilityWithOnlyNegativeWeights) { + CapacityModel m(0, 1); + m.AddTerm(SubsetIndex(0), ElementIndex(0), -1); + m.AddTerm(SubsetIndex(0), ElementIndex(1), -2); + m.AddTerm(SubsetIndex(0), ElementIndex(2), -3); + // Activation bounds: [-6, 0]. + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(1); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(1); + EXPECT_FALSE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-7); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(-7); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +TEST(CapacityModel, ComputeFeasibilityWithOnlyMixedWeights) { + CapacityModel m(0, 1); + m.AddTerm(SubsetIndex(0), ElementIndex(0), -1); + m.AddTerm(SubsetIndex(0), ElementIndex(1), 2); + m.AddTerm(SubsetIndex(0), ElementIndex(2), -3); + // Activation bounds: [-4, 2]. + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(3); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(3); + EXPECT_FALSE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-5); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(-5); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/set_cover/python/BUILD.bazel b/ortools/set_cover/python/BUILD.bazel new file mode 100644 index 0000000000..0aa6db6d4b --- /dev/null +++ b/ortools/set_cover/python/BUILD.bazel @@ -0,0 +1,44 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Python wrapper for .. +load("@pip_deps//:requirements.bzl", "requirement") +load("@pybind11_bazel//:build_defs.bzl", "pybind_extension") +load("@rules_python//python:defs.bzl", "py_test") + +# set_cover +pybind_extension( + name = "set_cover", + srcs = ["set_cover.cc"], + visibility = ["//visibility:public"], + deps = [ + "//ortools/set_cover:set_cover_cc_proto", + "//ortools/set_cover:set_cover_heuristics", + "//ortools/set_cover:set_cover_invariant", + "//ortools/set_cover:set_cover_model", + "//ortools/set_cover:set_cover_reader", + "@com_google_absl//absl/strings", + "@pybind11_protobuf//pybind11_protobuf:native_proto_caster", + ], +) + +py_test( + name = "set_cover_test", + srcs = ["set_cover_test.py"], + python_version = "PY3", + deps = [ + ":set_cover", + "//ortools/set_cover:set_cover_py_pb2", + requirement("absl-py"), + ], +) diff --git a/ortools/set_cover/python/CMakeLists.txt b/ortools/set_cover/python/CMakeLists.txt new file mode 100644 index 0000000000..ad47f5d34f --- /dev/null +++ b/ortools/set_cover/python/CMakeLists.txt @@ -0,0 +1,42 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# set_cover +pybind11_add_module(set_cover_pybind11 MODULE set_cover.cc) +set_target_properties(set_cover_pybind11 PROPERTIES + LIBRARY_OUTPUT_NAME "set_cover") + +# note: macOS is APPLE and also UNIX ! +if(APPLE) + set_target_properties(set_cover_pybind11 PROPERTIES + SUFFIX ".so" + INSTALL_RPATH "@loader_path;@loader_path/../../../${PYTHON_PROJECT}/.libs" + ) +elseif(UNIX) + set_target_properties(set_cover_pybind11 PROPERTIES + INSTALL_RPATH "$ORIGIN:$ORIGIN/../../../${PYTHON_PROJECT}/.libs" + ) +endif() + +target_link_libraries(set_cover_pybind11 PRIVATE + ${PROJECT_NAMESPACE}::ortools + pybind11_native_proto_caster +) +add_library(${PROJECT_NAMESPACE}::set_cover_pybind11 ALIAS set_cover_pybind11) + +if(BUILD_TESTING) + file(GLOB PYTHON_SRCS "*_test.py") + foreach(FILE_NAME IN LISTS PYTHON_SRCS) + add_python_test(FILE_NAME ${FILE_NAME}) + endforeach() +endif() diff --git a/ortools/set_cover/python/set_cover.cc b/ortools/set_cover/python/set_cover.cc new file mode 100644 index 0000000000..e184a911f9 --- /dev/null +++ b/ortools/set_cover/python/set_cover.cc @@ -0,0 +1,570 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A pybind11 wrapper for set_cover_*. + +#include +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "ortools/set_cover/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" +#include "ortools/set_cover/set_cover_reader.h" +#include "pybind11/numpy.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11/stl.h" +#include "pybind11_protobuf/native_proto_caster.h" + +using ::operations_research::BaseInt; +using ::operations_research::ClearRandomSubsets; +using ::operations_research::ElementDegreeSolutionGenerator; +using ::operations_research::ElementIndex; +using ::operations_research::GreedySolutionGenerator; +using ::operations_research::GuidedLocalSearch; +using ::operations_research::GuidedTabuSearch; +using ::operations_research::LazyElementDegreeSolutionGenerator; +using ::operations_research::RandomSolutionGenerator; +using ::operations_research::ReadFimiDat; +using ::operations_research::ReadOrlibRail; +using ::operations_research::ReadOrlibScp; +using ::operations_research::ReadSetCoverProto; +using ::operations_research::ReadSetCoverSolutionProto; +using ::operations_research::ReadSetCoverSolutionText; +using ::operations_research::WriteOrlibRail; +using ::operations_research::WriteOrlibScp; +using ::operations_research::WriteSetCoverProto; +using ::operations_research::WriteSetCoverSolutionProto; +using ::operations_research::WriteSetCoverSolutionText; + +using ::operations_research::SetCoverDecision; +using ::operations_research::SetCoverInvariant; +using ::operations_research::SetCoverModel; +using ::operations_research::SparseColumn; +using ::operations_research::SparseRow; +using ::operations_research::SteepestSearch; +using ::operations_research::SubsetBoolVector; +using ::operations_research::SubsetCostVector; +using ::operations_research::SubsetIndex; +using ::operations_research::TabuList; +using ::operations_research::TrivialSolutionGenerator; + +namespace py = pybind11; +using ::py::arg; +using ::py::make_iterator; + +std::vector VectorIntToVectorSubsetIndex( + absl::Span ints) { + std::vector subs; + std::transform(ints.begin(), ints.end(), subs.begin(), + [](int subset) -> SubsetIndex { return SubsetIndex(subset); }); + return subs; +} + +SubsetCostVector VectorDoubleToSubsetCostVector( + absl::Span doubles) { + SubsetCostVector costs(doubles.begin(), doubles.end()); + return costs; +} + +class IntIterator { + public: + using value_type = int; + using difference_type = std::ptrdiff_t; + using pointer = int*; + using reference = int&; + using iterator_category = std::input_iterator_tag; + + explicit IntIterator(int max_value) + : max_value_(max_value), current_value_(0) {} + + int operator*() const { return current_value_; } + IntIterator& operator++() { + ++current_value_; + return *this; + } + + static IntIterator begin(int max_value) { return IntIterator{max_value}; } + static IntIterator end(int max_value) { return {max_value, max_value}; } + + friend bool operator==(const IntIterator& lhs, const IntIterator& rhs) { + return lhs.max_value_ == rhs.max_value_ && + lhs.current_value_ == rhs.current_value_; + } + + private: + IntIterator(int max_value, int current_value) + : max_value_(max_value), current_value_(current_value) {} + + const int max_value_; + int current_value_; +}; + +PYBIND11_MODULE(set_cover, m) { + pybind11_protobuf::ImportNativeProtoCasters(); + + // set_cover_model.h + py::class_(m, "SetCoverModelStats") + .def_readwrite("min", &SetCoverModel::Stats::min) + .def_readwrite("max", &SetCoverModel::Stats::max) + .def_readwrite("median", &SetCoverModel::Stats::median) + .def_readwrite("mean", &SetCoverModel::Stats::mean) + .def_readwrite("stddev", &SetCoverModel::Stats::stddev) + .def("debug_string", &SetCoverModel::Stats::DebugString); + + py::class_(m, "SetCoverModel") + .def(py::init<>()) + .def_property_readonly("num_elements", &SetCoverModel::num_elements) + .def_property_readonly("num_subsets", &SetCoverModel::num_subsets) + .def_property_readonly("num_nonzeros", &SetCoverModel::num_nonzeros) + .def_property_readonly("fill_rate", &SetCoverModel::FillRate) + .def_property_readonly( + "subset_costs", + [](SetCoverModel& model) -> const std::vector& { + return model.subset_costs().get(); + }) + .def_property_readonly( + "columns", + [](SetCoverModel& model) -> std::vector> { + // Due to the inner StrongVector, make a deep copy. Anyway, + // columns() returns a const ref, so this keeps the semantics, not + // the efficiency. + std::vector> columns(model.columns().size()); + std::transform( + model.columns().begin(), model.columns().end(), columns.begin(), + [](const SparseColumn& column) -> std::vector { + std::vector col(column.size()); + std::transform(column.begin(), column.end(), col.begin(), + [](ElementIndex element) -> BaseInt { + return element.value(); + }); + return col; + }); + return columns; + }) + .def_property_readonly( + "rows", + [](SetCoverModel& model) -> std::vector> { + // Due to the inner StrongVector, make a deep copy. Anyway, + // rows() returns a const ref, so this keeps the semantics, not + // the efficiency. + std::vector> rows(model.rows().size()); + std::transform(model.rows().begin(), model.rows().end(), + rows.begin(), + [](const SparseRow& row) -> std::vector { + std::vector r(row.size()); + std::transform(row.begin(), row.end(), r.begin(), + [](SubsetIndex element) -> BaseInt { + return element.value(); + }); + return r; + }); + return rows; + }) + .def_property_readonly("row_view_is_valid", + &SetCoverModel::row_view_is_valid) + .def("SubsetRange", + [](SetCoverModel& model) { + return make_iterator<>(IntIterator::begin(model.num_subsets()), + IntIterator::end(model.num_subsets())); + }) + .def("ElementRange", + [](SetCoverModel& model) { + return make_iterator<>(IntIterator::begin(model.num_elements()), + IntIterator::end(model.num_elements())); + }) + .def_property_readonly("all_subsets", + [](SetCoverModel& model) -> std::vector { + std::vector subsets; + std::transform( + model.all_subsets().begin(), + model.all_subsets().end(), subsets.begin(), + [](const SubsetIndex element) -> BaseInt { + return element.value(); + }); + return subsets; + }) + .def("add_empty_subset", &SetCoverModel::AddEmptySubset, arg("cost")) + .def( + "add_element_to_last_subset", + [](SetCoverModel& model, BaseInt element) { + model.AddElementToLastSubset(element); + }, + arg("element")) + .def( + "set_subset_cost", + [](SetCoverModel& model, BaseInt subset, double cost) { + model.SetSubsetCost(subset, cost); + }, + arg("subset"), arg("cost")) + .def( + "add_element_to_subset", + [](SetCoverModel& model, BaseInt element, BaseInt subset) { + model.AddElementToSubset(element, subset); + }, + arg("subset"), arg("cost")) + .def("create_sparse_row_view", &SetCoverModel::CreateSparseRowView) + .def("sort_elements_in_subsets", &SetCoverModel::SortElementsInSubsets) + .def("compute_feasibility", &SetCoverModel::ComputeFeasibility) + .def( + "reserve_num_subsets", + [](SetCoverModel& model, BaseInt num_subsets) { + model.ReserveNumSubsets(num_subsets); + }, + arg("num_subsets")) + .def( + "reserve_num_elements_in_subset", + [](SetCoverModel& model, BaseInt num_elements, BaseInt subset) { + model.ReserveNumElementsInSubset(num_elements, subset); + }, + arg("num_elements"), arg("subset")) + .def("export_model_as_proto", &SetCoverModel::ExportModelAsProto) + .def("import_model_from_proto", &SetCoverModel::ImportModelFromProto) + .def("compute_cost_stats", &SetCoverModel::ComputeCostStats) + .def("compute_row_stats", &SetCoverModel::ComputeRowStats) + .def("compute_column_stats", &SetCoverModel::ComputeColumnStats) + .def("compute_row_deciles", &SetCoverModel::ComputeRowDeciles) + .def("compute_column_deciles", &SetCoverModel::ComputeRowDeciles); + + // TODO(user): wrap IntersectingSubsetsIterator. + + // set_cover_invariant.h + py::class_(m, "SetCoverDecision") + .def(py::init<>()) + .def(py::init([](BaseInt subset, bool value) -> SetCoverDecision* { + return new SetCoverDecision(SubsetIndex(subset), value); + }), + arg("subset"), arg("value")) + .def("subset", + [](const SetCoverDecision& decision) -> BaseInt { + return decision.subset().value(); + }) + .def("decision", &SetCoverDecision::decision); + + py::enum_(m, "consistency_level") + .value("COST_AND_COVERAGE", + SetCoverInvariant::ConsistencyLevel::kCostAndCoverage) + .value("FREE_AND_UNCOVERED", + SetCoverInvariant::ConsistencyLevel::kFreeAndUncovered) + .value("REDUNDANCY", SetCoverInvariant::ConsistencyLevel::kRedundancy); + + py::class_(m, "SetCoverInvariant") + .def(py::init()) + .def("initialize", &SetCoverInvariant::Initialize) + .def("clear", &SetCoverInvariant::Clear) + .def("model", &SetCoverInvariant::model) + .def_property( + "model", + // Expected semantics: give a pointer to Python **while + // keeping ownership** in C++. + [](SetCoverInvariant& invariant) -> std::shared_ptr { + // https://pybind11.readthedocs.io/en/stable/advanced/smart_ptrs.html#std-shared-ptr + std::shared_ptr ptr(invariant.model()); + return ptr; + }, + [](SetCoverInvariant& invariant, const SetCoverModel& model) { + *invariant.model() = model; + }) + .def("cost", &SetCoverInvariant::cost) + .def("num_uncovered_elements", &SetCoverInvariant::num_uncovered_elements) + .def("is_selected", + [](SetCoverInvariant& invariant) -> std::vector { + return invariant.is_selected().get(); + }) + .def("num_free_elements", + [](SetCoverInvariant& invariant) -> std::vector { + return invariant.num_free_elements().get(); + }) + .def("num_coverage_le_1_elements", + [](SetCoverInvariant& invariant) -> std::vector { + return invariant.num_coverage_le_1_elements().get(); + }) + .def("coverage", + [](SetCoverInvariant& invariant) -> std::vector { + return invariant.coverage().get(); + }) + .def( + "compute_coverage_in_focus", + [](SetCoverInvariant& invariant, + absl::Span focus) -> std::vector { + return invariant + .ComputeCoverageInFocus(VectorIntToVectorSubsetIndex(focus)) + .get(); + }, + arg("focus")) + .def("is_redundant", + [](SetCoverInvariant& invariant) -> std::vector { + return invariant.is_redundant().get(); + }) + .def("trace", &SetCoverInvariant::trace) + .def("clear_trace", &SetCoverInvariant::ClearTrace) + .def("clear_removability_information", + &SetCoverInvariant::ClearRemovabilityInformation) + .def("newly_removable_subsets", + &SetCoverInvariant::newly_removable_subsets) + .def("newly_non_removable_subsets", + &SetCoverInvariant::newly_non_removable_subsets) + .def("compress_trace", &SetCoverInvariant::CompressTrace) + .def("load_solution", + [](SetCoverInvariant& invariant, + absl::Span solution) -> void { + SubsetBoolVector sol(solution.begin(), solution.end()); + return invariant.LoadSolution(sol); + }) + .def("check_consistency", &SetCoverInvariant::CheckConsistency) + .def( + "compute_is_redundant", + [](SetCoverInvariant& invariant, BaseInt subset) -> bool { + return invariant.ComputeIsRedundant(SubsetIndex(subset)); + }, + arg("subset")) + .def("recompute", &SetCoverInvariant::Recompute) + .def( + "select", + [](SetCoverInvariant& invariant, BaseInt subset, + SetCoverInvariant::ConsistencyLevel consistency) { + invariant.Select(SubsetIndex(subset), consistency); + }, + arg("subset"), arg("consistency")) + .def( + "deselect", + [](SetCoverInvariant& invariant, BaseInt subset, + SetCoverInvariant::ConsistencyLevel consistency) { + invariant.Deselect(SubsetIndex(subset), consistency); + }, + arg("subset"), arg("consistency")) + .def("export_solution_as_proto", + &SetCoverInvariant::ExportSolutionAsProto) + .def("import_solution_from_proto", + &SetCoverInvariant::ImportSolutionFromProto); + + // set_cover_heuristics.h + py::class_(m, "TrivialSolutionGenerator") + .def(py::init()) + .def("next_solution", + [](TrivialSolutionGenerator& heuristic) -> bool { + return heuristic.NextSolution(); + }) + .def("next_solution", + [](TrivialSolutionGenerator& heuristic, + absl::Span focus) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus)); + }); + + py::class_(m, "RandomSolutionGenerator") + .def(py::init()) + .def("next_solution", + [](RandomSolutionGenerator& heuristic) -> bool { + return heuristic.NextSolution(); + }) + .def("next_solution", + [](RandomSolutionGenerator& heuristic, + absl::Span focus) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus)); + }); + + py::class_(m, "GreedySolutionGenerator") + .def(py::init()) + .def("next_solution", + [](GreedySolutionGenerator& heuristic) -> bool { + return heuristic.NextSolution(); + }) + .def("next_solution", + [](GreedySolutionGenerator& heuristic, + absl::Span focus) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus)); + }) + .def("next_solution", + [](GreedySolutionGenerator& heuristic, + absl::Span focus, + absl::Span costs) -> bool { + return heuristic.NextSolution( + VectorIntToVectorSubsetIndex(focus), + VectorDoubleToSubsetCostVector(costs)); + }); + + py::class_(m, + "ElementDegreeSolutionGenerator") + .def(py::init()) + .def("next_solution", + [](ElementDegreeSolutionGenerator& heuristic) -> bool { + return heuristic.NextSolution(); + }) + .def("next_solution", + [](ElementDegreeSolutionGenerator& heuristic, + absl::Span focus) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus)); + }) + .def("next_solution", + [](ElementDegreeSolutionGenerator& heuristic, + absl::Span focus, + absl::Span costs) -> bool { + return heuristic.NextSolution( + VectorIntToVectorSubsetIndex(focus), + VectorDoubleToSubsetCostVector(costs)); + }); + + py::class_( + m, "LazyElementDegreeSolutionGenerator") + .def(py::init()) + .def("next_solution", + [](LazyElementDegreeSolutionGenerator& heuristic) -> bool { + return heuristic.NextSolution(); + }) + .def("next_solution", + [](LazyElementDegreeSolutionGenerator& heuristic, + absl::Span focus) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus)); + }) + .def("next_solution", + [](LazyElementDegreeSolutionGenerator& heuristic, + absl::Span focus, + absl::Span costs) -> bool { + return heuristic.NextSolution( + VectorIntToVectorSubsetIndex(focus), + VectorDoubleToSubsetCostVector(costs)); + }); + + py::class_(m, "SteepestSearch") + .def(py::init()) + .def("next_solution", + [](SteepestSearch& heuristic, int num_iterations) -> bool { + return heuristic.NextSolution(num_iterations); + }) + .def("next_solution", + [](SteepestSearch& heuristic, absl::Span focus, + int num_iterations) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus), + num_iterations); + }) + .def("next_solution", + [](SteepestSearch& heuristic, absl::Span focus, + absl::Span costs, int num_iterations) -> bool { + return heuristic.NextSolution( + VectorIntToVectorSubsetIndex(focus), + VectorDoubleToSubsetCostVector(costs), num_iterations); + }); + + py::class_(m, "GuidedLocalSearch") + .def(py::init()) + .def("initialize", &GuidedLocalSearch::Initialize) + .def("next_solution", + [](GuidedLocalSearch& heuristic, int num_iterations) -> bool { + return heuristic.NextSolution(num_iterations); + }) + .def("next_solution", + [](GuidedLocalSearch& heuristic, absl::Span focus, + int num_iterations) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus), + num_iterations); + }); + + // Specialization for T = SubsetIndex ~= BaseInt (aka int for Python, whatever + // the size of BaseInt). + // A base type doesn't work, because TabuList uses `T::value` in the + // constructor. + py::class_>(m, "TabuList") + .def(py::init([](int size) -> TabuList* { + return new TabuList(SubsetIndex(size)); + }), + arg("size")) + .def("size", &TabuList::size) + .def("init", &TabuList::Init, arg("size")) + .def( + "add", + [](TabuList& list, BaseInt t) -> void { + return list.Add(SubsetIndex(t)); + }, + arg("t")) + .def( + "contains", + [](TabuList& list, BaseInt t) -> bool { + return list.Contains(SubsetIndex(t)); + }, + arg("t")); + + py::class_(m, "GuidedTabuSearch") + .def(py::init()) + .def("initialize", &GuidedTabuSearch::Initialize) + .def("next_solution", + [](GuidedTabuSearch& heuristic, int num_iterations) -> bool { + return heuristic.NextSolution(num_iterations); + }) + .def("next_solution", + [](GuidedTabuSearch& heuristic, absl::Span focus, + int num_iterations) -> bool { + return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus), + num_iterations); + }) + .def("get_lagrangian_factor", &GuidedTabuSearch::SetLagrangianFactor, + arg("factor")) + .def("set_lagrangian_factor", &GuidedTabuSearch::GetLagrangianFactor) + .def("set_epsilon", &GuidedTabuSearch::SetEpsilon, arg("r")) + .def("get_epsilon", &GuidedTabuSearch::GetEpsilon) + .def("set_penalty_factor", &GuidedTabuSearch::SetPenaltyFactor, + arg("factor")) + .def("get_penalty_factor", &GuidedTabuSearch::GetPenaltyFactor) + .def("set_tabu_list_size", &GuidedTabuSearch::SetTabuListSize, + arg("size")) + .def("get_tabu_list_size", &GuidedTabuSearch::GetTabuListSize); + + m.def( + "clear_random_subsets", + [](BaseInt num_subsets, SetCoverInvariant* inv) -> std::vector { + const std::vector cleared = + ClearRandomSubsets(num_subsets, inv); + return {cleared.begin(), cleared.end()}; + }); + m.def("clear_random_subsets", + [](const std::vector& focus, BaseInt num_subsets, + SetCoverInvariant* inv) -> std::vector { + const std::vector cleared = ClearRandomSubsets( + VectorIntToVectorSubsetIndex(focus), num_subsets, inv); + return {cleared.begin(), cleared.end()}; + }); + + m.def( + "clear_most_covered_elements", + [](BaseInt num_subsets, SetCoverInvariant* inv) -> std::vector { + const std::vector cleared = + ClearMostCoveredElements(num_subsets, inv); + return {cleared.begin(), cleared.end()}; + }); + m.def("clear_most_covered_elements", + [](absl::Span focus, BaseInt num_subsets, + SetCoverInvariant* inv) -> std::vector { + const std::vector cleared = ClearMostCoveredElements( + VectorIntToVectorSubsetIndex(focus), num_subsets, inv); + return {cleared.begin(), cleared.end()}; + }); + + // set_cover_reader.h + m.def("read_orlib_scp", &ReadOrlibScp); + m.def("read_orlib_rail", &ReadOrlibRail); + m.def("read_fimi_dat", &ReadFimiDat); + m.def("read_set_cover_proto", &ReadSetCoverProto); + m.def("write_orlib_scp", &WriteOrlibScp); + m.def("write_orlib_rail", &WriteOrlibRail); + m.def("write_set_cover_proto", &WriteSetCoverProto); + m.def("write_set_cover_solution_text", &WriteSetCoverSolutionText); + m.def("write_set_cover_solution_proto", &WriteSetCoverSolutionProto); + m.def("read_set_cover_solution_text", &ReadSetCoverSolutionText); + m.def("read_set_cover_solution_proto", &ReadSetCoverSolutionProto); + + // set_cover_lagrangian.h + // TODO(user): add support for SetCoverLagrangian. +} diff --git a/ortools/set_cover/python/set_cover_test.py b/ortools/set_cover/python/set_cover_test.py new file mode 100644 index 0000000000..555a686d22 --- /dev/null +++ b/ortools/set_cover/python/set_cover_test.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from absl import app +from absl.testing import absltest + +from ortools.set_cover.python import set_cover + + +def create_initial_cover_model(): + model = set_cover.SetCoverModel() + model.add_empty_subset(1.0) + model.add_element_to_last_subset(0) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(1) + model.add_element_to_last_subset(2) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(1) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(2) + return model + + +def create_knights_cover_model(num_rows: int, num_cols: int) -> set_cover.SetCoverModel: + model = set_cover.SetCoverModel() + knight_row_move = [2, 1, -1, -2, -2, -1, 1, 2] + knight_col_move = [1, 2, 2, 1, -1, -2, -2, -1] + + for row in range(num_rows): + for col in range(num_cols): + model.add_empty_subset(1.0) + model.add_element_to_last_subset(row * num_cols + col) + + for i in range(8): + new_row = row + knight_row_move[i] + new_col = col + knight_col_move[i] + if 0 <= new_row < num_rows and 0 <= new_col < num_cols: + model.add_element_to_last_subset(new_row * num_cols + new_col) + + return model + + +# This test case is mostly a Python port of set_cover_test.cc. +class SetCoverTest(absltest.TestCase): + + def test_save_reload(self): + model = create_knights_cover_model(10, 10) + model.sort_elements_in_subsets() + proto = model.export_model_as_proto() + reloaded = set_cover.SetCoverModel() + reloaded.import_model_from_proto(proto) + + self.assertEqual(model.num_subsets, reloaded.num_subsets) + self.assertEqual(model.num_elements, reloaded.num_elements) + self.assertEqual(model.subset_costs, reloaded.subset_costs) + self.assertEqual(model.columns, reloaded.columns) + if model.row_view_is_valid and reloaded.row_view_is_valid: + self.assertEqual(model.rows, reloaded.rows) + + def test_save_reload_twice(self): + model = create_knights_cover_model(3, 3) + inv = set_cover.SetCoverInvariant(model) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + greedy_proto = inv.export_solution_as_proto() + + steepest = set_cover.SteepestSearch(inv) + self.assertTrue(steepest.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + steepest_proto = inv.export_solution_as_proto() + + inv.import_solution_from_proto(greedy_proto) + self.assertTrue(steepest.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + reloaded_proto = inv.export_solution_as_proto() + self.assertEqual(str(steepest_proto), str(reloaded_proto)) + + def test_initial_values(self): + model = create_initial_cover_model() + self.assertTrue(model.compute_feasibility()) + + inv = set_cover.SetCoverInvariant(model) + trivial = set_cover.TrivialSolutionGenerator(inv) + self.assertTrue(trivial.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + self.assertEqual(inv.num_uncovered_elements(), 0) + steepest = set_cover.SteepestSearch(inv) + self.assertTrue(steepest.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + def test_infeasible(self): + model = set_cover.SetCoverModel() + model.add_empty_subset(1.0) + model.add_element_to_last_subset(0) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(3) + self.assertFalse(model.compute_feasibility()) + + def test_knights_cover_creation(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + + def test_knights_cover_greedy(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + steepest = set_cover.SteepestSearch(inv) + self.assertTrue(steepest.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_degree(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + degree = set_cover.ElementDegreeSolutionGenerator(inv) + self.assertTrue(degree.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + steepest = set_cover.SteepestSearch(inv) + self.assertTrue(steepest.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_gls(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + gls = set_cover.GuidedLocalSearch(inv) + self.assertTrue(gls.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_random(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + random = set_cover.RandomSolutionGenerator(inv) + self.assertTrue(random.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + steepest = set_cover.SteepestSearch(inv) + self.assertTrue(steepest.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_trivial(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + trivial = set_cover.TrivialSolutionGenerator(inv) + self.assertTrue(trivial.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + steepest = set_cover.SteepestSearch(inv) + self.assertTrue(steepest.next_solution(500)) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + # TODO(user): KnightsCoverGreedyAndTabu, KnightsCoverGreedyRandomClear, + # KnightsCoverElementDegreeRandomClear, KnightsCoverRandomClearMip, + # KnightsCoverMip + + +def main(_): + absltest.main() + + +if __name__ == "__main__": + app.run(main) diff --git a/ortools/set_cover/samples/CMakeLists.txt b/ortools/set_cover/samples/CMakeLists.txt new file mode 100644 index 0000000000..8bb2b71af6 --- /dev/null +++ b/ortools/set_cover/samples/CMakeLists.txt @@ -0,0 +1,44 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if(NOT BUILD_SAMPLES) + return() +endif() + +if(BUILD_CXX_SAMPLES) + file(GLOB CXX_SRCS "*.cc") + foreach(SAMPLE IN LISTS CXX_SRCS) + add_cxx_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() + +if(BUILD_PYTHON_SAMPLES) + file(GLOB PYTHON_SRCS "*.py") + foreach(SAMPLE IN LISTS PYTHON_SRCS) + add_python_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() + +if(BUILD_JAVA_SAMPLES) + file(GLOB JAVA_SRCS "*.java") + foreach(SAMPLE IN LISTS JAVA_SRCS) + add_java_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() + +if(BUILD_DOTNET_SAMPLES) + file(GLOB DOTNET_SRCS "*.cs") + foreach(SAMPLE IN LISTS DOTNET_SRCS) + add_dotnet_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() diff --git a/ortools/set_cover/samples/code_samples_cc_test.sh b/ortools/set_cover/samples/code_samples_cc_test.sh new file mode 100755 index 0000000000..996d2d1a74 --- /dev/null +++ b/ortools/set_cover/samples/code_samples_cc_test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +DEFINE_string sample "" "sample code." + +function test::operations_research_examples::code_samples_set_cover() { + declare -r DIR="${TEST_SRCDIR}/ortools/set_cover/samples" + EXPECT_SUCCEED "${DIR}/${FLAGS_sample}_cc" +} + +gbash::unit::main "$@" diff --git a/ortools/set_cover/samples/code_samples_py_test.sh b/ortools/set_cover/samples/code_samples_py_test.sh new file mode 100755 index 0000000000..a7f44f2844 --- /dev/null +++ b/ortools/set_cover/samples/code_samples_py_test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +source gbash.sh || exit +source module gbash_unit.sh + +DEFINE_string sample "" "sample code." + +function test::operations_research_examples::code_samples_set_cover_py() { + declare -r DIR="${TEST_SRCDIR}/ortools/set_cover/samples" + EXPECT_SUCCEED "${DIR}/${FLAGS_sample}_py3" +} + +gbash::unit::main "$@" diff --git a/ortools/algorithms/samples/set_cover.cc b/ortools/set_cover/samples/set_cover.cc similarity index 92% rename from ortools/algorithms/samples/set_cover.cc rename to ortools/set_cover/samples/set_cover.cc index 0a22765462..c5de9030b2 100644 --- a/ortools/algorithms/samples/set_cover.cc +++ b/ortools/set_cover/samples/set_cover.cc @@ -16,9 +16,9 @@ #include #include "absl/log/log.h" -#include "ortools/algorithms/set_cover_heuristics.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" +#include "ortools/set_cover/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" // [END import] namespace operations_research { diff --git a/ortools/algorithms/samples/set_cover.py b/ortools/set_cover/samples/set_cover.py similarity index 97% rename from ortools/algorithms/samples/set_cover.py rename to ortools/set_cover/samples/set_cover.py index baf8abaebd..c6d5b48aef 100755 --- a/ortools/algorithms/samples/set_cover.py +++ b/ortools/set_cover/samples/set_cover.py @@ -16,7 +16,7 @@ # [START program] # [START import] -from ortools.algorithms.python import set_cover +from ortools.set_cover.python import set_cover # [END import] diff --git a/ortools/algorithms/set_cover.proto b/ortools/set_cover/set_cover.proto similarity index 94% rename from ortools/algorithms/set_cover.proto rename to ortools/set_cover/set_cover.proto index ac181a1f5d..70f60e648b 100644 --- a/ortools/algorithms/set_cover.proto +++ b/ortools/set_cover/set_cover.proto @@ -28,7 +28,7 @@ message SetCoverProto { optional double cost = 1; // The list of elements in the subset. - repeated int32 element = 2 [packed = true]; + repeated int64 element = 2 [packed = true]; } // The list of subsets in the model. @@ -66,10 +66,10 @@ message SetCoverSolutionResponse { // The number of subsets that are selected in the solution. This is used // to decompress their indices below. - optional int32 num_subsets = 2; + optional int64 num_subsets = 2; // The list of the subsets selected in the solution. - repeated int32 subset = 3 [packed = true]; + repeated int64 subset = 3 [packed = true]; // The cost of the solution, as computed by the algorithm. optional double cost = 4; diff --git a/ortools/algorithms/set_cover_heuristics.cc b/ortools/set_cover/set_cover_heuristics.cc similarity index 94% rename from ortools/algorithms/set_cover_heuristics.cc rename to ortools/set_cover/set_cover_heuristics.cc index e81bac48a6..910d1c38c4 100644 --- a/ortools/algorithms/set_cover_heuristics.cc +++ b/ortools/set_cover/set_cover_heuristics.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/algorithms/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_heuristics.h" #include #include @@ -29,9 +29,10 @@ #include "absl/random/random.h" #include "absl/types/span.h" #include "ortools/algorithms/adjustable_k_ary_heap.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/logging.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -120,28 +121,47 @@ bool GreedySolutionGenerator::NextSolution(absl::Span focus, subset_priorities.push_back({priority, subset.value()}); } } + const SetCoverModel& model = *inv_->model(); + const BaseInt num_subsets = model.num_subsets(); + const SparseColumnView& colunms = model.columns(); + const SparseRowView& rows = model.rows(); + // The priority queue maintains the maximum number of elements covered by unit // of cost. We chose 16 as the arity of the heap after some testing. // TODO(user): research more about the best value for Arity. AdjustableKAryHeap pq( - subset_priorities, inv_->model()->num_subsets()); - while (!pq.IsEmpty()) { + subset_priorities, num_subsets); + SubsetBoolVector subset_seen(num_subsets, false); + std::vector subsets_to_remove; + subsets_to_remove.reserve(focus.size()); + while (!pq.IsEmpty() || inv_->num_uncovered_elements() > 0) { + // LOG_EVERY_N_SEC(INFO, 5) + // << "Queue size: " << pq.heap_size() + // << ", #uncovered elements: " << inv_->num_uncovered_elements(); const SubsetIndex best_subset(pq.TopIndex()); pq.Pop(); inv_->Select(best_subset, CL::kFreeAndUncovered); // NOMUTANTS -- reason, for C++ - if (inv_->num_uncovered_elements() == 0) break; - for (IntersectingSubsetsIterator it(*inv_->model(), best_subset); - !it.at_end(); ++it) { - const SubsetIndex subset = *it; - const BaseInt marginal_impact(inv_->num_free_elements()[subset]); - if (marginal_impact > 0) { - const float priority = marginal_impact / costs[subset]; - pq.Update({priority, subset.value()}); - } else { - pq.Remove(subset.value()); + subset_seen[best_subset] = true; + subsets_to_remove.push_back(best_subset); + for (const ElementIndex element : colunms[best_subset]) { + for (const SubsetIndex subset : rows[element]) { + if (subset_seen[subset]) continue; + subset_seen[subset] = true; + const BaseInt marginal_impact(inv_->num_free_elements()[subset]); + if (marginal_impact > 0) { + const float priority = marginal_impact / costs[subset]; + pq.Update({priority, subset.value()}); + } else { + pq.Remove(subset.value()); + } + subsets_to_remove.push_back(subset); } } + for (const SubsetIndex subset : subsets_to_remove) { + subset_seen[subset] = false; + } + subsets_to_remove.clear(); DVLOG(1) << "Cost = " << inv_->cost() << " num_uncovered_elements = " << inv_->num_uncovered_elements(); } @@ -686,7 +706,11 @@ bool GuidedTabuSearch::NextSolution(absl::Span focus, UpdatePenalties(focus); tabu_list_.Add(best_subset); - inv_->Flip(best_subset, CL::kFreeAndUncovered); + if (inv_->is_selected()[best_subset]) { + inv_->Deselect(best_subset, CL::kFreeAndUncovered); + } else { + inv_->Select(best_subset, CL::kFreeAndUncovered); + } // TODO(user): make the cost computation incremental. augmented_cost = std::accumulate(augmented_costs_.begin(), augmented_costs_.end(), 0.0); @@ -695,9 +719,6 @@ bool GuidedTabuSearch::NextSolution(absl::Span focus, << inv_->cost() << ", best cost = ," << best_cost << ", penalized cost = ," << augmented_cost; if (inv_->cost() < best_cost) { - LOG(INFO) << "Updated best cost, " << "Iteration, " << iteration - << ", current cost = ," << inv_->cost() << ", best cost = ," - << best_cost << ", penalized cost = ," << augmented_cost; best_cost = inv_->cost(); best_choices = inv_->is_selected(); } @@ -754,17 +775,19 @@ bool GuidedLocalSearch::NextSolution(absl::Span focus, for (int iteration = 0; !priority_heap_.IsEmpty() && iteration < num_iterations; ++iteration) { - // Improve current solution respective to the current penalties. + // Improve current solution respective to the current penalties by flipping + // the best subset. const SubsetIndex best_subset(priority_heap_.TopIndex()); if (inv_->is_selected()[best_subset]) { utility_heap_.Insert({0, best_subset.value()}); + inv_->Deselect(best_subset, CL::kRedundancy); } else { utility_heap_.Insert( {static_cast(inv_->model()->subset_costs()[best_subset] / (1 + penalties_[best_subset])), best_subset.value()}); + inv_->Select(best_subset, CL::kRedundancy); } - inv_->Flip(best_subset, CL::kRedundancy); // Flip the best subset. DCHECK(!utility_heap_.IsEmpty()); // Getting the subset with highest utility. utility_heap_ is not empty, diff --git a/ortools/algorithms/set_cover_heuristics.h b/ortools/set_cover/set_cover_heuristics.h similarity index 98% rename from ortools/algorithms/set_cover_heuristics.h rename to ortools/set_cover/set_cover_heuristics.h index 27a8b97fc2..e1571c70cd 100644 --- a/ortools/algorithms/set_cover_heuristics.h +++ b/ortools/set_cover/set_cover_heuristics.h @@ -11,15 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_HEURISTICS_H_ -#define OR_TOOLS_ALGORITHMS_SET_COVER_HEURISTICS_H_ +#ifndef OR_TOOLS_SET_COVER_SET_COVER_HEURISTICS_H_ +#define OR_TOOLS_SET_COVER_SET_COVER_HEURISTICS_H_ #include #include "absl/types/span.h" #include "ortools/algorithms/adjustable_k_ary_heap.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_invariant.h" namespace operations_research { @@ -499,4 +499,4 @@ std::vector ClearMostCoveredElements( SetCoverInvariant* inv); } // namespace operations_research -#endif // OR_TOOLS_ALGORITHMS_SET_COVER_HEURISTICS_H_ +#endif // OR_TOOLS_SET_COVER_SET_COVER_HEURISTICS_H_ diff --git a/ortools/algorithms/set_cover_invariant.cc b/ortools/set_cover/set_cover_invariant.cc similarity index 97% rename from ortools/algorithms/set_cover_invariant.cc rename to ortools/set_cover/set_cover_invariant.cc index 425518a50c..061582b3ed 100644 --- a/ortools/algorithms/set_cover_invariant.cc +++ b/ortools/set_cover/set_cover_invariant.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/algorithms/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_invariant.h" #include #include @@ -21,9 +21,10 @@ #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/types/span.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/logging.h" #include "ortools/base/mathutil.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -301,15 +302,6 @@ BaseInt SetCoverInvariant::ComputeNumFreeElements(SubsetIndex subset) const { return num_free_elements; } -void SetCoverInvariant::Flip(SubsetIndex subset, - ConsistencyLevel target_consistency) { - if (!is_selected_[subset]) { - Select(subset, target_consistency); - } else { - Deselect(subset, target_consistency); - } -} - void SetCoverInvariant::Select(SubsetIndex subset, ConsistencyLevel target_consistency) { const bool update_redundancy_info = target_consistency >= CL::kRedundancy; diff --git a/ortools/algorithms/set_cover_invariant.h b/ortools/set_cover/set_cover_invariant.h similarity index 93% rename from ortools/algorithms/set_cover_invariant.h rename to ortools/set_cover/set_cover_invariant.h index 532b6d834c..08ad3d7be7 100644 --- a/ortools/algorithms/set_cover_invariant.h +++ b/ortools/set_cover/set_cover_invariant.h @@ -11,16 +11,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_INVARIANT_H_ -#define OR_TOOLS_ALGORITHMS_SET_COVER_INVARIANT_H_ +#ifndef OR_TOOLS_SET_COVER_SET_COVER_INVARIANT_H_ +#define OR_TOOLS_SET_COVER_SET_COVER_INVARIANT_H_ #include #include #include "absl/log/check.h" #include "absl/types/span.h" -#include "ortools/algorithms/set_cover.pb.h" -#include "ortools/algorithms/set_cover_model.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover.pb.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -179,17 +180,6 @@ class SetCoverInvariant { // Computes the number of free (uncovered) elements in the given subset. BaseInt ComputeNumFreeElements(SubsetIndex subset) const; - // Includes subset in the solution by setting is_selected_[subset] to true - // without updating the invariant. Only updates the cost and the coverage. - // TODO(user): Merge with Select. Introduce consistency levels and maybe split - // the invariant into three. - void SelectNoUpdate(SubsetIndex subset); - - // Flips is_selected_[subset] to its negation, by calling Select or Deselect - // depending on value. Updates the invariant incrementally to the given - // consistency level. - void Flip(SubsetIndex subset, ConsistencyLevel consistency); - // Includes subset in the solution by setting is_selected_[subset] to true // and incrementally updating the invariant to the given consistency level. void Select(SubsetIndex subset, ConsistencyLevel consistency); @@ -288,4 +278,4 @@ class SetCoverInvariant { }; } // namespace operations_research -#endif // OR_TOOLS_ALGORITHMS_SET_COVER_INVARIANT_H_ +#endif // OR_TOOLS_SET_COVER_SET_COVER_INVARIANT_H_ diff --git a/ortools/algorithms/set_cover_lagrangian.cc b/ortools/set_cover/set_cover_lagrangian.cc similarity index 98% rename from ortools/algorithms/set_cover_lagrangian.cc rename to ortools/set_cover/set_cover_lagrangian.cc index ca076b53c7..b01c36cffc 100644 --- a/ortools/algorithms/set_cover_lagrangian.cc +++ b/ortools/set_cover/set_cover_lagrangian.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/algorithms/set_cover_lagrangian.h" +#include "ortools/set_cover/set_cover_lagrangian.h" #include #include @@ -22,9 +22,10 @@ #include "absl/log/check.h" #include "absl/synchronization/blocking_counter.h" #include "ortools/algorithms/adjustable_k_ary_heap.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/threadpool.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -99,7 +100,8 @@ void FillReducedCostsSlice(SubsetIndex slice_start, SubsetIndex slice_end, } BaseInt BlockSize(BaseInt size, int num_threads) { - return 1 + (size - 1) / num_threads; + // Traditional formula to compute std::ceil(size / num_threads). + return (size + num_threads - 1) / num_threads; } } // namespace diff --git a/ortools/algorithms/set_cover_lagrangian.h b/ortools/set_cover/set_cover_lagrangian.h similarity index 95% rename from ortools/algorithms/set_cover_lagrangian.h rename to ortools/set_cover/set_cover_lagrangian.h index ec207cb037..56b32dec14 100644 --- a/ortools/algorithms/set_cover_lagrangian.h +++ b/ortools/set_cover/set_cover_lagrangian.h @@ -11,17 +11,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_LAGRANGIAN_H_ -#define OR_TOOLS_ALGORITHMS_SET_COVER_LAGRANGIAN_H_ +#ifndef OR_TOOLS_SET_COVER_SET_COVER_LAGRANGIAN_H_ +#define OR_TOOLS_SET_COVER_SET_COVER_LAGRANGIAN_H_ #include -#include #include #include -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/threadpool.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -160,4 +160,4 @@ class SetCoverLagrangian { } // namespace operations_research -#endif // OR_TOOLS_ALGORITHMS_SET_COVER_LAGRANGIAN_H_ +#endif // OR_TOOLS_SET_COVER_SET_COVER_LAGRANGIAN_H_ diff --git a/ortools/algorithms/set_cover_mip.cc b/ortools/set_cover/set_cover_mip.cc similarity index 96% rename from ortools/algorithms/set_cover_mip.cc rename to ortools/set_cover/set_cover_mip.cc index bfb095e568..1c7d346187 100644 --- a/ortools/algorithms/set_cover_mip.cc +++ b/ortools/set_cover/set_cover_mip.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/algorithms/set_cover_mip.h" +#include "ortools/set_cover/set_cover_mip.h" #include #include @@ -19,10 +19,11 @@ #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/types/span.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/lp_data/lp_types.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { diff --git a/ortools/algorithms/set_cover_mip.h b/ortools/set_cover/set_cover_mip.h similarity index 90% rename from ortools/algorithms/set_cover_mip.h rename to ortools/set_cover/set_cover_mip.h index ea40a5b445..f06c7613ed 100644 --- a/ortools/algorithms/set_cover_mip.h +++ b/ortools/set_cover/set_cover_mip.h @@ -11,12 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_MIP_H_ -#define OR_TOOLS_ALGORITHMS_SET_COVER_MIP_H_ +#ifndef OR_TOOLS_SET_COVER_SET_COVER_MIP_H_ +#define OR_TOOLS_SET_COVER_SET_COVER_MIP_H_ #include "absl/types/span.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_invariant.h" namespace operations_research { enum class SetCoverMipSolver : int { @@ -67,4 +67,4 @@ class SetCoverMip { }; } // namespace operations_research -#endif // OR_TOOLS_ALGORITHMS_SET_COVER_MIP_H_ +#endif // OR_TOOLS_SET_COVER_SET_COVER_MIP_H_ diff --git a/ortools/algorithms/set_cover_model.cc b/ortools/set_cover/set_cover_model.cc similarity index 91% rename from ortools/algorithms/set_cover_model.cc rename to ortools/set_cover/set_cover_model.cc index 0d0e5f59f5..78c027f71d 100644 --- a/ortools/algorithms/set_cover_model.cc +++ b/ortools/set_cover/set_cover_model.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/algorithms/set_cover_model.h" +#include "ortools/set_cover/set_cover_model.h" #include #include @@ -32,7 +32,8 @@ #include "absl/strings/str_format.h" #include "absl/types/span.h" #include "ortools/algorithms/radix_sort.h" -#include "ortools/algorithms/set_cover.pb.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover.pb.h" namespace operations_research { @@ -159,15 +160,10 @@ SetCoverModel SetCoverModel::GenerateRandomModelFrom( subset_already_contains_element[element] = false; } } - LOG(INFO) << "Finished genreating the model with " << num_elements_covered - << " elements covered."; // It can happen -- rarely in practice -- that some of the elements cannot be // covered. Let's add them to randomly chosen subsets. if (num_elements_covered != num_elements) { - LOG(INFO) << "Generated model with " << num_elements - num_elements_covered - << " elements that cannot be covered. Adding them to random " - "subsets."; SubsetBoolVector element_already_in_subset(num_subsets, false); for (ElementIndex element(0); element.value() < num_elements; ++element) { LOG_EVERY_N_SEC(INFO, 5) << absl::StrFormat( @@ -200,11 +196,7 @@ SetCoverModel SetCoverModel::GenerateRandomModelFrom( ++num_elements_covered; } } - LOG(INFO) << "Finished generating subsets for elements that were not " - "covered in the original model."; } - LOG(INFO) << "Finished generating the model. There are " - << num_elements - num_elements_covered << " uncovered elements."; CHECK_EQ(num_elements_covered, num_elements); @@ -237,6 +229,8 @@ void SetCoverModel::UpdateAllSubsetsList() { } void SetCoverModel::AddEmptySubset(Cost cost) { + // TODO(user): refine the logic for is_unicost_ and is_unicost_valid_. + is_unicost_valid_ = false; elements_in_subsets_are_sorted_ = false; subset_costs_.push_back(cost); columns_.push_back(SparseColumn()); @@ -262,6 +256,9 @@ void SetCoverModel::AddElementToLastSubset(ElementIndex element) { } void SetCoverModel::SetSubsetCost(BaseInt subset, Cost cost) { + // TODO(user): refine the logic for is_unicost_ and is_unicost_valid_. + // NOMUTANTS -- this is a performance optimization. + is_unicost_valid_ = false; elements_in_subsets_are_sorted_ = false; CHECK(std::isfinite(cost)); DCHECK_GE(subset, 0); @@ -338,14 +335,14 @@ void SetCoverModel::CreateSparseRowView() { rows_.resize(num_elements_, SparseRow()); ElementToIntVector row_sizes(num_elements_, 0); for (const SubsetIndex subset : SubsetRange()) { - // Sort the columns. It's not super-critical to improve performance here - // as this needs to be done only once. BaseInt* data = reinterpret_cast(columns_[subset].data()); RadixSort(absl::MakeSpan(data, columns_[subset].size())); ElementIndex preceding_element(-1); for (const ElementIndex element : columns_[subset]) { - DCHECK_GT(element, preceding_element); // Fail if there is a repetition. + CHECK_GT(element, preceding_element) + << "Repetition in column " + << subset; // Fail if there is a repetition. ++row_sizes[element]; preceding_element = element; } @@ -372,11 +369,14 @@ bool SetCoverModel::ComputeFeasibility() const { for (const Cost cost : subset_costs_) { CHECK_GT(cost, 0.0); } + SubsetIndex column_index(0); for (const SparseColumn& column : columns_) { - CHECK_GT(column.size(), 0); + // DLOG_IF(INFO, column.empty()) << "Empty column " << column_index.value(); for (const ElementIndex element : column) { ++coverage[element]; } + // NOMUTANTS -- column_index is only used for logging in debug mode. + ++column_index; } for (const ElementIndex element : ElementRange()) { CHECK_GE(coverage[element], 0); @@ -409,7 +409,6 @@ SetCoverProto SetCoverModel::ExportModelAsProto() const { subset_proto->add_element(element.value()); } } - LOG(INFO) << "Finished exporting the model."; return message; } @@ -497,15 +496,27 @@ class StatsAccumulator { } // namespace template -SetCoverModel::Stats ComputeStats(std::vector sizes) { +SetCoverModel::Stats ComputeStats(std::vector samples) { SetCoverModel::Stats stats; - stats.min = *std::min_element(sizes.begin(), sizes.end()); - stats.max = *std::max_element(sizes.begin(), sizes.end()); - stats.mean = std::accumulate(sizes.begin(), sizes.end(), 0.0) / sizes.size(); - std::nth_element(sizes.begin(), sizes.begin() + sizes.size() / 2, - sizes.end()); - stats.median = sizes[sizes.size() / 2]; - stats.stddev = StandardDeviation(sizes); + stats.min = *std::min_element(samples.begin(), samples.end()); + stats.max = *std::max_element(samples.begin(), samples.end()); + stats.mean = + std::accumulate(samples.begin(), samples.end(), 0.0) / samples.size(); + auto const q1 = samples.size() / 4; + auto const q2 = samples.size() / 2; + auto const q3 = q1 + q2; + // The first call to nth_element is O(n). The 2nd and 3rd calls are O(n / 2). + // Basically it's equivalent to running nth_element twice. + // One should be tempted to use a faster sorting algorithm like radix sort, + // it is not sure that we would gain a lot. There would be no gain in + // complexity whatsoever anyway. On top of that, this code is called at most + // one per problem, so it's not worth the effort. + std::nth_element(samples.begin(), samples.begin() + q2, samples.end()); + std::nth_element(samples.begin(), samples.begin() + q1, samples.begin() + q2); + std::nth_element(samples.begin() + q2, samples.begin() + q3, samples.end()); + stats.median = samples[samples.size() / 2]; + stats.iqr = samples[q3] - samples[q1]; + stats.stddev = StandardDeviation(samples); return stats; } diff --git a/ortools/algorithms/set_cover_model.h b/ortools/set_cover/set_cover_model.h similarity index 83% rename from ortools/algorithms/set_cover_model.h rename to ortools/set_cover/set_cover_model.h index 2e4d1c76e2..04c782c821 100644 --- a/ortools/algorithms/set_cover_model.h +++ b/ortools/set_cover/set_cover_model.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_MODEL_H_ -#define OR_TOOLS_ALGORITHMS_SET_COVER_MODEL_H_ +#ifndef OR_TOOLS_SET_COVER_SET_COVER_MODEL_H_ +#define OR_TOOLS_SET_COVER_SET_COVER_MODEL_H_ #include #include @@ -20,9 +20,10 @@ #include "absl/log/check.h" #include "absl/strings/str_cat.h" -#include "ortools/algorithms/set_cover.pb.h" #include "ortools/base/strong_int.h" #include "ortools/base/strong_vector.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover.pb.h" // Representation class for the weighted set-covering problem. // @@ -50,55 +51,6 @@ // cardinalities of all the subsets. namespace operations_research { -// Basic non-strict type for cost. The speed penalty for using double is ~2%. -using Cost = double; - -// Base non-strict integer type for counting elements and subsets. -// Using ints makes it possible to represent problems with more than 2 billion -// (2e9) elements and subsets. If need arises one day, BaseInt can be split -// into SubsetBaseInt and ElementBaseInt. -// Quick testing has shown a slowdown of about 20-25% when using int64_t. -using BaseInt = int32_t; - -// We make heavy use of strong typing to avoid obvious mistakes. -// Subset index. -DEFINE_STRONG_INT_TYPE(SubsetIndex, BaseInt); - -// Element index. -DEFINE_STRONG_INT_TYPE(ElementIndex, BaseInt); - -// Position in a vector. The vector may either represent a column, i.e. a -// subset with all its elements, or a row, i,e. the list of subsets which -// contain a given element. -DEFINE_STRONG_INT_TYPE(ColumnEntryIndex, BaseInt); -DEFINE_STRONG_INT_TYPE(RowEntryIndex, BaseInt); - -using SubsetRange = util_intops::StrongIntRange; -using ElementRange = util_intops::StrongIntRange; -using ColumnEntryRange = util_intops::StrongIntRange; - -using SubsetCostVector = util_intops::StrongVector; -using ElementCostVector = util_intops::StrongVector; - -using SparseColumn = util_intops::StrongVector; -using SparseRow = util_intops::StrongVector; - -using ElementToIntVector = util_intops::StrongVector; -using SubsetToIntVector = util_intops::StrongVector; - -// Views of the sparse vectors. These need not be aligned as it's their contents -// that need to be aligned. -using SparseColumnView = util_intops::StrongVector; -using SparseRowView = util_intops::StrongVector; - -using SubsetBoolVector = util_intops::StrongVector; -using ElementBoolVector = util_intops::StrongVector; - -// Useful for representing permutations, -using ElementToElementVector = - util_intops::StrongVector; -using SubsetToSubsetVector = - util_intops::StrongVector; // Main class for describing a weighted set-covering problem. class SetCoverModel { @@ -111,6 +63,8 @@ class SetCoverModel { row_view_is_valid_(false), elements_in_subsets_are_sorted_(false), subset_costs_(), + is_unicost_(true), + is_unicost_valid_(false), columns_(), rows_(), all_subsets_() {} @@ -161,6 +115,23 @@ class SetCoverModel { // Vector of costs for each subset. const SubsetCostVector& subset_costs() const { return subset_costs_; } + // Returns true if all subset costs are equal to 1.0. This is a fast check + // that is only valid if the subset costs are not modified. + bool is_unicost() { + if (is_unicost_valid_) { + return is_unicost_; + } + is_unicost_ = true; + for (const Cost cost : subset_costs_) { + if (cost != 1.0) { + is_unicost_ = false; + break; + } + } + is_unicost_valid_ = true; + return is_unicost_; + } + // Column view of the set covering problem. const SparseColumnView& columns() const { return columns_; } @@ -244,10 +215,12 @@ class SetCoverModel { double median; double mean; double stddev; + double iqr; // Interquartile range. std::string DebugString() const { return absl::StrCat("min = ", min, ", max = ", max, ", mean = ", mean, - ", median = ", median, ", stddev = ", stddev, ", "); + ", median = ", median, ", stddev = ", stddev, ", ", + "iqr = ", iqr); } }; @@ -298,6 +271,12 @@ class SetCoverModel { // Costs for each subset. SubsetCostVector subset_costs_; + // True when all subset costs are equal to 1.0. + bool is_unicost_; + + // True when is_unicost_ is up-to-date. + bool is_unicost_valid_; + // Vector of columns. Each column corresponds to a subset and contains the // elements of the given subset. // This takes NNZ (number of non-zeros) BaseInts, or |E| * |S| * fill_rate. @@ -340,7 +319,7 @@ class IntersectingSubsetsIterator { seed_subset_(seed_subset), model_(model), subset_seen_(model_.columns().size(), false) { - CHECK(model_.row_view_is_valid()); + DCHECK(model_.row_view_is_valid()); subset_seen_[seed_subset] = true; // Avoid iterating on `seed_subset`. ++(*this); // Move to the first intersecting subset. } @@ -359,11 +338,12 @@ class IntersectingSubsetsIterator { DCHECK(!at_end()); const SparseRowView& rows = model_.rows(); const SparseColumn& column = model_.columns()[seed_subset_]; - for (; element_entry_ < ColumnEntryIndex(column.size()); ++element_entry_) { + const ColumnEntryIndex column_size = ColumnEntryIndex(column.size()); + for (; element_entry_ < column_size; ++element_entry_) { const ElementIndex current_element = column[element_entry_]; const SparseRow& current_row = rows[current_element]; - for (; subset_entry_ < RowEntryIndex(current_row.size()); - ++subset_entry_) { + const RowEntryIndex current_row_size = RowEntryIndex(current_row.size()); + for (; subset_entry_ < current_row_size; ++subset_entry_) { intersecting_subset_ = current_row[subset_entry_]; if (!subset_seen_[intersecting_subset_]) { subset_seen_[intersecting_subset_] = true; @@ -398,4 +378,4 @@ class IntersectingSubsetsIterator { } // namespace operations_research -#endif // OR_TOOLS_ALGORITHMS_SET_COVER_MODEL_H_ +#endif // OR_TOOLS_SET_COVER_SET_COVER_MODEL_H_ diff --git a/ortools/algorithms/set_cover_reader.cc b/ortools/set_cover/set_cover_reader.cc similarity index 76% rename from ortools/algorithms/set_cover_reader.cc rename to ortools/set_cover/set_cover_reader.cc index a8c3b922f5..284ee5a503 100644 --- a/ortools/algorithms/set_cover_reader.cc +++ b/ortools/set_cover/set_cover_reader.cc @@ -11,10 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/algorithms/set_cover_reader.h" +#include "ortools/set_cover/set_cover_reader.h" #include +#include #include #include #include @@ -29,11 +30,13 @@ #include "absl/strings/str_format.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" -#include "ortools/algorithms/set_cover.pb.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/file.h" +#include "ortools/base/filesystem.h" #include "ortools/base/helpers.h" #include "ortools/base/options.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover.pb.h" +#include "ortools/set_cover/set_cover_model.h" #include "ortools/util/filelineiter.h" namespace operations_research { @@ -105,30 +108,42 @@ int64_t SetCoverReader::ParseNextInteger() { return value; } +namespace { +double Percent(SubsetIndex subset, BaseInt total) { + return subset.value() == 0 ? 0 : 100.0 * subset.value() / total; +} + +double Percent(ElementIndex element, BaseInt total) { + return element.value() == 0 ? 0 : 100.0 * element.value() / total; +} +} // namespace + // This is a row-based format where the elements are 1-indexed. SetCoverModel ReadOrlibScp(absl::string_view filename) { + CHECK_OK(file::Exists(filename, file::Defaults())); SetCoverModel model; File* file(file::OpenOrDie(filename, "r", file::Defaults())); SetCoverReader reader(file); const ElementIndex num_rows(reader.ParseNextInteger()); const SubsetIndex num_cols(reader.ParseNextInteger()); - model.ReserveNumSubsets(num_cols.value()); + model.ReserveNumSubsets(num_cols); for (SubsetIndex subset : SubsetRange(num_cols)) { const double cost(reader.ParseNextDouble()); - model.SetSubsetCost(subset.value(), cost); + model.SetSubsetCost(subset, cost); } for (ElementIndex element : ElementRange(num_rows)) { LOG_EVERY_N_SEC(INFO, 5) << absl::StrFormat("Reading element %d (%.1f%%)", element.value(), - 100.0 * element.value() / model.num_elements()); + Percent(element, model.num_elements())); const RowEntryIndex row_size(reader.ParseNextInteger()); for (RowEntryIndex entry(0); entry < row_size; ++entry) { // Correct the 1-indexing. - const int subset(reader.ParseNextInteger() - 1); - model.AddElementToSubset(element.value(), subset); + const SubsetIndex subset(reader.ParseNextInteger() - 1); + model.AddElementToSubset(element, subset); } } - LOG(INFO) << "Finished reading the model."; + LOG(INFO) << "Read " << model.num_subsets() << " subsets, " + << model.num_elements() << " elements"; file->Close(file::Defaults()).IgnoreError(); model.CreateSparseRowView(); return model; @@ -136,59 +151,96 @@ SetCoverModel ReadOrlibScp(absl::string_view filename) { // This is a column-based format where the elements are 1-indexed. SetCoverModel ReadOrlibRail(absl::string_view filename) { + CHECK_OK(file::Exists(filename, file::Defaults())); SetCoverModel model; File* file(file::OpenOrDie(filename, "r", file::Defaults())); SetCoverReader reader(file); const ElementIndex num_rows(reader.ParseNextInteger()); - const BaseInt num_cols(reader.ParseNextInteger()); + const SubsetIndex num_cols(reader.ParseNextInteger()); model.ReserveNumSubsets(num_cols); - for (BaseInt subset(0); subset < num_cols; ++subset) { + for (SubsetIndex subset : SubsetRange(num_cols)) { LOG_EVERY_N_SEC(INFO, 5) - << absl::StrFormat("Reading subset %d (%.1f%%)", subset, - 100.0 * subset / model.num_subsets()); + << absl::StrFormat("Reading subset %d (%.1f%%)", subset.value(), + Percent(subset, model.num_subsets())); const double cost(reader.ParseNextDouble()); model.SetSubsetCost(subset, cost); const ColumnEntryIndex column_size(reader.ParseNextInteger()); - model.ReserveNumElementsInSubset(column_size.value(), subset); + model.ReserveNumElementsInSubset(column_size.value(), subset.value()); for (const ColumnEntryIndex _ : ColumnEntryRange(column_size)) { // Correct the 1-indexing. const ElementIndex element(reader.ParseNextInteger() - 1); - model.AddElementToSubset(element.value(), subset); + model.AddElementToSubset(element, subset); } } - LOG(INFO) << "Finished reading the model."; + LOG(INFO) << "Read " << model.num_subsets() << " subsets, " + << model.num_elements() << " elements"; file->Close(file::Defaults()).IgnoreError(); model.CreateSparseRowView(); return model; } SetCoverModel ReadFimiDat(absl::string_view filename) { + CHECK_OK(file::Exists(filename, file::Defaults())); SetCoverModel model; - BaseInt subset(0); + SubsetIndex subset(0); + // Read the file once to discover the smallest element index. + BaseInt smallest_element = std::numeric_limits::max(); + BaseInt largest_element = 0; + for (const std::string& line : FileLines(filename)) { + std::vector elements = absl::StrSplit(line, ' '); + if (elements.back().empty() || elements.back()[0] == '\0') { + elements.pop_back(); + } + for (const std::string& number_str : elements) { + BaseInt element; + CHECK(absl::SimpleAtoi(number_str, &element)); + smallest_element = std::min(smallest_element, element); + largest_element = std::max(largest_element, element); + } + } + DLOG(INFO) << "Smallest element: " << smallest_element + << ", Largest element: " << largest_element; + ElementBoolVector element_seen(largest_element + 1, false); for (const std::string& line : FileLines(filename)) { LOG_EVERY_N_SEC(INFO, 5) - << absl::StrFormat("Reading subset %d (%.1f%%)", subset, - 100.0 * subset / model.num_subsets()); + << absl::StrFormat("Reading subset %d", subset.value()); std::vector elements = absl::StrSplit(line, ' '); if (elements.back().empty() || elements.back()[0] == '\0') { elements.pop_back(); } model.AddEmptySubset(1); - for (const std::string& number : elements) { - BaseInt element; - CHECK(absl::SimpleAtoi(number, &element)); - CHECK_GT(element, 0); - // Correct the 1-indexing. - model.AddElementToLastSubset(ElementIndex(element - 1)); + // As there can be repetitions in the data, we need to keep track of the + // elements already added to the subset. + std::vector elements_list; + for (const std::string& number_str : elements) { + BaseInt raw_element; + CHECK(absl::SimpleAtoi(number_str, &raw_element)); + // Re-index the elements starting from 0. + ElementIndex element(raw_element - smallest_element); + if (element_seen[element]) { + DLOG(INFO) << "Element " << element << " already in subset " + << subset.value(); + continue; + } + element_seen[element] = true; + elements_list.push_back(element); + CHECK_GE(element.value(), 0); + model.AddElementToLastSubset(element); + } + // Clean up the list of elements. + for (const ElementIndex element : elements_list) { + element_seen[element] = false; } ++subset; } - LOG(INFO) << "Finished reading the model."; + LOG(INFO) << "Read " << model.num_subsets() << " subsets, " + << model.num_elements() << " elements"; model.CreateSparseRowView(); return model; } SetCoverModel ReadSetCoverProto(absl::string_view filename, bool binary) { + CHECK_OK(file::Exists(filename, file::Defaults())); SetCoverModel model; SetCoverProto message; if (binary) { @@ -255,7 +307,7 @@ void WriteOrlibScp(const SetCoverModel& model, absl::string_view filename) { for (const ElementIndex element : model.ElementRange()) { LOG_EVERY_N_SEC(INFO, 5) << absl::StrFormat("Writing element %d (%.1f%%)", element.value(), - 100.0 * element.value() / model.num_elements()); + Percent(element, model.num_elements())); formatter.Append(absl::StrCat(model.rows()[element].size(), "\n")); for (const SubsetIndex subset : model.rows()[element]) { formatter.Append(subset.value() + 1); @@ -276,7 +328,7 @@ void WriteOrlibRail(const SetCoverModel& model, absl::string_view filename) { for (const SubsetIndex subset : model.SubsetRange()) { LOG_EVERY_N_SEC(INFO, 5) << absl::StrFormat("Writing subset %d (%.1f%%)", subset.value(), - 100.0 * subset.value() / model.num_subsets()); + Percent(subset, model.num_subsets())); formatter.Append(model.subset_costs()[subset]); formatter.Append(static_cast(model.columns()[subset].size())); for (const ElementIndex element : model.columns()[subset]) { @@ -299,6 +351,7 @@ void WriteSetCoverProto(const SetCoverModel& model, absl::string_view filename, } SubsetBoolVector ReadSetCoverSolutionText(absl::string_view filename) { + CHECK_OK(file::Exists(filename, file::Defaults())); SubsetBoolVector solution; File* file(file::OpenOrDie(filename, "r", file::Defaults())); SetCoverReader reader(file); @@ -316,6 +369,7 @@ SubsetBoolVector ReadSetCoverSolutionText(absl::string_view filename) { SubsetBoolVector ReadSetCoverSolutionProto(absl::string_view filename, bool binary) { + CHECK_OK(file::Exists(filename, file::Defaults())); SubsetBoolVector solution; SetCoverSolutionResponse message; if (binary) { diff --git a/ortools/algorithms/set_cover_reader.h b/ortools/set_cover/set_cover_reader.h similarity index 96% rename from ortools/algorithms/set_cover_reader.h rename to ortools/set_cover/set_cover_reader.h index 324aa801ca..13076cd28d 100644 --- a/ortools/algorithms/set_cover_reader.h +++ b/ortools/set_cover/set_cover_reader.h @@ -11,11 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_READER_H_ -#define OR_TOOLS_ALGORITHMS_SET_COVER_READER_H_ +#ifndef OR_TOOLS_SET_COVER_SET_COVER_READER_H_ +#define OR_TOOLS_SET_COVER_SET_COVER_READER_H_ #include "absl/strings/string_view.h" -#include "ortools/algorithms/set_cover_model.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -106,4 +106,4 @@ void WriteSetCoverSolutionProto(const SetCoverModel& model, } // namespace operations_research -#endif // OR_TOOLS_ALGORITHMS_SET_COVER_READER_H_ +#endif // OR_TOOLS_SET_COVER_SET_COVER_READER_H_ diff --git a/ortools/algorithms/set_cover_solve.cc b/ortools/set_cover/set_cover_solve.cc similarity index 97% rename from ortools/algorithms/set_cover_solve.cc rename to ortools/set_cover/set_cover_solve.cc index 707fff19b3..75356ea98e 100644 --- a/ortools/algorithms/set_cover_solve.cc +++ b/ortools/set_cover/set_cover_solve.cc @@ -21,12 +21,13 @@ #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" -#include "ortools/algorithms/set_cover_heuristics.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" -#include "ortools/algorithms/set_cover_reader.h" #include "ortools/base/init_google.h" #include "ortools/base/timer.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" +#include "ortools/set_cover/set_cover_reader.h" ABSL_FLAG(std::string, input, "", "REQUIRED: Input file name."); ABSL_FLAG(std::string, input_fmt, "", diff --git a/ortools/algorithms/set_cover_test.cc b/ortools/set_cover/set_cover_test.cc similarity index 98% rename from ortools/algorithms/set_cover_test.cc rename to ortools/set_cover/set_cover_test.cc index 6ab6d5056d..9b5e762a5d 100644 --- a/ortools/algorithms/set_cover_test.cc +++ b/ortools/set_cover/set_cover_test.cc @@ -20,13 +20,14 @@ #include "absl/strings/str_cat.h" #include "benchmark/benchmark.h" #include "gtest/gtest.h" -#include "ortools/algorithms/set_cover.pb.h" -#include "ortools/algorithms/set_cover_heuristics.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_mip.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/gmock.h" #include "ortools/base/parse_text_proto.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover.pb.h" +#include "ortools/set_cover/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_mip.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { namespace {