[bazel] Update examples

This commit is contained in:
Guillaume Chatelet
2025-10-10 09:17:40 +00:00
committed by Corentin Le Molgat
parent 1533e23537
commit 5e06a6c7e2
11 changed files with 2413 additions and 662 deletions

View File

@@ -34,3 +34,12 @@ compile_pip_requirements(
requirements_in = "notebook_requirements.in",
requirements_txt = "notebook_requirements.txt",
)
package(default_visibility = ["//visibility:public"])
filegroup(
name = "test_runner_template",
testonly = 1,
srcs = ["test_runner_template.sh"],
visibility = ["//visibility:public"],
)

105
bazel/run_binary_test.bzl Normal file
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.
"""run_binary_test will run a xx_binary as test with the given args."""
load("@bazel_skylib//rules:expand_template.bzl", "expand_template")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
def parse_label(label):
"""Parse a label into (package, name).
Args:
label: string in relative or absolute form.
Returns:
Pair of strings: package, relative_name
Raises:
ValueError for malformed label (does not do an exhaustive validation)
"""
if label.startswith("//"):
label = label[2:] # drop the leading //
colon_split = label.split(":")
if len(colon_split) == 1: # no ":" in label
pkg = label
_, _, target = label.rpartition("/")
else:
pkg, target = colon_split # fails if len(colon_split) != 2
else:
colon_split = label.split(":")
if len(colon_split) == 1: # no ":" in label
pkg, target = native.package_name(), label
else:
pkg2, target = colon_split # fails if len(colon_split) != 2
pkg = native.package_name() + ("/" + pkg2 if pkg2 else "")
return pkg, target
def get_check_contains_code(line):
return """
if ! grep -qF "{line}" "${{LOGFILE}}"; then
cat "${{LOGFILE}}"
echo "---------------------------------------------------------------"
echo "FAILURE: string '{line}' was not found in the output."
echo "---------------------------------------------------------------"
exit 1
fi
""".format(line = line)
def run_binary_test(
name,
binary,
template = "//bazel:test_runner_template",
args = [],
data = [],
grep_lines = [],
**kwargs):
"""Create a sh_test to run the given binary as test.
Args:
name: name of the test target.
binary: name of the binary target to run.
template: template file for executing the binary target.
args: args to use to run the binary.
data: data files required by this test.
grep_lines: lines to grep for in the log file.
**kwargs: other attributes that are applicable to tests, size, tags, etc.
"""
shell_script = name + ".sh"
# Get the path to the binary we want to run.
binary_pkg, binary_name = parse_label(binary)
binary_path = "/".join([binary_pkg, binary_name])
# We would like to include args in the generated shell script, so that "blaze-bin/.../test" can
# be run manually. Unfortunately `expand_template` does not resolve $(location) and other Make
# variables so we only pass them in `sh_test` below.
expand_template(
name = name + "_gensh",
template = template,
out = shell_script,
testonly = 1,
substitutions = {
"{{binary_path}}": binary_path,
"{{post_script}}": "\n".join([get_check_contains_code(line) for line in grep_lines]),
},
)
sh_test(
name = name,
testonly = 1,
srcs = [shell_script],
data = data + [binary],
args = args,
**kwargs
)

View File

@@ -0,0 +1,94 @@
# 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.
"""run_binary_test will run a xx_binary as test with the given args.
"""
load("@bazel_skylib//rules:expand_template.bzl", "expand_template")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
def parse_label(label):
"""Parse a label into (package, name).
Args:
label: string in relative or absolute form.
Returns:
Pair of strings: package, relative_name
Raises:
ValueError for malformed label (does not do an exhaustive validation)
"""
if label.startswith("//"):
label = label[2:] # drop the leading //
colon_split = label.split(":")
if len(colon_split) == 1: # no ":" in label
pkg = label
_, _, target = label.rpartition("/")
else:
pkg, target = colon_split # fails if len(colon_split) != 2
else:
colon_split = label.split(":")
if len(colon_split) == 1: # no ":" in label
pkg, target = native.package_name(), label
else:
pkg2, target = colon_split # fails if len(colon_split) != 2
pkg = native.package_name() + ("/" + pkg2 if pkg2 else "")
return pkg, target
def run_binary_test(
name,
binary,
template = "//bazel:test_runner_template",
args = [],
data = [],
**kwargs):
"""Create a sh_test to run the given binary as test.
Args:
name: name of the test target.
binary: name of the binary target to run.
template: template file for executing the binary target.
args: args to use to run the binary.
data: data files required by this test.
**kwargs: other attributes that are applicable to tests, size, tags, etc.
"""
shell_script = name + ".sh"
# Get the path to the binary we want to run.
binary_pkg, binary_name = parse_label(binary)
binary_path = "/".join([binary_pkg, binary_name])
# We would like to Include args in the generated shell script, so the "blaze-bin/.../test" can
# be run manually. Unfortunately expand_template does not resolve $(location) and other Make
# variables so we only pass them in `sh_test` below.
expand_template(
name = name + "_gensh",
template = template,
out = shell_script,
testonly = 1,
substitutions = {
"{package_name}": native.package_name(),
"{target}": name,
"{binary_path}": binary_path,
},
)
sh_test(
name = name,
testonly = 1,
srcs = [shell_script],
data = data + [binary],
args = args,
**kwargs
)

