diff --git a/WORKSPACE b/WORKSPACE index b71b51c019..491648c3c9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -106,3 +106,35 @@ cc_library( ) """ ) + +http_archive( + name = "rules_python", + sha256 = "9fcf91dbcc31fde6d1edb15f117246d912c33c36f44cf681976bd886538deba6", + strip_prefix = "rules_python-0.8.0", + url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.8.0.tar.gz", +) + +load("@rules_python//python:pip.bzl", "pip_install") + +# Create a central external repo, @ortools_deps, that contains Bazel targets for all the +# third-party packages specified in the python_deps.txt file. +pip_install( + name = "ortools_deps", + requirements = "//bazel:python_deps.txt", +) + +git_repository( + name = "pybind11_bazel", + commit = "72cbbf1fbc830e487e3012862b7b720001b70672", + remote = "https://github.com/pybind/pybind11_bazel.git", +) + +new_git_repository( + name = "pybind11", + build_file = "@pybind11_bazel//:pybind11.BUILD", + tag = "v2.9.1", + remote = "https://github.com/pybind/pybind11.git", +) + +load("@pybind11_bazel//:python_configure.bzl", "python_configure") +python_configure(name = "local_config_python", python_version = "3") diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel index cdcf075313..375e0137b9 100644 --- a/bazel/BUILD.bazel +++ b/bazel/BUILD.bazel @@ -9,4 +9,5 @@ exports_files([ "bliss-0.73.patch", # "zlib.BUILD", "archive_helper.bzl", + "python_deps.txt", ]) diff --git a/bazel/python_deps.txt b/bazel/python_deps.txt new file mode 100644 index 0000000000..7c3b2560f0 --- /dev/null +++ b/bazel/python_deps.txt @@ -0,0 +1,3 @@ +absl-py >= 0.13 +numpy >= 1.13.3 +protobuf >= 3.19.4 diff --git a/ortools/linear_solver/BUILD.bazel b/ortools/linear_solver/BUILD.bazel index 9c9a1d0a6c..a1cf2a7d38 100644 --- a/ortools/linear_solver/BUILD.bazel +++ b/ortools/linear_solver/BUILD.bazel @@ -1,5 +1,6 @@ load("@rules_cc//cc:defs.bzl", "cc_proto_library") load("@bazel_skylib//rules:copy_file.bzl", "copy_file") +load("@com_google_protobuf//:protobuf.bzl", "py_proto_library") package(default_visibility = ["//visibility:public"]) @@ -41,6 +42,13 @@ cc_proto_library( deps = [":linear_solver_proto"], ) +py_proto_library( + name = "linear_solver_py_pb2", + srcs = ["linear_solver.proto"], + deps = ["//ortools/util:optional_boolean_py_pb2"], + visibility = ["//visibility:public"], +) + # You can include the interfaces to different solvers by invoking '--define' # flags. By default GLOP, BOP, SCIP, GUROBI, and CP-SAT interface are included. # diff --git a/ortools/linear_solver/sat_proto_solver.cc b/ortools/linear_solver/sat_proto_solver.cc index b20a46ac5f..831de61f9c 100644 --- a/ortools/linear_solver/sat_proto_solver.cc +++ b/ortools/linear_solver/sat_proto_solver.cc @@ -24,6 +24,7 @@ #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_solver.h" #include "ortools/sat/lp_utils.h" +#include "ortools/sat/parameters_validation.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/util/logging.h" #include "ortools/util/time_limit.h" @@ -80,13 +81,17 @@ sat::CpSolverStatus FromMPSolverResponseStatus(MPSolverResponseStatus status) { MPSolutionResponse InfeasibleResponse(SolverLogger& logger, std::string message) { + SOLVER_LOG(&logger, "Infeasible model detected in sat_solve_proto.\n", + message); + // This is needed for our benchmark scripts. - MPSolutionResponse response; if (logger.LoggingIsEnabled()) { sat::CpSolverResponse cp_response; cp_response.set_status(sat::CpSolverStatus::INFEASIBLE); SOLVER_LOG(&logger, CpSolverResponseStats(cp_response)); } + + MPSolutionResponse response; response.set_status(MPSolverResponseStatus::MPSOLVER_INFEASIBLE); response.set_status_str(message); return response; @@ -94,13 +99,16 @@ MPSolutionResponse InfeasibleResponse(SolverLogger& logger, MPSolutionResponse ModelInvalidResponse(SolverLogger& logger, std::string message) { + SOLVER_LOG(&logger, "Invalid input detected in sat_solve_proto.\n", message); + // This is needed for our benchmark scripts. - MPSolutionResponse response; if (logger.LoggingIsEnabled()) { sat::CpSolverResponse cp_response; cp_response.set_status(sat::CpSolverStatus::MODEL_INVALID); SOLVER_LOG(&logger, CpSolverResponseStats(cp_response)); } + + MPSolutionResponse response; response.set_status(MPSolverResponseStatus::MPSOLVER_MODEL_INVALID); response.set_status_str(message); return response; @@ -175,6 +183,14 @@ absl::StatusOr SatSolveProto( return ModelInvalidResponse(logger, "Extra CP-SAT validation failed."); } + { + const std::string error = sat::ValidateParameters(params); + if (!error.empty()) { + return ModelInvalidResponse( + logger, absl::StrCat("Invalid CP-SAT parameters: ", error)); + } + } + // This is good to do before any presolve. if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model, &logger)) { return InfeasibleResponse(logger, diff --git a/ortools/model_builder/python/BUILD.bazel b/ortools/model_builder/python/BUILD.bazel new file mode 100644 index 0000000000..a32e4502f0 --- /dev/null +++ b/ortools/model_builder/python/BUILD.bazel @@ -0,0 +1,44 @@ +# Python wrapper for model_builder. + +load("@ortools_deps//:requirements.bzl", "requirement") +load("@pybind11_bazel//:build_defs.bzl", "pybind_extension") +load("@rules_python//python:defs.bzl", "py_library") + +pybind_extension( + name = "pywrap_model_builder_helper", + srcs = ["pywrap_model_builder_helper.cc"], + visibility = ["//visibility:public"], + deps = [ + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_exporter", + "//ortools/model_builder/wrappers:model_builder_helper", + "@com_google_absl//absl/strings", + "@eigen//:eigen3", + ], +) + +py_library( + name = "model_builder_helper", + srcs = ["model_builder_helper.py"], + data = [ + ":pywrap_model_builder_helper.so", + ], + visibility = ["//visibility:public"], + deps = [ + requirement("numpy"), + "//ortools/linear_solver:linear_solver_py_pb2", + ], +) + +py_library( + name = "model_builder", + srcs = ["model_builder.py"], + data = [ + ":pywrap_model_builder_helper.so", + ], + visibility = ["//visibility:public"], + deps = [ + ":model_builder_helper", + "//ortools/linear_solver:linear_solver_py_pb2", + ], +) diff --git a/ortools/model_builder/python/model_builder.py b/ortools/model_builder/python/model_builder.py index d437a50a5f..7a85282848 100644 --- a/ortools/model_builder/python/model_builder.py +++ b/ortools/model_builder/python/model_builder.py @@ -157,7 +157,7 @@ class LinearExpr(object): return _Sum(self, arg) def __radd__(self, arg): - return self.__add(arg) + return self.__add__(arg) def __sub__(self, arg): if mbh.is_zero(arg): @@ -219,8 +219,6 @@ class LinearExpr(object): def __eq__(self, arg): if arg is None: return False - if isinstance(self, Variable) and isinstance(arg, Variable): - return VarCompVar(self, arg, True) if mbh.is_a_number(arg): arg = mbh.assert_is_a_number(arg) return BoundedLinearExpression(self, arg, arg) @@ -242,8 +240,6 @@ class LinearExpr(object): return BoundedLinearExpression(self - arg, -math.inf, 0) def __ne__(self, arg): - if isinstance(self, Variable) and isinstance(arg, Variable): - return VarCompVar(self, arg, False) return NotImplemented def __lt__(self, arg): @@ -540,6 +536,25 @@ class Variable(LinearExpr): def objective_coefficient(self, coeff): return self.__helper.set_var_objective_coefficient(self.__index, coeff) + def __eq__(self, arg): + if arg is None: + return False + if isinstance(arg, Variable): + return VarCompVar(self, arg, True) + else: + if mbh.is_a_number(arg): + arg = mbh.assert_is_a_number(arg) + return BoundedLinearExpression(self, arg, arg) + else: + return BoundedLinearExpression(self - arg, 0, 0) + + def __ne__(self, arg): + if arg is None: + return True + if isinstance(arg, Variable): + return VarCompVar(self, arg, False) + return NotImplemented + def __hash__(self): return hash((self.__helper, self.__index)) @@ -559,7 +574,7 @@ class VarCompVar(object): return f'{self.__left} == {self.__right}' def __repr__(self): - return f'VarEqVar({self.__left}, {self.__right}, {self.__is_equality})' + return f'VarCompVar({self.__left}, {self.__right}, {self.__is_equality})' @property def left(self): @@ -574,7 +589,7 @@ class VarCompVar(object): return self.__is_equality def __bool__(self): - return (self.__left == self.__right) == self.__is_equality + return (self.__left.index == self.__right.index) == self.__is_equality class BoundedLinearExpression(object): diff --git a/ortools/model_builder/samples/BUILD.bazel b/ortools/model_builder/samples/BUILD.bazel index 9deb3135d6..7e7c6692f1 100644 --- a/ortools/model_builder/samples/BUILD.bazel +++ b/ortools/model_builder/samples/BUILD.bazel @@ -1 +1,11 @@ # Samples code the model builder library. + +load(":code_samples.bzl", "code_sample_py") + +code_sample_py(name = "assignment_mb") + +code_sample_py(name = "bin_packing_mb") + +code_sample_py(name = "simple_lp_program_mb") + +code_sample_py(name = "simple_mip_program_mb") \ No newline at end of file diff --git a/ortools/model_builder/samples/code_samples.bzl b/ortools/model_builder/samples/code_samples.bzl index 5d90930612..58f11605d6 100644 --- a/ortools/model_builder/samples/code_samples.bzl +++ b/ortools/model_builder/samples/code_samples.bzl @@ -1,5 +1,8 @@ """Helper macro to compile and test code samples.""" +load("@ortools_deps//:requirements.bzl", "requirement") +load("@rules_python//python:defs.bzl", "py_binary") + def code_sample_cc(name): native.cc_binary( name = name, @@ -23,3 +26,26 @@ def code_sample_cc(name): ], ) +def code_sample_py(name): + py_binary( + name = name + "_py3", + srcs = [name + ".py"], + main = name + ".py", + deps = [ + requirement("absl-py"), + "//ortools/model_builder/python:model_builder", + ], + python_version = "PY3", + srcs_version = "PY3", + ) + + native.sh_test( + name = name + "_py_test", + size = "small", + srcs = ["code_samples_py_test.sh"], + args = [name], + data = [ + ":" + name + "_py3", + ], + ) + diff --git a/ortools/model_builder/samples/code_samples_py_test.sh b/ortools/model_builder/samples/code_samples_py_test.sh new file mode 100755 index 0000000000..a4432bb7ce --- /dev/null +++ b/ortools/model_builder/samples/code_samples_py_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +declare -r DIR="${TEST_SRCDIR}/com_google_ortools/ortools/model_builder/samples" + +function test::ortools::code_samples_model_builder_py() { + "${DIR}/$1_py3" +} + +test::ortools::code_samples_model_builder_py $1 diff --git a/ortools/model_builder/wrappers/BUILD.bazel b/ortools/model_builder/wrappers/BUILD.bazel new file mode 100644 index 0000000000..956d6467f9 --- /dev/null +++ b/ortools/model_builder/wrappers/BUILD.bazel @@ -0,0 +1,31 @@ +# ModelBuilder: a lightweight implementation of the linear_solver API + +# Public exports. +exports_files( + [ + "README.md", + "BUILD.bazel", + "CMakeLists.txt", + ] + glob([ + "*.cc", + "*.h", + ]), +) + +cc_library( + name = "model_builder_helper", + srcs = ["model_builder_helper.cc"], + hdrs = ["model_builder_helper.h"], + visibility = ["//visibility:public"], + copts = [ + "-DUSE_SCIP", + ], + deps = [ + "//ortools/linear_solver", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_exporter", + "//ortools/lp_data:lp_parser", + "//ortools/lp_data:mps_reader", + "//ortools/util:logging", + ], +) diff --git a/ortools/model_builder/wrappers/model_builder_helper.cc b/ortools/model_builder/wrappers/model_builder_helper.cc index 5e50f4ac6b..de58f7fee9 100644 --- a/ortools/model_builder/wrappers/model_builder_helper.cc +++ b/ortools/model_builder/wrappers/model_builder_helper.cc @@ -269,7 +269,7 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { } break; } -#if defined(USE_SCIP) +#if defined(USE_SCIP) case MPModelRequest::SCIP_MIXED_INTEGER_PROGRAMMING: { // TODO(user): Enable log_callback support. // TODO(user): Enable interrupt_solve. @@ -279,7 +279,7 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { } break; } -#endif // defined(USE_SCIP) +#endif // defined(USE_SCIP) default: { response_->set_status( MPSolverResponseStatus::MPSOLVER_SOLVER_TYPE_UNAVAILABLE);