diff --git a/cmake/cpp.cmake b/cmake/cpp.cmake index 4087633bb7..44eb5585de 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) @@ -539,6 +540,7 @@ foreach(SUBPROJECT IN ITEMS packing routing scheduling + set_cover port util) add_subdirectory(ortools/${SUBPROJECT}) diff --git a/cmake/python.cmake b/cmake/python.cmake index 8e641b016a..fdb8e12dcf 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -151,6 +151,7 @@ file(GLOB_RECURSE OR_TOOLS_PROTO_PY_FILES RELATIVE ${PROJECT_SOURCE_DIR} "ortools/routing/*.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") @@ -290,6 +291,7 @@ foreach(SUBPROJECT IN ITEMS routing sat scheduling + set_cover util) add_subdirectory(ortools/${SUBPROJECT}/python) endforeach() @@ -351,6 +353,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 "") @@ -624,8 +628,6 @@ add_custom_command( $ ${PYTHON_PROJECT}/init/python COMMAND ${CMAKE_COMMAND} -E copy $ ${PYTHON_PROJECT}/algorithms/python - COMMAND ${CMAKE_COMMAND} -E copy - $ ${PYTHON_PROJECT}/algorithms/python COMMAND ${CMAKE_COMMAND} -E copy $ ${PYTHON_PROJECT}/graph/python COMMAND ${CMAKE_COMMAND} -E copy @@ -657,6 +659,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 @@ -665,7 +669,6 @@ add_custom_command( DEPENDS init_pybind11 knapsack_solver_pybind11 - set_cover_pybind11 linear_sum_assignment_pybind11 max_flow_pybind11 min_cost_flow_pybind11 @@ -679,6 +682,7 @@ add_custom_command( $ cp_model_helper_pybind11 rcpsp_pybind11 + set_cover_pybind11 sorted_interval_list_pybind11 WORKING_DIRECTORY python COMMAND_EXPAND_LISTS) @@ -704,7 +708,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 . @@ -719,6 +722,7 @@ add_custom_command( COMMAND ${stubgen_EXECUTABLE} -p ortools.routing.python.model --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 bf23790929..8b2e36c828 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -250,163 +250,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..19e4d8e7d1 100644 --- a/ortools/algorithms/python/CMakeLists.txt +++ b/ortools/algorithms/python/CMakeLists.txt @@ -30,33 +30,3 @@ 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) - add_python_test(FILE_NAME ${FILE_NAME}) - endforeach() -endif() diff --git a/ortools/python/setup.py.in b/ortools/python/setup.py.in index 94cf33e86e..261be3a075 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'], @@ -125,6 +124,10 @@ setup( '$', '*.pyi' ], + '@PYTHON_PROJECT@.set_cover.python':[ + '$', + '*.pyi' + ], '@PYTHON_PROJECT@.util.python':[ '$', '*.pyi' diff --git a/ortools/set_cover/BUILD.bazel b/ortools/set_cover/BUILD.bazel new file mode 100644 index 0000000000..b2cf7cce74 --- /dev/null +++ b/ortools/set_cover/BUILD.bazel @@ -0,0 +1,268 @@ +# 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("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_python//python:proto.bzl", "py_proto_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 = "set_cover_lagrangian", + srcs = ["set_cover_lagrangian.cc"], + hdrs = ["set_cover_lagrangian.h"], + deps = [ + ":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 = [ + ":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 = [ + ":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 = [ + ":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 = [ + ":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_library( + name = "assignment", + srcs = ["assignment.cc"], + hdrs = ["assignment.h"], + deps = [ + ":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 = [ + ":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", + ], +) + +# 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 = [ + ":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", + ":set_cover_model", + "//ortools/base:gmock_main", + "//ortools/base:message_matchers", + ], +) + +cc_library( + name = "capacity_invariant", + srcs = ["capacity_invariant.cc"], + hdrs = ["capacity_invariant.h"], + deps = [ + ":capacity_model", + ":set_cover_model", + "@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..6fdf81af88 --- /dev/null +++ b/ortools/set_cover/assignment.cc @@ -0,0 +1,126 @@ +// 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 "ortools/base/logging.h" +#include "ortools/base/mathutil.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..69f1b95127 --- /dev/null +++ b/ortools/set_cover/assignment.h @@ -0,0 +1,104 @@ +// 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/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/capacity.proto b/ortools/set_cover/capacity.proto new file mode 100644 index 0000000000..962f38d6a6 --- /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`). + int32 subset = 1; + + message ElementWeightPair { + // The element this weight corresponds to (value of `element` in + // `SetCoverProto.Subset`). + int32 element = 1; + + // The weight of the element. + double 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. + double min_capacity = 2; + + // The maximum amount of resource that can be consumed. At least one of + // `min_capacity` and `max_capacity` must be present. + double 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..686add2feb --- /dev/null +++ b/ortools/set_cover/capacity_invariant.cc @@ -0,0 +1,112 @@ +// 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 "absl/log/check.h" +#include "ortools/base/logging.h" +#include "ortools/set_cover/capacity_model.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { + +void CapacityInvariant::Clear() { + current_slack_ = 0.0; + is_selected_.assign(set_cover_model_->num_subsets(), false); +} + +double CapacityInvariant::ComputeSlackChange(const SubsetIndex subset) const { + double slack_change = 0.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. + slack_change += model_->GetTermCapacityWeight(term); + } + } + return slack_change; +} + +bool CapacityInvariant::SlackChangeFitsConstraint( + const double slack_change) const { + const double new_slack = current_slack_ + slack_change; + return new_slack >= model_->GetMinimumCapacity() && + new_slack <= model_->GetMaximumCapacity(); +} + +bool CapacityInvariant::Flip(SubsetIndex subset) { + DCHECK_LT(subset.value(), set_cover_model_->num_subsets()) + << "Invalid subset: " << subset; + return !is_selected_[subset] ? Select(subset) : Deselect(subset); +} + +bool CapacityInvariant::CanFlip(SubsetIndex subset) const { + DCHECK_LT(subset.value(), set_cover_model_->num_subsets()) + << "Invalid subset: " << subset; + return !is_selected_[subset] ? CanSelect(subset) : CanDeselect(subset); +} + +bool CapacityInvariant::Select(SubsetIndex subset) { + DVLOG(1) << "[Capacity constraint] Selecting subset " << subset; + DCHECK(!is_selected_[subset]); + + const double slack_change = ComputeSlackChange(subset); + if (!SlackChangeFitsConstraint(slack_change)) { + DVLOG(1) << "[Capacity constraint] Selecting subset " << subset + << ": infeasible"; + return false; + } + + 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 double 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 double slack_change = -ComputeSlackChange(subset); + if (!SlackChangeFitsConstraint(slack_change)) { + DVLOG(1) << "[Capacity constraint] Deselecting subset " << subset + << ": infeasible"; + return false; + } + + 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 double 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..5d3d43d15a --- /dev/null +++ b/ortools/set_cover/capacity_invariant.h @@ -0,0 +1,107 @@ +// 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/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 this flipping move + // and incrementally updates the invariant. Otherwise, returns `false` and + // does not change the object. + // + // Flips is_selected_[subset] to its negation, by calling Select or Deselect + // depending on value. + bool Flip(SubsetIndex subset); + + // Returns `true` when the constraint would not be violated if this flipping + // move is performed. Otherwise, returns `false`. The object never changes. + bool CanFlip(SubsetIndex subset) const; + + // 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. + operations_research::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. + double ComputeSlackChange(SubsetIndex subset) const; + + // Determines whether the given slack change violates the constraint + // (`false`) or not (`true`). + bool SlackChangeFitsConstraint(double 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..b6f6f79579 --- /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.CanFlip(SubsetIndex(0))); // All moves are possible. + EXPECT_TRUE(cinv.CanFlip(SubsetIndex(1))); + + EXPECT_TRUE(cinv.Flip(SubsetIndex(0))); // Select returns true. + EXPECT_TRUE(cinv.CanFlip(SubsetIndex(0))); // Undoing: still valid. + EXPECT_FALSE(cinv.CanFlip(SubsetIndex(1))); // Impossible move. + EXPECT_FALSE(cinv.Flip(SubsetIndex(1))); // Select returns false. + // Current assignment: [true, false]. Current activation: 1. + + EXPECT_TRUE(cinv.Flip(SubsetIndex(0))); // Deselect returns true. + EXPECT_TRUE(cinv.CanFlip(SubsetIndex(0))); // Undoing: still valid. + EXPECT_TRUE(cinv.CanFlip(SubsetIndex(1))); // Valid when 0 not selected. + EXPECT_TRUE(cinv.Flip(SubsetIndex(1))); // Select returns true. + EXPECT_FALSE(cinv.CanFlip(SubsetIndex(0))); // Impossible move. + EXPECT_TRUE(cinv.CanFlip(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..d0dd1ed7c0 --- /dev/null +++ b/ortools/set_cover/capacity_model.cc @@ -0,0 +1,131 @@ +// 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 + +#include "absl/log/check.h" +#include "ortools/base/logging.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { +void CapacityModel::AddTerm(SubsetIndex subset, ElementIndex element, + CapacityWeight weight) { + CHECK(std::isfinite(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(!std::isnan(min_capacity)); + CHECK_NE(min_capacity, std::numeric_limits::max()); + min_capacity_ = min_capacity; +} + +void CapacityModel::SetMaximumCapacity(CapacityWeight max_capacity) { + CHECK(!std::isnan(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..544e970966 --- /dev/null +++ b/ortools/set_cover/capacity_model.h @@ -0,0 +1,156 @@ +// 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 "absl/log/check.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.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 = double; + +// 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(!std::isnan(min_capacity_)); + CHECK(!std::isnan(max_capacity_)); + 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. + // This will CHECK-fail if the weight is infinite or a NaN. + 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..3a6d89ada6 --- /dev/null +++ b/ortools/set_cover/capacity_model_test.cc @@ -0,0 +1,231 @@ +// 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" +#include "ortools/base/message_matchers.h" +#include "ortools/set_cover/set_cover_model.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, ConstructorRejectsNaN) { + EXPECT_DEATH(CapacityModel(std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN()), + "isnan"); +} + +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, WithMinimumWeightRejectsNaN) { + EXPECT_DEATH(CapacityModel::WithMinimumWeight( + std::numeric_limits::quiet_NaN()), + "isnan"); +} + +TEST(CapacityModel, WithMaximumWeightRejectsNaN) { + EXPECT_DEATH(CapacityModel::WithMaximumWeight( + std::numeric_limits::quiet_NaN()), + "isnan"); +} + +TEST(CapacityModel, SetMinimumCapacityRejectsNaN) { + CapacityModel m(0.0, 1.0); + EXPECT_DEATH(m.SetMinimumCapacity(std::numeric_limits::quiet_NaN()), + "isnan"); +} + +TEST(CapacityModel, SetMinimumCapacityRejectsPlusInfinity) { + CapacityModel m(0.0, 1.0); + EXPECT_DEATH(m.SetMinimumCapacity(std::numeric_limits::max()), + "max"); +} + +TEST(CapacityModel, SetMaximumCapacityRejectsNaN) { + CapacityModel m(0.0, 1.0); + EXPECT_DEATH(m.SetMaximumCapacity(std::numeric_limits::quiet_NaN()), + "isnan"); +} + +TEST(CapacityModel, SetMaximumCapacityRejectsMinusInfinity) { + CapacityModel m(0.0, 1.0); + EXPECT_DEATH(m.SetMaximumCapacity(std::numeric_limits::min()), + "min"); +} + +TEST(CapacityModel, AddTermRejectsNaN) { + CapacityModel m(0.0, 1.0); + EXPECT_DEATH(m.AddTerm(SubsetIndex(0), ElementIndex(0), + std::numeric_limits::quiet_NaN()), + "isfinite"); +} + +TEST(CapacityModel, AddTermRejectsPlusInf) { + CapacityModel m(0.0, 1.0); + EXPECT_DEATH(m.AddTerm(SubsetIndex(0), ElementIndex(0), + std::numeric_limits::infinity()), + "isfinite"); +} + +TEST(CapacityModel, AddTermRejectsMinusInf) { + CapacityModel m(0.0, 1.0); + EXPECT_DEATH(m.AddTerm(SubsetIndex(0), ElementIndex(0), + -std::numeric_limits::infinity()), + "isfinite"); +} + +TEST(CapacityModel, ComputeFeasibilityWithNoTerms) { + CapacityModel m(0.0, 1.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-1.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(0.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-2.0); + m.SetMaximumCapacity(-1.0); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +TEST(CapacityModel, ComputeFeasibilityWithOnlyPositiveWeights) { + CapacityModel m(0.0, 1.0); + m.AddTerm(SubsetIndex(0), ElementIndex(0), 1.0); + m.AddTerm(SubsetIndex(0), ElementIndex(1), 2.0); + m.AddTerm(SubsetIndex(0), ElementIndex(2), 3.0); + // Activation bounds: [0.0, 6.0]. + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-1.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(-1.0); + EXPECT_FALSE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(7.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(7.0); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +TEST(CapacityModel, ComputeFeasibilityWithOnlyNegativeWeights) { + CapacityModel m(0.0, 1.0); + m.AddTerm(SubsetIndex(0), ElementIndex(0), -1.0); + m.AddTerm(SubsetIndex(0), ElementIndex(1), -2.0); + m.AddTerm(SubsetIndex(0), ElementIndex(2), -3.0); + // Activation bounds: [-6.0, 0.0]. + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(1.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(1.0); + EXPECT_FALSE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-7.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(-7.0); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +TEST(CapacityModel, ComputeFeasibilityWithOnlyMixedWeights) { + CapacityModel m(0.0, 1.0); + m.AddTerm(SubsetIndex(0), ElementIndex(0), -1.0); + m.AddTerm(SubsetIndex(0), ElementIndex(1), 2.0); + m.AddTerm(SubsetIndex(0), ElementIndex(2), -3.0); + // Activation bounds: [-4.0, 2.0]. + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(3.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(3.0); + EXPECT_FALSE(m.ComputeFeasibility()); + + m.SetMinimumCapacity(-5.0); + EXPECT_TRUE(m.ComputeFeasibility()); + + m.SetMaximumCapacity(-5.0); + EXPECT_FALSE(m.ComputeFeasibility()); +} + +// TEST(CapacityModel, ImportModelFromProto) { +// CapacityModel m(0.0, 1.0); +// EXPECT_THAT(m.ExportModelAsProto(), EqualsProto(R"pb(min_capacity: 0.0 +// max_capacity: 1.0)pb")); + +// m.AddTerm(SubsetIndex(0), ElementIndex(0), 1.0); +// EXPECT_THAT(m.ExportModelAsProto(), +// EqualsProto(R"pb(min_capacity: 0.0 +// max_capacity: 1.0 +// capacity_term { +// subset: 0 +// element_weights { element: 0 weight: 1.0 } +// })pb")); + +// m.AddTerm(SubsetIndex(0), ElementIndex(1), 1.0); +// EXPECT_THAT(m.ExportModelAsProto(), +// EqualsProto( +// R"pb(min_capacity: 0.0 +// max_capacity: 1.0 +// capacity_term { +// subset: 0 +// element_weights { element: 0 weight: 1.0 } +// element_weights { element: 1 weight: 1.0 } +// })pb")); +// } + +// TEST(CapacityModel, ImportModelFromProtoHasCanonicalOrder) { +// // Reverse order for the terms compared to +// // CapacityModel_ImportModelFromProto, same order in the proto. +// CapacityModel m(0.0, 1.0); +// m.AddTerm(SubsetIndex(0), ElementIndex(1), 1.0); +// m.AddTerm(SubsetIndex(0), ElementIndex(0), 1.0); +// EXPECT_THAT(m.ExportModelAsProto(), +// EqualsProto( +// R"pb(min_capacity: 0.0 +// max_capacity: 1.0 +// capacity_term { +// subset: 0 +// element_weights { element: 0 weight: 1.0 } +// element_weights { element: 1 weight: 1.0 } +// })pb")); +// } + +} // 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/algorithms/python/set_cover.cc b/ortools/set_cover/python/set_cover.cc similarity index 98% rename from ortools/algorithms/python/set_cover.cc rename to ortools/set_cover/python/set_cover.cc index 5e91cad05f..b81f1f201d 100644 --- a/ortools/algorithms/python/set_cover.cc +++ b/ortools/set_cover/python/set_cover.cc @@ -20,10 +20,10 @@ #include #include "absl/types/span.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/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" @@ -300,7 +300,7 @@ PYBIND11_MODULE(set_cover, m) { .def( "compute_coverage_in_focus", [](SetCoverInvariant& invariant, - const std::vector& focus) -> std::vector { + absl::Span focus) -> std::vector { return invariant .ComputeCoverageInFocus(VectorIntToVectorSubsetIndex(focus)) .get(); @@ -321,7 +321,7 @@ PYBIND11_MODULE(set_cover, m) { .def("compress_trace", &SetCoverInvariant::CompressTrace) .def("load_solution", [](SetCoverInvariant& invariant, - const std::vector& solution) -> void { + absl::Span solution) -> void { SubsetBoolVector sol(solution.begin(), solution.end()); return invariant.LoadSolution(sol); }) @@ -453,7 +453,7 @@ PYBIND11_MODULE(set_cover, m) { return heuristic.NextSolution(num_iterations); }) .def("next_solution", - [](SteepestSearch& heuristic, const std::vector& focus, + [](SteepestSearch& heuristic, absl::Span focus, int num_iterations) -> bool { return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus), num_iterations); @@ -512,7 +512,7 @@ PYBIND11_MODULE(set_cover, m) { return heuristic.NextSolution(num_iterations); }) .def("next_solution", - [](GuidedTabuSearch& heuristic, const std::vector& focus, + [](GuidedTabuSearch& heuristic, absl::Span focus, int num_iterations) -> bool { return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus), num_iterations); diff --git a/ortools/algorithms/python/set_cover_test.py b/ortools/set_cover/python/set_cover_test.py similarity index 99% rename from ortools/algorithms/python/set_cover_test.py rename to ortools/set_cover/python/set_cover_test.py index fdee247433..555a686d22 100644 --- a/ortools/algorithms/python/set_cover_test.py +++ b/ortools/set_cover/python/set_cover_test.py @@ -15,7 +15,7 @@ from absl import app from absl.testing import absltest -from ortools.algorithms.python import set_cover +from ortools.set_cover.python import set_cover def create_initial_cover_model(): 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 7d7892335e..735a142cd8 100644 --- a/ortools/algorithms/samples/set_cover.cc +++ b/ortools/set_cover/samples/set_cover.cc @@ -15,10 +15,10 @@ // [START import] #include -#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/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 100% rename from ortools/algorithms/set_cover.proto rename to ortools/set_cover/set_cover.proto diff --git a/ortools/algorithms/set_cover_heuristics.cc b/ortools/set_cover/set_cover_heuristics.cc similarity index 99% rename from ortools/algorithms/set_cover_heuristics.cc rename to ortools/set_cover/set_cover_heuristics.cc index 27a36f846b..a3b06a9c5f 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 @@ -28,9 +28,9 @@ #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/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { 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..8c8a96b51c 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/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.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 99% rename from ortools/algorithms/set_cover_invariant.cc rename to ortools/set_cover/set_cover_invariant.cc index 00146aabc2..23d0c19847 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 @@ -20,9 +20,9 @@ #include "absl/log/check.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/set_cover_model.h" namespace operations_research { diff --git a/ortools/algorithms/set_cover_invariant.h b/ortools/set_cover/set_cover_invariant.h similarity index 97% rename from ortools/algorithms/set_cover_invariant.h rename to ortools/set_cover/set_cover_invariant.h index 532b6d834c..f273099cbf 100644 --- a/ortools/algorithms/set_cover_invariant.h +++ b/ortools/set_cover/set_cover_invariant.h @@ -11,16 +11,16 @@ // 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/set_cover.pb.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -288,4 +288,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..5f3ad6b93f 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,9 @@ #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/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" namespace operations_research { @@ -99,7 +99,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 96% rename from ortools/algorithms/set_cover_lagrangian.h rename to ortools/set_cover/set_cover_lagrangian.h index ec207cb037..9116721196 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/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 97% rename from ortools/algorithms/set_cover_mip.cc rename to ortools/set_cover/set_cover_mip.cc index 13c9453196..4d8622b9e8 100644 --- a/ortools/algorithms/set_cover_mip.cc +++ b/ortools/set_cover/set_cover_mip.cc @@ -11,18 +11,18 @@ // 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 #include "absl/log/check.h" #include "absl/types/span.h" -#include "ortools/algorithms/set_cover_invariant.h" -#include "ortools/algorithms/set_cover_model.h" #include "ortools/base/logging.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/lp_data/lp_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..df78d246df 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/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.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 99% rename from ortools/algorithms/set_cover_model.cc rename to ortools/set_cover/set_cover_model.cc index ff7a8e3f73..fa1ca69903 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 @@ -31,8 +31,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/base/logging.h" +#include "ortools/set_cover/set_cover.pb.h" namespace operations_research { diff --git a/ortools/algorithms/set_cover_model.h b/ortools/set_cover/set_cover_model.h similarity index 98% rename from ortools/algorithms/set_cover_model.h rename to ortools/set_cover/set_cover_model.h index 2e4d1c76e2..cc43415e2e 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,9 @@ #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/set_cover.pb.h" // Representation class for the weighted set-covering problem. // @@ -398,4 +398,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 40092a5b2f..1efe7d7094 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 @@ -28,12 +29,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/logging.h" #include "ortools/base/options.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 +107,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 +150,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 +306,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 +327,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 +350,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 +368,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 98% rename from ortools/algorithms/set_cover_solve.cc rename to ortools/set_cover/set_cover_solve.cc index 95b139a116..cc14d8e335 100644 --- a/ortools/algorithms/set_cover_solve.cc +++ b/ortools/set_cover/set_cover_solve.cc @@ -20,13 +20,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/logging.h" #include "ortools/base/timer.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 bb343485d8..ff1b16f642 100644 --- a/ortools/algorithms/set_cover_test.cc +++ b/ortools/set_cover/set_cover_test.cc @@ -19,14 +19,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/logging.h" #include "ortools/base/parse_text_proto.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 {