View File

@@ -0,0 +1,19 @@
#!/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.
set -e # Fail on error
declare -r LOGFILE="${TEST_TMPDIR}/log.txt"
{{binary_path}} "$@" > "${LOGFILE}"
{{post_script}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +0,0 @@
// 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 <numeric>
#include <string>
#include "absl/flags/flag.h"
#include "absl/log/globals.h"
#include "absl/status/status.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "ortools/base/commandlineflags.h"
#include "ortools/base/file.h"
#include "ortools/base/helpers.h"
#include "ortools/base/init_google.h"
#include "ortools/base/logging.h"
#include "ortools/base/timer.h"
#include "ortools/packing/arc_flow_builder.h"
#include "ortools/packing/arc_flow_solver.h"
#include "ortools/packing/vector_bin_packing.pb.h"
#include "ortools/packing/vector_bin_packing_parser.h"
ABSL_FLAG(std::string, input, "", "Vector Bin Packing (.vpb) data file name.");
ABSL_FLAG(std::string, params, "num_workers:16,max_time_in_seconds:10",
"Parameters in solver specific text format.");
ABSL_FLAG(std::string, solver, "sat", "Solver to use: sat, scip");
ABSL_FLAG(double, time_limit, 900.0, "Time limit in seconds");
ABSL_FLAG(int, threads, 1, "Number of threads");
ABSL_FLAG(bool, display_proto, false, "Print the input protobuf");
ABSL_FLAG(int, max_bins, -1,
"Maximum number of bins: default = -1 meaning no limits");
namespace operations_research {
void ParseAndSolve(const std::string& filename, absl::string_view solver,
const std::string& params) {
std::string problem_name = filename;
const size_t found = problem_name.find_last_of("/\\");
if (found != std::string::npos) {
problem_name = problem_name.substr(found + 1);
}
if (absl::EndsWith(problem_name, ".vbp")) {
// TODO(user): Move naming code to parser.
problem_name.resize(problem_name.size() - 4);
}
packing::vbp::VectorBinPackingProblem data;
packing::vbp::VbpParser parser;
if (!parser.ParseFile(filename)) {
LOG(FATAL) << "Cannot read " << filename;
}
data = parser.problem();
data.set_name(problem_name);
if (data.max_bins() != 0) {
LOG(WARNING)
<< "Ignoring max_bins value. The feasibility problem is not supported.";
}
LOG(INFO) << "Solving vector packing problem '" << data.name() << "' with "
<< data.item_size() << " item types, and "
<< data.resource_capacity_size() << " dimensions.";
if (absl::GetFlag(FLAGS_display_proto)) {
LOG(INFO) << data;
}
// Build optimization model.
MPSolver::OptimizationProblemType solver_type;
MPSolver::ParseSolverType(solver, &solver_type);
packing::vbp::VectorBinPackingSolution solution =
packing::SolveVectorBinPackingWithArcFlow(
data, solver_type, params, absl::GetFlag(FLAGS_time_limit),
absl::GetFlag(FLAGS_threads), absl::GetFlag(FLAGS_max_bins));
if (!solution.bins().empty()) {
for (int b = 0; b < solution.bins_size(); ++b) {
LOG(INFO) << "Bin " << b;
const packing::vbp::VectorBinPackingOneBinInSolution& bin =
solution.bins(b);
for (int i = 0; i < bin.item_indices_size(); ++i) {
LOG(INFO) << " - item: " << bin.item_indices(i)
<< ", copies: " << bin.item_copies(i);
}
}
}
}
} // namespace operations_research
int main(int argc, char** argv) {
absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo);
InitGoogle(argv[0], &argc, &argv, true);
if (absl::GetFlag(FLAGS_input).empty()) {
LOG(FATAL) << "Please supply a data file with --input=";
}
operations_research::ParseAndSolve(absl::GetFlag(FLAGS_input),
absl::GetFlag(FLAGS_solver),
absl::GetFlag(FLAGS_params));
return EXIT_SUCCESS;
}

