move set_cover

This commit is contained in:
Mizux Seiha
2025-03-04 21:06:53 +01:00
parent a695779281
commit 56fde74a11
45 changed files with 2801 additions and 420 deletions

View File

@@ -412,6 +412,7 @@ file(GLOB_RECURSE OR_TOOLS_PROTO_FILES RELATIVE ${PROJECT_SOURCE_DIR}
"ortools/packing/*.proto"
"ortools/sat/*.proto"
"ortools/scheduling/*.proto"
"ortools/set_cover/*.proto"
"ortools/util/*.proto"
)
if(USE_PDLP OR BUILD_MATH_OPT)
@@ -524,6 +525,7 @@ foreach(SUBPROJECT IN ITEMS
lp_data
packing
scheduling
set_cover
port
util)
add_subdirectory(ortools/${SUBPROJECT})

View File

@@ -150,6 +150,7 @@ file(GLOB_RECURSE OR_TOOLS_PROTO_PY_FILES RELATIVE ${PROJECT_SOURCE_DIR}
"ortools/packing/*.proto"
"ortools/sat/*.proto"
"ortools/scheduling/*.proto"
"ortools/set_cover/*.proto"
"ortools/util/*.proto"
)
list(REMOVE_ITEM OR_TOOLS_PROTO_PY_FILES "ortools/constraint_solver/demon_profiler.proto")
@@ -288,6 +289,7 @@ foreach(SUBPROJECT IN ITEMS
constraint_solver
sat
scheduling
set_cover
util)
add_subdirectory(ortools/${SUBPROJECT}/python)
endforeach()
@@ -345,6 +347,8 @@ file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/sat/python/__init__.py CONTENT "")
file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/sat/colab/__init__.py CONTENT "")
file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/scheduling/__init__.py CONTENT "")
file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/scheduling/python/__init__.py CONTENT "")
file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/set_cover/__init__.py CONTENT "")
file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/set_cover/python/__init__.py CONTENT "")
file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/util/__init__.py CONTENT "")
file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/util/python/__init__.py CONTENT "")
@@ -645,6 +649,8 @@ add_custom_command(
$<TARGET_FILE:cp_model_helper_pybind11> ${PYTHON_PROJECT}/sat/python
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:rcpsp_pybind11> ${PYTHON_PROJECT}/scheduling/python
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:set_cover_pybind11> ${PYTHON_PROJECT}/set_cover/python
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:sorted_interval_list_pybind11> ${PYTHON_PROJECT}/util/python
COMMAND ${CMAKE_COMMAND} -E touch ${PROJECT_BINARY_DIR}/python/pybind11_timestamp
@@ -653,7 +659,6 @@ add_custom_command(
DEPENDS
init_pybind11
knapsack_solver_pybind11
set_cover_pybind11
linear_sum_assignment_pybind11
max_flow_pybind11
min_cost_flow_pybind11
@@ -664,6 +669,7 @@ add_custom_command(
$<TARGET_NAME_IF_EXISTS:pdlp_pybind11>
cp_model_helper_pybind11
rcpsp_pybind11
set_cover_pybind11
sorted_interval_list_pybind11
WORKING_DIRECTORY python
COMMAND_EXPAND_LISTS)
@@ -689,7 +695,6 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E remove -f stub_timestamp
COMMAND ${stubgen_EXECUTABLE} -p ortools.init.python.init --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.algorithms.python.knapsack_solver --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.algorithms.python.set_cover --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.linear_sum_assignment --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.max_flow --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.min_cost_flow --output .
@@ -701,6 +706,7 @@ add_custom_command(
COMMAND ${stubgen_EXECUTABLE} -p ortools.pdlp.python.pdlp --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.sat.python.cp_model_helper --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.scheduling.python.rcpsp --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.set_cover.python.set_cover --output .
COMMAND ${stubgen_EXECUTABLE} -p ortools.util.python.sorted_interval_list --output .
COMMAND ${CMAKE_COMMAND} -E touch ${PROJECT_BINARY_DIR}/python/stub_timestamp
MAIN_DEPENDENCY

View File

@@ -12,10 +12,7 @@
# limitations under the License.
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library")
load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
load("@rules_cc//cc:defs.bzl", "cc_library")
load("@rules_python//python:proto.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -68,6 +65,7 @@ cc_library(
hdrs = ["binary_search.h"],
deps = [
"//ortools/base",
"@com_google_absl//absl/base:log_severity",
"@com_google_absl//absl/functional:function_ref",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/numeric:int128",
@@ -166,10 +164,9 @@ cc_test(
srcs = ["hungarian_test.cc"],
deps = [
":hungarian",
"//ortools/base",
"//ortools/base:gmock_main",
"//ortools/base:map_util",
"//ortools/base:types",
"@com_google_absl//absl/base:core_headers",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/types:span",
@@ -250,163 +247,6 @@ cc_test(
# query matching library.
# Weighted set covering library.
proto_library(
name = "set_cover_proto",
srcs = ["set_cover.proto"],
deps = [
"//ortools/util:int128_proto",
],
)
cc_proto_library(
name = "set_cover_cc_proto",
deps = [":set_cover_proto"],
)
py_proto_library(
name = "set_cover_py_pb2",
deps = [":set_cover_proto"],
)
cc_library(
name = "set_cover_lagrangian",
srcs = ["set_cover_lagrangian.cc"],
hdrs = ["set_cover_lagrangian.h"],
deps = [
":adjustable_k_ary_heap",
":set_cover_invariant",
":set_cover_model",
"//ortools/base:threadpool",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/synchronization",
],
)
cc_library(
name = "set_cover_model",
srcs = ["set_cover_model.cc"],
hdrs = ["set_cover_model.h"],
deps = [
":radix_sort",
":set_cover_cc_proto",
"//ortools/base:intops",
"//ortools/base:strong_vector",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/numeric:bits",
"@com_google_absl//absl/random",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_invariant",
srcs = ["set_cover_invariant.cc"],
hdrs = ["set_cover_invariant.h"],
deps = [
":set_cover_cc_proto",
":set_cover_model",
"//ortools/base",
"//ortools/base:mathutil",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_heuristics",
srcs = ["set_cover_heuristics.cc"],
hdrs = ["set_cover_heuristics.h"],
deps = [
":adjustable_k_ary_heap",
":set_cover_invariant",
":set_cover_model",
"//ortools/base",
"@com_google_absl//absl/base",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/numeric:bits",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_mip",
srcs = ["set_cover_mip.cc"],
hdrs = ["set_cover_mip.h"],
deps = [
":set_cover_invariant",
":set_cover_model",
"//ortools/linear_solver",
"//ortools/lp_data:base",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_reader",
srcs = ["set_cover_reader.cc"],
hdrs = ["set_cover_reader.h"],
deps = [
":set_cover_cc_proto",
":set_cover_model",
"//ortools/base:file",
"//ortools/util:filelineiter",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/strings:string_view",
],
)
cc_binary(
name = "set_cover_solve",
srcs = ["set_cover_solve.cc"],
deps = [
":set_cover_heuristics",
":set_cover_invariant",
":set_cover_model",
":set_cover_reader",
"//ortools/base",
"//ortools/base:timer",
"@com_google_absl//absl/flags:flag",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/time",
],
)
cc_test(
name = "set_cover_test",
size = "medium",
timeout = "eternal",
srcs = ["set_cover_test.cc"],
deps = [
":set_cover_cc_proto",
":set_cover_heuristics",
":set_cover_invariant",
":set_cover_mip",
":set_cover_model",
"//ortools/base:gmock_main",
"//ortools/base:parse_text_proto",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
"@com_google_benchmark//:benchmark",
],
)
# Graph automorphism libraries.
cc_library(
name = "dense_doubly_linked_list",

View File

@@ -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)

View File

@@ -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"),
],
)

View File

@@ -31,29 +31,6 @@ endif()
target_link_libraries(knapsack_solver_pybind11 PRIVATE ${PROJECT_NAMESPACE}::ortools)
add_library(${PROJECT_NAMESPACE}::knapsack_solver_pybind11 ALIAS knapsack_solver_pybind11)
# set_cover
pybind11_add_module(set_cover_pybind11 MODULE set_cover.cc)
set_target_properties(set_cover_pybind11 PROPERTIES
LIBRARY_OUTPUT_NAME "set_cover")
# note: macOS is APPLE and also UNIX !
if(APPLE)
set_target_properties(set_cover_pybind11 PROPERTIES
SUFFIX ".so"
INSTALL_RPATH "@loader_path;@loader_path/../../../${PYTHON_PROJECT}/.libs"
)
elseif(UNIX)
set_target_properties(set_cover_pybind11 PROPERTIES
INSTALL_RPATH "$ORIGIN:$ORIGIN/../../../${PYTHON_PROJECT}/.libs"
)
endif()
target_link_libraries(set_cover_pybind11 PRIVATE
${PROJECT_NAMESPACE}::ortools
pybind11_native_proto_caster
)
add_library(${PROJECT_NAMESPACE}::set_cover_pybind11 ALIAS set_cover_pybind11)
if(BUILD_TESTING)
file(GLOB PYTHON_SRCS "*_test.py")
foreach(FILE_NAME IN LISTS PYTHON_SRCS)

View File

@@ -59,7 +59,6 @@ setup(
],
'@PYTHON_PROJECT@.algorithms.python':[
'$<TARGET_FILE_NAME:knapsack_solver_pybind11>',
'$<TARGET_FILE_NAME:set_cover_pybind11>',
'*.pyi'
],
'@PYTHON_PROJECT@.bop':['*.pyi'],
@@ -113,6 +112,10 @@ setup(
'$<TARGET_FILE_NAME:rcpsp_pybind11>',
'*.pyi'
],
'@PYTHON_PROJECT@.set_cover.python':[
'$<TARGET_FILE_NAME:set_cover_pybind11>',
'*.pyi'
],
'@PYTHON_PROJECT@.util.python':[
'$<TARGET_FILE_NAME:sorted_interval_list_pybind11>',
'*.pyi'

View File

@@ -21,9 +21,6 @@
#include "absl/algorithm/container.h"
#include "absl/log/check.h"
#include "ortools/algorithms/set_cover_heuristics.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/base/logging.h"
#include "ortools/base/stl_util.h"
#include "ortools/sat/diffn_util.h"
@@ -33,6 +30,9 @@
#include "ortools/sat/no_overlap_2d_helper.h"
#include "ortools/sat/synchronization.h"
#include "ortools/sat/util.h"
#include "ortools/set_cover/set_cover_heuristics.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
namespace sat {

View File

@@ -0,0 +1,287 @@
# Copyright 2010-2025 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library")
load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library")
load("@rules_cc//cc:defs.bzl", "cc_library")
package(default_visibility = ["//visibility:public"])
# Description:
# A solver for weighted set covering with side constraints
proto_library(
name = "set_cover_proto",
srcs = ["set_cover.proto"],
deps = [
"//ortools/util:int128_proto",
],
)
cc_proto_library(
name = "set_cover_cc_proto",
deps = [":set_cover_proto"],
)
py_proto_library(
name = "set_cover_py_pb2",
deps = [":set_cover_proto"],
)
cc_library(
name = "base_types",
hdrs = ["base_types.h"],
deps = [
"//ortools/base:intops",
"//ortools/base:strong_vector",
],
)
cc_library(
name = "set_cover_lagrangian",
srcs = ["set_cover_lagrangian.cc"],
hdrs = ["set_cover_lagrangian.h"],
deps = [
":base_types",
":set_cover_invariant",
":set_cover_model",
"//ortools/algorithms:adjustable_k_ary_heap",
"//ortools/base:threadpool",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/synchronization",
],
)
cc_library(
name = "set_cover_model",
srcs = ["set_cover_model.cc"],
hdrs = ["set_cover_model.h"],
deps = [
":base_types",
":set_cover_cc_proto",
"//ortools/algorithms:radix_sort",
"//ortools/base:intops",
"//ortools/base:strong_vector",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/numeric:bits",
"@com_google_absl//absl/random",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_invariant",
srcs = ["set_cover_invariant.cc"],
hdrs = ["set_cover_invariant.h"],
deps = [
":base_types",
":set_cover_cc_proto",
":set_cover_model",
"//ortools/base",
"//ortools/base:mathutil",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_heuristics",
srcs = ["set_cover_heuristics.cc"],
hdrs = ["set_cover_heuristics.h"],
deps = [
":base_types",
":set_cover_invariant",
":set_cover_model",
"//ortools/algorithms:adjustable_k_ary_heap",
"//ortools/base",
"@com_google_absl//absl/base",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/numeric:bits",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_mip",
srcs = ["set_cover_mip.cc"],
hdrs = ["set_cover_mip.h"],
deps = [
":base_types",
":set_cover_invariant",
":set_cover_model",
"//ortools/linear_solver",
"//ortools/lp_data:base",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "set_cover_reader",
srcs = ["set_cover_reader.cc"],
hdrs = ["set_cover_reader.h"],
deps = [
":base_types",
":set_cover_cc_proto",
":set_cover_model",
"//ortools/base:file",
"//ortools/util:filelineiter",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/strings:string_view",
],
)
cc_library(
name = "assignment",
srcs = ["assignment.cc"],
hdrs = ["assignment.h"],
deps = [
":base_types",
":capacity_invariant",
":set_cover_invariant",
":set_cover_model",
"//ortools/base:mathutil",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
],
)
cc_test(
name = "assignment_test",
srcs = ["assignment_test.cc"],
deps = [
":assignment",
":set_cover_invariant",
":set_cover_model",
"//ortools/base:gmock_main",
"@com_google_absl//absl/log:check",
],
)
cc_binary(
name = "set_cover_solve",
srcs = ["set_cover_solve.cc"],
deps = [
":base_types",
":set_cover_heuristics",
":set_cover_invariant",
":set_cover_model",
":set_cover_reader",
"//ortools/base",
"//ortools/base:timer",
"@com_google_absl//absl/flags:flag",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/time",
],
)
cc_test(
name = "set_cover_test",
size = "medium",
timeout = "eternal",
srcs = ["set_cover_test.cc"],
deps = [
":base_types",
":set_cover_cc_proto",
":set_cover_heuristics",
":set_cover_invariant",
":set_cover_mip",
":set_cover_model",
"//ortools/base:gmock_main",
"//ortools/base:parse_text_proto",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
"@com_google_benchmark//:benchmark",
],
)
# Side constraint: capacity.
proto_library(
name = "capacity_proto",
srcs = ["capacity.proto"],
)
cc_proto_library(
name = "capacity_cc_proto",
deps = [":capacity_proto"],
)
py_proto_library(
name = "capacity_py_pb2",
deps = [":capacity_proto"],
)
cc_library(
name = "capacity_model",
srcs = ["capacity_model.cc"],
hdrs = ["capacity_model.h"],
deps = [
":base_types",
":capacity_cc_proto",
":set_cover_model",
"//ortools/base:intops",
"//ortools/base:strong_vector",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
],
)
cc_test(
name = "capacity_model_test",
srcs = ["capacity_model_test.cc"],
deps = [
":capacity_model",
"//ortools/base:gmock_main",
],
)
cc_library(
name = "capacity_invariant",
srcs = ["capacity_invariant.cc"],
hdrs = ["capacity_invariant.h"],
deps = [
":base_types",
":capacity_model",
":set_cover_model",
"//ortools/util:saturated_arithmetic",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
],
)
cc_test(
name = "capacity_invariant_test",
srcs = ["capacity_invariant_test.cc"],
deps = [
":capacity_invariant",
":capacity_model",
":set_cover_model",
"//ortools/base:gmock_main",
],
)

View File

@@ -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()

View File

@@ -0,0 +1,127 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/set_cover/assignment.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "ortools/base/mathutil.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/capacity_invariant.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
void SetCoverAssignment::Clear() {
cost_ = Cost(0.0);
values_.assign(model_.subset_costs().size(), false);
DCHECK_EQ(values_.size(), model_.subset_costs().size())
<< "The cost vector (length: " << model_.subset_costs().size()
<< ") is inconsistent with the assignment (length: " << values_.size()
<< ")";
}
void SetCoverAssignment::AttachInvariant(SetCoverInvariant* i) {
CHECK(constraint_ == nullptr);
constraint_ = i;
}
void SetCoverAssignment::AttachInvariant(CapacityInvariant* i) {
CHECK(constraint_ != nullptr);
side_constraints_.push_back(i);
// TODO(user): call i->SetAssignment or similar so that each and every
// constraint uses the same solution storage.
}
void SetCoverAssignment::SetValue(
SubsetIndex subset, bool is_selected,
SetCoverInvariant::ConsistencyLevel set_cover_consistency) {
DVLOG(1) << "[Assignment] Subset " << subset << " becoming " << is_selected
<< "; used to be " << values_[subset];
DCHECK(CheckConsistency());
if (values_[subset] == is_selected) return;
values_[subset] = is_selected;
if (is_selected) {
cost_ += model_.subset_costs()[subset];
if (constraint_) {
constraint_->Select(subset, set_cover_consistency);
}
for (CapacityInvariant* const capacity_constraint : side_constraints_) {
capacity_constraint->Select(subset);
}
} else {
cost_ -= model_.subset_costs()[subset];
if (constraint_) {
constraint_->Deselect(subset, set_cover_consistency);
}
for (CapacityInvariant* const capacity_constraint : side_constraints_) {
capacity_constraint->Deselect(subset);
}
}
DCHECK(CheckConsistency());
}
SetCoverSolutionResponse SetCoverAssignment::ExportSolutionAsProto() const {
SetCoverSolutionResponse message;
message.set_num_subsets(values_.size());
message.set_cost(cost_);
for (SubsetIndex subset(0);
subset < SubsetIndex(model_.subset_costs().size()); ++subset) {
if (values_[subset]) {
message.add_subset(subset.value());
}
}
return message;
}
void SetCoverAssignment::LoadAssignment(const SubsetBoolVector& solution) {
DCHECK_EQ(solution.size(), values_.size());
values_ = solution;
cost_ = ComputeCost(values_);
}
void SetCoverAssignment::ImportSolutionFromProto(
const SetCoverSolutionResponse& message) {
values_.resize(SubsetIndex(message.num_subsets()), false);
cost_ = Cost(0.0);
for (auto s : message.subset()) {
SubsetIndex subset(s);
values_[subset] = true;
cost_ += model_.subset_costs()[subset];
}
CHECK(MathUtil::AlmostEquals(message.cost(), cost_));
DCHECK(CheckConsistency());
}
bool SetCoverAssignment::CheckConsistency() const {
Cost cst = ComputeCost(values_);
CHECK(MathUtil::AlmostEquals(cost_, cst));
return true;
}
Cost SetCoverAssignment::ComputeCost(const SubsetBoolVector& choices) const {
Cost cst = 0.0;
SubsetIndex subset(0);
for (const bool b : choices) {
if (b) {
cst += model_.subset_costs()[subset];
}
++subset;
}
return cst;
}
} // namespace operations_research

View File

@@ -0,0 +1,105 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_SET_COVER_ASSIGNMENT_H_
#define OR_TOOLS_SET_COVER_ASSIGNMENT_H_
#include <vector>
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/capacity_invariant.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
// `SetCoverAssignment` stores a possibly partial, possibly infeasible solution
// to a `SetCoverModel`. It only stores a solution and no metadata,
// so that it can be shared efficiently among constraints.
//
// This class is equivalent to an `Assignment` object in the CP/routing solver.
// (//ortools/routing).
class SetCoverAssignment {
public:
// Constructs an empty set covering assignment.
//
// The model size or costs must not change after the invariant was built.
// The caller must guarantee that the model outlives the assignment without
// changing its costs.
explicit SetCoverAssignment(const SetCoverModel& m)
: model_(m), constraint_(nullptr), side_constraints_({}) {
Clear();
}
// Clears the current assignment.
void Clear();
// Adds a constraint to the problem. At least one set-covering constraint is
// required; use side constraints as required (no set-covering constraint can
// be a side constraint).
void AttachInvariant(SetCoverInvariant* i);
void AttachInvariant(CapacityInvariant* i);
// Returns the cost of current solution.
Cost cost() const { return cost_; }
// Returns the subset assignment vector.
const SubsetBoolVector& assignment() const { return values_; }
// Sets the subset's assignment to the given bool.
void SetValue(SubsetIndex subset, bool is_selected,
SetCoverInvariant::ConsistencyLevel set_cover_consistency);
// Returns the current solution as a proto.
SetCoverSolutionResponse ExportSolutionAsProto() const;
// Loads the solution and recomputes the data in the invariant.
//
// The given assignment must fit the model of this assignment.
void LoadAssignment(const SubsetBoolVector& solution);
// Imports the solution from a proto.
//
// The given assignment must fit the model of this assignment.
void ImportSolutionFromProto(const SetCoverSolutionResponse& message);
// Checks the consistency of the solution (between the selected subsets and
// the solution cost).
bool CheckConsistency() const;
private:
// Computes the cost for the given choices.
Cost ComputeCost(const SubsetBoolVector& choices) const;
// The weighted set covering model on which the solver is run.
const SetCoverModel& model_;
// Current cost of the assignment.
Cost cost_;
// Current assignment. Takes |S| bits.
SubsetBoolVector values_;
// Constraints that this assignment must respect. The constraints are checked
// every time the assignment changes (with the methods `Flip`, `Select`, and
// `Deselect`).
//
// For now, the only side constraints are capacity constraints.
SetCoverInvariant* constraint_;
// TODO(user): merge the several constraints into one invariant.
std::vector<CapacityInvariant*> side_constraints_;
};
} // namespace operations_research
#endif // OR_TOOLS_SET_COVER_ASSIGNMENT_H_

View File

@@ -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

View File

@@ -0,0 +1,70 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_SET_COVER_BASE_TYPES_H_
#define OR_TOOLS_SET_COVER_BASE_TYPES_H_
#include <cstdint>
#include "ortools/base/strong_int.h"
#include "ortools/base/strong_vector.h"
namespace operations_research {
// Basic non-strict type for cost. The speed penalty for using double is ~2%.
using Cost = double;
// Base non-strict integer type for counting elements and subsets.
// Using ints makes it possible to represent problems with more than 2 billion
// (2e9) elements and subsets. If need arises one day, BaseInt can be split
// into SubsetBaseInt and ElementBaseInt.
// Quick testing has shown a slowdown of about 20-25% when using int64_t.
using BaseInt = int32_t;
// We make heavy use of strong typing to avoid obvious mistakes.
// Subset index.
DEFINE_STRONG_INT_TYPE(SubsetIndex, BaseInt);
// Element index.
DEFINE_STRONG_INT_TYPE(ElementIndex, BaseInt);
// Position in a vector. The vector may either represent a column, i.e. a
// subset with all its elements, or a row, i,e. the list of subsets which
// contain a given element.
DEFINE_STRONG_INT_TYPE(ColumnEntryIndex, BaseInt);
DEFINE_STRONG_INT_TYPE(RowEntryIndex, BaseInt);
using SubsetRange = util_intops::StrongIntRange<SubsetIndex>;
using ElementRange = util_intops::StrongIntRange<ElementIndex>;
using ColumnEntryRange = util_intops::StrongIntRange<ColumnEntryIndex>;
using SubsetCostVector = util_intops::StrongVector<SubsetIndex, Cost>;
using ElementCostVector = util_intops::StrongVector<ElementIndex, Cost>;
using SparseColumn = util_intops::StrongVector<ColumnEntryIndex, ElementIndex>;
using SparseRow = util_intops::StrongVector<RowEntryIndex, SubsetIndex>;
using ElementToIntVector = util_intops::StrongVector<ElementIndex, BaseInt>;
using SubsetToIntVector = util_intops::StrongVector<SubsetIndex, BaseInt>;
// Views of the sparse vectors. These need not be aligned as it's their contents
// that need to be aligned.
using SparseColumnView = util_intops::StrongVector<SubsetIndex, SparseColumn>;
using SparseRowView = util_intops::StrongVector<ElementIndex, SparseRow>;
using SubsetBoolVector = util_intops::StrongVector<SubsetIndex, bool>;
using ElementBoolVector = util_intops::StrongVector<ElementIndex, bool>;
} // namespace operations_research
#endif // OR_TOOLS_SET_COVER_BASE_TYPES_H_

View File

@@ -0,0 +1,75 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package operations_research;
option java_package = "com.google.ortools.setcover";
option java_multiple_files = true;
// Represents a capacity constraint to be used in conjunction with a
// SetCoverProto. This constraint only considers one dimension.
//
// Such a capacity constraint mathematically looks like:
// min_capacity <= \sum_{e in elements} weight_e * x_e <= max_capacity
// where either `min_capacity` or `max_capacity` can be omitted. `x_e` indicates
// for a given solution `x` whether the element `e` is selected and counts for
// this capacity constraint (`x_e == 1`) or not (`x_e == 0`). The weights are
// given in `capacity_term`, each of them being a reference to an element being
// present in a subset (in set-covering parlance) and its weight.
//
// For instance, this constraint can be used together with a set-covering
// problem where parcels (element) must be covered by trucks (subsets) while
// respecting truck capacities (this object). Each element can be covered by a
// given set of trucks (set-covering problem); if an element is taken within a
// truck, it uses some capacity for this truck (such as weight).
//
// In particular, this representation does not imply that a given element must
// have the same weight in all the capacity constraints of a set-covering
// problem (e.g., the same parcel might have different weights depending on
// which truck is being considered).
message CapacityConstraintProto {
message CapacityTerm {
// The subset this weight corresponds to (index of the subset in the
// `subset` repeated field in `SetCoverProto`).
int64 subset = 1;
message ElementWeightPair {
// The element this weight corresponds to (value of `element` in
// `SetCoverProto.Subset`).
int64 element = 1;
// The weight of the element.
int64 weight = 2;
}
repeated ElementWeightPair element_weights = 2;
}
// The list of terms in the constraint.
//
// The list is supposed to be in canonical form, which means it is sorted
// first by increasing subset index then increasing element index.
// No duplicate term is allowed (two terms for the same element in the same
// subset).
repeated CapacityTerm capacity_term = 1;
// The minimum amount of resource that must be consumed. At least one of
// `min_capacity` and `max_capacity` must be present.
int64 min_capacity = 2;
// The maximum amount of resource that can be consumed. At least one of
// `min_capacity` and `max_capacity` must be present.
int64 max_capacity = 3;
}

View File

@@ -0,0 +1,123 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/set_cover/capacity_invariant.h"
#include <limits>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/capacity_model.h"
#include "ortools/set_cover/set_cover_model.h"
#include "ortools/util/saturated_arithmetic.h"
namespace operations_research {
void CapacityInvariant::Clear() {
current_slack_ = 0.0;
is_selected_.assign(set_cover_model_->num_subsets(), false);
}
namespace {
// Returns true if the addition of `q` to `sum` overflows, for q >= 0.
bool PositiveAddOverflows(CapacityWeight sum, CapacityWeight q) {
DCHECK_GE(q, 0);
return sum > std::numeric_limits<CapacityWeight>::max() - q;
}
// Returns true if the addition of `q` to `sum` overflows, for q <= 0.
bool NegativeAddOverflows(CapacityWeight sum, CapacityWeight q) {
DCHECK_LE(q, 0);
return sum < std::numeric_limits<CapacityWeight>::min() - q;
}
} // namespace
CapacityWeight CapacityInvariant::ComputeSlackChange(
const SubsetIndex subset) const {
CapacityWeight slack_change = 0;
for (CapacityTermIndex term : model_->TermRange()) {
if (model_->GetTermSubsetIndex(term) == subset) {
// Hypothesis: GetTermSubsetIndex(term) is an element of the subset.
// This information is stored in a SetCoverModel instance.
const CapacityWeight term_weight = model_->GetTermCapacityWeight(term);
// Make sure that the slack change will not overflow.
CHECK(!PositiveAddOverflows(slack_change, term_weight));
slack_change += term_weight;
}
}
return slack_change;
}
bool CapacityInvariant::SlackChangeFitsConstraint(
CapacityWeight slack_change) const {
CHECK(!AddOverflows(current_slack_, slack_change));
const CapacityWeight new_slack = current_slack_ + slack_change;
return new_slack >= model_->GetMinimumCapacity() &&
new_slack <= model_->GetMaximumCapacity();
}
bool CapacityInvariant::Select(SubsetIndex subset) {
DVLOG(1) << "[Capacity constraint] Selecting subset " << subset;
DCHECK(!is_selected_[subset]);
const CapacityWeight slack_change = ComputeSlackChange(subset);
if (!SlackChangeFitsConstraint(slack_change)) {
DVLOG(1) << "[Capacity constraint] Selecting subset " << subset
<< ": infeasible";
return false;
}
CHECK(!PositiveAddOverflows(current_slack_, slack_change));
is_selected_[subset] = true;
current_slack_ += slack_change;
DVLOG(1) << "[Capacity constraint] New slack: " << current_slack_;
return true;
}
bool CapacityInvariant::CanSelect(SubsetIndex subset) const {
DVLOG(1) << "[Capacity constraint] Can select subset " << subset << "?";
DCHECK(!is_selected_[subset]);
const CapacityWeight slack_change = ComputeSlackChange(subset);
DVLOG(1) << "[Capacity constraint] New slack if selecting: "
<< current_slack_ + slack_change;
return SlackChangeFitsConstraint(slack_change);
}
bool CapacityInvariant::Deselect(SubsetIndex subset) {
DVLOG(1) << "[Capacity constraint] Deselecting subset " << subset;
DCHECK(is_selected_[subset]);
const CapacityWeight slack_change = -ComputeSlackChange(subset);
if (!SlackChangeFitsConstraint(slack_change)) {
DVLOG(1) << "[Capacity constraint] Deselecting subset " << subset
<< ": infeasible";
return false;
}
CHECK(!NegativeAddOverflows(current_slack_, slack_change));
is_selected_[subset] = false;
current_slack_ += slack_change;
DVLOG(1) << "[Capacity constraint] New slack: " << current_slack_;
return true;
}
bool CapacityInvariant::CanDeselect(SubsetIndex subset) const {
DVLOG(1) << "[Capacity constraint] Can deselect subset " << subset << "?";
DCHECK(is_selected_[subset]);
const CapacityWeight slack_change = -ComputeSlackChange(subset);
DVLOG(1) << "[Capacity constraint] New slack if deselecting: "
<< current_slack_ + slack_change;
return SlackChangeFitsConstraint(slack_change);
}
} // namespace operations_research

View File

@@ -0,0 +1,96 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_SET_COVER_CAPACITY_INVARIANT_H_
#define OR_TOOLS_SET_COVER_CAPACITY_INVARIANT_H_
#include "absl/log/check.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/capacity_model.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
class CapacityInvariant {
public:
// Constructs an empty capacity invariant state.
// The model may not change after the invariant was built.
explicit CapacityInvariant(CapacityModel* m, SetCoverModel* sc)
: model_(m), set_cover_model_(sc) {
DCHECK(model_->ComputeFeasibility());
Clear();
}
// Clears the invariant.
void Clear();
// Returns `true` when the constraint is not violated by selecting all of the
// items in the subset and incrementally updates the invariant. Otherwise,
// returns `false` and does not change the object. (If the subset is already
// selected, the behavior is undefined.)
bool Select(SubsetIndex subset);
// Returns `true` when the constraint would not be violated by selecting all
// of the items in the subset. Otherwise, returns `false`. The object never
// changes. (If the subset is already selected, the behavior is undefined.)
bool CanSelect(SubsetIndex subset) const;
// Returns `true` when the constraint is not violated by unselecting all of
// the items in the subset and incrementally updates the invariant. Otherwise,
// returns `false` and does not change the object. (If the subset is already
// not selected, the behavior is undefined.)
bool Deselect(SubsetIndex subset);
// Returns `true` when the constraint would not be violated by unselecting all
// of the items in the subset. Otherwise, returns `false`. The object never
// changes. (If the subset is already not selected, the behavior is
// undefined.)
bool CanDeselect(SubsetIndex subset) const;
// TODO(user): implement the functions where you only select/deselect an
// item of a subset (instead of all items at once). The behavior gets much
// more interesting: if two subsets cover one item and the two item-subset
// combinations are terms in this capacity constraint, only one of them counts
// towards the capacity.
//
// The solver is not yet ready for this move: you need to
// decide which subset covers a given item, instead of ensuring that an item
// is covered by at least one subset. Currently, we could aggregate the terms
// per subset to make the code much faster when (de)selecting at the cost of
// increased initialization times.
private:
// The capacity-constraint model on which the invariant runs.
CapacityModel* model_;
// The set-cover model on which the invariant runs.
SetCoverModel* set_cover_model_;
// Current slack of the constraint.
CapacityWeight current_slack_;
// Current solution assignment.
// TODO(user): reuse the assignment of a SetCoverInvariant.
SubsetBoolVector is_selected_;
// Determines the change in slack when (de)selecting the given subset.
// The returned value is nonnegative; add it to the slack when selecting
// and subtract it when deselecting.
CapacityWeight ComputeSlackChange(SubsetIndex subset) const;
// Determines whether the given slack change violates the constraint
// (`false`) or not (`true`).
bool SlackChangeFitsConstraint(CapacityWeight slack_change) const;
};
} // namespace operations_research
#endif // OR_TOOLS_SET_COVER_CAPACITY_INVARIANT_H_

View File

@@ -0,0 +1,55 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/set_cover/capacity_invariant.h"
#include "gtest/gtest.h"
#include "ortools/set_cover/capacity_model.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
namespace {
TEST(CapacityModel, ChecksConstraintViolation) {
// Compatibility constraint: choose either of the two subsets.
SetCoverModel sc;
sc.AddEmptySubset(1.0);
sc.AddElementToLastSubset(ElementIndex(0));
sc.AddEmptySubset(1.0);
sc.AddElementToLastSubset(ElementIndex(0));
CapacityModel m(0.0, 1.0);
m.AddTerm(SubsetIndex(0), ElementIndex(0), CapacityWeight(1.0));
m.AddTerm(SubsetIndex(1), ElementIndex(0), CapacityWeight(1.0));
EXPECT_TRUE(m.ComputeFeasibility());
CapacityInvariant cinv(&m, &sc);
// Current assignment: [false, false]. Current activation: 0.
EXPECT_TRUE(cinv.CanSelect(SubsetIndex(0))); // All moves are possible.
EXPECT_TRUE(cinv.CanSelect(SubsetIndex(1)));
EXPECT_TRUE(cinv.Select(SubsetIndex(0)));
EXPECT_TRUE(cinv.CanDeselect(SubsetIndex(0))); // Undoing: still valid.
EXPECT_FALSE(cinv.CanSelect(SubsetIndex(1))); // Impossible move.
EXPECT_FALSE(cinv.Select(SubsetIndex(1)));
// Current assignment: [true, false]. Current activation: 1.
EXPECT_TRUE(cinv.Deselect(SubsetIndex(0)));
EXPECT_TRUE(cinv.CanSelect(SubsetIndex(0))); // Undoing: still valid.
EXPECT_TRUE(cinv.CanSelect(SubsetIndex(1))); // Valid when 0 not selected.
EXPECT_TRUE(cinv.Select(SubsetIndex(1)));
EXPECT_FALSE(cinv.CanSelect(SubsetIndex(0))); // Impossible move.
EXPECT_TRUE(cinv.CanDeselect(SubsetIndex(1))); // Undoing: still valid.
}
} // namespace
} // namespace operations_research

View File

@@ -0,0 +1,127 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/set_cover/capacity_model.h"
#include <algorithm>
#include <limits>
#include <numeric>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
void CapacityModel::AddTerm(SubsetIndex subset, ElementIndex element,
CapacityWeight weight) {
subsets_.push_back(subset);
elements_.push_back(element);
weights_.push_back(weight);
CHECK_EQ(elements_.size(), subsets_.size());
CHECK_EQ(elements_.size(), weights_.size());
}
void CapacityModel::SetMinimumCapacity(CapacityWeight min_capacity) {
CHECK_NE(min_capacity, std::numeric_limits<CapacityWeight>::max());
min_capacity_ = min_capacity;
}
void CapacityModel::SetMaximumCapacity(CapacityWeight max_capacity) {
CHECK_NE(max_capacity, std::numeric_limits<CapacityWeight>::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<CapacityTermIndex> CapacityModel::CanonicalIndexing() {
std::vector<CapacityTermIndex> 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

View File

@@ -0,0 +1,155 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_SET_COVER_CAPACITY_MODEL_H_
#define OR_TOOLS_SET_COVER_CAPACITY_MODEL_H_
#include <cmath>
#include <cstdint>
#include <limits>
#include <vector>
#include "absl/log/check.h"
#include "ortools/base/strong_int.h"
#include "ortools/base/strong_vector.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/capacity.pb.h"
#include "ortools/set_cover/set_cover_model.h"
// Representation class for the capacity side-constraint for a weighted
// set-covering problem.
//
// This constraint restricts the selection of elements within subsets that
// respect the constraint. Such a constraint can mix elements in any subset.
//
// Using the same mixed-integer-programming formulation as `set_cover_model.h`,
// this class corresponds to the following constraint:
// min_capacity <= \sum_{e in elements} weight_e * x_e <= max_capacity
namespace operations_research {
// Basic type for weights. For now, the same as `Cost` for the set covering.
using CapacityWeight = int64_t;
// Term index in a capacity constraint.
DEFINE_STRONG_INT_TYPE(CapacityTermIndex, BaseInt);
// The terms are represented as three aligned vectors: the element, the subset,
// and the weight. Each vector is indexed by the term.
using CapacityElements =
util_intops::StrongVector<CapacityTermIndex, ElementIndex>;
using CapacitySubsets =
util_intops::StrongVector<CapacityTermIndex, SubsetIndex>;
using CapacityWeights =
util_intops::StrongVector<CapacityTermIndex, CapacityWeight>;
// Main class for describing a single capacity constraint in the context of a
// set-covering problem.
class CapacityModel {
public:
// Builds an empty capacity constraint.
//
// Use either WithMinimumWeight or WithMaximumWeight to set only one of the
// two bounds.
CapacityModel(CapacityWeight min, CapacityWeight max)
: elements_(),
subsets_(),
weights_(),
min_capacity_(min),
max_capacity_(max) {
// At least one bound must be set. Otherwise, the constraint is vacuous.
CHECK(min_capacity_ != std::numeric_limits<CapacityWeight>::min() ||
max_capacity_ != std::numeric_limits<CapacityWeight>::max());
}
static CapacityModel WithMinimumWeight(CapacityWeight min) {
return CapacityModel(min, std::numeric_limits<CapacityWeight>::max());
}
static CapacityModel WithMaximumWeight(CapacityWeight max) {
return CapacityModel(std::numeric_limits<CapacityWeight>::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<CapacityTermIndex> TermRange() const {
return util_intops::StrongIntRange<CapacityTermIndex>(
CapacityTermIndex(num_terms()));
}
// Adds a new term to the constraint.
void AddTerm(SubsetIndex subset, ElementIndex element, CapacityWeight weight);
// Returns the element, subset, or capacity of the given term.
ElementIndex GetTermElementIndex(CapacityTermIndex term) const {
return elements_[term];
}
SubsetIndex GetTermSubsetIndex(CapacityTermIndex term) const {
return subsets_[term];
}
CapacityWeight GetTermCapacityWeight(CapacityTermIndex term) const {
return weights_[term];
}
// Sets the lower/upper bounds for the constraint.
// This will CHECK-fail if a capacity is a NaN.
void SetMinimumCapacity(CapacityWeight min_capacity);
void SetMaximumCapacity(CapacityWeight max_capacity);
// Returns the lower/upper bounds for the constraint.
CapacityWeight GetMinimumCapacity() const { return min_capacity_; }
CapacityWeight GetMaximumCapacity() const { return max_capacity_; }
// Returns true if the constraint is feasible, i.e. there is at least one
// assignment that satisfies the constraint.
bool ComputeFeasibility() const;
// Reserves num_terms terms in the model.
void ReserveNumTerms(BaseInt num_terms) {
ReserveNumTerms(CapacityTermIndex(num_terms));
}
void ReserveNumTerms(CapacityTermIndex num_terms) {
subsets_.reserve(num_terms);
elements_.reserve(num_terms);
weights_.reserve(num_terms);
}
// Returns the model as a CapacityConstraintProto.
//
// The function is not const because the terms need to be sorted for the
// representation as a protobuf to be canonical.
CapacityConstraintProto ExportModelAsProto();
// Imports the model from a CapacityConstraintProto.
void ImportModelFromProto(const CapacityConstraintProto& proto);
private:
// The terms in the constraint.
CapacityElements elements_;
CapacitySubsets subsets_;
CapacityWeights weights_;
// The bounds of the constraint. Both are always active at the same time.
// An inactive constraint corresponds to a capacity set to ±∞.
CapacityWeight min_capacity_;
CapacityWeight max_capacity_;
// Returns a canonical indexing of the constraint, i.e. reading the terms in
// this order yields the order that is explained in the proto.
std::vector<CapacityTermIndex> CanonicalIndexing();
};
} // namespace operations_research
#endif // OR_TOOLS_SET_COVER_CAPACITY_MODEL_H_

View File

@@ -0,0 +1,135 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/set_cover/capacity_model.h"
#include <limits>
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
namespace operations_research {
namespace {
using ::testing::EqualsProto;
TEST(CapacityModel, ConstructorRequiresOneBound) {
EXPECT_DEATH(CapacityModel(std::numeric_limits<CapacityWeight>::min(),
std::numeric_limits<CapacityWeight>::max()),
"min");
}
TEST(CapacityModel, WithMinimumWeightRequiresNonVacuousMinimum) {
EXPECT_DEATH(CapacityModel::WithMinimumWeight(
std::numeric_limits<CapacityWeight>::min()),
"min");
}
TEST(CapacityModel, WithMaximumWeightRequiresNonVacuousMaximum) {
EXPECT_DEATH(CapacityModel::WithMaximumWeight(
std::numeric_limits<CapacityWeight>::max()),
"min");
}
TEST(CapacityModel, SetMinimumCapacityRejectsPlusInfinity) {
CapacityModel m(0, 1);
EXPECT_DEATH(m.SetMinimumCapacity(std::numeric_limits<CapacityWeight>::max()),
"max");
}
TEST(CapacityModel, SetMaximumCapacityRejectsMinusInfinity) {
CapacityModel m(0, 1);
EXPECT_DEATH(m.SetMaximumCapacity(std::numeric_limits<CapacityWeight>::min()),
"min");
}
TEST(CapacityModel, ComputeFeasibilityWithNoTerms) {
CapacityModel m(0, 1);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMinimumCapacity(-1);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMaximumCapacity(0);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMinimumCapacity(-2);
m.SetMaximumCapacity(-1);
EXPECT_FALSE(m.ComputeFeasibility());
}
TEST(CapacityModel, ComputeFeasibilityWithOnlyPositiveWeights) {
CapacityModel m(0, 1);
m.AddTerm(SubsetIndex(0), ElementIndex(0), 1);
m.AddTerm(SubsetIndex(0), ElementIndex(1), 2);
m.AddTerm(SubsetIndex(0), ElementIndex(2), 3);
// Activation bounds: [0, 6].
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMinimumCapacity(-1);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMaximumCapacity(-1);
EXPECT_FALSE(m.ComputeFeasibility());
m.SetMaximumCapacity(7);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMinimumCapacity(7);
EXPECT_FALSE(m.ComputeFeasibility());
}
TEST(CapacityModel, ComputeFeasibilityWithOnlyNegativeWeights) {
CapacityModel m(0, 1);
m.AddTerm(SubsetIndex(0), ElementIndex(0), -1);
m.AddTerm(SubsetIndex(0), ElementIndex(1), -2);
m.AddTerm(SubsetIndex(0), ElementIndex(2), -3);
// Activation bounds: [-6, 0].
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMaximumCapacity(1);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMinimumCapacity(1);
EXPECT_FALSE(m.ComputeFeasibility());
m.SetMinimumCapacity(-7);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMaximumCapacity(-7);
EXPECT_FALSE(m.ComputeFeasibility());
}
TEST(CapacityModel, ComputeFeasibilityWithOnlyMixedWeights) {
CapacityModel m(0, 1);
m.AddTerm(SubsetIndex(0), ElementIndex(0), -1);
m.AddTerm(SubsetIndex(0), ElementIndex(1), 2);
m.AddTerm(SubsetIndex(0), ElementIndex(2), -3);
// Activation bounds: [-4, 2].
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMaximumCapacity(3);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMinimumCapacity(3);
EXPECT_FALSE(m.ComputeFeasibility());
m.SetMinimumCapacity(-5);
EXPECT_TRUE(m.ComputeFeasibility());
m.SetMaximumCapacity(-5);
EXPECT_FALSE(m.ComputeFeasibility());
}
} // namespace
} // namespace operations_research

View File

@@ -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"),
],
)

View File

@@ -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()

View File

@@ -0,0 +1,570 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// A pybind11 wrapper for set_cover_*.
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <memory>
#include <vector>
#include "absl/types/span.h"
#include "ortools/set_cover/set_cover_heuristics.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
#include "ortools/set_cover/set_cover_reader.h"
#include "pybind11/numpy.h"
#include "pybind11/pybind11.h"
#include "pybind11/pytypes.h"
#include "pybind11/stl.h"
#include "pybind11_protobuf/native_proto_caster.h"
using ::operations_research::BaseInt;
using ::operations_research::ClearRandomSubsets;
using ::operations_research::ElementDegreeSolutionGenerator;
using ::operations_research::ElementIndex;
using ::operations_research::GreedySolutionGenerator;
using ::operations_research::GuidedLocalSearch;
using ::operations_research::GuidedTabuSearch;
using ::operations_research::LazyElementDegreeSolutionGenerator;
using ::operations_research::RandomSolutionGenerator;
using ::operations_research::ReadFimiDat;
using ::operations_research::ReadOrlibRail;
using ::operations_research::ReadOrlibScp;
using ::operations_research::ReadSetCoverProto;
using ::operations_research::ReadSetCoverSolutionProto;
using ::operations_research::ReadSetCoverSolutionText;
using ::operations_research::WriteOrlibRail;
using ::operations_research::WriteOrlibScp;
using ::operations_research::WriteSetCoverProto;
using ::operations_research::WriteSetCoverSolutionProto;
using ::operations_research::WriteSetCoverSolutionText;
using ::operations_research::SetCoverDecision;
using ::operations_research::SetCoverInvariant;
using ::operations_research::SetCoverModel;
using ::operations_research::SparseColumn;
using ::operations_research::SparseRow;
using ::operations_research::SteepestSearch;
using ::operations_research::SubsetBoolVector;
using ::operations_research::SubsetCostVector;
using ::operations_research::SubsetIndex;
using ::operations_research::TabuList;
using ::operations_research::TrivialSolutionGenerator;
namespace py = pybind11;
using ::py::arg;
using ::py::make_iterator;
std::vector<SubsetIndex> VectorIntToVectorSubsetIndex(
absl::Span<const BaseInt> ints) {
std::vector<SubsetIndex> subs;
std::transform(ints.begin(), ints.end(), subs.begin(),
[](int subset) -> SubsetIndex { return SubsetIndex(subset); });
return subs;
}
SubsetCostVector VectorDoubleToSubsetCostVector(
absl::Span<const double> doubles) {
SubsetCostVector costs(doubles.begin(), doubles.end());
return costs;
}
class IntIterator {
public:
using value_type = int;
using difference_type = std::ptrdiff_t;
using pointer = int*;
using reference = int&;
using iterator_category = std::input_iterator_tag;
explicit IntIterator(int max_value)
: max_value_(max_value), current_value_(0) {}
int operator*() const { return current_value_; }
IntIterator& operator++() {
++current_value_;
return *this;
}
static IntIterator begin(int max_value) { return IntIterator{max_value}; }
static IntIterator end(int max_value) { return {max_value, max_value}; }
friend bool operator==(const IntIterator& lhs, const IntIterator& rhs) {
return lhs.max_value_ == rhs.max_value_ &&
lhs.current_value_ == rhs.current_value_;
}
private:
IntIterator(int max_value, int current_value)
: max_value_(max_value), current_value_(current_value) {}
const int max_value_;
int current_value_;
};
PYBIND11_MODULE(set_cover, m) {
pybind11_protobuf::ImportNativeProtoCasters();
// set_cover_model.h
py::class_<SetCoverModel::Stats>(m, "SetCoverModelStats")
.def_readwrite("min", &SetCoverModel::Stats::min)
.def_readwrite("max", &SetCoverModel::Stats::max)
.def_readwrite("median", &SetCoverModel::Stats::median)
.def_readwrite("mean", &SetCoverModel::Stats::mean)
.def_readwrite("stddev", &SetCoverModel::Stats::stddev)
.def("debug_string", &SetCoverModel::Stats::DebugString);
py::class_<SetCoverModel>(m, "SetCoverModel")
.def(py::init<>())
.def_property_readonly("num_elements", &SetCoverModel::num_elements)
.def_property_readonly("num_subsets", &SetCoverModel::num_subsets)
.def_property_readonly("num_nonzeros", &SetCoverModel::num_nonzeros)
.def_property_readonly("fill_rate", &SetCoverModel::FillRate)
.def_property_readonly(
"subset_costs",
[](SetCoverModel& model) -> const std::vector<double>& {
return model.subset_costs().get();
})
.def_property_readonly(
"columns",
[](SetCoverModel& model) -> std::vector<std::vector<BaseInt>> {
// Due to the inner StrongVector, make a deep copy. Anyway,
// columns() returns a const ref, so this keeps the semantics, not
// the efficiency.
std::vector<std::vector<BaseInt>> columns(model.columns().size());
std::transform(
model.columns().begin(), model.columns().end(), columns.begin(),
[](const SparseColumn& column) -> std::vector<BaseInt> {
std::vector<BaseInt> col(column.size());
std::transform(column.begin(), column.end(), col.begin(),
[](ElementIndex element) -> BaseInt {
return element.value();
});
return col;
});
return columns;
})
.def_property_readonly(
"rows",
[](SetCoverModel& model) -> std::vector<std::vector<BaseInt>> {
// Due to the inner StrongVector, make a deep copy. Anyway,
// rows() returns a const ref, so this keeps the semantics, not
// the efficiency.
std::vector<std::vector<BaseInt>> rows(model.rows().size());
std::transform(model.rows().begin(), model.rows().end(),
rows.begin(),
[](const SparseRow& row) -> std::vector<BaseInt> {
std::vector<BaseInt> r(row.size());
std::transform(row.begin(), row.end(), r.begin(),
[](SubsetIndex element) -> BaseInt {
return element.value();
});
return r;
});
return rows;
})
.def_property_readonly("row_view_is_valid",
&SetCoverModel::row_view_is_valid)
.def("SubsetRange",
[](SetCoverModel& model) {
return make_iterator<>(IntIterator::begin(model.num_subsets()),
IntIterator::end(model.num_subsets()));
})
.def("ElementRange",
[](SetCoverModel& model) {
return make_iterator<>(IntIterator::begin(model.num_elements()),
IntIterator::end(model.num_elements()));
})
.def_property_readonly("all_subsets",
[](SetCoverModel& model) -> std::vector<BaseInt> {
std::vector<BaseInt> subsets;
std::transform(
model.all_subsets().begin(),
model.all_subsets().end(), subsets.begin(),
[](const SubsetIndex element) -> BaseInt {
return element.value();
});
return subsets;
})
.def("add_empty_subset", &SetCoverModel::AddEmptySubset, arg("cost"))
.def(
"add_element_to_last_subset",
[](SetCoverModel& model, BaseInt element) {
model.AddElementToLastSubset(element);
},
arg("element"))
.def(
"set_subset_cost",
[](SetCoverModel& model, BaseInt subset, double cost) {
model.SetSubsetCost(subset, cost);
},
arg("subset"), arg("cost"))
.def(
"add_element_to_subset",
[](SetCoverModel& model, BaseInt element, BaseInt subset) {
model.AddElementToSubset(element, subset);
},
arg("subset"), arg("cost"))
.def("create_sparse_row_view", &SetCoverModel::CreateSparseRowView)
.def("sort_elements_in_subsets", &SetCoverModel::SortElementsInSubsets)
.def("compute_feasibility", &SetCoverModel::ComputeFeasibility)
.def(
"reserve_num_subsets",
[](SetCoverModel& model, BaseInt num_subsets) {
model.ReserveNumSubsets(num_subsets);
},
arg("num_subsets"))
.def(
"reserve_num_elements_in_subset",
[](SetCoverModel& model, BaseInt num_elements, BaseInt subset) {
model.ReserveNumElementsInSubset(num_elements, subset);
},
arg("num_elements"), arg("subset"))
.def("export_model_as_proto", &SetCoverModel::ExportModelAsProto)
.def("import_model_from_proto", &SetCoverModel::ImportModelFromProto)
.def("compute_cost_stats", &SetCoverModel::ComputeCostStats)
.def("compute_row_stats", &SetCoverModel::ComputeRowStats)
.def("compute_column_stats", &SetCoverModel::ComputeColumnStats)
.def("compute_row_deciles", &SetCoverModel::ComputeRowDeciles)
.def("compute_column_deciles", &SetCoverModel::ComputeRowDeciles);
// TODO(user): wrap IntersectingSubsetsIterator.
// set_cover_invariant.h
py::class_<SetCoverDecision>(m, "SetCoverDecision")
.def(py::init<>())
.def(py::init([](BaseInt subset, bool value) -> SetCoverDecision* {
return new SetCoverDecision(SubsetIndex(subset), value);
}),
arg("subset"), arg("value"))
.def("subset",
[](const SetCoverDecision& decision) -> BaseInt {
return decision.subset().value();
})
.def("decision", &SetCoverDecision::decision);
py::enum_<SetCoverInvariant::ConsistencyLevel>(m, "consistency_level")
.value("COST_AND_COVERAGE",
SetCoverInvariant::ConsistencyLevel::kCostAndCoverage)
.value("FREE_AND_UNCOVERED",
SetCoverInvariant::ConsistencyLevel::kFreeAndUncovered)
.value("REDUNDANCY", SetCoverInvariant::ConsistencyLevel::kRedundancy);
py::class_<SetCoverInvariant>(m, "SetCoverInvariant")
.def(py::init<SetCoverModel*>())
.def("initialize", &SetCoverInvariant::Initialize)
.def("clear", &SetCoverInvariant::Clear)
.def("model", &SetCoverInvariant::model)
.def_property(
"model",
// Expected semantics: give a pointer to Python **while
// keeping ownership** in C++.
[](SetCoverInvariant& invariant) -> std::shared_ptr<SetCoverModel> {
// https://pybind11.readthedocs.io/en/stable/advanced/smart_ptrs.html#std-shared-ptr
std::shared_ptr<SetCoverModel> ptr(invariant.model());
return ptr;
},
[](SetCoverInvariant& invariant, const SetCoverModel& model) {
*invariant.model() = model;
})
.def("cost", &SetCoverInvariant::cost)
.def("num_uncovered_elements", &SetCoverInvariant::num_uncovered_elements)
.def("is_selected",
[](SetCoverInvariant& invariant) -> std::vector<bool> {
return invariant.is_selected().get();
})
.def("num_free_elements",
[](SetCoverInvariant& invariant) -> std::vector<BaseInt> {
return invariant.num_free_elements().get();
})
.def("num_coverage_le_1_elements",
[](SetCoverInvariant& invariant) -> std::vector<BaseInt> {
return invariant.num_coverage_le_1_elements().get();
})
.def("coverage",
[](SetCoverInvariant& invariant) -> std::vector<BaseInt> {
return invariant.coverage().get();
})
.def(
"compute_coverage_in_focus",
[](SetCoverInvariant& invariant,
absl::Span<const BaseInt> focus) -> std::vector<BaseInt> {
return invariant
.ComputeCoverageInFocus(VectorIntToVectorSubsetIndex(focus))
.get();
},
arg("focus"))
.def("is_redundant",
[](SetCoverInvariant& invariant) -> std::vector<bool> {
return invariant.is_redundant().get();
})
.def("trace", &SetCoverInvariant::trace)
.def("clear_trace", &SetCoverInvariant::ClearTrace)
.def("clear_removability_information",
&SetCoverInvariant::ClearRemovabilityInformation)
.def("newly_removable_subsets",
&SetCoverInvariant::newly_removable_subsets)
.def("newly_non_removable_subsets",
&SetCoverInvariant::newly_non_removable_subsets)
.def("compress_trace", &SetCoverInvariant::CompressTrace)
.def("load_solution",
[](SetCoverInvariant& invariant,
absl::Span<const bool> solution) -> void {
SubsetBoolVector sol(solution.begin(), solution.end());
return invariant.LoadSolution(sol);
})
.def("check_consistency", &SetCoverInvariant::CheckConsistency)
.def(
"compute_is_redundant",
[](SetCoverInvariant& invariant, BaseInt subset) -> bool {
return invariant.ComputeIsRedundant(SubsetIndex(subset));
},
arg("subset"))
.def("recompute", &SetCoverInvariant::Recompute)
.def(
"select",
[](SetCoverInvariant& invariant, BaseInt subset,
SetCoverInvariant::ConsistencyLevel consistency) {
invariant.Select(SubsetIndex(subset), consistency);
},
arg("subset"), arg("consistency"))
.def(
"deselect",
[](SetCoverInvariant& invariant, BaseInt subset,
SetCoverInvariant::ConsistencyLevel consistency) {
invariant.Deselect(SubsetIndex(subset), consistency);
},
arg("subset"), arg("consistency"))
.def("export_solution_as_proto",
&SetCoverInvariant::ExportSolutionAsProto)
.def("import_solution_from_proto",
&SetCoverInvariant::ImportSolutionFromProto);
// set_cover_heuristics.h
py::class_<TrivialSolutionGenerator>(m, "TrivialSolutionGenerator")
.def(py::init<SetCoverInvariant*>())
.def("next_solution",
[](TrivialSolutionGenerator& heuristic) -> bool {
return heuristic.NextSolution();
})
.def("next_solution",
[](TrivialSolutionGenerator& heuristic,
absl::Span<const BaseInt> focus) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus));
});
py::class_<RandomSolutionGenerator>(m, "RandomSolutionGenerator")
.def(py::init<SetCoverInvariant*>())
.def("next_solution",
[](RandomSolutionGenerator& heuristic) -> bool {
return heuristic.NextSolution();
})
.def("next_solution",
[](RandomSolutionGenerator& heuristic,
absl::Span<const BaseInt> focus) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus));
});
py::class_<GreedySolutionGenerator>(m, "GreedySolutionGenerator")
.def(py::init<SetCoverInvariant*>())
.def("next_solution",
[](GreedySolutionGenerator& heuristic) -> bool {
return heuristic.NextSolution();
})
.def("next_solution",
[](GreedySolutionGenerator& heuristic,
absl::Span<const BaseInt> focus) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus));
})
.def("next_solution",
[](GreedySolutionGenerator& heuristic,
absl::Span<const BaseInt> focus,
absl::Span<const double> costs) -> bool {
return heuristic.NextSolution(
VectorIntToVectorSubsetIndex(focus),
VectorDoubleToSubsetCostVector(costs));
});
py::class_<ElementDegreeSolutionGenerator>(m,
"ElementDegreeSolutionGenerator")
.def(py::init<SetCoverInvariant*>())
.def("next_solution",
[](ElementDegreeSolutionGenerator& heuristic) -> bool {
return heuristic.NextSolution();
})
.def("next_solution",
[](ElementDegreeSolutionGenerator& heuristic,
absl::Span<const BaseInt> focus) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus));
})
.def("next_solution",
[](ElementDegreeSolutionGenerator& heuristic,
absl::Span<const BaseInt> focus,
absl::Span<const double> costs) -> bool {
return heuristic.NextSolution(
VectorIntToVectorSubsetIndex(focus),
VectorDoubleToSubsetCostVector(costs));
});
py::class_<LazyElementDegreeSolutionGenerator>(
m, "LazyElementDegreeSolutionGenerator")
.def(py::init<SetCoverInvariant*>())
.def("next_solution",
[](LazyElementDegreeSolutionGenerator& heuristic) -> bool {
return heuristic.NextSolution();
})
.def("next_solution",
[](LazyElementDegreeSolutionGenerator& heuristic,
absl::Span<const BaseInt> focus) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus));
})
.def("next_solution",
[](LazyElementDegreeSolutionGenerator& heuristic,
absl::Span<const BaseInt> focus,
absl::Span<const double> costs) -> bool {
return heuristic.NextSolution(
VectorIntToVectorSubsetIndex(focus),
VectorDoubleToSubsetCostVector(costs));
});
py::class_<SteepestSearch>(m, "SteepestSearch")
.def(py::init<SetCoverInvariant*>())
.def("next_solution",
[](SteepestSearch& heuristic, int num_iterations) -> bool {
return heuristic.NextSolution(num_iterations);
})
.def("next_solution",
[](SteepestSearch& heuristic, absl::Span<const BaseInt> focus,
int num_iterations) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus),
num_iterations);
})
.def("next_solution",
[](SteepestSearch& heuristic, absl::Span<const BaseInt> focus,
absl::Span<const double> costs, int num_iterations) -> bool {
return heuristic.NextSolution(
VectorIntToVectorSubsetIndex(focus),
VectorDoubleToSubsetCostVector(costs), num_iterations);
});
py::class_<GuidedLocalSearch>(m, "GuidedLocalSearch")
.def(py::init<SetCoverInvariant*>())
.def("initialize", &GuidedLocalSearch::Initialize)
.def("next_solution",
[](GuidedLocalSearch& heuristic, int num_iterations) -> bool {
return heuristic.NextSolution(num_iterations);
})
.def("next_solution",
[](GuidedLocalSearch& heuristic, absl::Span<const BaseInt> focus,
int num_iterations) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus),
num_iterations);
});
// Specialization for T = SubsetIndex ~= BaseInt (aka int for Python, whatever
// the size of BaseInt).
// A base type doesn't work, because TabuList uses `T::value` in the
// constructor.
py::class_<TabuList<SubsetIndex>>(m, "TabuList")
.def(py::init([](int size) -> TabuList<SubsetIndex>* {
return new TabuList<SubsetIndex>(SubsetIndex(size));
}),
arg("size"))
.def("size", &TabuList<SubsetIndex>::size)
.def("init", &TabuList<SubsetIndex>::Init, arg("size"))
.def(
"add",
[](TabuList<SubsetIndex>& list, BaseInt t) -> void {
return list.Add(SubsetIndex(t));
},
arg("t"))
.def(
"contains",
[](TabuList<SubsetIndex>& list, BaseInt t) -> bool {
return list.Contains(SubsetIndex(t));
},
arg("t"));
py::class_<GuidedTabuSearch>(m, "GuidedTabuSearch")
.def(py::init<SetCoverInvariant*>())
.def("initialize", &GuidedTabuSearch::Initialize)
.def("next_solution",
[](GuidedTabuSearch& heuristic, int num_iterations) -> bool {
return heuristic.NextSolution(num_iterations);
})
.def("next_solution",
[](GuidedTabuSearch& heuristic, absl::Span<const BaseInt> focus,
int num_iterations) -> bool {
return heuristic.NextSolution(VectorIntToVectorSubsetIndex(focus),
num_iterations);
})
.def("get_lagrangian_factor", &GuidedTabuSearch::SetLagrangianFactor,
arg("factor"))
.def("set_lagrangian_factor", &GuidedTabuSearch::GetLagrangianFactor)
.def("set_epsilon", &GuidedTabuSearch::SetEpsilon, arg("r"))
.def("get_epsilon", &GuidedTabuSearch::GetEpsilon)
.def("set_penalty_factor", &GuidedTabuSearch::SetPenaltyFactor,
arg("factor"))
.def("get_penalty_factor", &GuidedTabuSearch::GetPenaltyFactor)
.def("set_tabu_list_size", &GuidedTabuSearch::SetTabuListSize,
arg("size"))
.def("get_tabu_list_size", &GuidedTabuSearch::GetTabuListSize);
m.def(
"clear_random_subsets",
[](BaseInt num_subsets, SetCoverInvariant* inv) -> std::vector<BaseInt> {
const std::vector<SubsetIndex> cleared =
ClearRandomSubsets(num_subsets, inv);
return {cleared.begin(), cleared.end()};
});
m.def("clear_random_subsets",
[](const std::vector<BaseInt>& focus, BaseInt num_subsets,
SetCoverInvariant* inv) -> std::vector<BaseInt> {
const std::vector<SubsetIndex> cleared = ClearRandomSubsets(
VectorIntToVectorSubsetIndex(focus), num_subsets, inv);
return {cleared.begin(), cleared.end()};
});
m.def(
"clear_most_covered_elements",
[](BaseInt num_subsets, SetCoverInvariant* inv) -> std::vector<BaseInt> {
const std::vector<SubsetIndex> cleared =
ClearMostCoveredElements(num_subsets, inv);
return {cleared.begin(), cleared.end()};
});
m.def("clear_most_covered_elements",
[](absl::Span<const BaseInt> focus, BaseInt num_subsets,
SetCoverInvariant* inv) -> std::vector<BaseInt> {
const std::vector<SubsetIndex> cleared = ClearMostCoveredElements(
VectorIntToVectorSubsetIndex(focus), num_subsets, inv);
return {cleared.begin(), cleared.end()};
});
// set_cover_reader.h
m.def("read_orlib_scp", &ReadOrlibScp);
m.def("read_orlib_rail", &ReadOrlibRail);
m.def("read_fimi_dat", &ReadFimiDat);
m.def("read_set_cover_proto", &ReadSetCoverProto);
m.def("write_orlib_scp", &WriteOrlibScp);
m.def("write_orlib_rail", &WriteOrlibRail);
m.def("write_set_cover_proto", &WriteSetCoverProto);
m.def("write_set_cover_solution_text", &WriteSetCoverSolutionText);
m.def("write_set_cover_solution_proto", &WriteSetCoverSolutionProto);
m.def("read_set_cover_solution_text", &ReadSetCoverSolutionText);
m.def("read_set_cover_solution_proto", &ReadSetCoverSolutionProto);
// set_cover_lagrangian.h
// TODO(user): add support for SetCoverLagrangian.
}