View File

@@ -11,103 +11,622 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# BUILD file to run python examples.
load("@pip_deps//:requirements.bzl", "requirement")
load("@rules_python//python:py_binary.bzl", "py_binary")
load("//bazel:run_binary_test.bzl", "run_binary_test")
load(":code_samples.bzl", "code_sample_compile_py", "code_sample_py", "code_sample_test_arg_py")
package(default_visibility = ["//visibility:public"])
code_sample_compile_py("arc_flow_cutting_stock_sat")
py_binary(
name = "arc_flow_cutting_stock_sat_py3",
srcs = ["arc_flow_cutting_stock_sat.py"],
main = "arc_flow_cutting_stock_sat.py",
deps = [
requirement("absl-py"),
requirement("numpy"),
"//ortools/linear_solver/python:model_builder",
"//ortools/sat/python:cp_model",
],
)
code_sample_py("assignment_with_constraints_sat")
py_binary(
name = "assignment_with_constraints_sat_py3",
srcs = ["assignment_with_constraints_sat.py"],
main = "assignment_with_constraints_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("balance_group_sat")
run_binary_test(
name = "assignment_with_constraints_sat_py_test",
size = "medium",
binary = ":assignment_with_constraints_sat_py3",
)
code_sample_py("bus_driver_scheduling_sat")
py_binary(
name = "balance_group_sat_py3",
srcs = ["balance_group_sat.py"],
main = "balance_group_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("car_sequencing_optimization_sat")
run_binary_test(
name = "balance_group_sat_py_test",
size = "medium",
binary = ":balance_group_sat_py3",
)
code_sample_py("chemical_balance_sat")
py_binary(
name = "bus_driver_scheduling_sat_py3",
srcs = ["bus_driver_scheduling_sat.py"],
main = "bus_driver_scheduling_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("clustering_sat")
run_binary_test(
name = "bus_driver_scheduling_sat_py_test",
size = "medium",
args = ["--params=max_time_in_seconds:40"],
binary = ":bus_driver_scheduling_sat_py3",
)
code_sample_py("cover_rectangle_sat")
py_binary(
name = "car_sequencing_optimization_sat_py3",
srcs = ["car_sequencing_optimization_sat.py"],
main = "car_sequencing_optimization_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("flexible_job_shop_sat")
run_binary_test(
name = "car_sequencing_optimization_sat_py_test",
size = "small",
binary = ":car_sequencing_optimization_sat_py3",
)
code_sample_py("gate_scheduling_sat")
py_binary(
name = "chemical_balance_sat_py3",
srcs = ["chemical_balance_sat.py"],
main = "chemical_balance_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("golomb_sat")
run_binary_test(
name = "chemical_balance_sat_py_test",
size = "medium",
binary = ":chemical_balance_sat_py3",
)
code_sample_py("hidato_sat")
py_binary(
name = "clustering_sat_py3",
srcs = ["clustering_sat.py"],
main = "clustering_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("jobshop_ft06_distance_sat")
run_binary_test(
name = "clustering_sat_py_test",
size = "medium",
binary = ":clustering_sat_py3",
)
code_sample_py("jobshop_ft06_sat")
py_binary(
name = "cover_rectangle_sat_py3",
srcs = ["cover_rectangle_sat.py"],
main = "cover_rectangle_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("jobshop_with_maintenance_sat")
run_binary_test(
name = "cover_rectangle_sat_py_test",
size = "medium",
binary = ":cover_rectangle_sat_py3",
)
code_sample_py("knapsack_2d_sat")
py_binary(
name = "flexible_job_shop_sat_py3",
srcs = ["flexible_job_shop_sat.py"],
main = "flexible_job_shop_sat.py",
deps = ["//ortools/sat/python:cp_model"],
)
code_sample_compile_py("line_balancing_sat")
run_binary_test(
name = "flexible_job_shop_sat_py_test",
binary = ":flexible_job_shop_sat_py3",
)
code_sample_test_arg_py(
name = "line_balancing_sat",
args = ["--input $(rootpath //examples/python/testdata:salbp_20_1.alb)"],
py_binary(
name = "gate_scheduling_sat_py3",
srcs = ["gate_scheduling_sat.py"],
main = "gate_scheduling_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/colab:visualization",
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "gate_scheduling_sat_py_test",
binary = ":gate_scheduling_sat_py3",
)
py_binary(
name = "golomb_sat_py3",
srcs = ["golomb_sat.py"],
main = "golomb_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "golomb_sat_py_test",
size = "medium",
binary = ":golomb_sat_py3",
)
py_binary(
name = "hidato_sat_py3",
srcs = ["hidato_sat.py"],
main = "hidato_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/colab:visualization",
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "hidato_sat_py_test",
binary = ":hidato_sat_py3",
)
py_binary(
name = "jobshop_ft06_distance_sat_py3",
srcs = ["jobshop_ft06_distance_sat.py"],
main = "jobshop_ft06_distance_sat.py",
deps = ["//ortools/sat/python:cp_model"],
)
run_binary_test(
name = "jobshop_ft06_distance_sat_py_test",
binary = ":jobshop_ft06_distance_sat_py3",
)
py_binary(
name = "jobshop_ft06_sat_py3",
srcs = ["jobshop_ft06_sat.py"],
main = "jobshop_ft06_sat.py",
deps = [
"//ortools/sat/colab:visualization",
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "jobshop_ft06_sat_py_test",
binary = ":jobshop_ft06_sat_py3",
)
py_binary(
name = "jobshop_with_maintenance_sat_py3",
srcs = ["jobshop_with_maintenance_sat.py"],
main = "jobshop_with_maintenance_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "jobshop_with_maintenance_sat_py_test",
size = "medium",
binary = ":jobshop_with_maintenance_sat_py3",
)
py_binary(
name = "knapsack_2d_sat_py3",
srcs = ["knapsack_2d_sat.py"],
main = "knapsack_2d_sat.py",
deps = [
requirement("absl-py"),
requirement("numpy"),
requirement("pandas"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "knapsack_2d_sat_py_test",
size = "medium",
binary = ":knapsack_2d_sat_py3",
)
py_binary(
name = "line_balancing_sat_py3",
srcs = ["line_balancing_sat.py"],
main = "line_balancing_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "line_balancing_sat_salbp_20_1_py_test",
args = ["--input=$(rootpath //examples/python/testdata:salbp_20_1.alb)"],
binary = ":line_balancing_sat_py3",
data = ["//examples/python/testdata:salbp_20_1.alb"],
suffix = "salbp_20_1",
grep_lines = ["objective: 3"],
)
code_sample_py("maximize_combinations_sat")
code_sample_py("maze_escape_sat")
code_sample_py("no_wait_baking_scheduling_sat")
code_sample_py("pell_equation_sat")
code_sample_py("pentominoes_sat")
code_sample_py("prize_collecting_tsp_sat")
code_sample_py("prize_collecting_vrp_sat")
code_sample_py("qubo_sat")
code_sample_compile_py("rcpsp_sat")
code_sample_test_arg_py(
name = "rcpsp_sat",
args = ["--input $(rootpath //ortools/scheduling/testdata:j301_1.sm)"],
data = ["//ortools/scheduling/testdata:j301_1.sm"],
suffix = "j301_1",
py_binary(
name = "maximize_combinations_sat_py3",
srcs = ["maximize_combinations_sat.py"],
main = "maximize_combinations_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_test_arg_py(
name = "rcpsp_sat",
args = ["--input $(rootpath //ortools/scheduling/testdata:c1510_1.mm.txt)"],
run_binary_test(
name = "maximize_combinations_sat_py_test",
size = "medium",
binary = ":maximize_combinations_sat_py3",
)
py_binary(
name = "maze_escape_sat_py3",
srcs = ["maze_escape_sat.py"],
main = "maze_escape_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "maze_escape_sat_py_test",
binary = ":maze_escape_sat_py3",
)
py_binary(
name = "music_playlist_sat_py3",
srcs = ["music_playlist_sat.py"],
main = "music_playlist_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "music_playlist_sat_py_test",
binary = ":music_playlist_sat_py3",
)
py_binary(
name = "no_wait_baking_scheduling_sat_py3",
srcs = ["no_wait_baking_scheduling_sat.py"],
main = "no_wait_baking_scheduling_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "no_wait_baking_scheduling_sat_py_test",
size = "medium",
binary = ":no_wait_baking_scheduling_sat_py3",
)
py_binary(
name = "pell_equation_sat_py3",
srcs = ["pell_equation_sat.py"],
main = "pell_equation_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "pell_equation_sat_py_test",
size = "medium",
binary = ":pell_equation_sat_py3",
)
py_binary(
name = "pentominoes_sat_py3",
srcs = ["pentominoes_sat.py"],
main = "pentominoes_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "pentominoes_sat_py_test",
size = "medium",
binary = ":pentominoes_sat_py3",
)
py_binary(
name = "prize_collecting_tsp_sat_py3",
srcs = ["prize_collecting_tsp_sat.py"],
main = "prize_collecting_tsp_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "prize_collecting_tsp_sat_py_test",
size = "medium",
binary = ":prize_collecting_tsp_sat_py3",
)
py_binary(
name = "prize_collecting_vrp_sat_py3",
srcs = ["prize_collecting_vrp_sat.py"],
main = "prize_collecting_vrp_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "prize_collecting_vrp_sat_py_test",
size = "medium",
binary = ":prize_collecting_vrp_sat_py3",
)
py_binary(
name = "qubo_sat_py3",
srcs = ["qubo_sat.py"],
main = "qubo_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "qubo_sat_py_test",
size = "medium",
binary = ":qubo_sat_py3",
)
run_binary_test(
name = "rcpsp_sat_c1510_1_py_test",
args = ["--input=$(rootpath //ortools/scheduling/testdata:c1510_1.mm.txt)"],
binary = ":rcpsp_sat_py3",
data = ["//ortools/scheduling/testdata:c1510_1.mm.txt"],
suffix = "c1510_1",
grep_lines = ["objective: 21"],
)
code_sample_py("shift_scheduling_sat")
run_binary_test(
name = "rcpsp_sat_j301_1_py_test",
args = ["--input=$(rootpath //ortools/scheduling/testdata:j301_1.sm)"],
binary = ":rcpsp_sat_py3",
data = ["//ortools/scheduling/testdata:j301_1.sm"],
grep_lines = ["objective: 43"],
)
code_sample_py("single_machine_scheduling_with_setup_release_due_dates_sat")
py_binary(
name = "rcpsp_sat_py3",
srcs = ["rcpsp_sat.py"],
main = "rcpsp_sat.py",
deps = [
"//ortools/scheduling:rcpsp_py_pb2",
"//ortools/scheduling/python:rcpsp",
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("spread_robots_sat")
py_binary(
name = "shift_scheduling_sat_py3",
srcs = ["shift_scheduling_sat.py"],
main = "shift_scheduling_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("steel_mill_slab_sat")
run_binary_test(
name = "shift_scheduling_sat_py_test",
args = ["--params=max_time_in_seconds:10"],
binary = ":shift_scheduling_sat_py3",
)
code_sample_py("sudoku_sat")
py_binary(
name = "single_machine_scheduling_with_setup_release_due_dates_sat_py3",
srcs = ["single_machine_scheduling_with_setup_release_due_dates_sat.py"],
main = "single_machine_scheduling_with_setup_release_due_dates_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("task_allocation_sat")
run_binary_test(
name = "single_machine_scheduling_with_setup_release_due_dates_sat_py_test",
size = "medium",
binary = ":single_machine_scheduling_with_setup_release_due_dates_sat_py3",
)
code_sample_py("tasks_and_workers_assignment_sat")
py_binary(
name = "spread_robots_sat_py3",
srcs = ["spread_robots_sat.py"],
main = "spread_robots_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("test_scheduling_sat")
run_binary_test(
name = "spread_robots_sat_py_test",
binary = ":spread_robots_sat_py3",
)
code_sample_py("tsp_sat")
py_binary(
name = "steel_mill_slab_sat_py3",
srcs = ["steel_mill_slab_sat.py"],
main = "steel_mill_slab_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
code_sample_py("vendor_scheduling_sat")
run_binary_test(
name = "steel_mill_slab_sat_py_test",
binary = ":steel_mill_slab_sat_py3",
)
code_sample_py("wedding_optimal_chart_sat")
py_binary(
name = "sudoku_sat_py3",
srcs = ["sudoku_sat.py"],
main = "sudoku_sat.py",
deps = ["//ortools/sat/python:cp_model"],
)
code_sample_py("zebra_sat")
run_binary_test(
name = "sudoku_sat_py_test",
binary = ":sudoku_sat_py3",
)
py_binary(
name = "task_allocation_sat_py3",
srcs = ["task_allocation_sat.py"],
main = "task_allocation_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "task_allocation_sat_py_test",
size = "medium",
binary = ":task_allocation_sat_py3",
)
py_binary(
name = "tasks_and_workers_assignment_sat_py3",
srcs = ["tasks_and_workers_assignment_sat.py"],
main = "tasks_and_workers_assignment_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "tasks_and_workers_assignment_sat_py_test",
size = "medium",
binary = ":tasks_and_workers_assignment_sat_py3",
)
py_binary(
name = "test_scheduling_sat_py3",
srcs = ["test_scheduling_sat.py"],
main = "test_scheduling_sat.py",
deps = [
requirement("absl-py"),
requirement("pandas"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "test_scheduling_sat_py_test",
size = "medium",
binary = ":test_scheduling_sat_py3",
)
py_binary(
name = "tsp_sat_py3",
srcs = ["tsp_sat.py"],
main = "tsp_sat.py",
deps = ["//ortools/sat/python:cp_model"],
)
run_binary_test(
name = "tsp_sat_py_test",
size = "medium",
binary = ":tsp_sat_py3",
)
py_binary(
name = "vendor_scheduling_sat_py3",
srcs = ["vendor_scheduling_sat.py"],
main = "vendor_scheduling_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "vendor_scheduling_sat_py_test",
size = "medium",
binary = ":vendor_scheduling_sat_py3",
)
py_binary(
name = "wedding_optimal_chart_sat_py3",
srcs = ["wedding_optimal_chart_sat.py"],
main = "wedding_optimal_chart_sat.py",
deps = [
requirement("absl-py"),
"//ortools/sat/python:cp_model",
],
)
run_binary_test(
name = "wedding_optimal_chart_sat_py_test",
size = "medium",
binary = ":wedding_optimal_chart_sat_py3",
)
py_binary(
name = "zebra_sat_py3",
srcs = ["zebra_sat.py"],
main = "zebra_sat.py",
deps = ["//ortools/sat/python:cp_model"],
)
run_binary_test(
name = "zebra_sat_py_test",
binary = ":zebra_sat_py3",
)

View File

@@ -1,66 +0,0 @@
# 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.
"""Helper macro to compile and test code samples."""
load("@pip_deps//:requirements.bzl", "requirement")
load("@rules_python//python:py_binary.bzl", "py_binary")
load("@rules_python//python:py_test.bzl", "py_test")
PYTHON_DEPS = [
"//ortools/init/python:init",
"//ortools/linear_solver/python:model_builder",
"//ortools/sat/python:cp_model",
"//ortools/sat/colab:visualization",
"//ortools/scheduling:rcpsp_py_proto",
"//ortools/scheduling/python:rcpsp",
requirement("absl-py"),
requirement("numpy"),
requirement("pandas"),
requirement("protobuf"),
requirement("python_dateutil"),
requirement("pytz"),
requirement("six"),
]
def code_sample_compile_py(name):
py_binary(
name = name + "_py3",
srcs = [name + ".py"],
main = name + ".py",
deps = PYTHON_DEPS,
)
def code_sample_test_py(name):
py_test(
name = name + "_py_test",
size = "medium",
srcs = [name + ".py"],
main = name + ".py",
deps = PYTHON_DEPS,
)
def code_sample_test_arg_py(name, suffix, args, data):
py_test(
name = name + "_" + suffix + "_py_test",
size = "medium",
srcs = [name + ".py"],
main = name + ".py",
data = data,
args = args,
deps = PYTHON_DEPS,
)
def code_sample_py(name):
code_sample_compile_py(name)
code_sample_test_py(name)

View File

@@ -0,0 +1,309 @@
#!/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.
"""Create a balanced music playlist.
Create a music playlist by selecting tunes from a list of tunes.
Each tune has a duration in seconds and a music genre (e.g. Rock, Disco, Techno,
etc).
The total playlist duration must be as close as possible to a given total
duration. Each tune can appear at most once in the playlist. All existing
genres must appear at least once in the playlist. Two consecutive tunes must be
of different genres. There is a positive cost to go from a genre to another, and
the playlist must minimize this cost overall.
"""
from collections.abc import Sequence
from absl import app
from ortools.sat.python import cp_model
def Solve():
"""Solves the music playlist problem."""
# --------------------
# 1. Data
# --------------------
tunes = [
("Song 01", 202, "Pop"),
("Song 02", 233, "Techno"),
("Song 03", 108, "Disco"),
("Song 04", 281, "Disco"),
("Song 05", 129, "Techno"),
("Song 06", 122, "Techno"),
("Song 07", 244, "Pop"),
("Song 08", 178, "Techno"),
("Song 09", 213, "Techno"),
("Song 10", 124, "Rock"),
("Song 11", 120, "Disco"),
("Song 12", 196, "Rock"),
("Song 13", 249, "Disco"),
("Song 14", 294, "Disco"),
("Song 15", 103, "Techno"),
("Song 16", 179, "Disco"),
("Song 17", 146, "Disco"),
("Song 18", 126, "Techno"),
("Song 19", 100, "Pop"),
("Song 20", 122, "Disco"),
("Song 21", 190, "Disco"),
("Song 22", 181, "Techno"),
("Song 23", 273, "Pop"),
("Song 24", 121, "Disco"),
("Song 25", 159, "Pop"),
("Song 26", 234, "Rock"),
("Song 27", 169, "Rock"),
("Song 28", 151, "Rock"),
("Song 29", 142, "Techno"),
("Song 30", 245, "Pop"),
("Song 31", 281, "Techno"),
("Song 32", 154, "Rock"),
("Song 33", 148, "Disco"),
("Song 34", 120, "Pop"),
("Song 35", 163, "Disco"),
("Song 36", 158, "Pop"),
("Song 37", 235, "Rock"),
("Song 38", 106, "Techno"),
("Song 39", 117, "Disco"),
("Song 40", 110, "Pop"),
("Song 41", 144, "Rock"),
("Song 42", 156, "Disco"),
("Song 43", 204, "Rock"),
("Song 44", 108, "Pop"),
("Song 45", 255, "Pop"),
("Song 46", 165, "Rock"),
("Song 47", 290, "Disco"),
("Song 48", 242, "Pop"),
("Song 49", 272, "Rock"),
("Song 50", 212, "Pop"),
]
# Genre transition costs. A higher cost means a less desirable transition.
genre_transition_costs = {
"Rock": {"Pop": 3, "Disco": 5, "Techno": 7},
"Pop": {"Rock": 3, "Disco": 6, "Techno": 8},
"Disco": {"Rock": 5, "Pop": 6, "Techno": 9},
"Techno": {"Rock": 7, "Pop": 8, "Disco": 9},
}
num_tunes = len(tunes)
all_tunes = range(num_tunes)
# Playlist target duration in seconds.
target_duration = 60 * 60 # 1 hour
# We use a circuit constraint to model the playlist. In the circuit constraint
# graph, each node is a tune, and each arc represents a pair of consecutive
# tunes in the playlist. We introduce a dummy node to represent the start and
# the end of the playlist.
#
# The constraint that two consecutive tunes must be of different genres is
# encoded by not creating an arc between two tunes that are of the same genre.
# This is crucial in the modelisation of this problem: it reduces the number
# of variables in the model, and it avoids additional constraints to ensure
# two consecutive tunes are of different genres.
# Dummy node representing the start and end of the playlist.
dummy_node = num_tunes
# `possible_successors[i]` contains the list of nodes that can be reached
# after node `i`.
possible_successors = {}
possible_successors[dummy_node] = [dummy_node]
for i in all_tunes:
# Any node can be the first tune in the playlist.
possible_successors[dummy_node].append(i)
# Any node can be the last tune in the playlist.
possible_successors[i] = [dummy_node]
genre_i = tunes[i][2]
for j in all_tunes:
genre_j = tunes[j][2]
# If `i` and `j` are of different genres, we can go from `i` to `j`.
if genre_i != genre_j:
possible_successors[i].append(j)
# --------------------
# 2. Model
# --------------------
model = cp_model.CpModel()
# --------------------
# 3. Decision Variables
# --------------------
# `literals[i][j]` is true if tune `j` follows tune `i` in the playlist.
literals = {}
# --------------------
# 4. Constraints
# --------------------
# 4.1 Two consecutive tunes must be of different genres.
# This is encoded in possible_successors, which doesn't contain any arcs
# between two tunes that are of the same genre. Now we just have to add a
# circuit constraint.
# `arcs` contains the list of possible arcs in the circuit graph, each arc
# is a tuple (i, j, literals[i][j]).
arcs = []
def AddArc(i, j):
literals[(i, j)] = model.new_bool_var(f"lit_{i}_{j}")
arcs.append((i, j, literals[(i, j)]))
# Add all possible arcs between different nodes.
for i, successors in possible_successors.items():
for j in successors:
AddArc(i, j)
# Add self-arcs to let tunes not be in the playlist.
for i in all_tunes:
AddArc(i, i)
# Add a circuit constraint with the arcs.
model.add_circuit(arcs)
# 4.2 All genres must appear at least once.
# This is encoded by adding a constraint that the sum of all literals for a
# given genre is at least 1.
# `is_active[i]` is true iff tune `i` is in the playlist, i.e. if its self-arc
# is not active in the circuit.
is_active = {}
for i in all_tunes:
is_active[i] = literals[(i, i)].Not()
# `genre_tunes[genre]` contains the list of tunes that are of genre `genre`.
genre_tunes = {}
for genre in genre_transition_costs:
genre_tunes[genre] = []
for i in all_tunes:
genre_tunes[tunes[i][2]].append(i)
# For each genre, at least one tune must be active: the sum of all literals
# for this genre is at least 1.
for t in genre_tunes.values():
model.add(sum(is_active[i] for i in t) >= 1)
# --------------------
# 5. Objective
# --------------------
# 5.1. Minimize genre transition costs.
# Add a total_transition_cost variable representing the sum of all transition
# costs in the playlist.
max_transition_cost = 0
for genre_costs in genre_transition_costs.values():
for cost in genre_costs.values():
max_transition_cost = max(cost, max_transition_cost)
total_transition_cost_upper_bound = (num_tunes - 1) * max_transition_cost
total_transition_cost = model.new_int_var(
0, total_transition_cost_upper_bound, "total_transition_cost"
)
transition_cost_terms = []
for i, successors in possible_successors.items():
if i == dummy_node:
continue
genre_i = tunes[i][2]
for j in successors:
if j == dummy_node:
continue
genre_j = tunes[j][2]
cost = genre_transition_costs[genre_i][genre_j]
transition_cost_terms.append(cost * literals[(i, j)])
model.add(total_transition_cost == sum(transition_cost_terms))
# 5.2. Minimize the deviation between the target duration and the actual total
# duration.
# Add a total_duration variable representing the duration of all active tunes.
total_duration_upper_bound = sum([t[1] for t in tunes])
total_duration = model.new_int_var(0, total_duration_upper_bound, "total_duration")
model.add(total_duration == sum(tunes[i][1] * is_active[i] for i in all_tunes))
# Minimize the absolute difference from the target duration.
deviation = model.new_int_var(0, target_duration, "deviation")
model.add_abs_equality(deviation, total_duration - target_duration)
# 5.3 Combine the objectives.
#
# You can add a weight to prioritize one over the other.
# For example, `model.minimize(10 * total_transition_cost + deviation)`
model.minimize(total_transition_cost + deviation)
# --------------------
# 6. Solve
# --------------------
solver = cp_model.CpSolver()
# Set a time limit for the solver
solver.parameters.max_time_in_seconds = 30.0
status = solver.solve(model)
# -----------------------
# 7. Print the solution
# -----------------------
if status == cp_model.OPTIMAL:
print("Found Optimal Playlist:")
elif status == cp_model.FEASIBLE:
print("Found Feasible Playlist:")
else:
print("No solution found.")
return
print(f" Total Transition Cost: {solver.value(total_transition_cost)}")
print(
f" Playlist Duration: {solver.value(total_duration)} seconds "
f"({solver.value(total_duration) / 60:.2f} minutes)"
)
print(
f" Deviation from target duration ({target_duration}):"
f" {solver.value(deviation)} seconds"
)
print("-" * 30)
# Reconstruct the playlist sequence by starting from the dummy node.
playlist = []
current_node = dummy_node
while True:
# Find the successor of the current node.
next_node = dummy_node
for next_node in possible_successors[current_node]:
if solver.value(literals[(current_node, next_node)]):
break
if next_node == dummy_node:
break # We've completed the loop back to the start.
playlist.append(next_node)
current_node = next_node
if not playlist:
print("Empty playlist.")
else:
for i in playlist:
(name, duration, genre) = tunes[i]
print(f"{i+1}. {name} ({genre}) - {duration}s")
def main(argv: Sequence[str]) -> None:
if len(argv) > 1:
raise app.UsageError("Too many command-line arguments.")
Solve()
if __name__ == "__main__":
app.run(main)

View File

@@ -12,13 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
source gbash.sh || exit
source module gbash_unit.sh
function test::operations_research_examples::cvrptw() {
declare -r DIR="${TEST_SRCDIR}/ortools/routing/samples"
EXPECT_SUCCEED '${DIR}/cvrptw --vrp_use_deterministic_random_seed'
}
gbash::unit::main "$@"
set -x
exec {binary_path} "$@"