View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python3
# Copyright 2010-2025 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from absl import app
from absl.testing import absltest
from ortools.set_cover.python import set_cover
def create_initial_cover_model():
model = set_cover.SetCoverModel()
model.add_empty_subset(1.0)
model.add_element_to_last_subset(0)
model.add_empty_subset(1.0)
model.add_element_to_last_subset(1)
model.add_element_to_last_subset(2)
model.add_empty_subset(1.0)
model.add_element_to_last_subset(1)
model.add_empty_subset(1.0)
model.add_element_to_last_subset(2)
return model
def create_knights_cover_model(num_rows: int, num_cols: int) -> set_cover.SetCoverModel:
model = set_cover.SetCoverModel()
knight_row_move = [2, 1, -1, -2, -2, -1, 1, 2]
knight_col_move = [1, 2, 2, 1, -1, -2, -2, -1]
for row in range(num_rows):
for col in range(num_cols):
model.add_empty_subset(1.0)
model.add_element_to_last_subset(row * num_cols + col)
for i in range(8):
new_row = row + knight_row_move[i]
new_col = col + knight_col_move[i]
if 0 <= new_row < num_rows and 0 <= new_col < num_cols:
model.add_element_to_last_subset(new_row * num_cols + new_col)
return model
# This test case is mostly a Python port of set_cover_test.cc.
class SetCoverTest(absltest.TestCase):
def test_save_reload(self):
model = create_knights_cover_model(10, 10)
model.sort_elements_in_subsets()
proto = model.export_model_as_proto()
reloaded = set_cover.SetCoverModel()
reloaded.import_model_from_proto(proto)
self.assertEqual(model.num_subsets, reloaded.num_subsets)
self.assertEqual(model.num_elements, reloaded.num_elements)
self.assertEqual(model.subset_costs, reloaded.subset_costs)
self.assertEqual(model.columns, reloaded.columns)
if model.row_view_is_valid and reloaded.row_view_is_valid:
self.assertEqual(model.rows, reloaded.rows)
def test_save_reload_twice(self):
model = create_knights_cover_model(3, 3)
inv = set_cover.SetCoverInvariant(model)
greedy = set_cover.GreedySolutionGenerator(inv)
self.assertTrue(greedy.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
greedy_proto = inv.export_solution_as_proto()
steepest = set_cover.SteepestSearch(inv)
self.assertTrue(steepest.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
steepest_proto = inv.export_solution_as_proto()
inv.import_solution_from_proto(greedy_proto)
self.assertTrue(steepest.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
reloaded_proto = inv.export_solution_as_proto()
self.assertEqual(str(steepest_proto), str(reloaded_proto))
def test_initial_values(self):
model = create_initial_cover_model()
self.assertTrue(model.compute_feasibility())
inv = set_cover.SetCoverInvariant(model)
trivial = set_cover.TrivialSolutionGenerator(inv)
self.assertTrue(trivial.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE)
)
greedy = set_cover.GreedySolutionGenerator(inv)
self.assertTrue(greedy.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
self.assertEqual(inv.num_uncovered_elements(), 0)
steepest = set_cover.SteepestSearch(inv)
self.assertTrue(steepest.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE)
)
def test_infeasible(self):
model = set_cover.SetCoverModel()
model.add_empty_subset(1.0)
model.add_element_to_last_subset(0)
model.add_empty_subset(1.0)
model.add_element_to_last_subset(3)
self.assertFalse(model.compute_feasibility())
def test_knights_cover_creation(self):
model = create_knights_cover_model(16, 16)
self.assertTrue(model.compute_feasibility())
def test_knights_cover_greedy(self):
model = create_knights_cover_model(16, 16)
self.assertTrue(model.compute_feasibility())
inv = set_cover.SetCoverInvariant(model)
greedy = set_cover.GreedySolutionGenerator(inv)
self.assertTrue(greedy.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
steepest = set_cover.SteepestSearch(inv)
self.assertTrue(steepest.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
def test_knights_cover_degree(self):
model = create_knights_cover_model(16, 16)
self.assertTrue(model.compute_feasibility())
inv = set_cover.SetCoverInvariant(model)
degree = set_cover.ElementDegreeSolutionGenerator(inv)
self.assertTrue(degree.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE)
)
steepest = set_cover.SteepestSearch(inv)
self.assertTrue(steepest.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
def test_knights_cover_gls(self):
model = create_knights_cover_model(16, 16)
self.assertTrue(model.compute_feasibility())
inv = set_cover.SetCoverInvariant(model)
greedy = set_cover.GreedySolutionGenerator(inv)
self.assertTrue(greedy.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
gls = set_cover.GuidedLocalSearch(inv)
self.assertTrue(gls.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
def test_knights_cover_random(self):
model = create_knights_cover_model(16, 16)
self.assertTrue(model.compute_feasibility())
inv = set_cover.SetCoverInvariant(model)
random = set_cover.RandomSolutionGenerator(inv)
self.assertTrue(random.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE)
)
steepest = set_cover.SteepestSearch(inv)
self.assertTrue(steepest.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
def test_knights_cover_trivial(self):
model = create_knights_cover_model(16, 16)
self.assertTrue(model.compute_feasibility())
inv = set_cover.SetCoverInvariant(model)
trivial = set_cover.TrivialSolutionGenerator(inv)
self.assertTrue(trivial.next_solution())
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE)
)
steepest = set_cover.SteepestSearch(inv)
self.assertTrue(steepest.next_solution(500))
self.assertTrue(
inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED)
)
# TODO(user): KnightsCoverGreedyAndTabu, KnightsCoverGreedyRandomClear,
# KnightsCoverElementDegreeRandomClear, KnightsCoverRandomClearMip,
# KnightsCoverMip
def main(_):
absltest.main()
if __name__ == "__main__":
app.run(main)

View File

@@ -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()

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -16,9 +16,9 @@
#include <cstdlib>
#include "absl/log/log.h"
#include "ortools/algorithms/set_cover_heuristics.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/set_cover/set_cover_heuristics.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
// [END import]
namespace operations_research {

View File

@@ -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]

View File

@@ -28,7 +28,7 @@ message SetCoverProto {
optional double cost = 1;
// The list of elements in the subset.
repeated int32 element = 2 [packed = true];
repeated int64 element = 2 [packed = true];
}
// The list of subsets in the model.
@@ -66,10 +66,10 @@ message SetCoverSolutionResponse {
// The number of subsets that are selected in the solution. This is used
// to decompress their indices below.
optional int32 num_subsets = 2;
optional int64 num_subsets = 2;
// The list of the subsets selected in the solution.
repeated int32 subset = 3 [packed = true];
repeated int64 subset = 3 [packed = true];
// The cost of the solution, as computed by the algorithm.
optional double cost = 4;

View File

@@ -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 <algorithm>
#include <climits>
@@ -29,9 +29,10 @@
#include "absl/random/random.h"
#include "absl/types/span.h"
#include "ortools/algorithms/adjustable_k_ary_heap.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/base/logging.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
@@ -120,28 +121,47 @@ bool GreedySolutionGenerator::NextSolution(absl::Span<const SubsetIndex> focus,
subset_priorities.push_back({priority, subset.value()});
}
}
const SetCoverModel& model = *inv_->model();
const BaseInt num_subsets = model.num_subsets();
const SparseColumnView& colunms = model.columns();
const SparseRowView& rows = model.rows();
// The priority queue maintains the maximum number of elements covered by unit
// of cost. We chose 16 as the arity of the heap after some testing.
// TODO(user): research more about the best value for Arity.
AdjustableKAryHeap<float, SubsetIndex::ValueType, 16, true> pq(
subset_priorities, inv_->model()->num_subsets());
while (!pq.IsEmpty()) {
subset_priorities, num_subsets);
SubsetBoolVector subset_seen(num_subsets, false);
std::vector<SubsetIndex> subsets_to_remove;
subsets_to_remove.reserve(focus.size());
while (!pq.IsEmpty() || inv_->num_uncovered_elements() > 0) {
// LOG_EVERY_N_SEC(INFO, 5)
// << "Queue size: " << pq.heap_size()
// << ", #uncovered elements: " << inv_->num_uncovered_elements();
const SubsetIndex best_subset(pq.TopIndex());
pq.Pop();
inv_->Select(best_subset, CL::kFreeAndUncovered);
// NOMUTANTS -- reason, for C++
if (inv_->num_uncovered_elements() == 0) break;
for (IntersectingSubsetsIterator it(*inv_->model(), best_subset);
!it.at_end(); ++it) {
const SubsetIndex subset = *it;
const BaseInt marginal_impact(inv_->num_free_elements()[subset]);
if (marginal_impact > 0) {
const float priority = marginal_impact / costs[subset];
pq.Update({priority, subset.value()});
} else {
pq.Remove(subset.value());
subset_seen[best_subset] = true;
subsets_to_remove.push_back(best_subset);
for (const ElementIndex element : colunms[best_subset]) {
for (const SubsetIndex subset : rows[element]) {
if (subset_seen[subset]) continue;
subset_seen[subset] = true;
const BaseInt marginal_impact(inv_->num_free_elements()[subset]);
if (marginal_impact > 0) {
const float priority = marginal_impact / costs[subset];
pq.Update({priority, subset.value()});
} else {
pq.Remove(subset.value());
}
subsets_to_remove.push_back(subset);
}
}
for (const SubsetIndex subset : subsets_to_remove) {
subset_seen[subset] = false;
}
subsets_to_remove.clear();
DVLOG(1) << "Cost = " << inv_->cost()
<< " num_uncovered_elements = " << inv_->num_uncovered_elements();
}
@@ -686,7 +706,11 @@ bool GuidedTabuSearch::NextSolution(absl::Span<const SubsetIndex> focus,
UpdatePenalties(focus);
tabu_list_.Add(best_subset);
inv_->Flip(best_subset, CL::kFreeAndUncovered);
if (inv_->is_selected()[best_subset]) {
inv_->Deselect(best_subset, CL::kFreeAndUncovered);
} else {
inv_->Select(best_subset, CL::kFreeAndUncovered);
}
// TODO(user): make the cost computation incremental.
augmented_cost =
std::accumulate(augmented_costs_.begin(), augmented_costs_.end(), 0.0);
@@ -695,9 +719,6 @@ bool GuidedTabuSearch::NextSolution(absl::Span<const SubsetIndex> focus,
<< inv_->cost() << ", best cost = ," << best_cost
<< ", penalized cost = ," << augmented_cost;
if (inv_->cost() < best_cost) {
LOG(INFO) << "Updated best cost, " << "Iteration, " << iteration
<< ", current cost = ," << inv_->cost() << ", best cost = ,"
<< best_cost << ", penalized cost = ," << augmented_cost;
best_cost = inv_->cost();
best_choices = inv_->is_selected();
}
@@ -754,17 +775,19 @@ bool GuidedLocalSearch::NextSolution(absl::Span<const SubsetIndex> focus,
for (int iteration = 0;
!priority_heap_.IsEmpty() && iteration < num_iterations; ++iteration) {
// Improve current solution respective to the current penalties.
// Improve current solution respective to the current penalties by flipping
// the best subset.
const SubsetIndex best_subset(priority_heap_.TopIndex());
if (inv_->is_selected()[best_subset]) {
utility_heap_.Insert({0, best_subset.value()});
inv_->Deselect(best_subset, CL::kRedundancy);
} else {
utility_heap_.Insert(
{static_cast<float>(inv_->model()->subset_costs()[best_subset] /
(1 + penalties_[best_subset])),
best_subset.value()});
inv_->Select(best_subset, CL::kRedundancy);
}
inv_->Flip(best_subset, CL::kRedundancy); // Flip the best subset.
DCHECK(!utility_heap_.IsEmpty());
// Getting the subset with highest utility. utility_heap_ is not empty,

View File

@@ -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 <vector>
#include "absl/types/span.h"
#include "ortools/algorithms/adjustable_k_ary_heap.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_invariant.h"
namespace operations_research {
@@ -499,4 +499,4 @@ std::vector<SubsetIndex> ClearMostCoveredElements(
SetCoverInvariant* inv);
} // namespace operations_research
#endif // OR_TOOLS_ALGORITHMS_SET_COVER_HEURISTICS_H_
#endif // OR_TOOLS_SET_COVER_SET_COVER_HEURISTICS_H_

View File

@@ -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 <algorithm>
#include <limits>
@@ -21,9 +21,10 @@
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/types/span.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
@@ -301,15 +302,6 @@ BaseInt SetCoverInvariant::ComputeNumFreeElements(SubsetIndex subset) const {
return num_free_elements;
}
void SetCoverInvariant::Flip(SubsetIndex subset,
ConsistencyLevel target_consistency) {
if (!is_selected_[subset]) {
Select(subset, target_consistency);
} else {
Deselect(subset, target_consistency);
}
}
void SetCoverInvariant::Select(SubsetIndex subset,
ConsistencyLevel target_consistency) {
const bool update_redundancy_info = target_consistency >= CL::kRedundancy;

View File

@@ -11,16 +11,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_INVARIANT_H_
#define OR_TOOLS_ALGORITHMS_SET_COVER_INVARIANT_H_
#ifndef OR_TOOLS_SET_COVER_SET_COVER_INVARIANT_H_
#define OR_TOOLS_SET_COVER_SET_COVER_INVARIANT_H_
#include <tuple>
#include <vector>
#include "absl/log/check.h"
#include "absl/types/span.h"
#include "ortools/algorithms/set_cover.pb.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover.pb.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
@@ -179,17 +180,6 @@ class SetCoverInvariant {
// Computes the number of free (uncovered) elements in the given subset.
BaseInt ComputeNumFreeElements(SubsetIndex subset) const;
// Includes subset in the solution by setting is_selected_[subset] to true
// without updating the invariant. Only updates the cost and the coverage.
// TODO(user): Merge with Select. Introduce consistency levels and maybe split
// the invariant into three.
void SelectNoUpdate(SubsetIndex subset);
// Flips is_selected_[subset] to its negation, by calling Select or Deselect
// depending on value. Updates the invariant incrementally to the given
// consistency level.
void Flip(SubsetIndex subset, ConsistencyLevel consistency);
// Includes subset in the solution by setting is_selected_[subset] to true
// and incrementally updating the invariant to the given consistency level.
void Select(SubsetIndex subset, ConsistencyLevel consistency);
@@ -288,4 +278,4 @@ class SetCoverInvariant {
};
} // namespace operations_research
#endif // OR_TOOLS_ALGORITHMS_SET_COVER_INVARIANT_H_
#endif // OR_TOOLS_SET_COVER_SET_COVER_INVARIANT_H_

View File

@@ -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 <algorithm>
#include <cstdlib>
@@ -22,9 +22,10 @@
#include "absl/log/check.h"
#include "absl/synchronization/blocking_counter.h"
#include "ortools/algorithms/adjustable_k_ary_heap.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/base/threadpool.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
@@ -99,7 +100,8 @@ void FillReducedCostsSlice(SubsetIndex slice_start, SubsetIndex slice_end,
}
BaseInt BlockSize(BaseInt size, int num_threads) {
return 1 + (size - 1) / num_threads;
// Traditional formula to compute std::ceil(size / num_threads).
return (size + num_threads - 1) / num_threads;
}
} // namespace

View File

@@ -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 <memory>
#include <new>
#include <tuple>
#include <vector>
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/base/threadpool.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
@@ -160,4 +160,4 @@ class SetCoverLagrangian {
} // namespace operations_research
#endif // OR_TOOLS_ALGORITHMS_SET_COVER_LAGRANGIAN_H_
#endif // OR_TOOLS_SET_COVER_SET_COVER_LAGRANGIAN_H_

View File

@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/algorithms/set_cover_mip.h"
#include "ortools/set_cover/set_cover_mip.h"
#include <cstdint>
#include <limits>
@@ -19,10 +19,11 @@
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/types/span.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/lp_data/lp_types.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {

View File

@@ -11,12 +11,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_ALGORITHMS_SET_COVER_MIP_H_
#define OR_TOOLS_ALGORITHMS_SET_COVER_MIP_H_
#ifndef OR_TOOLS_SET_COVER_SET_COVER_MIP_H_
#define OR_TOOLS_SET_COVER_SET_COVER_MIP_H_
#include "absl/types/span.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_invariant.h"
namespace operations_research {
enum class SetCoverMipSolver : int {
@@ -67,4 +67,4 @@ class SetCoverMip {
};
} // namespace operations_research
#endif // OR_TOOLS_ALGORITHMS_SET_COVER_MIP_H_
#endif // OR_TOOLS_SET_COVER_SET_COVER_MIP_H_

View File

@@ -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 <algorithm>
#include <cmath>
@@ -32,7 +32,8 @@
#include "absl/strings/str_format.h"
#include "absl/types/span.h"
#include "ortools/algorithms/radix_sort.h"
#include "ortools/algorithms/set_cover.pb.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover.pb.h"
namespace operations_research {
@@ -159,15 +160,10 @@ SetCoverModel SetCoverModel::GenerateRandomModelFrom(
subset_already_contains_element[element] = false;
}
}
LOG(INFO) << "Finished genreating the model with " << num_elements_covered
<< " elements covered.";
// It can happen -- rarely in practice -- that some of the elements cannot be
// covered. Let's add them to randomly chosen subsets.
if (num_elements_covered != num_elements) {
LOG(INFO) << "Generated model with " << num_elements - num_elements_covered
<< " elements that cannot be covered. Adding them to random "
"subsets.";
SubsetBoolVector element_already_in_subset(num_subsets, false);
for (ElementIndex element(0); element.value() < num_elements; ++element) {
LOG_EVERY_N_SEC(INFO, 5) << absl::StrFormat(
@@ -200,11 +196,7 @@ SetCoverModel SetCoverModel::GenerateRandomModelFrom(
++num_elements_covered;
}
}
LOG(INFO) << "Finished generating subsets for elements that were not "
"covered in the original model.";
}
LOG(INFO) << "Finished generating the model. There are "
<< num_elements - num_elements_covered << " uncovered elements.";
CHECK_EQ(num_elements_covered, num_elements);
@@ -237,6 +229,8 @@ void SetCoverModel::UpdateAllSubsetsList() {
}
void SetCoverModel::AddEmptySubset(Cost cost) {
// TODO(user): refine the logic for is_unicost_ and is_unicost_valid_.
is_unicost_valid_ = false;
elements_in_subsets_are_sorted_ = false;
subset_costs_.push_back(cost);
columns_.push_back(SparseColumn());
@@ -262,6 +256,9 @@ void SetCoverModel::AddElementToLastSubset(ElementIndex element) {
}
void SetCoverModel::SetSubsetCost(BaseInt subset, Cost cost) {
// TODO(user): refine the logic for is_unicost_ and is_unicost_valid_.
// NOMUTANTS -- this is a performance optimization.
is_unicost_valid_ = false;
elements_in_subsets_are_sorted_ = false;
CHECK(std::isfinite(cost));
DCHECK_GE(subset, 0);
@@ -338,14 +335,14 @@ void SetCoverModel::CreateSparseRowView() {
rows_.resize(num_elements_, SparseRow());
ElementToIntVector row_sizes(num_elements_, 0);
for (const SubsetIndex subset : SubsetRange()) {
// Sort the columns. It's not super-critical to improve performance here
// as this needs to be done only once.
BaseInt* data = reinterpret_cast<BaseInt*>(columns_[subset].data());
RadixSort(absl::MakeSpan(data, columns_[subset].size()));
ElementIndex preceding_element(-1);
for (const ElementIndex element : columns_[subset]) {
DCHECK_GT(element, preceding_element); // Fail if there is a repetition.
CHECK_GT(element, preceding_element)
<< "Repetition in column "
<< subset; // Fail if there is a repetition.
++row_sizes[element];
preceding_element = element;
}
@@ -372,11 +369,14 @@ bool SetCoverModel::ComputeFeasibility() const {
for (const Cost cost : subset_costs_) {
CHECK_GT(cost, 0.0);
}
SubsetIndex column_index(0);
for (const SparseColumn& column : columns_) {
CHECK_GT(column.size(), 0);
// DLOG_IF(INFO, column.empty()) << "Empty column " << column_index.value();
for (const ElementIndex element : column) {
++coverage[element];
}
// NOMUTANTS -- column_index is only used for logging in debug mode.
++column_index;
}
for (const ElementIndex element : ElementRange()) {
CHECK_GE(coverage[element], 0);
@@ -409,7 +409,6 @@ SetCoverProto SetCoverModel::ExportModelAsProto() const {
subset_proto->add_element(element.value());
}
}
LOG(INFO) << "Finished exporting the model.";
return message;
}
@@ -497,15 +496,27 @@ class StatsAccumulator {
} // namespace
template <typename T>
SetCoverModel::Stats ComputeStats(std::vector<T> sizes) {
SetCoverModel::Stats ComputeStats(std::vector<T> samples) {
SetCoverModel::Stats stats;
stats.min = *std::min_element(sizes.begin(), sizes.end());
stats.max = *std::max_element(sizes.begin(), sizes.end());
stats.mean = std::accumulate(sizes.begin(), sizes.end(), 0.0) / sizes.size();
std::nth_element(sizes.begin(), sizes.begin() + sizes.size() / 2,
sizes.end());
stats.median = sizes[sizes.size() / 2];
stats.stddev = StandardDeviation(sizes);
stats.min = *std::min_element(samples.begin(), samples.end());
stats.max = *std::max_element(samples.begin(), samples.end());
stats.mean =
std::accumulate(samples.begin(), samples.end(), 0.0) / samples.size();
auto const q1 = samples.size() / 4;
auto const q2 = samples.size() / 2;
auto const q3 = q1 + q2;
// The first call to nth_element is O(n). The 2nd and 3rd calls are O(n / 2).
// Basically it's equivalent to running nth_element twice.
// One should be tempted to use a faster sorting algorithm like radix sort,
// it is not sure that we would gain a lot. There would be no gain in
// complexity whatsoever anyway. On top of that, this code is called at most
// one per problem, so it's not worth the effort.
std::nth_element(samples.begin(), samples.begin() + q2, samples.end());
std::nth_element(samples.begin(), samples.begin() + q1, samples.begin() + q2);
std::nth_element(samples.begin() + q2, samples.begin() + q3, samples.end());
stats.median = samples[samples.size() / 2];
stats.iqr = samples[q3] - samples[q1];
stats.stddev = StandardDeviation(samples);
return stats;
}

View File

@@ -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 <cstdint>
#include <string>
@@ -20,9 +20,10 @@
#include "absl/log/check.h"
#include "absl/strings/str_cat.h"
#include "ortools/algorithms/set_cover.pb.h"
#include "ortools/base/strong_int.h"
#include "ortools/base/strong_vector.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover.pb.h"
// Representation class for the weighted set-covering problem.
//
@@ -50,55 +51,6 @@
// cardinalities of all the subsets.
namespace operations_research {
// Basic non-strict type for cost. The speed penalty for using double is ~2%.
using Cost = double;
// Base non-strict integer type for counting elements and subsets.
// Using ints makes it possible to represent problems with more than 2 billion
// (2e9) elements and subsets. If need arises one day, BaseInt can be split
// into SubsetBaseInt and ElementBaseInt.
// Quick testing has shown a slowdown of about 20-25% when using int64_t.
using BaseInt = int32_t;
// We make heavy use of strong typing to avoid obvious mistakes.
// Subset index.
DEFINE_STRONG_INT_TYPE(SubsetIndex, BaseInt);
// Element index.
DEFINE_STRONG_INT_TYPE(ElementIndex, BaseInt);
// Position in a vector. The vector may either represent a column, i.e. a
// subset with all its elements, or a row, i,e. the list of subsets which
// contain a given element.
DEFINE_STRONG_INT_TYPE(ColumnEntryIndex, BaseInt);
DEFINE_STRONG_INT_TYPE(RowEntryIndex, BaseInt);
using SubsetRange = util_intops::StrongIntRange<SubsetIndex>;
using ElementRange = util_intops::StrongIntRange<ElementIndex>;
using ColumnEntryRange = util_intops::StrongIntRange<ColumnEntryIndex>;
using SubsetCostVector = util_intops::StrongVector<SubsetIndex, Cost>;
using ElementCostVector = util_intops::StrongVector<ElementIndex, Cost>;
using SparseColumn = util_intops::StrongVector<ColumnEntryIndex, ElementIndex>;
using SparseRow = util_intops::StrongVector<RowEntryIndex, SubsetIndex>;
using ElementToIntVector = util_intops::StrongVector<ElementIndex, BaseInt>;
using SubsetToIntVector = util_intops::StrongVector<SubsetIndex, BaseInt>;
// Views of the sparse vectors. These need not be aligned as it's their contents
// that need to be aligned.
using SparseColumnView = util_intops::StrongVector<SubsetIndex, SparseColumn>;
using SparseRowView = util_intops::StrongVector<ElementIndex, SparseRow>;
using SubsetBoolVector = util_intops::StrongVector<SubsetIndex, bool>;
using ElementBoolVector = util_intops::StrongVector<ElementIndex, bool>;
// Useful for representing permutations,
using ElementToElementVector =
util_intops::StrongVector<ElementIndex, ElementIndex>;
using SubsetToSubsetVector =
util_intops::StrongVector<SubsetIndex, SubsetIndex>;
// Main class for describing a weighted set-covering problem.
class SetCoverModel {
@@ -111,6 +63,8 @@ class SetCoverModel {
row_view_is_valid_(false),
elements_in_subsets_are_sorted_(false),
subset_costs_(),
is_unicost_(true),
is_unicost_valid_(false),
columns_(),
rows_(),
all_subsets_() {}
@@ -161,6 +115,23 @@ class SetCoverModel {
// Vector of costs for each subset.
const SubsetCostVector& subset_costs() const { return subset_costs_; }
// Returns true if all subset costs are equal to 1.0. This is a fast check
// that is only valid if the subset costs are not modified.
bool is_unicost() {
if (is_unicost_valid_) {
return is_unicost_;
}
is_unicost_ = true;
for (const Cost cost : subset_costs_) {
if (cost != 1.0) {
is_unicost_ = false;
break;
}
}
is_unicost_valid_ = true;
return is_unicost_;
}
// Column view of the set covering problem.
const SparseColumnView& columns() const { return columns_; }
@@ -244,10 +215,12 @@ class SetCoverModel {
double median;
double mean;
double stddev;
double iqr; // Interquartile range.
std::string DebugString() const {
return absl::StrCat("min = ", min, ", max = ", max, ", mean = ", mean,
", median = ", median, ", stddev = ", stddev, ", ");
", median = ", median, ", stddev = ", stddev, ", ",
"iqr = ", iqr);
}
};
@@ -298,6 +271,12 @@ class SetCoverModel {
// Costs for each subset.
SubsetCostVector subset_costs_;
// True when all subset costs are equal to 1.0.
bool is_unicost_;
// True when is_unicost_ is up-to-date.
bool is_unicost_valid_;
// Vector of columns. Each column corresponds to a subset and contains the
// elements of the given subset.
// This takes NNZ (number of non-zeros) BaseInts, or |E| * |S| * fill_rate.
@@ -340,7 +319,7 @@ class IntersectingSubsetsIterator {
seed_subset_(seed_subset),
model_(model),
subset_seen_(model_.columns().size(), false) {
CHECK(model_.row_view_is_valid());
DCHECK(model_.row_view_is_valid());
subset_seen_[seed_subset] = true; // Avoid iterating on `seed_subset`.
++(*this); // Move to the first intersecting subset.
}
@@ -359,11 +338,12 @@ class IntersectingSubsetsIterator {
DCHECK(!at_end());
const SparseRowView& rows = model_.rows();
const SparseColumn& column = model_.columns()[seed_subset_];
for (; element_entry_ < ColumnEntryIndex(column.size()); ++element_entry_) {
const ColumnEntryIndex column_size = ColumnEntryIndex(column.size());
for (; element_entry_ < column_size; ++element_entry_) {
const ElementIndex current_element = column[element_entry_];
const SparseRow& current_row = rows[current_element];
for (; subset_entry_ < RowEntryIndex(current_row.size());
++subset_entry_) {
const RowEntryIndex current_row_size = RowEntryIndex(current_row.size());
for (; subset_entry_ < current_row_size; ++subset_entry_) {
intersecting_subset_ = current_row[subset_entry_];
if (!subset_seen_[intersecting_subset_]) {
subset_seen_[intersecting_subset_] = true;
@@ -398,4 +378,4 @@ class IntersectingSubsetsIterator {
} // namespace operations_research
#endif // OR_TOOLS_ALGORITHMS_SET_COVER_MODEL_H_
#endif // OR_TOOLS_SET_COVER_SET_COVER_MODEL_H_

View File

@@ -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 <sys/types.h>
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstdint>
@@ -29,11 +30,13 @@
#include "absl/strings/str_format.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "ortools/algorithms/set_cover.pb.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/base/file.h"
#include "ortools/base/filesystem.h"
#include "ortools/base/helpers.h"
#include "ortools/base/options.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover.pb.h"
#include "ortools/set_cover/set_cover_model.h"
#include "ortools/util/filelineiter.h"
namespace operations_research {
@@ -105,30 +108,42 @@ int64_t SetCoverReader::ParseNextInteger() {
return value;
}
namespace {
double Percent(SubsetIndex subset, BaseInt total) {
return subset.value() == 0 ? 0 : 100.0 * subset.value() / total;
}
double Percent(ElementIndex element, BaseInt total) {
return element.value() == 0 ? 0 : 100.0 * element.value() / total;
}
} // namespace
// This is a row-based format where the elements are 1-indexed.
SetCoverModel ReadOrlibScp(absl::string_view filename) {
CHECK_OK(file::Exists(filename, file::Defaults()));
SetCoverModel model;
File* file(file::OpenOrDie(filename, "r", file::Defaults()));
SetCoverReader reader(file);
const ElementIndex num_rows(reader.ParseNextInteger());
const SubsetIndex num_cols(reader.ParseNextInteger());
model.ReserveNumSubsets(num_cols.value());
model.ReserveNumSubsets(num_cols);
for (SubsetIndex subset : SubsetRange(num_cols)) {
const double cost(reader.ParseNextDouble());
model.SetSubsetCost(subset.value(), cost);
model.SetSubsetCost(subset, cost);
}
for (ElementIndex element : ElementRange(num_rows)) {
LOG_EVERY_N_SEC(INFO, 5)
<< absl::StrFormat("Reading element %d (%.1f%%)", element.value(),
100.0 * element.value() / model.num_elements());
Percent(element, model.num_elements()));
const RowEntryIndex row_size(reader.ParseNextInteger());
for (RowEntryIndex entry(0); entry < row_size; ++entry) {
// Correct the 1-indexing.
const int subset(reader.ParseNextInteger() - 1);
model.AddElementToSubset(element.value(), subset);
const SubsetIndex subset(reader.ParseNextInteger() - 1);
model.AddElementToSubset(element, subset);
}
}
LOG(INFO) << "Finished reading the model.";
LOG(INFO) << "Read " << model.num_subsets() << " subsets, "
<< model.num_elements() << " elements";
file->Close(file::Defaults()).IgnoreError();
model.CreateSparseRowView();
return model;
@@ -136,59 +151,96 @@ SetCoverModel ReadOrlibScp(absl::string_view filename) {
// This is a column-based format where the elements are 1-indexed.
SetCoverModel ReadOrlibRail(absl::string_view filename) {
CHECK_OK(file::Exists(filename, file::Defaults()));
SetCoverModel model;
File* file(file::OpenOrDie(filename, "r", file::Defaults()));
SetCoverReader reader(file);
const ElementIndex num_rows(reader.ParseNextInteger());
const BaseInt num_cols(reader.ParseNextInteger());
const SubsetIndex num_cols(reader.ParseNextInteger());
model.ReserveNumSubsets(num_cols);
for (BaseInt subset(0); subset < num_cols; ++subset) {
for (SubsetIndex subset : SubsetRange(num_cols)) {
LOG_EVERY_N_SEC(INFO, 5)
<< absl::StrFormat("Reading subset %d (%.1f%%)", subset,
100.0 * subset / model.num_subsets());
<< absl::StrFormat("Reading subset %d (%.1f%%)", subset.value(),
Percent(subset, model.num_subsets()));
const double cost(reader.ParseNextDouble());
model.SetSubsetCost(subset, cost);
const ColumnEntryIndex column_size(reader.ParseNextInteger());
model.ReserveNumElementsInSubset(column_size.value(), subset);
model.ReserveNumElementsInSubset(column_size.value(), subset.value());
for (const ColumnEntryIndex _ : ColumnEntryRange(column_size)) {
// Correct the 1-indexing.
const ElementIndex element(reader.ParseNextInteger() - 1);
model.AddElementToSubset(element.value(), subset);
model.AddElementToSubset(element, subset);
}
}
LOG(INFO) << "Finished reading the model.";
LOG(INFO) << "Read " << model.num_subsets() << " subsets, "
<< model.num_elements() << " elements";
file->Close(file::Defaults()).IgnoreError();
model.CreateSparseRowView();
return model;
}
SetCoverModel ReadFimiDat(absl::string_view filename) {
CHECK_OK(file::Exists(filename, file::Defaults()));
SetCoverModel model;
BaseInt subset(0);
SubsetIndex subset(0);
// Read the file once to discover the smallest element index.
BaseInt smallest_element = std::numeric_limits<BaseInt>::max();
BaseInt largest_element = 0;
for (const std::string& line : FileLines(filename)) {
std::vector<std::string> 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<std::string> 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<ElementIndex> elements_list;
for (const std::string& number_str : elements) {
BaseInt raw_element;
CHECK(absl::SimpleAtoi(number_str, &raw_element));
// Re-index the elements starting from 0.
ElementIndex element(raw_element - smallest_element);
if (element_seen[element]) {
DLOG(INFO) << "Element " << element << " already in subset "
<< subset.value();
continue;
}
element_seen[element] = true;
elements_list.push_back(element);
CHECK_GE(element.value(), 0);
model.AddElementToLastSubset(element);
}
// Clean up the list of elements.
for (const ElementIndex element : elements_list) {
element_seen[element] = false;
}
++subset;
}
LOG(INFO) << "Finished reading the model.";
LOG(INFO) << "Read " << model.num_subsets() << " subsets, "
<< model.num_elements() << " elements";
model.CreateSparseRowView();
return model;
}
SetCoverModel ReadSetCoverProto(absl::string_view filename, bool binary) {
CHECK_OK(file::Exists(filename, file::Defaults()));
SetCoverModel model;
SetCoverProto message;
if (binary) {
@@ -255,7 +307,7 @@ void WriteOrlibScp(const SetCoverModel& model, absl::string_view filename) {
for (const ElementIndex element : model.ElementRange()) {
LOG_EVERY_N_SEC(INFO, 5)
<< absl::StrFormat("Writing element %d (%.1f%%)", element.value(),
100.0 * element.value() / model.num_elements());
Percent(element, model.num_elements()));
formatter.Append(absl::StrCat(model.rows()[element].size(), "\n"));
for (const SubsetIndex subset : model.rows()[element]) {
formatter.Append(subset.value() + 1);
@@ -276,7 +328,7 @@ void WriteOrlibRail(const SetCoverModel& model, absl::string_view filename) {
for (const SubsetIndex subset : model.SubsetRange()) {
LOG_EVERY_N_SEC(INFO, 5)
<< absl::StrFormat("Writing subset %d (%.1f%%)", subset.value(),
100.0 * subset.value() / model.num_subsets());
Percent(subset, model.num_subsets()));
formatter.Append(model.subset_costs()[subset]);
formatter.Append(static_cast<BaseInt>(model.columns()[subset].size()));
for (const ElementIndex element : model.columns()[subset]) {
@@ -299,6 +351,7 @@ void WriteSetCoverProto(const SetCoverModel& model, absl::string_view filename,
}
SubsetBoolVector ReadSetCoverSolutionText(absl::string_view filename) {
CHECK_OK(file::Exists(filename, file::Defaults()));
SubsetBoolVector solution;
File* file(file::OpenOrDie(filename, "r", file::Defaults()));
SetCoverReader reader(file);
@@ -316,6 +369,7 @@ SubsetBoolVector ReadSetCoverSolutionText(absl::string_view filename) {
SubsetBoolVector ReadSetCoverSolutionProto(absl::string_view filename,
bool binary) {
CHECK_OK(file::Exists(filename, file::Defaults()));
SubsetBoolVector solution;
SetCoverSolutionResponse message;
if (binary) {

View File

@@ -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_

View File

@@ -21,12 +21,13 @@
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "ortools/algorithms/set_cover_heuristics.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/algorithms/set_cover_reader.h"
#include "ortools/base/init_google.h"
#include "ortools/base/timer.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_heuristics.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_model.h"
#include "ortools/set_cover/set_cover_reader.h"
ABSL_FLAG(std::string, input, "", "REQUIRED: Input file name.");
ABSL_FLAG(std::string, input_fmt, "",

View File

@@ -20,13 +20,14 @@
#include "absl/strings/str_cat.h"
#include "benchmark/benchmark.h"
#include "gtest/gtest.h"
#include "ortools/algorithms/set_cover.pb.h"
#include "ortools/algorithms/set_cover_heuristics.h"
#include "ortools/algorithms/set_cover_invariant.h"
#include "ortools/algorithms/set_cover_mip.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/base/gmock.h"
#include "ortools/base/parse_text_proto.h"
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover.pb.h"
#include "ortools/set_cover/set_cover_heuristics.h"
#include "ortools/set_cover/set_cover_invariant.h"
#include "ortools/set_cover/set_cover_mip.h"
#include "ortools/set_cover/set_cover_model.h"
namespace operations_research {
namespace {