diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c82fbecb6..97d71e6d05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -339,7 +339,11 @@ if(BUILD_PYTHON) "NOT BUILD_DEPS" ON) message(STATUS "Python: Build pybind11: ${BUILD_pybind11}") - CMAKE_DEPENDENT_OPTION(BUILD_PYTHON_DOC "Build the Python doc" OFF "NOT BUILD_DOC" ON) + CMAKE_DEPENDENT_OPTION(BUILD_pybind11_protobuf "Build the pybind11_protobuf dependency Library" OFF + "NOT BUILD_DEPS" ON) + message(STATUS "Python: Build pybind11_protobuf: ${BUILD_pybind11_protobuf}") + +CMAKE_DEPENDENT_OPTION(BUILD_PYTHON_DOC "Build the Python doc" OFF "NOT BUILD_DOC" ON) message(STATUS "Python: Build doc: ${BUILD_PYTHON_DOC}") CMAKE_DEPENDENT_OPTION(BUILD_VENV "Create Python venv in BINARY_DIR/python/venv" OFF diff --git a/WORKSPACE b/WORKSPACE index d51352587b..8a0c92a696 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -73,7 +73,7 @@ git_repository( ## Protobuf git_repository( name = "com_google_protobuf", - tag = "v23.2", + tag = "v23.3", patches = ["//patches:protobuf-v23.2.patch"], patch_args = ["-p1"], remote = "https://github.com/protocolbuffers/protobuf.git", @@ -214,9 +214,22 @@ new_git_repository( remote = "https://github.com/pybind/pybind11.git", ) +new_git_repository( + name = "pybind11_protobuf", + #build_file = "@pybind11_bazel//:pybind11.BUILD", + #tag = "v2.10.3", + commit = "5baa2dc9d93e3b608cde86dfa4b8c63aeab4ac78", + remote = "https://github.com/pybind/pybind11_protobuf.git", +) + load("@pybind11_bazel//:python_configure.bzl", "python_configure") python_configure(name = "local_config_python", python_version = "3") +bind( + name = "python_headers", + actual = "@local_config_python//:python_headers", +) + # Java support (with junit 5) ## Bazel Java rules. git_repository( diff --git a/cmake/dependencies/CMakeLists.txt b/cmake/dependencies/CMakeLists.txt index c39e62c700..52eec2cba2 100644 --- a/cmake/dependencies/CMakeLists.txt +++ b/cmake/dependencies/CMakeLists.txt @@ -103,9 +103,9 @@ if(BUILD_Protobuf) FetchContent_Declare( protobuf GIT_REPOSITORY "https://github.com/protocolbuffers/protobuf.git" - GIT_TAG "v23.2" + GIT_TAG "v23.3" GIT_SUBMODULES "" - PATCH_COMMAND git apply --ignore-whitespace "${CMAKE_CURRENT_LIST_DIR}/../../patches/protobuf-v23.2.patch") + PATCH_COMMAND git apply --ignore-whitespace "${CMAKE_CURRENT_LIST_DIR}/../../patches/protobuf-v23.3.patch") FetchContent_MakeAvailable(protobuf) list(POP_BACK CMAKE_MESSAGE_INDENT) message(CHECK_PASS "fetched") @@ -171,6 +171,23 @@ if(BUILD_PYTHON AND BUILD_pybind11) message(CHECK_PASS "fetched") endif() +if(BUILD_PYTHON AND BUILD_pybind11_protobuf) + message(CHECK_START "Fetching pybind11_protobuf") + list(APPEND CMAKE_MESSAGE_INDENT " ") + #set(PYBIND11_INSTALL ON) + #set(PYBIND11_TEST OFF) + FetchContent_Declare( + pybind11_protobuf + GIT_REPOSITORY "https://github.com/pybind/pybind11_protobuf.git" + GIT_TAG "main" + PATCH_COMMAND git apply --ignore-whitespace "${CMAKE_CURRENT_LIST_DIR}/../../patches/pybind11_protobuf.patch" + ) + FetchContent_MakeAvailable(pybind11_protobuf) + list(POP_BACK CMAKE_MESSAGE_INDENT) + message(CHECK_PASS "fetched") +endif() + + # ############################################################################## # GLPK # ############################################################################## diff --git a/cmake/deps.cmake b/cmake/deps.cmake index 8ca3aba547..15508d41fa 100644 --- a/cmake/deps.cmake +++ b/cmake/deps.cmake @@ -153,4 +153,8 @@ if(BUILD_PYTHON) if(NOT BUILD_pybind11) find_package(pybind11 REQUIRED) endif() + + if(NOT BUILD_pybind11_protobuf) + find_package(pybind11_protobuf REQUIRED) + endif() endif() diff --git a/cmake/python.cmake b/cmake/python.cmake index 61ed053f74..a3eb1000f0 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -268,6 +268,7 @@ file(COPY file(COPY ortools/linear_solver/python/model_builder.py ortools/linear_solver/python/model_builder_helper.py + ortools/linear_solver/python/pandas_model.py DESTINATION ${PYTHON_PROJECT_DIR}/linear_solver/python) file(COPY ortools/sat/python/cp_model.py diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index 711c64242d..09b1210900 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -59,6 +59,7 @@ cc_library( ":graph", "//ortools/base:hash", "//ortools/base:map_util", + "@com_google_absl//absl/container:btree", "@com_google_absl//absl/container:inlined_vector", ], ) diff --git a/ortools/graph/util.h b/ortools/graph/util.h index 12e2e41422..bd8b9917f5 100644 --- a/ortools/graph/util.h +++ b/ortools/graph/util.h @@ -25,6 +25,7 @@ #include #include +#include "absl/container/btree_map.h" #include "absl/container/flat_hash_map.h" #include "absl/container/inlined_vector.h" #include "ortools/base/hash.h" @@ -353,7 +354,7 @@ void RemoveCyclesFromPath(const Graph& graph, std::vector* arc_path) { if (arc_path->empty()) return; // This maps each node to the latest arc in the given path that leaves it. - std::map last_arc_leaving_node; + absl::btree_map last_arc_leaving_node; for (const int arc : *arc_path) last_arc_leaving_node[graph.Tail(arc)] = arc; // Special case for the destination. diff --git a/ortools/linear_solver/model_exporter.h b/ortools/linear_solver/model_exporter.h index 74f961f351..dc53a415b4 100644 --- a/ortools/linear_solver/model_exporter.h +++ b/ortools/linear_solver/model_exporter.h @@ -15,12 +15,8 @@ #define OR_TOOLS_LINEAR_SOLVER_MODEL_EXPORTER_H_ #include -#include #include "absl/status/statusor.h" -#include "absl/strings/str_format.h" -#include "ortools/base/hash.h" -#include "ortools/base/macros.h" #include "ortools/linear_solver/linear_solver.pb.h" namespace operations_research { diff --git a/ortools/linear_solver/python/BUILD.bazel b/ortools/linear_solver/python/BUILD.bazel index 4f552d203e..edc2e7a3c7 100644 --- a/ortools/linear_solver/python/BUILD.bazel +++ b/ortools/linear_solver/python/BUILD.bazel @@ -26,6 +26,7 @@ pybind_extension( "//ortools/linear_solver/wrappers:model_builder_helper", "@com_google_absl//absl/strings", "@eigen//:eigen3", + "@pybind11_protobuf//pybind11_protobuf:native_proto_caster", ], ) @@ -83,3 +84,25 @@ py_test( requirement("numpy"), ], ) + +py_library( + name = "pandas_model", + srcs = ["pandas_model.py"], + deps = [ + ":pywrap_model_builder_helper", + requirement("numpy"), + requirement("pandas"), + "//ortools/linear_solver:linear_solver_py_pb2", + ], +) + +py_test( + name = "pandas_model_test", + srcs = ["pandas_model_test.py"], + deps = [ + ":pandas_model", + requirement("absl-py"), + requirement("pandas"), + "//ortools/linear_solver:linear_solver_py_pb2", + ], +) \ No newline at end of file diff --git a/ortools/linear_solver/python/CMakeLists.txt b/ortools/linear_solver/python/CMakeLists.txt index ac25c9bddd..b85e4ec7eb 100644 --- a/ortools/linear_solver/python/CMakeLists.txt +++ b/ortools/linear_solver/python/CMakeLists.txt @@ -48,6 +48,9 @@ endif() pybind11_add_module(pywrap_model_builder_helper MODULE pywrap_model_builder_helper.cc) +target_include_directories(pywrap_model_builder_helper PRIVATE + ${protobuf_SOURCE_DIR}) + # note: macOS is APPLE and also UNIX ! if(APPLE) set_target_properties(pywrap_model_builder_helper PROPERTIES @@ -63,7 +66,11 @@ elseif(UNIX) ) endif() -target_link_libraries(pywrap_model_builder_helper PRIVATE ${PROJECT_NAMESPACE}::ortools) +target_link_libraries(pywrap_model_builder_helper PRIVATE + ${PROJECT_NAMESPACE}::ortools + pybind11_native_proto_caster +) + add_library(${PROJECT_NAMESPACE}::pywrap_model_builder_helper ALIAS pywrap_model_builder_helper) if(BUILD_TESTING) diff --git a/ortools/linear_solver/python/model_builder.py b/ortools/linear_solver/python/model_builder.py index f89bf83340..08ebaebc67 100644 --- a/ortools/linear_solver/python/model_builder.py +++ b/ortools/linear_solver/python/model_builder.py @@ -10,6 +10,7 @@ # 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. + """Methods for building and solving model_builder models. The following two sections describe the main @@ -41,19 +42,29 @@ from numpy.lib import mixins from ortools.linear_solver.python import model_builder_helper as mbh from ortools.linear_solver.python import pywrap_model_builder_helper as pwmb + # Custom types. NumberT = Union[numbers.Number, np.number] IntegerT = Union[numbers.Integral, np.integer] -LinearExprT = Union['LinearExpr', NumberT] -ConstraintT = Union['VarCompVar', 'BoundedLinearExpression', bool] +LinearExprT = Union["LinearExpr", NumberT] +ConstraintT = Union["VarCompVar", "BoundedLinearExpression", bool] ShapeT = Union[IntegerT, Sequence[IntegerT]] -VariablesT = Union['VariableContainer', 'Variable'] -NumpyFuncT = Callable[[ - 'VariableContainer', - Optional[Union[NumberT, npt.NDArray[np.number], Sequence[NumberT]]], -], LinearExprT,] -SliceT = Union[slice, int, List[int], 'ellipsis', - Tuple[Union[int, slice, List[int], 'ellipsis'], ...],] +VariablesT = Union["VariableContainer", "Variable"] +NumpyFuncT = Callable[ + [ + "VariableContainer", + Optional[Union[NumberT, npt.NDArray[np.number], Sequence[NumberT]]], + ], + LinearExprT, +] +SliceT = Union[ + slice, + int, + List[int], + "ellipsis", + Tuple[Union[int, slice, List[int], "ellipsis"], ...], +] + # Forward solve statuses. SolveStatus = pwmb.SolveStatus @@ -62,62 +73,60 @@ SolveStatus = pwmb.SolveStatus class LinearExpr: """Holds an linear expression. - A linear expression is built from constants and variables. - For example, `x + 2.0 * (y - z + 1.0)`. + A linear expression is built from constants and variables. + For example, `x + 2.0 * (y - z + 1.0)`. - Linear expressions are used in ModelBuilder models in constraints and in the - objective: + Linear expressions are used in ModelBuilder models in constraints and in the + objective: - * You can define linear constraints as in: + * You can define linear constraints as in: - ``` - model.add(x + 2 * y <= 5.0) - model.add(sum(array_of_vars) == 5.0) - ``` + ``` + model.add(x + 2 * y <= 5.0) + model.add(sum(array_of_vars) == 5.0) + ``` - * In ModelBuilder, the objective is a linear expression: + * In ModelBuilder, the objective is a linear expression: - ``` - model.minimize(x + 2.0 * y + z) - ``` + ``` + model.minimize(x + 2.0 * y + z) + ``` - * For large arrays, using the LinearExpr class is faster that using the python - `sum()` function. You can create constraints and the objective from lists of - linear expressions or coefficients as follows: + * For large arrays, using the LinearExpr class is faster that using the python + `sum()` function. You can create constraints and the objective from lists of + linear expressions or coefficients as follows: - ``` - model.minimize(model_builder.LinearExpr.sum(expressions)) - model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0) - ``` - """ + ``` + model.minimize(model_builder.LinearExpr.sum(expressions)) + model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0) + ``` + """ @classmethod def sum( # pytype: disable=annotation-type-mismatch # numpy-scalars - cls, - expressions: Sequence[LinearExprT], - *, - constant: NumberT = 0.0) -> LinearExprT: + cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0 + ) -> LinearExprT: """Creates `sum(expressions) + constant`. - It can perform simple simplifications and returns different objects, - including the input. + It can perform simple simplifications and returns different objects, + including the input. - Args: - expressions: a sequence of linear expressions or constants. - constant: a numerical constant. + Args: + expressions: a sequence of linear expressions or constants. + constant: a numerical constant. - Returns: - a LinearExpr instance or a numerical constant. - """ + Returns: + a LinearExpr instance or a numerical constant. + """ checked_constant: np.double = mbh.assert_is_a_number(constant) if not expressions: return checked_constant if len(expressions) == 1 and mbh.is_zero(checked_constant): return expressions[0] - return LinearExpr.weighted_sum(expressions, - np.ones(len(expressions)), - constant=checked_constant) + return LinearExpr.weighted_sum( + expressions, np.ones(len(expressions)), constant=checked_constant + ) @classmethod def weighted_sum( # pytype: disable=annotation-type-mismatch # numpy-scalars @@ -129,21 +138,22 @@ class LinearExpr: ) -> LinearExprT: """Creates `sum(expressions[i] * coefficients[i]) + constant`. - It can perform simple simplifications and returns different object, - including the input. + It can perform simple simplifications and returns different object, + including the input. - Args: - expressions: a sequence of linear expressions or constants. - coefficients: a sequence of numerical constants. - constant: a numerical constant. + Args: + expressions: a sequence of linear expressions or constants. + coefficients: a sequence of numerical constants. + constant: a numerical constant. - Returns: - a LinearExpr instance or a numerical constant. - """ + Returns: + a LinearExpr instance or a numerical constant. + """ if len(expressions) != len(coefficients): raise ValueError( - 'LinearExpr.weighted_sum: expressions and coefficients have' - ' different lengths') + "LinearExpr.weighted_sum: expressions and coefficients have" + " different lengths" + ) checked_constant: np.double = mbh.assert_is_a_number(constant) if not expressions: return checked_constant @@ -183,16 +193,16 @@ class LinearExpr: ) -> LinearExprT: """Creates `expression * coefficient + constant`. - It can perform simple simplifications and returns different object, - including the input. - Args: - expression: a linear expression or a constant. - coefficient: a numerical constant. - constant: a numerical constant. + It can perform simple simplifications and returns different object, + including the input. + Args: + expression: a linear expression or a constant. + coefficient: a numerical constant. + constant: a numerical constant. - Returns: - a LinearExpr instance or a numerical constant. - """ + Returns: + a LinearExpr instance or a numerical constant. + """ checked_coefficient: np.double = mbh.assert_is_a_number(coefficient) checked_constant: np.double = mbh.assert_is_a_number(constant) @@ -201,8 +211,7 @@ class LinearExpr: if mbh.is_one(checked_coefficient) and mbh.is_zero(checked_constant): return expression if mbh.is_a_number(expression): - return np.double( - expression) * checked_coefficient + checked_constant + return np.double(expression) * checked_coefficient + checked_constant if isinstance(expression, Variable): return _WeightedSum( variable_indices=np.array([expression.index], dtype=np.int32), @@ -213,11 +222,9 @@ class LinearExpr: return _WeightedSum( variable_indices=np.copy(expression.variable_indices), coefficients=expression.coefficients * checked_coefficient, - constant=expression.constant * checked_coefficient + - checked_constant, + constant=expression.constant * checked_coefficient + checked_constant, ) - raise TypeError( - f'Unknown expression {expression!r} of type {type(expression)}') + raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}") def __hash__(self): return object.__hash__(self) @@ -228,7 +235,9 @@ class LinearExpr: def __add__(self, arg: LinearExprT) -> LinearExprT: if mbh.is_a_number(arg): return LinearExpr.sum([self], constant=arg) - return LinearExpr.weighted_sum([self, arg], [1.0, 1.0], constant=0.0) # pytype: disable=wrong-arg-types # numpy-scalars + return LinearExpr.weighted_sum( + [self, arg], [1.0, 1.0], constant=0.0 + ) # pytype: disable=wrong-arg-types # numpy-scalars def __radd__(self, arg: LinearExprT): return self.__add__(arg) @@ -236,10 +245,14 @@ class LinearExpr: def __sub__(self, arg: LinearExprT): if mbh.is_a_number(arg): return LinearExpr.sum([self], constant=arg * -1.0) - return LinearExpr.weighted_sum([self, arg], [1.0, -1.0], constant=0.0) # pytype: disable=wrong-arg-types # numpy-scalars + return LinearExpr.weighted_sum( + [self, arg], [1.0, -1.0], constant=0.0 + ) # pytype: disable=wrong-arg-types # numpy-scalars def __rsub__(self, arg: LinearExprT): - return LinearExpr.weighted_sum([self, arg], [-1.0, 1.0], constant=0.0) # pytype: disable=wrong-arg-types # numpy-scalars + return LinearExpr.weighted_sum( + [self, arg], [-1.0, 1.0], constant=0.0 + ) # pytype: disable=wrong-arg-types # numpy-scalars def __mul__(self, arg: NumberT): arg = mbh.assert_is_a_number(arg) @@ -250,7 +263,7 @@ class LinearExpr: return self.multiply_by(arg) def multiply_by(self, arg: NumberT) -> LinearExprT: - raise NotImplementedError('LinearExpr.multiply_by') + raise NotImplementedError("LinearExpr.multiply_by") def __rmul__(self, arg: NumberT): return self.__mul__(arg) @@ -258,8 +271,7 @@ class LinearExpr: def __div__(self, arg: NumberT): coeff = mbh.assert_is_a_number(arg) if mbh.is_zero(coeff): - raise ValueError( - 'Cannot call the division operator with a zero divisor') + raise ValueError("Cannot call the division operator with a zero divisor") return self.__mul__(1.0 / coeff) def __truediv__(self, _): @@ -287,36 +299,47 @@ class LinearExpr: return NotImplemented def __neg__(self): - return self.__mul__(-1.0) # pytype: disable=unsupported-operands # numpy-scalars + return self.__mul__( + -1.0 + ) # pytype: disable=unsupported-operands # numpy-scalars def __bool__(self): - raise NotImplementedError( - f'Cannot use a LinearExpr {self} as a Boolean value') + raise NotImplementedError(f"Cannot use a LinearExpr {self} as a Boolean value") def __eq__( - self, arg: Optional[LinearExprT] - ) -> Union[bool, 'BoundedLinearExpression']: + self, arg: Optional[LinearExprT] + ) -> Union[bool, "BoundedLinearExpression"]: if arg is None: return False 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) # pytype: disable=wrong-arg-types # numpy-scalars + return BoundedLinearExpression( + self - arg, 0, 0 + ) # pytype: disable=wrong-arg-types # numpy-scalars - def __ge__(self, arg: LinearExprT) -> 'BoundedLinearExpression': + def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression": if mbh.is_a_number(arg): arg = mbh.assert_is_a_number(arg) - return BoundedLinearExpression(self, arg, math.inf) # pytype: disable=wrong-arg-types # numpy-scalars + return BoundedLinearExpression( + self, arg, math.inf + ) # pytype: disable=wrong-arg-types # numpy-scalars else: - return BoundedLinearExpression(self - arg, 0, math.inf) # pytype: disable=wrong-arg-types # numpy-scalars + return BoundedLinearExpression( + self - arg, 0, math.inf + ) # pytype: disable=wrong-arg-types # numpy-scalars - def __le__(self, arg: LinearExprT) -> 'BoundedLinearExpression': + def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression": if mbh.is_a_number(arg): arg = mbh.assert_is_a_number(arg) - return BoundedLinearExpression(self, -math.inf, arg) # pytype: disable=wrong-arg-types # numpy-scalars + return BoundedLinearExpression( + self, -math.inf, arg + ) # pytype: disable=wrong-arg-types # numpy-scalars else: - return BoundedLinearExpression(self - arg, -math.inf, 0) # pytype: disable=wrong-arg-types # numpy-scalars + return BoundedLinearExpression( + self - arg, -math.inf, 0 + ) # pytype: disable=wrong-arg-types # numpy-scalars def __ne__(self, arg: LinearExprT): return NotImplemented @@ -332,16 +355,17 @@ class _WeightedSum(LinearExpr): """Represents sum(ai * xi) + b.""" def __init__( - self, - *, - variable_indices: npt.NDArray[np.int32], - coefficients: npt.NDArray[np.double], - constant: np.double = np.double(0.0), + self, + *, + variable_indices: npt.NDArray[np.int32], + coefficients: npt.NDArray[np.double], + constant: np.double = np.double(0.0), ): super().__init__() self.__variable_indices: npt.NDArray[np.int32] = variable_indices - self.__coefficients: npt.NDArray[ - np.double] = mbh.assert_is_a_number_array(coefficients) + self.__coefficients: npt.NDArray[np.double] = mbh.assert_is_a_number_array( + coefficients + ) self.__constant: np.double = constant def multiply_by(self, arg: NumberT) -> LinearExprT: @@ -370,51 +394,53 @@ class _WeightedSum(LinearExpr): def pretty_string(self, helper: pwmb.ModelBuilderHelper) -> str: """Pretty print a linear expression into a string.""" - output: str = '' + output: str = "" for index, coeff in zip(self.variable_indices, self.coefficients): var_name = helper.var_name(index) if not var_name: - var_name = f'unnamed_var_{index}' + var_name = f"unnamed_var_{index}" if not output and mbh.is_one(coeff): output = var_name elif not output and mbh.is_minus_one(coeff): - output = f'-{var_name}' + output = f"-{var_name}" elif not output: - output = f'{coeff} * {var_name}' + output = f"{coeff} * {var_name}" elif mbh.is_one(coeff): - output += f' + {var_name}' + output += f" + {var_name}" elif mbh.is_minus_one(coeff): - output += f' - {var_name}' + output += f" - {var_name}" elif coeff > 0.0: - output += f' + {coeff} * {var_name}' + output += f" + {coeff} * {var_name}" elif coeff < 0.0: - output += ' - {-coeff} * {var_name}' + output += " - {-coeff} * {var_name}" if self.constant > 0: - output += f' + {self.constant}' + output += f" + {self.constant}" elif self.constant < 0: - output += f' - {-self.constant}' + output += f" - {-self.constant}" if not output: - output = '0.0' + output = "0.0" return output def __repr__(self): - return (f'WeightedSum(indices = {self.variable_indices}, coefficients =' - f' {self.coefficients}, constant = {self.constant})') + return ( + f"WeightedSum(indices = {self.variable_indices}, coefficients =" + f" {self.coefficients}, constant = {self.constant})" + ) class Variable(LinearExpr): """A variable (continuous or integral). - A Variable is an object that can take on any integer value within defined - ranges. Variables appear in constraint like: + A Variable is an object that can take on any integer value within defined + ranges. Variables appear in constraint like: - x + y >= 5 + x + y >= 5 - Solving a model is equivalent to finding, for each variable, a single value - from the set of initial values (called the initial domain), such that the - model is feasible, or optimal if you provided an objective function. - """ + Solving a model is equivalent to finding, for each variable, a single value + from the set of initial values (called the initial domain), such that the + model is feasible, or optimal if you provided an objective function. + """ def __init__( self, @@ -469,9 +495,9 @@ class Variable(LinearExpr): name = self.__helper.var_name(self.__index) if not name: if self.__helper.VarIsInteger(self.__index): - return 'unnamed_int_var_%i' % self.__index + return "unnamed_int_var_%i" % self.__index else: - return 'unnamed_num_var_%i' % self.__index + return "unnamed_num_var_%i" % self.__index return name def __repr__(self) -> str: @@ -482,14 +508,14 @@ class Variable(LinearExpr): is_integer = self.__helper.var_is_integral(index) if name: if is_integer: - return f'{name}(index={index}, lb={lb}, ub={ub}, integer)' + return f"{name}(index={index}, lb={lb}, ub={ub}, integer)" else: - return f'{name}(index={index}, lb={lb}, ub={ub})' + return f"{name}(index={index}, lb={lb}, ub={ub})" else: if is_integer: - return f'unnamed_var(index={index}, lb={lb}, ub={ub}, integer)' + return f"unnamed_var(index={index}, lb={lb}, ub={ub}, integer)" else: - return f'unnamed_var(index={index}, lb={lb}, ub={ub})' + return f"unnamed_var(index={index}, lb={lb}, ub={ub})" @property def name(self) -> str: @@ -549,7 +575,9 @@ class Variable(LinearExpr): arg = mbh.assert_is_a_number(arg) return BoundedLinearExpression(self, arg, arg) else: - return BoundedLinearExpression(self - arg, 0.0, 0.0) # pytype: disable=wrong-arg-types # numpy-scalars + return BoundedLinearExpression( + self - arg, 0.0, 0.0 + ) # pytype: disable=wrong-arg-types # numpy-scalars def __ne__(self, arg: LinearExprT) -> ConstraintT: if arg is None: @@ -562,7 +590,9 @@ class Variable(LinearExpr): return hash((self.__helper, self.__index)) def multiply_by(self, arg: NumberT) -> LinearExprT: - return LinearExpr.weighted_sum([self], [arg], constant=0.0) # pytype: disable=wrong-arg-types # numpy-scalars + return LinearExpr.weighted_sum( + [self], [arg], constant=0.0 + ) # pytype: disable=wrong-arg-types # numpy-scalars _REGISTERED_NUMPY_VARIABLE_FUNCS: Dict[Any, NumpyFuncT] = {} @@ -571,8 +601,7 @@ _REGISTERED_NUMPY_VARIABLE_FUNCS: Dict[Any, NumpyFuncT] = {} class VariableContainer(mixins.NDArrayOperatorsMixin): """Variable container.""" - def __init__(self, helper: pwmb.ModelBuilderHelper, - indices: npt.NDArray[np.int32]): + def __init__(self, helper: pwmb.ModelBuilderHelper, indices: npt.NDArray[np.int32]): self.__helper: pwmb.ModelBuilderHelper = helper self.__variable_indices: npt.NDArray[np.int32] = indices @@ -582,8 +611,9 @@ class VariableContainer(mixins.NDArrayOperatorsMixin): def __getitem__(self, pos: SliceT) -> VariablesT: # delegate the treatment of the 'pos' query to __variable_indices. - index_or_slice: Union[np.int32, npt.NDArray[np.int32]] = ( - self.__variable_indices[pos]) + index_or_slice: Union[ + np.int32, npt.NDArray[np.int32] + ] = self.__variable_indices[pos] if np.isscalar(index_or_slice): return Variable(self.__helper, index_or_slice, None, None, None) else: @@ -595,7 +625,7 @@ class VariableContainer(mixins.NDArrayOperatorsMixin): # pylint: disable=invalid-name @property - def T(self) -> 'VariableContainer': + def T(self) -> "VariableContainer": """Returns a view upon the transposed numpy array of variables.""" return VariableContainer(self.__helper, self.__variable_indices.T) @@ -611,22 +641,19 @@ class VariableContainer(mixins.NDArrayOperatorsMixin): """Returns the number of variables in the numpy array.""" return self.__variable_indices.size - def ravel(self) -> 'VariableContainer': + def ravel(self) -> "VariableContainer": """returns the ravel array of variables.""" return VariableContainer(self.__helper, self.__variable_indices.ravel()) - def flatten(self) -> 'VariableContainer': + def flatten(self) -> "VariableContainer": """returns the flattened array of variables.""" - return VariableContainer(self.__helper, - self.__variable_indices.flatten()) + return VariableContainer(self.__helper, self.__variable_indices.flatten()) def __str__(self) -> str: - return f'VariableContainer({self.__variable_indices})' + return f"VariableContainer({self.__variable_indices})" def __repr__(self) -> str: - return ( - f'VariableContainer({self.__helper}, {repr(self.__variable_indices)})' - ) + return f"VariableContainer({self.__helper}, {repr(self.__variable_indices)})" def __len__(self): return self.__variable_indices.shape[0] @@ -634,12 +661,13 @@ class VariableContainer(mixins.NDArrayOperatorsMixin): def __array_ufunc__( self, ufunc: np.ufunc, - method: Literal['__call__', 'reduce', 'reduceat', 'accumulate', 'outer', - 'inner'], + method: Literal[ + "__call__", "reduce", "reduceat", "accumulate", "outer", "inner" + ], *inputs: Any, **kwargs: Any, ) -> LinearExprT: - if method != '__call__': + if method != "__call__": return NotImplemented # pytype: disable=bad-return-type # numpy-scalars function = _REGISTERED_NUMPY_VARIABLE_FUNCS.get(ufunc) if function is None: @@ -650,8 +678,9 @@ class VariableContainer(mixins.NDArrayOperatorsMixin): return function(inputs[1], inputs[0], **kwargs) return NotImplemented # pytype: disable=bad-return-type # numpy-scalars - def __array_function__(self, func: Any, types: Any, inputs: Any, - kwargs: Any) -> LinearExprT: + def __array_function__( + self, func: Any, types: Any, inputs: Any, kwargs: Any + ) -> LinearExprT: function = _REGISTERED_NUMPY_VARIABLE_FUNCS.get(func) if function is None: return NotImplemented # pytype: disable=bad-return-type # numpy-scalars @@ -674,8 +703,8 @@ def _implements(np_function: Any) -> Callable[[NumpyFuncT], NumpyFuncT]: @_implements(np.sum) def sum_variable_container( # pytype: disable=annotation-type-mismatch # numpy-scalars - container: VariableContainer, - constant: NumberT = 0.0) -> LinearExprT: + container: VariableContainer, constant: NumberT = 0.0 +) -> LinearExprT: """Implementation of np.sum for VariableContainer objects.""" indices: npt.NDArray[np.int32] = container.variable_indices return _WeightedSum( @@ -693,8 +722,9 @@ def dot_variable_container( """Implementation of np.dot for VariableContainer objects.""" if len(container.shape) != 1: raise ValueError( - 'dot_variable_container only supports 1D variable containers (shape =' - f' {container.shape})') + "dot_variable_container only supports 1D variable containers (shape =" + f" {container.shape})" + ) indices: npt.NDArray[np.int32] = container.variable_indices if np.isscalar(arg): return _WeightedSum( @@ -722,12 +752,12 @@ class VarCompVar: def __str__(self) -> str: if self.__is_equality: - return f'{self.__left} == {self.__right}' + return f"{self.__left} == {self.__right}" else: - return f'{self.__left} != {self.__right}' + return f"{self.__left} != {self.__right}" def __repr__(self) -> str: - return f'VarCompVar({self.__left}, {self.__right}, {self.__is_equality})' + return f"VarCompVar({self.__left}, {self.__right}, {self.__is_equality})" @property def left(self) -> Variable: @@ -742,19 +772,18 @@ class VarCompVar: return self.__is_equality def __bool__(self) -> bool: - return bool( - self.__left.index == self.__right.index) == self.__is_equality + return bool(self.__left.index == self.__right.index) == self.__is_equality # TODO(user): investigate storing left and right expressions. class BoundedLinearExpression: """Represents a linear constraint: `lb <= linear expression <= ub`. - The only use of this class is to be added to the ModelBuilder through - `ModelBuilder.add(bounded expression)`, as in: + The only use of this class is to be added to the ModelBuilder through + `ModelBuilder.add(bounded expression)`, as in: - model.Add(x + 2 * y -1 >= z) - """ + model.Add(x + 2 * y -1 >= z) + """ def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT): self.__expr: LinearExprT = expr @@ -764,16 +793,17 @@ class BoundedLinearExpression: def __str__(self) -> str: if self.__lb > -math.inf and self.__ub < math.inf: if self.__lb == self.__ub: - return str(self.__expr) + ' == ' + str(self.__lb) + return str(self.__expr) + " == " + str(self.__lb) else: - return str(self.__lb) + ' <= ' + str( - self.__expr) + ' <= ' + str(self.__ub) + return ( + str(self.__lb) + " <= " + str(self.__expr) + " <= " + str(self.__ub) + ) elif self.__lb > -math.inf: - return str(self.__expr) + ' >= ' + str(self.__lb) + return str(self.__expr) + " >= " + str(self.__lb) elif self.__ub < math.inf: - return str(self.__expr) + ' <= ' + str(self.__ub) + return str(self.__expr) + " <= " + str(self.__ub) else: - return 'True (unbounded expr ' + str(self.__expr) + ')' + return "True (unbounded expr " + str(self.__expr) + ")" @property def expression(self) -> LinearExprT: @@ -789,18 +819,19 @@ class BoundedLinearExpression: def __bool__(self) -> bool: raise NotImplementedError( - f'Cannot use a BoundedLinearExpression {self} as a Boolean value') + f"Cannot use a BoundedLinearExpression {self} as a Boolean value" + ) class LinearConstraint: """Stores a linear equation. - Example: - x = model.new_num_var(0, 10, 'x') - y = model.new_num_var(0, 10, 'y') + Example: + x = model.new_num_var(0, 10, 'x') + y = model.new_num_var(0, 10, 'y') - linear_constraint = model.add(x + 2 * y == 5) - """ + linear_constraint = model.add(x + 2 * y == 5) + """ def __init__(self, helper: pwmb.ModelBuilderHelper): self.__index: np.int32 = helper.add_linear_constraint() @@ -847,70 +878,71 @@ class LinearConstraint: class ModelBuilder: """Methods for building a linear model. - Methods beginning with: + Methods beginning with: - * ```new_``` create integer, boolean, or interval variables. - * ```add_``` create new constraints and add them to the model. - """ + * ```new_``` create integer, boolean, or interval variables. + * ```add_``` create new constraints and add them to the model. + """ def __init__(self): self.__helper: pwmb.ModelBuilderHelper = pwmb.ModelBuilderHelper() # Integer variable. - def new_var(self, lb: NumberT, ub: NumberT, is_integer: bool, - name: Optional[str]) -> Variable: + def new_var( + self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] + ) -> Variable: """Create an integer variable with domain [lb, ub]. - Args: - lb: Lower bound of the variable. - ub: Upper bound of the variable. - is_integer: Indicates if the variable must take integral values. - name: The name of the variable. + Args: + lb: Lower bound of the variable. + ub: Upper bound of the variable. + is_integer: Indicates if the variable must take integral values. + name: The name of the variable. - Returns: - a variable whose domain is [lb, ub]. - """ + Returns: + a variable whose domain is [lb, ub]. + """ return Variable(self.__helper, lb, ub, is_integer, name) - def new_int_var(self, - lb: NumberT, - ub: NumberT, - name: Optional[str] = None) -> Variable: + def new_int_var( + self, lb: NumberT, ub: NumberT, name: Optional[str] = None + ) -> Variable: """Create an integer variable with domain [lb, ub]. - Args: - lb: Lower bound of the variable. - ub: Upper bound of the variable. - name: The name of the variable. + Args: + lb: Lower bound of the variable. + ub: Upper bound of the variable. + name: The name of the variable. - Returns: - a variable whose domain is [lb, ub]. - """ + Returns: + a variable whose domain is [lb, ub]. + """ return self.new_var(lb, ub, True, name) - def new_num_var(self, - lb: NumberT, - ub: NumberT, - name: Optional[str] = None) -> Variable: + def new_num_var( + self, lb: NumberT, ub: NumberT, name: Optional[str] = None + ) -> Variable: """Create an integer variable with domain [lb, ub]. - Args: - lb: Lower bound of the variable. - ub: Upper bound of the variable. - name: The name of the variable. + Args: + lb: Lower bound of the variable. + ub: Upper bound of the variable. + name: The name of the variable. - Returns: - a variable whose domain is [lb, ub]. - """ + Returns: + a variable whose domain is [lb, ub]. + """ return self.new_var(lb, ub, False, name) def new_bool_var(self, name: Optional[str] = None) -> Variable: """Creates a 0-1 variable with the given name.""" - return self.new_var(0, 1, True, name) # pytype: disable=wrong-arg-types # numpy-scalars + return self.new_var( + 0, 1, True, name + ) # pytype: disable=wrong-arg-types # numpy-scalars def new_constant(self, value: NumberT) -> Variable: """Declares a constant variable.""" @@ -935,35 +967,41 @@ class ModelBuilder: shape = np.shape(lower_bounds) elif shape != np.shape(lower_bounds): raise ValueError( - 'lower_bounds, upper_bounds, is_integral and shape must have' - ' compatible shapes (when defined)') + "lower_bounds, upper_bounds, is_integral and shape must have" + " compatible shapes (when defined)" + ) if not np.isscalar(upper_bounds): if shape is None: shape = np.shape(upper_bounds) elif shape != np.shape(upper_bounds): raise ValueError( - 'lower_bounds, upper_bounds, is_integral and shape must have' - ' compatible shapes (when defined)') + "lower_bounds, upper_bounds, is_integral and shape must have" + " compatible shapes (when defined)" + ) if not np.isscalar(is_integral): if shape is None: shape = np.shape(is_integral) elif shape != np.shape(is_integral): raise ValueError( - 'lower_bounds, upper_bounds, is_integral and shape must have' - ' compatible shapes (when defined)') + "lower_bounds, upper_bounds, is_integral and shape must have" + " compatible shapes (when defined)" + ) if shape is None: - raise ValueError('a shape must be defined') + raise ValueError("a shape must be defined") - name = name or '' + name = name or "" - if (np.isscalar(lower_bounds) and np.isscalar(upper_bounds) and - np.isscalar(is_integral)): - var_indices = self.__helper.add_var_array(shape, lower_bounds, - upper_bounds, is_integral, - name) + if ( + np.isscalar(lower_bounds) + and np.isscalar(upper_bounds) + and np.isscalar(is_integral) + ): + var_indices = self.__helper.add_var_array( + shape, lower_bounds, upper_bounds, is_integral, name + ) return VariableContainer(self.__helper, var_indices) # Convert scalars to np.arrays if needed. @@ -975,7 +1013,8 @@ class ModelBuilder: is_integral = np.full(shape, is_integral) var_indices = self.__helper.add_var_array_with_bounds( - lower_bounds, upper_bounds, is_integral, name) + lower_bounds, upper_bounds, is_integral, name + ) return VariableContainer(self.__helper, var_indices) def new_num_var_array( @@ -996,25 +1035,28 @@ class ModelBuilder: shape = np.shape(lower_bounds) elif shape != np.shape(lower_bounds): raise ValueError( - 'lower_bounds, upper_bounds, and shape must have' - ' compatible shapes (when defined)') + "lower_bounds, upper_bounds, and shape must have" + " compatible shapes (when defined)" + ) if not np.isscalar(upper_bounds): if shape is None: shape = np.shape(upper_bounds) elif shape != np.shape(upper_bounds): raise ValueError( - 'lower_bounds, upper_bounds, and shape must have' - ' compatible shapes (when defined)') + "lower_bounds, upper_bounds, and shape must have" + " compatible shapes (when defined)" + ) if shape is None: - raise ValueError('a shape must be defined') + raise ValueError("a shape must be defined") - name = name or '' + name = name or "" if np.isscalar(lower_bounds) and np.isscalar(upper_bounds): - var_indices = self.__helper.add_var_array(shape, lower_bounds, - upper_bounds, False, name) + var_indices = self.__helper.add_var_array( + shape, lower_bounds, upper_bounds, False, name + ) return VariableContainer(self.__helper, var_indices) # Convert scalars to np.arrays if needed. @@ -1024,7 +1066,8 @@ class ModelBuilder: upper_bounds = np.full(shape, upper_bounds) var_indices = self.__helper.add_var_array_with_bounds( - lower_bounds, upper_bounds, np.zeros(shape, dtype=bool), name) + lower_bounds, upper_bounds, np.zeros(shape, dtype=bool), name + ) return VariableContainer(self.__helper, var_indices) def new_int_var_array( @@ -1045,25 +1088,28 @@ class ModelBuilder: shape = np.shape(lower_bounds) elif shape != np.shape(lower_bounds): raise ValueError( - 'lower_bounds, upper_bounds, and shape must have' - ' compatible shapes (when defined)') + "lower_bounds, upper_bounds, and shape must have" + " compatible shapes (when defined)" + ) if not np.isscalar(upper_bounds): if shape is None: shape = np.shape(upper_bounds) elif shape != np.shape(upper_bounds): raise ValueError( - 'lower_bounds, upper_bounds, and shape must have' - ' compatible shapes (when defined)') + "lower_bounds, upper_bounds, and shape must have" + " compatible shapes (when defined)" + ) if shape is None: - raise ValueError('a shape must be defined') + raise ValueError("a shape must be defined") - name = name or '' + name = name or "" if np.isscalar(lower_bounds) and np.isscalar(upper_bounds): - var_indices = self.__helper.add_var_array(shape, lower_bounds, - upper_bounds, True, name) + var_indices = self.__helper.add_var_array( + shape, lower_bounds, upper_bounds, True, name + ) return VariableContainer(self.__helper, var_indices) # Convert scalars to np.arrays if needed. @@ -1073,7 +1119,8 @@ class ModelBuilder: upper_bounds = np.full(shape, upper_bounds) var_indices = self.__helper.add_var_array_with_bounds( - lower_bounds, upper_bounds, np.ones(shape, dtype=bool), name) + lower_bounds, upper_bounds, np.ones(shape, dtype=bool), name + ) return VariableContainer(self.__helper, var_indices) def new_bool_var_array( @@ -1085,7 +1132,7 @@ class ModelBuilder: if mbh.is_integral(shape): shape = [shape] - name = name or '' + name = name or "" var_indices = self.__helper.add_var_array(shape, 0.0, 1.0, True, name) return VariableContainer(self.__helper, var_indices) @@ -1118,53 +1165,61 @@ class ModelBuilder: elif isinstance(linear_expr, Variable): self.__helper.set_constraint_lower_bound(ct.index, lb) self.__helper.set_constraint_upper_bound(ct.index, ub) - self.__helper.add_term_to_constraint(ct.index, linear_expr.index, - 1.0) + self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0) elif isinstance(linear_expr, _WeightedSum): - self.__helper.set_constraint_lower_bound(ct.index, - lb - linear_expr.constant) - self.__helper.set_constraint_upper_bound(ct.index, - ub - linear_expr.constant) - self.__helper.add_terms_to_constraint(ct.index, - linear_expr.variable_indices, - linear_expr.coefficients) + self.__helper.set_constraint_lower_bound( + ct.index, lb - linear_expr.constant + ) + self.__helper.set_constraint_upper_bound( + ct.index, ub - linear_expr.constant + ) + self.__helper.add_terms_to_constraint( + ct.index, linear_expr.variable_indices, linear_expr.coefficients + ) else: raise TypeError( - f'Not supported: ModelBuilder.add_linear_constraint({linear_expr})' - f' with type {type(linear_expr)}') + f"Not supported: ModelBuilder.add_linear_constraint({linear_expr})" + f" with type {type(linear_expr)}" + ) return ct - def add(self, - ct: ConstraintT, - name: Optional[str] = None) -> LinearConstraint: + def add(self, ct: ConstraintT, name: Optional[str] = None) -> LinearConstraint: """Adds a `BoundedLinearExpression` to the model. - Args: - ct: A [`BoundedLinearExpression`](#boundedlinearexpression). - name: An optional name. + Args: + ct: A [`BoundedLinearExpression`](#boundedlinearexpression). + name: An optional name. - Returns: - An instance of the `Constraint` class. - """ + Returns: + An instance of the `Constraint` class. + """ if isinstance(ct, BoundedLinearExpression): - return self.add_linear_constraint(ct.expression, ct.lower_bound, - ct.upper_bound, name) + return self.add_linear_constraint( + ct.expression, ct.lower_bound, ct.upper_bound, name + ) elif isinstance(ct, VarCompVar): if not ct.is_equality: - raise TypeError('Not supported: ModelBuilder.Add(' + str(ct) + - ')') + raise TypeError("Not supported: ModelBuilder.Add(" + str(ct) + ")") new_ct = LinearConstraint(self.__helper) new_ct.lower_bound = 0.0 new_ct.upper_bound = 0.0 - new_ct.add_term(ct.left, 1.0) # pytype: disable=wrong-arg-types # numpy-scalars - new_ct.add_term(ct.right, -1.0) # pytype: disable=wrong-arg-types # numpy-scalars + new_ct.add_term( + ct.left, 1.0 + ) # pytype: disable=wrong-arg-types # numpy-scalars + new_ct.add_term( + ct.right, -1.0 + ) # pytype: disable=wrong-arg-types # numpy-scalars return new_ct elif ct and isinstance(ct, bool): - return self.add_linear_constraint(linear_expr=0.0) # Evaluate to True. # pytype: disable=wrong-arg-types # numpy-scalars + return self.add_linear_constraint( + linear_expr=0.0 + ) # Evaluate to True. # pytype: disable=wrong-arg-types # numpy-scalars elif not ct and isinstance(ct, bool): - return self.add_linear_constraint(1.0, 0.0, 0.0) # Evaluate to False. # pytype: disable=wrong-arg-types # numpy-scalars + return self.add_linear_constraint( + 1.0, 0.0, 0.0 + ) # Evaluate to False. # pytype: disable=wrong-arg-types # numpy-scalars else: - raise TypeError('Not supported: ModelBuilder.Add(' + str(ct) + ')') + raise TypeError("Not supported: ModelBuilder.Add(" + str(ct) + ")") @property def num_constraints(self) -> int: @@ -1188,10 +1243,12 @@ class ModelBuilder: elif isinstance(linear_expr, _WeightedSum): self.helper.set_objective_offset(linear_expr.constant) self.__helper.set_objective_coefficients( - linear_expr.variable_indices, linear_expr.coefficients) + linear_expr.variable_indices, linear_expr.coefficients + ) else: raise TypeError( - f'Not supported: ModelBuilder.minimize/maximize({linear_expr})') + f"Not supported: ModelBuilder.minimize/maximize({linear_expr})" + ) @property def objective_offset(self) -> np.double: @@ -1242,17 +1299,18 @@ class ModelBuilder: class ModelSolver: """Main solver class. - The purpose of this class is to search for a solution to the model provided - to the solve() method. + The purpose of this class is to search for a solution to the model provided + to the solve() method. - Once solve() is called, this class allows inspecting the solution found - with the value() method, as well as general statistics about the solve - procedure. - """ + Once solve() is called, this class allows inspecting the solution found + with the value() method, as well as general statistics about the solve + procedure. + """ def __init__(self, solver_name: str): self.__solve_helper: pwmb.ModelSolverHelper = pwmb.ModelSolverHelper( - solver_name) + solver_name + ) self.log_callback: Optional[Callable[[str], None]] = None def solver_is_supported(self) -> bool: @@ -1285,7 +1343,8 @@ class ModelSolver: """Checks that solve has run and has found a feasible solution.""" if not self.__solve_helper.has_solution(): raise RuntimeError( - 'solve() has not been called, or no solution has been found.') + "solve() has not been called, or no solution has been found." + ) def stop_search(self): """Stops the current search asynchronously.""" @@ -1299,11 +1358,11 @@ class ModelSolver: elif isinstance(expr, Variable): return self.__solve_helper.var_value(expr.index) elif isinstance(expr, _WeightedSum): - return self.__solve_helper.expression_value(expr.variable_indices, - expr.coefficients, - expr.constant) + return self.__solve_helper.expression_value( + expr.variable_indices, expr.coefficients, expr.constant + ) else: - raise TypeError(f'Unknown expression {expr!r} of type {type(expr)}') + raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}") def reduced_cost(self, var: Variable) -> np.double: """Returns the reduced cost of a linear expression after solve.""" @@ -1336,8 +1395,8 @@ class ModelSolver: def status_string(self) -> str: """Returns additional information of the last solve. - It can describe why the model is invalid. - """ + It can describe why the model is invalid. + """ return self.__solve_helper.status_string() @property diff --git a/ortools/linear_solver/python/model_builder_helper.py b/ortools/linear_solver/python/model_builder_helper.py index 41c6177514..9ce0ed55a4 100644 --- a/ortools/linear_solver/python/model_builder_helper.py +++ b/ortools/linear_solver/python/model_builder_helper.py @@ -10,6 +10,7 @@ # 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. + """helpers methods for the cp_model_builder module.""" import numbers @@ -28,32 +29,32 @@ def is_integral(x: Any) -> bool: def is_a_number(x: Any) -> bool: """Checks if x has either a number.Number or a np.double type.""" - return isinstance(x, numbers.Number) or isinstance( - x, np.double) or isinstance(x, np.integer) + return ( + isinstance(x, numbers.Number) + or isinstance(x, np.double) + or isinstance(x, np.integer) + ) def is_zero(x: Any) -> bool: """Checks if the x is 0 or 0.0.""" - return (is_integral(x) and int(x) == 0) or (is_a_number(x) and - float(x) == 0.0) + return (is_integral(x) and int(x) == 0) or (is_a_number(x) and float(x) == 0.0) def is_one(x: Any) -> bool: """Checks if x is 1 or 1.0.""" - return (is_integral(x) and int(x) == 1) or (is_a_number(x) and - float(x) == 1.0) + return (is_integral(x) and int(x) == 1) or (is_a_number(x) and float(x) == 1.0) def is_minus_one(x: Any) -> bool: """Checks if x is -1 or -1.0.""" - return (is_integral(x) and int(x) == -1) or (is_a_number(x) and - float(x) == -1.0) + return (is_integral(x) and int(x) == -1) or (is_a_number(x) and float(x) == -1.0) def assert_is_a_number(x: NumberT) -> np.double: """Asserts that x is a number and converts to a np.double.""" if not is_a_number(x): - raise TypeError('Not a number: %s' % x) + raise TypeError("Not a number: %s" % x) return np.double(x) diff --git a/ortools/linear_solver/python/model_builder_helper_test.py b/ortools/linear_solver/python/model_builder_helper_test.py index ff4a44e719..b9ebd07837 100644 --- a/ortools/linear_solver/python/model_builder_helper_test.py +++ b/ortools/linear_solver/python/model_builder_helper_test.py @@ -11,6 +11,7 @@ # 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. + """Tests for model_builder_helper.""" import gzip @@ -25,36 +26,35 @@ from ortools.linear_solver.python import pywrap_model_builder_helper class PywrapModelBuilderHelperTest(unittest.TestCase): - def test_export_model_proto_to_mps_string(self): model = pywrap_model_builder_helper.ModelBuilderHelper() - model.set_name('testmodel') + model.set_name("testmodel") result = model.export_to_mps_string() - self.assertIn('testmodel', result) - self.assertIn('ENDATA', result) + self.assertIn("testmodel", result) + self.assertIn("ENDATA", result) def test_export_model_proto_to_lp_string(self): model = pywrap_model_builder_helper.ModelBuilderHelper() model.set_maximize(True) lp_string = model.export_to_lp_string() - self.assertIn('Maximize', lp_string) + self.assertIn("Maximize", lp_string) def test_import_from_mps_string(self): model = pywrap_model_builder_helper.ModelBuilderHelper() - self.assertTrue(model.import_from_mps_string('NAME testmodel')) - self.assertEqual(model.name(), 'testmodel') + self.assertTrue(model.import_from_mps_string("NAME testmodel")) + self.assertEqual(model.name(), "testmodel") # ImportFromMpsFile doesn't read from files yet def test_import_from_mps_file(self): path = os.path.dirname(__file__) - mps_path = f'{path}/../testdata/maximization.mps' + mps_path = f"{path}/../testdata/maximization.mps" model = pywrap_model_builder_helper.ModelBuilderHelper() self.assertTrue(model.import_from_mps_file(mps_path)) - self.assertEqual(model.name(), 'SupportedMaximizationProblem') + self.assertEqual(model.name(), "SupportedMaximizationProblem") def test_import_from_lp_string(self): model = pywrap_model_builder_helper.ModelBuilderHelper() - model.import_from_lp_string('max:') + model.import_from_lp_string("max:") self.assertTrue(model.maximize()) # TODO(user): Add test_import_from_lp_file after the implementation is fixed @@ -62,21 +62,21 @@ class PywrapModelBuilderHelperTest(unittest.TestCase): def test_solve_with_glop(self): model = linear_solver_pb2.MPModelProto() model.variable.append( - linear_solver_pb2.MPVariableProto(lower_bound=0.0, - upper_bound=1.0, - objective_coefficient=1.0)) + linear_solver_pb2.MPVariableProto( + lower_bound=0.0, upper_bound=1.0, objective_coefficient=1.0 + ) + ) model.maximize = True request = linear_solver_pb2.MPModelRequest( model=model, - solver_type=linear_solver_pb2.MPModelRequest.GLOP_LINEAR_PROGRAMMING + solver_type=linear_solver_pb2.MPModelRequest.GLOP_LINEAR_PROGRAMMING, ) - solver_helper = pywrap_model_builder_helper.ModelSolverHelper('') - result = solver_helper.solve_serialized_request( - request.SerializeToString()) + solver_helper = pywrap_model_builder_helper.ModelSolverHelper("") + result = solver_helper.solve_serialized_request(request.SerializeToString()) response = linear_solver_pb2.MPSolutionResponse().FromString(result) self.assertEqual( - response.status, - linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL) + response.status, linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL + ) self.assertAlmostEqual(response.objective_value, 1.0) def test_solve_with_glop_direct(self): @@ -87,11 +87,11 @@ class PywrapModelBuilderHelperTest(unittest.TestCase): model.set_var_objective_coefficient(0, 1.0) model.set_maximize(True) - solver = pywrap_model_builder_helper.ModelSolverHelper('glop') + solver = pywrap_model_builder_helper.ModelSolverHelper("glop") solver.solve(model) self.assertEqual( - solver.status(), - linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL) + solver.status(), linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL + ) self.assertAlmostEqual(solver.objective_value(), 1.0) self.assertAlmostEqual(solver.var_value(0), 1.0) values = solver.variable_values() @@ -101,17 +101,17 @@ class PywrapModelBuilderHelperTest(unittest.TestCase): def test_solve_with_pdlp(self): model = linear_solver_pb2.MPModelProto() model.variable.append( - linear_solver_pb2.MPVariableProto(lower_bound=0.0, - upper_bound=1.0, - objective_coefficient=1.0)) + linear_solver_pb2.MPVariableProto( + lower_bound=0.0, upper_bound=1.0, objective_coefficient=1.0 + ) + ) model.maximize = True request = linear_solver_pb2.MPModelRequest( model=model, - solver_type=linear_solver_pb2.MPModelRequest.PDLP_LINEAR_PROGRAMMING + solver_type=linear_solver_pb2.MPModelRequest.PDLP_LINEAR_PROGRAMMING, ) - solver_helper = pywrap_model_builder_helper.ModelSolverHelper('') - result = solver_helper.solve_serialized_request( - request.SerializeToString()) + solver_helper = pywrap_model_builder_helper.ModelSolverHelper("") + result = solver_helper.solve_serialized_request(request.SerializeToString()) if result: response = linear_solver_pb2.MPSolutionResponse().FromString(result) self.assertEqual( @@ -120,30 +120,32 @@ class PywrapModelBuilderHelperTest(unittest.TestCase): ) self.assertAlmostEqual(response.objective_value, 1.0) else: - print('Solver not supported.') + print("Solver not supported.") # TODO(user): Test the log callback after the implementation is completed. def test_interrupt_solve(self): # This is an instance that we know Glop won't solve quickly. path = os.path.dirname(__file__) - mps_path = f'{path}/../testdata/large_model.mps.gz' - with gzip.open(mps_path, 'r') as f: + mps_path = f"{path}/../testdata/large_model.mps.gz" + with gzip.open(mps_path, "r") as f: mps_data = f.read() model_helper = pywrap_model_builder_helper.ModelBuilderHelper() self.assertTrue(model_helper.import_from_mps_string(mps_data)) - solver_helper = pywrap_model_builder_helper.ModelSolverHelper('glop') + solver_helper = pywrap_model_builder_helper.ModelSolverHelper("glop") result = [] solve_thread = threading.Thread( - target=lambda: result.append(solver_helper.solve(model_helper))) + target=lambda: result.append(solver_helper.solve(model_helper)) + ) solve_thread.start() self.assertTrue(solver_helper.interrupt_solve()) solve_thread.join(timeout=30.0) self.assertTrue(solver_helper.has_response()) self.assertEqual( solver_helper.status(), - pywrap_model_builder_helper.SolveStatus.CANCELLED_BY_USER) + pywrap_model_builder_helper.SolveStatus.CANCELLED_BY_USER, + ) def test_build_model(self): var_lb = np.array([-1.0]) @@ -154,8 +156,9 @@ class PywrapModelBuilderHelperTest(unittest.TestCase): constraint_matrix = sparse.csr_matrix(np.array([[1.0], [2.0]])) model = pywrap_model_builder_helper.ModelBuilderHelper() - model.fill_model_from_sparse_data(var_lb, var_ub, obj, con_lb, con_ub, - constraint_matrix) + model.fill_model_from_sparse_data( + var_lb, var_ub, obj, con_lb, con_ub, constraint_matrix + ) self.assertEqual(1, model.num_variables()) self.assertEqual(-1.0, model.var_lower_bound(0)) self.assertEqual(np.inf, model.var_upper_bound(0)) @@ -172,12 +175,12 @@ class PywrapModelBuilderHelperTest(unittest.TestCase): self.assertEqual([0], model.constraint_var_indices(1)) self.assertEqual([2.0], model.constraint_coefficients(1)) - var_array = model.add_var_array([10], 1.0, 5.0, True, 'var_') + var_array = model.add_var_array([10], 1.0, 5.0, True, "var_") self.assertEqual(1, var_array.ndim) self.assertEqual(10, var_array.size) self.assertEqual((10,), var_array.shape) - self.assertEqual(model.var_name(var_array[3]), 'var_3') + self.assertEqual(model.var_name(var_array[3]), "var_3") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/ortools/linear_solver/python/model_builder_test.py b/ortools/linear_solver/python/model_builder_test.py index c14ed46804..e14ba0137b 100644 --- a/ortools/linear_solver/python/model_builder_test.py +++ b/ortools/linear_solver/python/model_builder_test.py @@ -11,6 +11,7 @@ # 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. + """Tests for ModelBuilder.""" import math @@ -31,10 +32,10 @@ class ModelBuilderTest(unittest.TestCase): def run_minimal_linear_example(self, solver_name): """Minimal Linear Example.""" model = mb.ModelBuilder() - model.name = 'minimal_linear_example' - x1 = model.new_num_var(0.0, math.inf, 'x1') - x2 = model.new_num_var(0.0, math.inf, 'x2') - x3 = model.new_num_var(0.0, math.inf, 'x3') + model.name = "minimal_linear_example" + x1 = model.new_num_var(0.0, math.inf, "x1") + x2 = model.new_num_var(0.0, math.inf, "x2") + x3 = model.new_num_var(0.0, math.inf, "x3") self.assertEqual(3, model.num_variables) self.assertFalse(x1.is_integral) self.assertEqual(0.0, x1.lower_bound) @@ -50,8 +51,8 @@ class ModelBuilderTest(unittest.TestCase): c0 = model.add(x1 + x2 + x3 <= 100.0) self.assertEqual(100, c0.upper_bound) - c1 = model.add(10 * x1 + 4.0 * x2 + 5.0 * x3 <= 600.0, 'c1') - self.assertEqual('c1', c1.name) + c1 = model.add(10 * x1 + 4.0 * x2 + 5.0 * x3 <= 600.0, "c1") + self.assertEqual("c1", c1.name) c2 = model.add(2.0 * x1 + 2.0 * x2 + 6.0 * x3 <= 300.0) self.assertEqual(-math.inf, c2.lower_bound) @@ -59,61 +60,50 @@ class ModelBuilderTest(unittest.TestCase): self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model)) # The problem has an optimal solution. - self.assertAlmostEqual(733.333333 + model.objective_offset, - solver.objective_value, - places=self.NUM_PLACES) + self.assertAlmostEqual( + 733.333333 + model.objective_offset, + solver.objective_value, + places=self.NUM_PLACES, + ) self.assertAlmostEqual( solver.value(10.0 * x1 + 6 * x2 + 4.0 * x3 - 5.5), solver.objective_value, places=self.NUM_PLACES, ) - self.assertAlmostEqual(33.333333, - solver.value(x1), - places=self.NUM_PLACES) - self.assertAlmostEqual(66.666667, - solver.value(x2), - places=self.NUM_PLACES) + self.assertAlmostEqual(33.333333, solver.value(x1), places=self.NUM_PLACES) + self.assertAlmostEqual(66.666667, solver.value(x2), places=self.NUM_PLACES) self.assertAlmostEqual(0.0, solver.value(x3), places=self.NUM_PLACES) - dual_objective_value = (solver.dual_value(c0) * c0.upper_bound + - solver.dual_value(c1) * c1.upper_bound + - solver.dual_value(c2) * c2.upper_bound + - model.objective_offset) - self.assertAlmostEqual(solver.objective_value, - dual_objective_value, - places=self.NUM_PLACES) + dual_objective_value = ( + solver.dual_value(c0) * c0.upper_bound + + solver.dual_value(c1) * c1.upper_bound + + solver.dual_value(c2) * c2.upper_bound + + model.objective_offset + ) + self.assertAlmostEqual( + solver.objective_value, dual_objective_value, places=self.NUM_PLACES + ) # x1 and x2 are basic - self.assertAlmostEqual(0.0, - solver.reduced_cost(x1), - places=self.NUM_PLACES) - self.assertAlmostEqual(0.0, - solver.reduced_cost(x2), - places=self.NUM_PLACES) + self.assertAlmostEqual(0.0, solver.reduced_cost(x1), places=self.NUM_PLACES) + self.assertAlmostEqual(0.0, solver.reduced_cost(x2), places=self.NUM_PLACES) # x3 is non-basic - x3_expected_reduced_cost = (4.0 - 1.0 * solver.dual_value(c0) - - 5.0 * solver.dual_value(c1)) - self.assertAlmostEqual(x3_expected_reduced_cost, - solver.reduced_cost(x3), - places=self.NUM_PLACES) + x3_expected_reduced_cost = ( + 4.0 - 1.0 * solver.dual_value(c0) - 5.0 * solver.dual_value(c1) + ) + self.assertAlmostEqual( + x3_expected_reduced_cost, solver.reduced_cost(x3), places=self.NUM_PLACES + ) - self.assertAlmostEqual(100.0, - solver.activity(c0), - places=self.NUM_PLACES) - self.assertAlmostEqual(600.0, - solver.activity(c1), - places=self.NUM_PLACES) - self.assertAlmostEqual(200.0, - solver.activity(c2), - places=self.NUM_PLACES) + self.assertAlmostEqual(100.0, solver.activity(c0), places=self.NUM_PLACES) + self.assertAlmostEqual(600.0, solver.activity(c1), places=self.NUM_PLACES) + self.assertAlmostEqual(200.0, solver.activity(c2), places=self.NUM_PLACES) - self.assertIn('minimal_linear_example', - model.export_to_lp_string(False)) - self.assertIn('minimal_linear_example', - model.export_to_mps_string(False)) + self.assertIn("minimal_linear_example", model.export_to_lp_string(False)) + self.assertIn("minimal_linear_example", model.export_to_mps_string(False)) def test_minimal_linear_example(self): - self.run_minimal_linear_example('glop') + self.run_minimal_linear_example("glop") def test_import_from_mps_string(self): mps_data = """ @@ -138,14 +128,14 @@ ENDATA """ model = mb.ModelBuilder() self.assertTrue(model.import_from_mps_string(mps_data)) - self.assertEqual(model.name, 'SupportedMaximizationProblem') + self.assertEqual(model.name, "SupportedMaximizationProblem") def test_import_from_mps_file(self): path = os.path.dirname(__file__) - mps_path = f'{path}/../testdata/maximization.mps' + mps_path = f"{path}/../testdata/maximization.mps" model = mb.ModelBuilder() self.assertTrue(model.import_from_mps_file(mps_path)) - self.assertEqual(model.name, 'SupportedMaximizationProblem') + self.assertEqual(model.name, "SupportedMaximizationProblem") def test_import_from_lp_string(self): lp_data = """ @@ -162,80 +152,88 @@ ENDATA self.assertEqual(3, model.num_constraints) self.assertEqual(1, model.var_from_index(0).lower_bound) self.assertEqual(42, model.var_from_index(0).upper_bound) - self.assertEqual('x', model.var_from_index(0).name) + self.assertEqual("x", model.var_from_index(0).name) def test_import_from_lp_file(self): path = os.path.dirname(__file__) - lp_path = f'{path}/../testdata/small_model.lp' + lp_path = f"{path}/../testdata/small_model.lp" model = mb.ModelBuilder() self.assertTrue(model.import_from_lp_file(lp_path)) self.assertEqual(6, model.num_variables) self.assertEqual(3, model.num_constraints) self.assertEqual(1, model.var_from_index(0).lower_bound) self.assertEqual(42, model.var_from_index(0).upper_bound) - self.assertEqual('x', model.var_from_index(0).name) + self.assertEqual("x", model.var_from_index(0).name) def test_class_api(self): model = mb.ModelBuilder() - x = model.new_int_var(0, 10, 'x') - y = model.new_int_var(1, 10, 'y') - z = model.new_int_var(2, 10, 'z') - t = model.new_int_var(3, 10, 't') + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(1, 10, "y") + z = model.new_int_var(2, 10, "z") + t = model.new_int_var(3, 10, "t") e1 = mb.LinearExpr.sum([x, y, z]) expected_vars = np.array([0, 1, 2], dtype=np.int32) np_testing.assert_array_equal(expected_vars, e1.variable_indices) - np_testing.assert_array_equal(np.array([1, 1, 1], dtype=np.double), - e1.coefficients) + np_testing.assert_array_equal( + np.array([1, 1, 1], dtype=np.double), e1.coefficients + ) self.assertEqual(e1.constant, 0.0) - self.assertEqual(e1.pretty_string(model.helper), 'x + y + z') + self.assertEqual(e1.pretty_string(model.helper), "x + y + z") e2 = mb.LinearExpr.sum([e1, 4.0]) np_testing.assert_array_equal(expected_vars, e2.variable_indices) - np_testing.assert_array_equal(np.array([1, 1, 1], dtype=np.double), - e2.coefficients) + np_testing.assert_array_equal( + np.array([1, 1, 1], dtype=np.double), e2.coefficients + ) self.assertEqual(e2.constant, 4.0) - self.assertEqual(e2.pretty_string(model.helper), 'x + y + z + 4.0') + self.assertEqual(e2.pretty_string(model.helper), "x + y + z + 4.0") e3 = mb.LinearExpr.term(e2, 2) np_testing.assert_array_equal(expected_vars, e3.variable_indices) - np_testing.assert_array_equal(np.array([2, 2, 2], dtype=np.double), - e3.coefficients) + np_testing.assert_array_equal( + np.array([2, 2, 2], dtype=np.double), e3.coefficients + ) self.assertEqual(e3.constant, 8.0) - self.assertEqual(e3.pretty_string(model.helper), - '2.0 * x + 2.0 * y + 2.0 * z + 8.0') + self.assertEqual( + e3.pretty_string(model.helper), "2.0 * x + 2.0 * y + 2.0 * z + 8.0" + ) e4 = mb.LinearExpr.weighted_sum([x, t], [-1, 1], constant=2) - np_testing.assert_array_equal(np.array([0, 3], dtype=np.int32), - e4.variable_indices) - np_testing.assert_array_equal(np.array([-1, 1], dtype=np.double), - e4.coefficients) + np_testing.assert_array_equal( + np.array([0, 3], dtype=np.int32), e4.variable_indices + ) + np_testing.assert_array_equal( + np.array([-1, 1], dtype=np.double), e4.coefficients + ) self.assertEqual(e4.constant, 2.0) - self.assertEqual(e4.pretty_string(model.helper), '-x + t + 2.0') + self.assertEqual(e4.pretty_string(model.helper), "-x + t + 2.0") e4b = e4 * 3.0 - np_testing.assert_array_equal(np.array([0, 3], dtype=np.int32), - e4b.variable_indices) - np_testing.assert_array_equal(np.array([-3, 3], dtype=np.double), - e4b.coefficients) + np_testing.assert_array_equal( + np.array([0, 3], dtype=np.int32), e4b.variable_indices + ) + np_testing.assert_array_equal( + np.array([-3, 3], dtype=np.double), e4b.coefficients + ) self.assertEqual(e4b.constant, 6.0) - self.assertEqual(e4b.pretty_string(model.helper), - '-3.0 * x + 3.0 * t + 6.0') + self.assertEqual(e4b.pretty_string(model.helper), "-3.0 * x + 3.0 * t + 6.0") e5 = mb.LinearExpr.sum([e1, -3, e4]) - np_testing.assert_array_equal(np.array([0, 1, 2, 0, 3], dtype=np.int32), - e5.variable_indices) np_testing.assert_array_equal( - np.array([1, 1, 1, -1, 1], dtype=np.double), e5.coefficients) + np.array([0, 1, 2, 0, 3], dtype=np.int32), e5.variable_indices + ) + np_testing.assert_array_equal( + np.array([1, 1, 1, -1, 1], dtype=np.double), e5.coefficients + ) self.assertEqual(e5.constant, -1.0) - self.assertEqual(e5.pretty_string(model.helper), - 'x + y + z - x + t - 1.0') + self.assertEqual(e5.pretty_string(model.helper), "x + y + z - x + t - 1.0") e6 = mb.LinearExpr.term(x, 2.0, constant=1.0) - np_testing.assert_array_equal(np.array([0], dtype=np.int32), - e6.variable_indices) - np_testing.assert_array_equal(np.array([2], dtype=np.double), - e6.coefficients) + np_testing.assert_array_equal( + np.array([0], dtype=np.int32), e6.variable_indices + ) + np_testing.assert_array_equal(np.array([2], dtype=np.double), e6.coefficients) self.assertEqual(e6.constant, 1.0) e7 = mb.LinearExpr.term(x, 1.0, constant=0.0) @@ -246,11 +244,11 @@ ENDATA def test_variables(self): model = mb.ModelBuilder() - x = model.new_int_var(0.0, 4.0, 'x') + x = model.new_int_var(0.0, 4.0, "x") self.assertEqual(0, x.index) self.assertEqual(0.0, x.lower_bound) self.assertEqual(4.0, x.upper_bound) - self.assertEqual('x', x.name) + self.assertEqual("x", x.name) x.lower_bound = 1.0 x.upper_bound = 3.0 self.assertEqual(1.0, x.lower_bound) @@ -258,35 +256,32 @@ ENDATA self.assertTrue(x.is_integral) # Tests the equality operator. - y = model.new_int_var(0.0, 4.0, 'y') + y = model.new_int_var(0.0, 4.0, "y") x_copy = model.var_from_index(0) self.assertEqual(x, x) self.assertEqual(x, x_copy) self.assertNotEqual(x, y) # array - xs = model.new_int_var_array(shape=10, - lower_bounds=0.0, - upper_bounds=5.0, - name='xs_') + xs = model.new_int_var_array( + shape=10, lower_bounds=0.0, upper_bounds=5.0, name="xs_" + ) self.assertEqual(10, xs.size) - self.assertEqual('xs_4', str(xs[4])) + self.assertEqual("xs_4", str(xs[4])) lbs = np.array([1.0, 2.0, 3.0]) ubs = [3.0, 4.0, 5.0] - ys = model.new_int_var_array(lower_bounds=lbs, - upper_bounds=ubs, - name='ys_') - self.assertEqual('VariableContainer([12 13 14])', str(ys)) - zs = model.new_int_var_array(lower_bounds=[1.0, 2.0, 3], - upper_bounds=[4, 4, 4], - name='zs_') + ys = model.new_int_var_array(lower_bounds=lbs, upper_bounds=ubs, name="ys_") + self.assertEqual("VariableContainer([12 13 14])", str(ys)) + zs = model.new_int_var_array( + lower_bounds=[1.0, 2.0, 3], upper_bounds=[4, 4, 4], name="zs_" + ) self.assertEqual(3, zs.size) self.assertEqual((3,), zs.shape) - self.assertEqual('zs_1', str(zs[1])) - self.assertEqual('zs_2(index=17, lb=3.0, ub=4.0, integer)', repr(zs[2])) + self.assertEqual("zs_1", str(zs[1])) + self.assertEqual("zs_2(index=17, lb=3.0, ub=4.0, integer)", repr(zs[2])) self.assertTrue(zs[2].is_integral) - bs = model.new_bool_var_array([4, 5], 'bs_') + bs = model.new_bool_var_array([4, 5], "bs_") self.assertEqual((4, 5), bs.shape) self.assertEqual((5, 4), bs.T.shape) self.assertEqual(31, bs.index_at((2, 3))) @@ -300,31 +295,33 @@ ENDATA sum_bs = np.sum(bs) self.assertEqual(20, sum_bs.variable_indices.size) - np_testing.assert_array_equal(sum_bs.variable_indices, - bs.variable_indices.flatten()) + np_testing.assert_array_equal( + sum_bs.variable_indices, bs.variable_indices.flatten() + ) np_testing.assert_array_equal(sum_bs.coefficients, np.ones(20)) sum_bs_cte = np.sum(bs, 2.2) self.assertEqual(20, sum_bs_cte.variable_indices.size) - np_testing.assert_array_equal(sum_bs_cte.variable_indices, - bs.variable_indices.flatten()) + np_testing.assert_array_equal( + sum_bs_cte.variable_indices, bs.variable_indices.flatten() + ) np_testing.assert_array_equal(sum_bs.coefficients, np.ones(20)) self.assertEqual(sum_bs_cte.constant, 2.2) times_bs = np.dot(bs[1], 4) - np_testing.assert_array_equal(times_bs.variable_indices, - bs[1].variable_indices.flatten()) + np_testing.assert_array_equal( + times_bs.variable_indices, bs[1].variable_indices.flatten() + ) np_testing.assert_array_equal(times_bs.coefficients, np.full(5, 4.0)) times_bs_rev = np.dot(4, bs[2]) - np_testing.assert_array_equal(times_bs_rev.variable_indices, - bs[2].variable_indices.flatten()) - np_testing.assert_array_equal(times_bs_rev.coefficients, - np.full(5, 4.0)) + np_testing.assert_array_equal( + times_bs_rev.variable_indices, bs[2].variable_indices.flatten() + ) + np_testing.assert_array_equal(times_bs_rev.coefficients, np.full(5, 4.0)) dot_bs = np.dot(bs[2], np.array([1, 2, 3, 4, 5], dtype=np.double)) - np_testing.assert_array_equal(dot_bs.variable_indices, - bs[2].variable_indices) + np_testing.assert_array_equal(dot_bs.variable_indices, bs[2].variable_indices) np_testing.assert_array_equal(dot_bs.coefficients, [1, 2, 3, 4, 5]) # Tests the hash method. @@ -349,7 +346,7 @@ ENDATA lower_bounds=[[0.0, 1.0, 2.0], [0.0, 0.0, 2.0]], upper_bounds=4.0, is_integral=False, - name='y', + name="y", ) np_testing.assert_array_equal(y.shape, [2, 3]) @@ -357,7 +354,7 @@ ENDATA lower_bounds=0.0, upper_bounds=[[2.0, 1.0, 2.0], [3.0, 4.0, 2.0]], is_integral=False, - name='z', + name="z", ) np_testing.assert_array_equal(z.shape, [2, 3]) @@ -419,14 +416,14 @@ ENDATA y = model.new_num_var_array( lower_bounds=[[0.0, 1.0, 2.0], [0.0, 0.0, 2.0]], upper_bounds=4.0, - name='y', + name="y", ) np_testing.assert_array_equal(y.shape, [2, 3]) z = model.new_num_var_array( lower_bounds=0.0, upper_bounds=[[2.0, 1.0, 2.0], [3.0, 4.0, 2.0]], - name='z', + name="z", ) np_testing.assert_array_equal(z.shape, [2, 3]) @@ -469,14 +466,14 @@ ENDATA y = model.new_int_var_array( lower_bounds=[[0.0, 1.0, 2.0], [0.0, 0.0, 2.0]], upper_bounds=4.0, - name='y', + name="y", ) np_testing.assert_array_equal(y.shape, [2, 3]) z = model.new_int_var_array( lower_bounds=0.0, upper_bounds=[[2.0, 1.0, 2.0], [3.0, 4.0, 2.0]], - name='z', + name="z", ) np_testing.assert_array_equal(z.shape, [2, 3]) @@ -508,12 +505,12 @@ ENDATA def test_duplicate_variables(self): model = mb.ModelBuilder() - x = model.new_int_var(0.0, 4.0, 'x') - y = model.new_int_var(0.0, 4.0, 'y') - z = model.new_int_var(0.0, 4.0, 'z') + x = model.new_int_var(0.0, 4.0, "x") + y = model.new_int_var(0.0, 4.0, "y") + z = model.new_int_var(0.0, 4.0, "z") model.add(x + 2 * y == x - z) model.minimize(x + y + z) - solver = mb.ModelSolver('sat') + solver = mb.ModelSolver("sat") self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model)) def test_issue_3614(self): @@ -532,27 +529,34 @@ ENDATA y = {} v = {} for i in range(total_number_of_choices): - y[i] = model.new_bool_var(f'y_{i}') + y[i] = model.new_bool_var(f"y_{i}") for j in range(total_unique_products): for i in range(len(standalone_features)): - v[i, j] = model.new_bool_var(f'v_{(i,j)}') - model.add(v[i, j] == (y[i] + - (feature_bundle_incidence_matrix[(i, 0)] * - y[bundle_start_idx]))) + v[i, j] = model.new_bool_var(f"v_{(i,j)}") + model.add( + v[i, j] + == ( + y[i] + + ( + feature_bundle_incidence_matrix[(i, 0)] + * y[bundle_start_idx] + ) + ) + ) - solver = mb.ModelSolver('sat') + solver = mb.ModelSolver("sat") status = solver.solve(model) self.assertEqual(mb.SolveStatus.OPTIMAL, status) def test_varcompvar(self): model = mb.ModelBuilder() - x = model.new_int_var(0.0, 4.0, 'x') - y = model.new_int_var(0.0, 4.0, 'y') + x = model.new_int_var(0.0, 4.0, "x") + y = model.new_int_var(0.0, 4.0, "y") ct = x == y self.assertEqual(ct.left.index, x.index) self.assertEqual(ct.right.index, y.index) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/ortools/linear_solver/python/pandas_model.py b/ortools/linear_solver/python/pandas_model.py new file mode 100644 index 0000000000..8c78344a0e --- /dev/null +++ b/ortools/linear_solver/python/pandas_model.py @@ -0,0 +1,1598 @@ +# Copyright 2010-2022 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. + +"""Pandas-native API for optimization.""" + +import abc +import collections +import dataclasses +import enum +import math +import sys +import typing +from typing import Callable, Mapping, NoReturn, Optional, Union + +import numpy as np +import pandas as pd + +from ortools.linear_solver import linear_solver_pb2 +from ortools.linear_solver.python import pywrap_model_builder_helper as pwmb + +_Number = Union[int, float, np.number] +_LinearType = Union[_Number, "_LinearBase"] + +# The maximum number of terms to display in a linear expression's repr. +_MAX_LINEAR_EXPRESSION_REPR_TERMS = 5 + + +class _LinearBase(metaclass=abc.ABCMeta): + """Interface for types that can build linear expressions with +, -, * and /. + + Classes derived from LinearBase (plus float and int scalars) are used to build + expression trees describing a linear expression. Operation nodes of the + expression tree include: + + * _Sum: describes a deferred sum of LinearTypes. + * _Product: describes a deferred product of a scalar and a LinearType. + + Leaf nodes of the expression tree include: + + * float and int scalars. + * Variable: a single variable. + * LinearExpression: a flattened form of a linear expression. + """ + + def __add__(self, arg: _LinearType) -> "_Sum": + return _Sum(self, arg) + + def __radd__(self, arg: _LinearType) -> "_Sum": + return self.__add__(arg) + + def __sub__(self, arg: _LinearType) -> "_Sum": + return _Sum(self, -arg) + + def __rsub__(self, arg: _LinearType) -> "_Sum": + return _Sum(-self, arg) + + def __mul__(self, arg: _Number) -> "_Product": + return _Product(self, arg) + + def __rmul__(self, arg: _Number) -> "_Product": + return self.__mul__(arg) + + def __truediv__(self, coeff: _Number) -> "_Product": + return self.__mul__(1.0 / coeff) + + def __neg__(self) -> "_Product": + return _Product(self, -1) + + def __bool__(self) -> NoReturn: + raise NotImplementedError( + f"Cannot use a LinearExpression as a Boolean value: {self}" + ) + + def __eq__(self, arg: _LinearType) -> "_BoundedLinearExpression": + return _BoundedLinearExpression( + _expression=self - arg, _lower_bound=0, _upper_bound=0 + ) + + def __ge__(self, arg: _LinearType) -> "_BoundedLinearExpression": + return _BoundedLinearExpression( + _expression=self - arg, _lower_bound=0, _upper_bound=math.inf + ) + + def __le__(self, arg: _LinearType) -> "_BoundedLinearExpression": + return _BoundedLinearExpression( + _expression=self - arg, _lower_bound=-math.inf, _upper_bound=0 + ) + + +@dataclasses.dataclass(repr=False, eq=False, frozen=True) +class _LinearExpression(_LinearBase): + """For variables x, an expression: offset + sum_{i in I} coeff_i * x_i.""" + + __slots__ = ("_terms", "_offset") + + _terms: Mapping["_Variable", float] + _offset: float + + def __repr__(self): + return self.__str__() + + def __str__(self): + result = [str(self._offset)] + sorted_keys = sorted(self._terms.keys(), key=str) + num_displayed_terms = 0 + for variable in sorted_keys: + if num_displayed_terms == _MAX_LINEAR_EXPRESSION_REPR_TERMS: + result.append(" + ...") + break + coefficient = self._terms[variable] + if coefficient == 0.0: + continue + if coefficient > 0: + result.append(" + ") + else: + result.append(" - ") + if abs(coefficient) != 1.0: + result.append(f"{abs(coefficient)} * ") + result.append(f"{variable}") + num_displayed_terms += 1 + return "".join(result) + + +def _as_flat_linear_expression(base: _LinearType) -> _LinearExpression: + """Converts floats, ints and Linear objects to a LinearExpression.""" + # pylint: disable=protected-access + if isinstance(base, _LinearExpression): + return base + terms = collections.defaultdict(lambda: 0.0) + offset: float = 0.0 + to_process = [(base, 1.0)] + while to_process: # Flatten AST of LinearTypes. + expr, coeff = to_process.pop() + if isinstance(expr, _Sum): + to_process.append((expr._left, coeff)) + to_process.append((expr._right, coeff)) + elif isinstance(expr, _Variable): + terms[expr] += coeff + elif isinstance(expr, _Number): + offset += coeff * expr + elif isinstance(expr, _Product): + to_process.append((expr._expression, coeff * expr._coefficient)) + elif isinstance(expr, _LinearExpression): + offset += coeff * expr._offset + for variable, variable_coefficient in expr._terms.items(): + terms[variable] += coeff * variable_coefficient + else: + raise TypeError( + "Unrecognized linear expression: " + str(expr) + f" {type(expr)}" + ) + return _LinearExpression(terms, offset) + + +@dataclasses.dataclass(repr=False, eq=False, frozen=True) +class _Sum(_LinearBase): + """Represents the (deferred) sum of two expressions.""" + + __slots__ = ("_left", "_right") + + _left: _LinearType + _right: _LinearType + + def __repr__(self): + return self.__str__() + + def __str__(self): + return str(_as_flat_linear_expression(self)) + + +@dataclasses.dataclass(repr=False, eq=False, frozen=True) +class _Product(_LinearBase): + """Represents the (deferred) product of an expression by a constant.""" + + __slots__ = ("_expression", "_coefficient") + + _expression: _LinearBase + _coefficient: _Number + + def __repr__(self): + return self.__str__() + + def __str__(self): + return str(_as_flat_linear_expression(self)) + + +@dataclasses.dataclass(repr=False, eq=False, frozen=True) +class _Variable(_LinearBase): + """A variable (continuous or integral). + + A Variable is an object that can take on any value within its domain. + Variables (e.g. x and y) appear in constraints like: + + x + y >= 5 + + Solving a model is equivalent to finding, for each variable, a value in its + domain, such that all constraints are satisfied. + """ + + __slots__ = ("_helper", "_index") + + _helper: pwmb.ModelBuilderHelper + _index: int + + def __str__(self): + return self._name + + def __repr__(self): + return self.__str__() + + @property + def _name(self) -> str: + var_name = self._helper.var_name(self._index) + if var_name: + return var_name + return f"variable#{self._index}" + + @property + def _lower_bound(self) -> _Number: + """Returns the lower bound of the variable.""" + return self._helper.var_lower_bound(self._index) + + @property + def _upper_bound(self) -> _Number: + """Returns the upper bound of the variable.""" + return self._helper.var_upper_bound(self._index) + + @typing.overload + def __eq__(self, rhs: "_Variable") -> "_VarEqVar": + ... + + def __eq__(self, rhs: _LinearType) -> "_BoundedLinearBase": + if isinstance(rhs, _Variable): + return _VarEqVar(self, rhs) + return _BoundedLinearExpression( + _expression=self - rhs, _lower_bound=0, _upper_bound=0 + ) + + def __hash__(self): + return hash((self._helper, self._index)) + + +def _create_variable( + helper: pwmb.ModelBuilderHelper, + *, + name: str, + lower_bound: _Number, + upper_bound: _Number, + is_integral: bool, +) -> _Variable: + """Creates a new variable in the helper. + + Args: + helper (pwmb.ModelBuilderHelper): The helper to create the variable. + name (str): The name of the variable. + lower_bound (Union[int, float]): The lower bound of the variable. + upper_bound (Union[int, float]): The upper bound of the variable. + is_integral (bool): Whether the variable can only take integer values. + + Returns: + _Variable: A reference to the variable in the helper. + """ + index = helper.add_var() + helper.set_var_lower_bound(index, lower_bound) + helper.set_var_upper_bound(index, upper_bound) + helper.set_var_integrality(index, is_integral) + helper.set_var_name(index, name) + return _Variable(helper, index) + + +class _BoundedLinearBase(metaclass=abc.ABCMeta): + """Interface for types that can build bounded linear (boolean) expressions. + + Classes derived from _BoundedLinearBase are used to build linear constraints + to be satisfied. + + * BoundedLinearExpression: a linear expression with upper and lower bounds. + * VarEqVar: an equality comparison between two variables. + """ + + @abc.abstractmethod + def _create_linear_constraint( + self, helper: pwmb.ModelBuilderHelper, name: str + ) -> "_LinearConstraint": + """Creates a new linear constraint in the helper. + + Args: + helper (pwmb.ModelBuilderHelper): The helper to create the constraint. + name (str): The name of the linear constraint. + + Returns: + _LinearConstraint: A reference to the linear constraint in the helper. + """ + + +def _create_linear_constraint( + constraint: Union[bool, _BoundedLinearBase], + helper: pwmb.ModelBuilderHelper, + name: str, +): + """Creates a new linear constraint in the helper. + + It handles boolean values (which might arise in the construction of + _BoundedLinearExpressions). + + Args: + constraint: The constraint to be created. + helper: The helper to create the constraint. + name: The name of the constraint to be created. + + Returns: + _LinearConstraint: a constraint in the helper corresponding to the input. + + Raises: + TypeError: If constraint is an invalid type. + """ + if isinstance(constraint, bool): + bound = 1 # constraint that is always infeasible: 1 <= nothing <= 1 + if constraint: + bound = 0 # constraint that is always feasible: 0 <= nothing <= 0 + index = helper.add_linear_constraint() + helper.set_constraint_lower_bound(index, bound) + helper.set_constraint_upper_bound(index, bound) + helper.set_constraint_name(index, name) + return _LinearConstraint(helper, index) + if isinstance(constraint, _BoundedLinearBase): + # pylint: disable=protected-access + return constraint._create_linear_constraint(helper, name) + raise TypeError("invalid type={}".format(type(constraint))) + + +@dataclasses.dataclass(repr=False, eq=False, frozen=True) +class _BoundedLinearExpression(_BoundedLinearBase): + """Represents a linear constraint: `lb <= linear expression <= ub`.""" + + __slots__ = ("_expression", "_lower_bound", "_upper_bound") + + _expression: _LinearBase + _lower_bound: _Number + _upper_bound: _Number + + def __str__(self): + if math.isfinite(self._lower_bound) and math.isfinite(self._upper_bound): + if self._lower_bound == self._upper_bound: + return f"{self._expression} == {self._lower_bound}" + return f"{self._lower_bound} <= {self._expression} <= {self._upper_bound}" + if math.isfinite(self._lower_bound): + return f"{self._expression} >= {self._lower_bound}" + if math.isfinite(self._upper_bound): + return f"{self._expression} <= {self._upper_bound}" + return f"{self._expression} free" + + def __repr__(self): + return self.__str__() + + def __bool__(self) -> NoReturn: + raise NotImplementedError( + f"Cannot use a BoundedLinearExpression {self} as a Boolean value. If" + " this message is due to code like `x >= 0` where x is a `pd.Series`," + " you can write it as `x.apply(lambda expr: expr >= 0)` instead." + ) + + def _create_linear_constraint( + self, helper: pwmb.ModelBuilderHelper, name: str + ) -> "_LinearConstraint": + index = helper.add_linear_constraint() + expr = _as_flat_linear_expression(self._expression) + # pylint: disable=protected-access + for variable, coeff in expr._terms.items(): + helper.add_term_to_constraint(index, variable._index, coeff) + helper.set_constraint_lower_bound(index, self._lower_bound - expr._offset) + helper.set_constraint_upper_bound(index, self._upper_bound - expr._offset) + # pylint: enable=protected-access + helper.set_constraint_name(index, name) + return _LinearConstraint(helper, index) + + +@dataclasses.dataclass(repr=False, eq=False, frozen=True) +class _VarEqVar(_BoundedLinearBase): + """The result of the equality comparison between two Variables. + + We use an object here to delay the evaluation of equality so that we can use + the operator== in two use-cases: + + 1. when the user want to test that two Variable values reference the same + variable. This is supported by having this object support implicit + conversion to bool. + + 2. when the user want to use the equality to create a constraint of equality + between two variables. + """ + + __slots__ = ("_left", "_right") + + _left: _Variable + _right: _Variable + + def __str__(self): + return f"{self._left} == {self._right}" + + def __repr__(self): + return self.__str__() + + def __bool__(self) -> bool: + return hash(self._left) == hash(self._right) + + def _create_linear_constraint( + self, helper: pwmb.ModelBuilderHelper, name: str + ) -> "_LinearConstraint": + index = helper.add_linear_constraint() + helper.set_constraint_lower_bound(index, 0.0) + helper.set_constraint_upper_bound(index, 0.0) + # pylint: disable=protected-access + helper.add_term_to_constraint(index, self._left._index, 1.0) + helper.add_term_to_constraint(index, self._right._index, -1.0) + # pylint: enable=protected-access + helper.set_constraint_name(index, name) + return _LinearConstraint(helper, index) + + +@dataclasses.dataclass(repr=False, eq=False, frozen=True) +class _LinearConstraint: + """A linear constraint for an optimization model. + + A LinearConstraint adds the following restriction on feasible solutions to an + optimization model: + + lb <= sum_{i in I} a_i * x_i <= ub + + where x_i are the variables of the model. lb == ub is allowed and represents + the equality constraint: sum_{i in I} a_i * x_i == b. + """ + + __slots__ = ("_helper", "_index") + + _helper: pwmb.ModelBuilderHelper + _index: int + + @property + def _lower_bound(self) -> _Number: + return self._helper.constraint_lower_bound(self._index) + + @property + def _upper_bound(self) -> _Number: + return self._helper.constraint_upper_bound(self._index) + + @property + def _name(self) -> str: + constraint_name = self._helper.constraint_name(self._index) + if constraint_name: + return constraint_name + return f"linear_constraint#{self._index}" + + def __str__(self): + return self._name + + def __repr__(self): + return self.__str__() + + +_IndexOrSeries = Union[pd.Index, pd.Series] +_VariableOrConstraint = Union[_LinearConstraint, _Variable] + + +def _get_index(obj: _IndexOrSeries) -> pd.Index: + """Returns the indices of `obj` as a `pd.Index`.""" + if isinstance(obj, pd.Series): + return obj.index + return obj + + +def _attribute_series( + *, + func: Callable[[_VariableOrConstraint], _Number], + values: _IndexOrSeries, +) -> pd.Series: + """Returns the attributes of `values`. + + Args: + func: The function to call for getting the attribute data. + values: The values that the function will be applied (element-wise) to. + + Returns: + pd.Series: The attribute values. + """ + return pd.Series( + data=[func(v) for v in values], + index=_get_index(values), + ) + + +def _convert_to_series_and_validate_index( + value_or_series: Union[bool, _Number, pd.Series], index: pd.Index +) -> pd.Series: + """Returns a pd.Series of the given index with the corresponding values. + + Args: + value_or_series: the values to be converted (if applicable). + index: the index of the resulting pd.Series. + + Returns: + pd.Series: The set of values with the given index. + + Raises: + TypeError: If the type of `value_or_series` is not recognized. + ValueError: If the index does not match. + """ + if isinstance(value_or_series, (bool, _Number)): + result = pd.Series(data=value_or_series, index=index) + elif isinstance(value_or_series, pd.Series): + if value_or_series.index.equals(index): + result = value_or_series + else: + raise ValueError("index does not match") + else: + raise TypeError("invalid type={}".format(type(value_or_series))) + return result + + +@enum.unique +class ObjectiveSense(enum.Enum): + """The sense (maximize or minimize) of the optimization objective.""" + + MINIMIZE = enum.auto() + MAXIMIZE = enum.auto() + + +class OptimizationModel: + """Pandas-like API for optimization models. + + It is a wrapper around ortools, providing indexing functionality through + Pandas for representing index dimensions (such as nodes, edges, skus, etc). + """ + + __slots__ = ( + "_helper", + "_variables", + "_linear_constraints", + ) + + def __init__(self, name: str = "") -> None: + """Initializes an optimization model with the given name.""" + if not name.isidentifier(): + raise ValueError("name={} is not a valid identifier".format(name)) + self._helper: pwmb.ModelBuilderHelper = pwmb.ModelBuilderHelper() + self._helper.set_name(name) + self._variables: dict[str, pd.Series] = {} + self._linear_constraints: dict[str, pd.Series] = {} + + def __str__(self): + return ( + f"OptimizationModel(name={self.get_name()}) with the following" + f" schema:\n{self.get_schema()}" + ) + + def __repr__(self): + return self.__str__() + + def to_proto(self) -> linear_solver_pb2.MPModelProto: + """Exports the optimization model to a ProtoBuf format.""" + return pwmb.to_mpmodel_proto(self._helper) + + @typing.overload + def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: + ... + + @typing.overload + def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: + ... + + def _get_linear_constraints( + self, constraints: Optional[_IndexOrSeries] = None + ) -> _IndexOrSeries: + if constraints is None: + return self.get_linear_constraints() + return constraints + + @typing.overload + def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: + ... + + @typing.overload + def _get_variables(self, variables: pd.Series) -> pd.Series: + ... + + def _get_variables( + self, variables: Optional[_IndexOrSeries] = None + ) -> _IndexOrSeries: + if variables is None: + return self.get_variables() + return variables + + def create_linear_constraints( + self, + name: str, + bounded_exprs: Union[_BoundedLinearBase, pd.Series], + ) -> pd.Series: + """Sets a linear constraint set with the `name` based on `bounded_exprs`. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + ``` + + Args: + name (str): Required. The name of the linear constraint set. + bounded_exprs (Union[BoundedLinearBase, pd.Series]): Required. The linear + inequalities defining the constraints, indexed by the underlying + dimensions of the constraints. If a single BoundedLinearExpression is + passed in, it will be converted into a `pd.Series` with no underlying + dimension and with an index value of `0`. + + Returns: + pd.Series: The constraint set indexed by its corresponding dimensions. It + is equivalent to `get_linear_constraint_references(name=name)`. + + Raises: + ValueError: if `name` is not a valid identifier, or already exists. + TypeError: if `bounded_exprs` has an invalid type. + """ + if not name.isidentifier(): + raise ValueError("name={} is not a valid identifier".format(name)) + if name in self._linear_constraints: + raise ValueError( + "name={} already exists as a set of linear constraints".format(name) + ) + if isinstance(bounded_exprs, (bool, _BoundedLinearBase)): + bounded_exprs = pd.Series(bounded_exprs) + if not isinstance(bounded_exprs, pd.Series): + raise TypeError("invalid type={}".format(type(bounded_exprs))) + # Set the new linear constraints. + self._linear_constraints[name] = pd.Series( + index=bounded_exprs.index, + data=[ + _create_linear_constraint(bool_expr, self._helper, f"{name}[{i}]") + for (i, bool_expr) in zip(bounded_exprs.index, bounded_exprs) + ], + ) + return self.get_linear_constraint_references(name=name) + + def create_variables( + self, + name: str, + index: pd.Index, + lower_bound: Union[_Number, pd.Series] = -math.inf, + upper_bound: Union[_Number, pd.Series] = math.inf, + is_integer: Union[bool, pd.Series] = False, + ) -> pd.Series: + """Creates a set of (scalar-valued) variables with the given name. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + + model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + ``` + + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + lower_bound (Union[int, float, pd.Series]): Optional. A lower bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to -inf. + upper_bound (Union[int, float, pd.Series]): Optional. An upper bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to +inf. + is_integer (bool, pd.Series): Optional. Indicates if the variable can only + take integer values. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to False. + + Returns: + pd.Series: The variable set indexed by its corresponding dimensions. It is + equivalent to the result from `get_variable_references(name=name)`. + + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the `lowerbound` is greater than the `upperbound`. + ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` + does not match the input index. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError("name={} is not a valid identifier".format(name)) + if name in self._variables: + raise ValueError("name={} already exists".format(name)) + if ( + isinstance(lower_bound, _Number) + and isinstance(upper_bound, _Number) + and lower_bound > upper_bound + ): + raise ValueError( + "lower_bound={} is greater than upper_bound={} for variable set={}".format( + lower_bound, upper_bound, name + ) + ) + if ( + isinstance(is_integer, bool) + and is_integer + and isinstance(lower_bound, _Number) + and isinstance(upper_bound, _Number) + and math.isfinite(lower_bound) + and math.isfinite(upper_bound) + and math.ceil(lower_bound) > math.floor(upper_bound) + ): + raise ValueError( + "ceil(lower_bound={})={}".format(lower_bound, math.ceil(lower_bound)) + + " is greater than floor(" + + "upper_bound={})={}".format(upper_bound, math.floor(upper_bound)) + + " for variable set={}".format(name) + ) + lower_bounds = _convert_to_series_and_validate_index(lower_bound, index) + upper_bounds = _convert_to_series_and_validate_index(upper_bound, index) + is_integers = _convert_to_series_and_validate_index(is_integer, index) + self._variables[name] = pd.Series( + index=index, + data=[ + # pylint: disable=g-complex-comprehension + _create_variable( + helper=self._helper, + name=f"{name}[{i}]", + lower_bound=lower_bounds[i], + upper_bound=upper_bounds[i], + is_integral=is_integers[i], + ) + for i in index + ], + ) + return self.get_variable_references(name=name) + + def get_linear_constraints(self, name: Optional[str] = None) -> pd.Index: + """Gets the set of linear constraints. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + + model.get_linear_constraints() + ``` + + Args: + name (str): Optional. The name of the linear constraint set. If it is + unspecified, all linear constraints will be in scope. + + Returns: + pd.Index: The set of linear constraints. + + Raises: + KeyError: If name is provided but not found as a linear constraint set. + """ + if not self._linear_constraints: + return pd.Index(data=[], dtype=object, name="linear_constraint") + if name: + return pd.Index( + data=self.get_linear_constraint_references(name=name).values, + name="linear_constraint", + ) + return pd.concat( + [ + # pylint: disable=g-complex-comprehension + pd.Series( + dtype=object, + index=pd.Index(constraints.values, name="linear_constraint"), + ) + for constraints in self._linear_constraints.values() + ] + ).index + + def get_linear_constraint_expressions( + self, constraints: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the expressions of all linear constraints in the set. + + If `constraints` is a `pd.Index`, then the output will be indexed by the + constraints. If `constraints` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + + model.get_linear_constraint_expressions() + ``` + + Args: + constraints (Union[pd.Index, pd.Series]): Optional. The set of linear + constraints from which to get the expressions. If unspecified, all + linear constraints will be in scope. + + Returns: + pd.Series: The expressions of all linear constraints in the set. + """ + return _attribute_series( + # pylint: disable=g-long-lambda + func=lambda c: _as_flat_linear_expression( + # pylint: disable=g-complex-comprehension + sum( + coefficient * _Variable(self._helper, variable_id) + for variable_id, coefficient in zip( + # pylint: disable=protected-access + c._helper.constraint_var_indices(c._index), + c._helper.constraint_coefficients(c._index), + ) + ) + ), + values=self._get_linear_constraints(constraints), + ) + + def get_linear_constraint_lower_bounds( + self, constraints: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the lower bounds of all linear constraints in the set. + + If `constraints` is a `pd.Index`, then the output will be indexed by the + constraints. If `constraints` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + + model.get_linear_constraint_lower_bounds() + ``` + + Args: + constraints (Union[pd.Index, pd.Series]): Optional. The set of linear + constraints from which to get the lower bounds. If unspecified, all + linear constraints will be in scope. + + Returns: + pd.Series: The lower bounds of all linear constraints in the set. + """ + return _attribute_series( + func=lambda c: c._lower_bound, # pylint: disable=protected-access + values=self._get_linear_constraints(constraints), + ) + + def get_linear_constraint_references(self, name: str) -> pd.Series: + """Gets references to all linear constraints in the set. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + + model.get_linear_constraint_references(name='c') + ``` + + Args: + name (str): Required. The name of the linear constraint set. + + Returns: + pd.Series: The references of the linear constraints in the set, indexed by + their corresponding dimensions. + + Raises: + KeyError: If name is not found in the set of linear constraints. + """ + return self._linear_constraints[name] + + def get_linear_constraint_upper_bounds( + self, constraints: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the upper bounds of all linear constraints in the set. + + If `constraints` is a `pd.Index`, then the output will be indexed by the + constraints. If `constraints` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + + model.get_linear_constraint_upper_bounds() + ``` + + Args: + constraints (Union[pd.Index, pd.Series]): Optional. The set of linear + constraints. If unspecified, all linear constraints will be in scope. + + Returns: + pd.Series: The upper bounds of all linear constraints in the set. + """ + return _attribute_series( + func=lambda c: c._upper_bound, # pylint: disable=protected-access + values=self._get_linear_constraints(constraints), + ) + + def get_name(self) -> str: + """Returns the name of the model.""" + return self._helper.name() + + def get_objective_expression(self) -> _LinearExpression: + """Returns the objective expression of the model.""" + return _as_flat_linear_expression( + sum( + # pylint: disable=protected-access + variable * self._helper.var_objective_coefficient(variable._index) + for variable in self.get_variables() + ) + + self._helper.objective_offset() + ) + + def get_objective_sense(self) -> ObjectiveSense: + """Returns the objective sense of the model. + + If no objective has been set, it will return MINIMIZE. + """ + if self._helper.maximize(): + return ObjectiveSense.MAXIMIZE + return ObjectiveSense.MINIMIZE + + def get_schema(self) -> pd.DataFrame: + """Returns the schema of the model.""" + result = {"type": [], "name": [], "dimensions": [], "count": []} + for name, variables in self._variables.items(): + result["type"].append("variable") + result["name"].append(name) + result["dimensions"].append(variables.index.names) + result["count"].append(len(variables)) + for name, constraints in self._linear_constraints.items(): + result["type"].append("linear_constraint") + result["name"].append(name) + result["dimensions"].append(constraints.index.names) + result["count"].append(len(constraints)) + return pd.DataFrame(result) + + def get_variables(self, name: Optional[str] = None) -> pd.Index: + """Gets all variables in the set. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + + model.get_variables() + ``` + + Args: + name (str): Optional. The name of the variable set. If unspecified, all + variables will be in scope. + + Returns: + pd.Index: The set of variables in the set. + + Raises: + KeyError: if `name` is not found in the set of variables. + """ + if name: + return pd.Index( + data=self.get_variable_references(name=name).values, name="variable" + ) + return pd.concat( + [ + pd.Series( + dtype=object, + index=pd.Index(variables.values, name="variable"), + ) + for variables in self._variables.values() + ] + ).index + + def get_variable_lower_bounds( + self, variables: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the lower bounds of all variables in the set. + + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + + model.get_variable_lower_bounds() + ``` + + Args: + variables (Union[pd.Index, pd.Series]): Optional. The set of variables + from which to get the lower bounds. If unspecified, all variables will + be in scope. + + Returns: + pd.Series: The lower bounds of all variables in the set. + """ + return _attribute_series( + func=lambda v: v._lower_bound, # pylint: disable=protected-access + values=self._get_variables(variables), + ) + + def get_variable_references(self, name: str) -> pd.Series: + """Gets all variables in the set. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + + model.get_variable_references(name='x') + ``` + + Args: + name (str): Required. The name of the variable set. + + Returns: + pd.Series: The variable set indexed by its underlying dimensions. + + Raises: + KeyError: if `name` is not found in the set of variables. + """ + if name not in self._variables: + raise KeyError("There is no variable set named {}".format(name)) + return self._variables[name] + + def get_variable_upper_bounds( + self, variables: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the upper bounds of all variables in the set. + + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + + model.get_variable_upper_bounds() + ``` + + Args: + variables (Union[pd.Index, pd.Series]): Optional. The set of variables + from which to get the upper bounds. If unspecified, all variables will + be in scope. + + Returns: + pd.Series: The upper bounds of all variables in the set. + """ + return _attribute_series( + func=lambda v: v._upper_bound, # pylint: disable=protected-access + values=self._get_variables(variables), + ) + + def minimize(self, expression: _LinearType) -> None: + """Set the objective to minimize the given `expression`. + + This is equivalent to `.set_objective(expression, ObjectiveSense.MINIMIZE)`. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables(name='x', index=pd.Index(range(3))) + + model.minimize(expression=x.dot([10, 6, 4])) + ``` + + To clear the objective of the model, simply set a new objective to + minimize an expression of zero. + + Args: + expression (LinearType): Required. The expression to minimize. + """ + self.set_objective(expression, ObjectiveSense.MINIMIZE) + + def maximize(self, expression: _LinearType) -> None: + """Set the objective to maximize the given `expression`. + + This is equivalent to `.set_objective(expression, ObjectiveSense.MAXIMIZE)`. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables(name='x', index=pd.Index(range(3))) + + model.maximize(expression=x.dot([10, 6, 4])) + ``` + + To clear the objective of the model, simply set a new objective to + minimize an expression of zero. + + Args: + expression (LinearType): Required. The expression to maximize. + """ + self.set_objective(expression, ObjectiveSense.MAXIMIZE) + + def set_objective(self, expression: _LinearType, sense: ObjectiveSense) -> None: + """Sets the objective to maximize or minimize the given `expression`. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables(name='x', index=pd.Index(range(3))) + + model.set_objective( + expression=x.dot([10, 6, 4]), + sense=pdm.ObjectiveSense.MAXIMIZE, + ) + ``` + + To clear the objective of the model, simply set a new objective to + minimize an expression of zero. + + Args: + expression (LinearType): Required. The expression to maximize or minimize. + sense (ObjectiveSense): Required. Either `MAXIMIZE` or `MINIMIZE`. + """ + self._helper.clear_objective() + expr: _LinearExpression = _as_flat_linear_expression(expression) + # pylint: disable=protected-access + self._helper.set_objective_offset(expr._offset) + for variable, coeff in expr._terms.items(): + self._helper.set_var_objective_coefficient(variable._index, coeff) + # pylint: enable=protected-access + self._helper.set_maximize(sense == ObjectiveSense.MAXIMIZE) + + +@dataclasses.dataclass(frozen=True) +class SolveOptions: + """The options for solving the optimization model. + + Attributes: + time_limit_seconds (int): Optional. The time limit (in seconds) for solving + the optimization model. Defaults to `sys.maxsize`. + enable_output (bool): Optional. Whether to enable solver output logging. + Defaults to False. + solver_specific_parameters (str): Optional. The format for specifying the + individual parameters is solver-specific and currently undocumented. + Defaults to an empty string. + """ + + time_limit_seconds: int = sys.maxsize + enable_output: bool = False + solver_specific_parameters: str = "" + + +@enum.unique +class SolveStatus(enum.Enum): + """The status of solving the optimization problem. + + The solve status provides a guarantee for claims that can be made about + the optimization problem. The number of solve statuses might grow with future + versions of this package. + + Attributes: + UNKNOWN: The status of solving the optimization problem is unknown. This is + the default status. + OPTIMAL: The solution is feasible and proven by the solver to be optimal for + the problem's objective. + FEASIBLE: The solution is feasible, but the solver was unable to prove that + it is optimal. (I.e. the solution is feasible for all constraints up to + the underlying solver's tolerances.) + INFEASIBLE: The optimization problem is proven by the solver to be + infeasible. Therefore no solutions can be found. + UNBOUNDED: The optimization problem is feasible, but it has been proven by + the solver to have arbitrarily good solutions (i.e. there are no optimal + solutions). The solver might not provide any feasible solutions. + """ + + UNKNOWN = enum.auto() + OPTIMAL = enum.auto() + FEASIBLE = enum.auto() + INFEASIBLE = enum.auto() + UNBOUNDED = enum.auto() + + +_solve_status: dict[pwmb.SolveStatus, SolveStatus] = { + pwmb.SolveStatus.OPTIMAL: SolveStatus.OPTIMAL, + pwmb.SolveStatus.FEASIBLE: SolveStatus.FEASIBLE, + pwmb.SolveStatus.INFEASIBLE: SolveStatus.INFEASIBLE, + pwmb.SolveStatus.UNBOUNDED: SolveStatus.UNBOUNDED, +} + + +class _SolveResult: + """The result of solving an optimization model. + + It allows you to query the status of the solution process and inspect the + solution found (if any). In general, the workflow looks like: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + + solver = pdm.Solver(solver_type=) + result = solver.solve(model) + + if result.get_status() in (pdm.SolveStatus.OPTIMAL, pdm.SolveStatus.FEASIBLE): + # result.get_objective_value() and result.get_variable_values() will return + # non-NA values for a feasible (if not optimal) solution to the problem. + elif result.get_status() == pdm.SolveStatus.INFEASIBLE: + # result.get_objective_value() and result.get_variable_values() will return + # NA values. + else: + # result.get_objective_value() and result.get_variable_values() are not + # well-defined. + ``` + + (This class is marked internal because it has a constructor [and fields] that + are considered internal. All its public methods will be stable in future + versions from an API perspective.) + """ + + __slots__ = ("_model", "_solver", "_status") + + def __init__( + self, + model: OptimizationModel, + solver: pwmb.ModelSolverHelper, + status: pwmb.SolveStatus, + ): + self._model = model + self._solver = solver + self._status: SolveStatus = _solve_status.get(status, SolveStatus.UNKNOWN) + + def get_status(self) -> SolveStatus: + return self._status + + def get_variable_values( + self, + variables: Optional[_IndexOrSeries] = None, + ) -> pd.Series: + """Gets the variable values of variables in the set. + + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + model.set_objective( + expression=x.dot([10, 6, 4]), + sense=pdm.ObjectiveSense.MAXIMIZE, + ) + solver = pdm.Solver(solver_type=pdm.SolverType.GLOP) + run = solver.solve(model) + + run.get_variable_values() + ``` + + Args: + variables (Union[pd.Index, pd.Series]): Optional. The set of variables + from which to get the values. If unspecified, all variables will be in + scope. + + Returns: + pd.Series: The values of all variables in the set. + """ + # pylint: disable=protected-access + model_variables = self._model._get_variables(variables) + if not self._solver.has_solution(): + return _attribute_series(func=lambda v: pd.NA, values=model_variables) + return _attribute_series( + func=lambda v: self._solver.var_value(v._index), + values=model_variables, + ) + + def get_objective_value(self) -> float: + """Gets the objective value of the best primal feasible solution. + + Returns: + float: The objective value of the best feasible solution. It will return + NA if there are no feasible solutions. + """ + if not self._solver.has_solution(): + return pd.NA + return self._solver.objective_value() + + +@enum.unique +class SolverType(enum.Enum): + """The underlying solver to use. + + Attributes: + CP_SAT: Google's CP-SAT solver (first party). Supports problems where all + variables are `is_integer=True` and have finite upper and lower_bounds. + Experimental support is available to automatically rescale and discretize + problems with continuous variables. + GLOP: Google's GLOP linear solver (first party). It supports LP with primal + and dual simplex methods, but does not support problems with variables + where `is_integer=True`. + SCIP: Solving Constraint Integer Programs (SCIP) solver (third party). It + supports linear (LP) and mixed-integer linear (MILP) problems. + """ + + CP_SAT = enum.auto() + GLOP = enum.auto() + SCIP = enum.auto() + + +_solver_type_to_name: dict[SolverType, str] = { + SolverType.CP_SAT: "CP_SAT", + SolverType.GLOP: "GLOP", + SolverType.SCIP: "SCIP", +} + + +@dataclasses.dataclass(frozen=True) +class Solver: + """A solver factory for solvers of the corresponding type. + + The purpose of this class is to search for a solution to the model provided + to the .solve(...) method. It is immutable and does not support incremental + solves. Each call to .solve(model, options) manages its own state. + + In general, the workflow looks like: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + + model = ... + solver = pdm.Solver(solver_type=) + result = solver.solve(model=model, options=pdm.SolveOptions(...)) + + if result.get_status() in (pdm.SolveStatus.OPTIMAL, pdm.SolveStatus.FEASIBLE): + # result.get_objective_value() and result.get_variable_values() will return + # non-NA values for a feasible (if not optimal) solution to the problem. + elif result.get_status() == pdm.SolveStatus.INFEASIBLE: + # result.get_objective_value() and result.get_variable_values() will return + # NA values. + else: + # result.get_objective_value() and result.get_variable_values() are not + # well-defined. + ``` + + Attributes: + solver_type (SolverType): The type of solver to use (e.g. GLOP, SCIP). + """ + + solver_type: SolverType + + def solve( + self, + model: OptimizationModel, + options: SolveOptions = SolveOptions(), + ) -> _SolveResult: + """Solves an optimization model. + + It will overwrite the previous state of all variables and constraints with + the results of the solve. + + Example usage: + + ``` + # pylint: disable=line-too-long + from ortools.linear_solver.python import pandas_model as pdm + import pandas as pd + + model = pdm.OptimizationModel(name='example') + x = model.create_variables( + name='x', + index=pd.Index(range(3)), + lower_bound=0, + ) + model.create_linear_constraints( + name='c', + bounded_exprs=pd.Series([ + x.dot([10, 4, 5]) <= 600, + x.dot([2, 2, 6]) <= 300, + sum(x) <= 100, + ]), + ) + model.set_objective( + expression=x.dot([10, 6, 4]), + sense=pdm.ObjectiveSense.MAXIMIZE, + ) + + solver = pdm.Solver(solver_type=pdm.SolverType.GLOP) + solver.solve(model=model) + ``` + + Args: + model (OptimizationModel): Required. The model to be solved. + options (SolveOptions): Optional. The options to set for solving the + model. + + Returns: + SolveResult: The result of solving the model. + + Raises: + ValueError: If `options.solver_specific_parameters` is invalid for the + Solver (based on its `solver_type`). + RuntimeError: On a solve error. + """ + solver = pwmb.ModelSolverHelper(_solver_type_to_name[self.solver_type]) + solver.enable_output(options.enable_output) + solver.set_time_limit_in_seconds(options.time_limit_seconds) + if options.solver_specific_parameters: + # This does not panic if the parameters are not recognized by the solver. + solver.set_solver_specific_parameters(options.solver_specific_parameters) + solver.solve(model._helper) # pylint: disable=protected-access + return _SolveResult(model, solver, solver.status()) diff --git a/ortools/linear_solver/python/pandas_model_test.py b/ortools/linear_solver/python/pandas_model_test.py new file mode 100644 index 0000000000..81f0ce27d9 --- /dev/null +++ b/ortools/linear_solver/python/pandas_model_test.py @@ -0,0 +1,1748 @@ +#!/usr/bin/env python3 +# Copyright 2010-2022 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. + +import math +from typing import Callable, Union + +import pandas as pd + +#from google3.net.proto2.contrib.pyutil import compare +from google.protobuf import text_format +from absl.testing import absltest +from absl.testing import parameterized +from ortools.linear_solver import linear_solver_pb2 +from ortools.linear_solver.python import pandas_model as pdm + + +# This test suite should not be depended on as a public API. +class InternalHelperTest(absltest.TestCase): + def test_anonymous_variables(self): + helper = pdm.OptimizationModel(name="test_name")._helper + index = helper.add_var() + variable = pdm._Variable(helper, index) + self.assertEqual(variable._name, f"variable#{index}") + + def test_anonymous_constraints(self): + helper = pdm.OptimizationModel(name="test_name")._helper + index = helper.add_linear_constraint() + constraint = pdm._LinearConstraint(helper, index) + self.assertEqual(constraint._name, f"linear_constraint#{index}") + + +class LinearBaseTest(parameterized.TestCase): + def setUp(self): + super().setUp() + simple_model = pdm.OptimizationModel(name="test_name") + simple_model.create_variables(name="x", index=pd.Index(range(3), name="i")) + simple_model.create_variables(name="y", index=pd.Index(range(5), name="i")) + self.simple_model = simple_model + + @parameterized.named_parameters( + # Variable / Indexing + dict( + testcase_name="x[0]", + expr=lambda x, y: x[0], + expected_repr="x[0]", + ), + dict( + testcase_name="x[1]", + expr=lambda x, y: x[1], + expected_repr="x[1]", + ), + dict( + testcase_name="x[2]", + expr=lambda x, y: x[2], + expected_repr="x[2]", + ), + dict( + testcase_name="y[0]", + expr=lambda x, y: y[0], + expected_repr="y[0]", + ), + dict( + testcase_name="y[4]", + expr=lambda x, y: y[4], + expected_repr="y[4]", + ), + # Sum + dict( + testcase_name="x[0] + 5", + expr=lambda x, y: x[0] + 5, + expected_repr="5.0 + x[0]", + ), + dict( + testcase_name="x[0] - 5", + expr=lambda x, y: x[0] - 5, + expected_repr="-5.0 + x[0]", + ), + dict( + testcase_name="5 - x[0]", + expr=lambda x, y: 5 - x[0], + expected_repr="5.0 - x[0]", + ), + dict( + testcase_name="5 + x[0]", + expr=lambda x, y: 5 + x[0], + expected_repr="5.0 + x[0]", + ), + dict( + testcase_name="x[0] + y[0]", + expr=lambda x, y: x[0] + y[0], + expected_repr="0.0 + x[0] + y[0]", + ), + dict( + testcase_name="x[0] + y[0] + 5", + expr=lambda x, y: x[0] + y[0] + 5, + expected_repr="5.0 + x[0] + y[0]", + ), + dict( + testcase_name="5 + x[0] + y[0]", + expr=lambda x, y: 5 + x[0] + y[0], + expected_repr="5.0 + x[0] + y[0]", + ), + dict( + testcase_name="5 + x[0] - x[0]", + expr=lambda x, y: 5 + x[0] - x[0], + expected_repr="5.0", + ), + dict( + testcase_name="5 + x[0] - y[0]", + expr=lambda x, y: 5 + x[0] - y[0], + expected_repr="5.0 + x[0] - y[0]", + ), + dict( + testcase_name="x.sum()", + expr=lambda x, y: x.sum(), + expected_repr="0.0 + x[0] + x[1] + x[2]", + ), + dict( + testcase_name="x.add(y, fill_value=0).sum() + 5", + expr=lambda x, y: x.add(y, fill_value=0).sum() + 5, + expected_repr="5.0 + x[0] + x[1] + x[2] + y[0] + y[1] + ...", + ), + # Product + dict( + testcase_name="- x.sum()", + expr=lambda x, y: -x.sum(), + expected_repr="0.0 - x[0] - x[1] - x[2]", + ), + dict( + testcase_name="5 - x.sum()", + expr=lambda x, y: 5 - x.sum(), + expected_repr="5.0 - x[0] - x[1] - x[2]", + ), + dict( + testcase_name="x.sum() / 2.0", + expr=lambda x, y: x.sum() / 2.0, + expected_repr="0.0 + 0.5 * x[0] + 0.5 * x[1] + 0.5 * x[2]", + ), + dict( + testcase_name="(3 * x).sum()", + expr=lambda x, y: (3 * x).sum(), + expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]", + ), + dict( + testcase_name="(x * 3).sum()", + expr=lambda x, y: (x * 3).sum(), + expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]", + ), + dict( + testcase_name="x.sum() * 3", + expr=lambda x, y: x.sum() * 3, + expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]", + ), + dict( + testcase_name="3 * x.sum()", + expr=lambda x, y: 3 * x.sum(), + expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]", + ), + dict( + testcase_name="0 * x.sum() + y.sum()", + expr=lambda x, y: 0 * x.sum() + y.sum(), + expected_repr="0.0 + y[0] + y[1] + y[2] + y[3] + y[4]", + ), + # LinearExpression + dict( + testcase_name="_as_flat_linear_expression(x.sum())", + expr=lambda x, y: pdm._as_flat_linear_expression(x.sum()), + expected_repr="0.0 + x[0] + x[1] + x[2]", + ), + dict( + testcase_name=( + "_as_flat_linear_expression(_as_flat_linear_expression(x.sum()))" + ), + # pylint: disable=g-long-lambda + expr=lambda x, y: pdm._as_flat_linear_expression( + pdm._as_flat_linear_expression(x.sum()) + ), + expected_repr="0.0 + x[0] + x[1] + x[2]", + ), + dict( + testcase_name="""_as_flat_linear_expression(sum([ + _as_flat_linear_expression(x.sum()), + _as_flat_linear_expression(x.sum()), + ]))""", + # pylint: disable=g-long-lambda + expr=lambda x, y: pdm._as_flat_linear_expression( + sum( + [ + pdm._as_flat_linear_expression(x.sum()), + pdm._as_flat_linear_expression(x.sum()), + ] + ) + ), + expected_repr="0.0 + 2.0 * x[0] + 2.0 * x[1] + 2.0 * x[2]", + ), + ) + def test_repr(self, expr, expected_repr): + x = self.simple_model.get_variable_references("x") + y = self.simple_model.get_variable_references("y") + self.assertEqual(repr(expr(x, y)), expected_repr) + + +class LinearBaseErrorsTest(absltest.TestCase): + def test_unknown_linear_type(self): + with self.assertRaisesRegex(TypeError, r"Unrecognized linear expression"): + + class UnknownLinearType(pdm._LinearBase): + pass + + pdm._as_flat_linear_expression(UnknownLinearType()) + + def test_division_by_zero(self): + with self.assertRaises(ZeroDivisionError): + model = pdm.OptimizationModel(name="divide_by_zero") + x = model.create_variables(name="x", index=pd.Index(range(1))) + print(x / 0) + + def test_boolean_expression(self): + with self.assertRaisesRegex( + NotImplementedError, r"LinearExpression as a Boolean value" + ): + model = pdm.OptimizationModel(name="boolean_expression") + x = model.create_variables(name="x", index=pd.Index(range(1))) + bool(x.sum()) + + +class BoundedLinearBaseTest(parameterized.TestCase): + def setUp(self): + super().setUp() + simple_model = pdm.OptimizationModel(name="test_name") + simple_model.create_variables(name="x", index=pd.Index(range(3), name="i")) + simple_model.create_variables(name="y", index=pd.Index(range(5), name="i")) + self.simple_model = simple_model + + @parameterized.product( + lhs=( + lambda x, y: x.sum(), + lambda x, y: -x.sum(), + lambda x, y: x.sum() * 0, + lambda x, y: x.sum() * 3, + lambda x, y: x[0], + lambda x, y: x[1], + lambda x, y: x[2], + lambda x, y: -math.inf, + lambda x, y: -1, + lambda x, y: 0, + lambda x, y: 1, + lambda x, y: 1.1, + lambda x, y: math.inf, + ), + rhs=( + lambda x, y: y.sum(), + lambda x, y: -y.sum(), + lambda x, y: y.sum() * 0, + lambda x, y: y.sum() * 3, + lambda x, y: y[0], + lambda x, y: y[1], + lambda x, y: y[2], + lambda x, y: -math.inf, + lambda x, y: -1, + lambda x, y: 0, + lambda x, y: 1, + lambda x, y: 1.1, + lambda x, y: math.inf, + ), + op=( + lambda lhs, rhs: lhs == rhs, + lambda lhs, rhs: lhs <= rhs, + lambda lhs, rhs: lhs >= rhs, + ), + ) + def test_repr(self, lhs, rhs, op): + x = self.simple_model.get_variable_references("x") + y = self.simple_model.get_variable_references("y") + l: pdm._LinearType = lhs(x, y) + r: pdm._LinearType = rhs(x, y) + result = op(l, r) + if isinstance(l, pdm._LinearBase) or isinstance(r, pdm._LinearBase): + self.assertIsInstance(result, pdm._BoundedLinearBase) + self.assertIn("=", repr(result), msg="is one of ==, <=, or >=") + else: + self.assertIsInstance(result, bool) + + def test_doublesided_bounded_expressions(self): + x = self.simple_model.get_variable_references("x") + self.assertEqual( + "0 <= x[0] <= 1", repr(pdm._BoundedLinearExpression(x[0], 0, 1)) + ) + + def test_free_bounded_expressions(self): + x = self.simple_model.get_variable_references("x") + self.assertEqual( + "x[0] free", + repr(pdm._BoundedLinearExpression(x[0], -math.inf, math.inf)), + ) + + def test_var_eq_var_as_bool(self): + x = self.simple_model.get_variable_references("x") + y = self.simple_model.get_variable_references("y") + self.assertEqual(x[0], x[0]) + self.assertNotEqual(x[0], x[1]) + self.assertNotEqual(x[0], y[0]) + + self.assertEqual(x[1], x[1]) + self.assertNotEqual(x[1], x[0]) + self.assertNotEqual(x[1], y[1]) + + self.assertEqual(y[0], y[0]) + self.assertNotEqual(y[0], y[1]) + self.assertNotEqual(y[0], x[0]) + + self.assertEqual(y[1], y[1]) + self.assertNotEqual(y[1], y[0]) + self.assertNotEqual(y[1], x[1]) + + +class BoundedLinearBaseErrorsTest(absltest.TestCase): + def test_bounded_linear_expression_as_bool(self): + with self.assertRaisesRegex(NotImplementedError, "Boolean value"): + model = pdm.OptimizationModel(name="bounded_linear_expression_as_bool") + x = model.create_variables(name="x", index=pd.Index(range(1))) + bool(pdm._BoundedLinearExpression(x, 0, 1)) + + +class OptimizationModelMetadataTest(absltest.TestCase): + def test_name(self): + model = pdm.OptimizationModel(name="test_name") + self.assertEqual("test_name", model.get_name()) + + def test_schema_empty(self): + model = pdm.OptimizationModel(name="test_name") + self.assertIsInstance(model.get_schema(), pd.DataFrame) + + def test_schema_no_constraints(self): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables( + name="x", + index=pd.Index(range(3)), + lower_bound=0, + upper_bound=1, + is_integer=True, + ) + y = model.create_variables( + name="y", + index=pd.MultiIndex.from_tuples([(1, 2), (3, 4)], names=["i", "j"]), + lower_bound=0, + ) + z = model.create_variables( + name="z", + index=pd.MultiIndex.from_product(((1, 2), ("a", "b", "c"))), + lower_bound=-5, + upper_bound=10, + is_integer=True, + ) + schema = model.get_schema() + self.assertIsInstance(schema, pd.DataFrame) + self.assertLen(schema, 3) + self.assertSequenceAlmostEqual( + schema.columns, ["type", "name", "dimensions", "count"] + ) + self.assertSequenceAlmostEqual(schema.type, ["variable"] * 3) + self.assertSequenceAlmostEqual(schema.name, ["x", "y", "z"]) + self.assertSequenceAlmostEqual( + schema.dimensions, [(None,), ("i", "j"), (None, None)] + ) + self.assertSequenceAlmostEqual(schema["count"], [len(x), len(y), len(z)]) + self.assertEqual( + repr(model), + """OptimizationModel(name=test_name) with the following schema: + type name dimensions count +0 variable x [None] 3 +1 variable y ['i', 'j'] 2 +2 variable z [None, None] 6""", + ) + + def test_full_schema(self): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables( + name="x", + index=pd.Index(range(3)), + lower_bound=0, + upper_bound=1, + is_integer=True, + ) + y = model.create_variables( + name="y", + index=pd.MultiIndex.from_tuples([(1, 2), (3, 4)], names=["i", "j"]), + lower_bound=0, + ) + z = model.create_variables( + name="z", + index=pd.MultiIndex.from_product( + ((1, 2), ("a", "b", "c")), names=["i", "k"] + ), + lower_bound=-5, + upper_bound=10, + is_integer=True, + ) + c1 = model.create_linear_constraints( + name="x_sum_le_constant", + bounded_exprs=(x.sum() <= 10), + ) + c2 = model.create_linear_constraints( + name="y_groupbyj_sum_ge_constant", + bounded_exprs=y.groupby("j").sum().apply(lambda expr: expr >= 3), + ) + c3 = model.create_linear_constraints( + name="y_groupbyi_sum_eq_z_groupbyi_sum", + bounded_exprs=y.groupby("i") + .sum() + .sub(z.groupby("i").sum()) + .dropna() + .apply(lambda expr: expr == 0), + ) + schema = model.get_schema() + self.assertIsInstance(schema, pd.DataFrame) + self.assertLen(schema, 6) + self.assertSequenceAlmostEqual( + schema.columns, ["type", "name", "dimensions", "count"] + ) + self.assertSequenceAlmostEqual( + schema.type, ["variable"] * 3 + ["linear_constraint"] * 3 + ) + self.assertSequenceAlmostEqual( + schema.name, + [ + "x", + "y", + "z", + "x_sum_le_constant", + "y_groupbyj_sum_ge_constant", + "y_groupbyi_sum_eq_z_groupbyi_sum", + ], + ) + self.assertSequenceAlmostEqual( + schema.dimensions, + [(None,), ("i", "j"), ("i", "k"), (None,), ("j",), ("i",)], + ) + self.assertSequenceAlmostEqual( + schema["count"], [len(x), len(y), len(z), len(c1), len(c2), len(c3)] + ) + self.assertEqual( + repr(model), + """OptimizationModel(name=test_name) with the following schema: + type name dimensions count +0 variable x [None] 3 +1 variable y ['i', 'j'] 2 +2 variable z ['i', 'k'] 6 +3 linear_constraint x_sum_le_constant [None] 1 +4 linear_constraint y_groupbyj_sum_ge_constant ['j'] 2 +5 linear_constraint y_groupbyi_sum_eq_z_groupbyi_sum ['i'] 3""", + ) + + +class OptimizationModelErrorsTest(absltest.TestCase): + def test_name_errors(self): + with self.assertRaisesRegex(ValueError, r"not a valid identifier"): + pdm.OptimizationModel(name="") + + def test_create_variables_errors(self): + with self.assertRaisesRegex(TypeError, r"Non-index object"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="", index=pd.DataFrame()) + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index([0]), lower_bound="0") + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index([0]), upper_bound="0") + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index([0]), is_integer="True") + with self.assertRaisesRegex(ValueError, r"not a valid identifier"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="", index=pd.Index([0])) + with self.assertRaisesRegex(ValueError, r"already exists"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index([0])) + model.create_variables(name="x", index=pd.Index([0])) + with self.assertRaisesRegex(ValueError, r"is greater than"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables( + name="x", + index=pd.Index([0]), + lower_bound=0.2, + upper_bound=0.1, + ) + with self.assertRaisesRegex(ValueError, r"is greater than"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables( + name="x", + index=pd.Index([0]), + lower_bound=0.1, + upper_bound=0.2, + is_integer=True, + ) + with self.assertRaisesRegex(ValueError, r"index does not match"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables( + name="x", index=pd.Index([0]), lower_bound=pd.Series([1, 2]) + ) + with self.assertRaisesRegex(ValueError, r"index does not match"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables( + name="x", index=pd.Index([0]), upper_bound=pd.Series([1, 2]) + ) + with self.assertRaisesRegex(ValueError, r"index does not match"): + model = pdm.OptimizationModel(name="test_name") + model.create_variables( + name="x", index=pd.Index([0]), is_integer=pd.Series([False, True]) + ) + + def test_get_variables_errors(self): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index(range(3))) + with self.assertRaisesRegex(KeyError, r"no variable set named"): + model.get_variables(name="nonexistent_variable") + + def test_get_variable_references_errors(self): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index(range(3))) + with self.assertRaisesRegex(KeyError, r"no variable set named"): + model.get_variable_references(None) + with self.assertRaisesRegex(KeyError, r"no variable set named"): + model.get_variable_references(name="") + + def test_create_linear_constraints_errors(self): + with self.assertRaisesRegex(ValueError, r"not a valid identifier"): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables(name="x", index=pd.Index(range(1))) + model.create_linear_constraints(name="", bounded_exprs=x[0] == 0) + with self.assertRaisesRegex(ValueError, r"already exists"): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables(name="x", index=pd.Index(range(1))) + model.create_linear_constraints(name="c", bounded_exprs=x[0] <= 0) + model.create_linear_constraints(name="c", bounded_exprs=x[0] >= 0) + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = pdm.OptimizationModel(name="test_name") + model.create_linear_constraints(name="c", bounded_exprs="True") + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = pdm.OptimizationModel(name="test_name") + model.create_linear_constraints(name="c", bounded_exprs=pd.Series(["T"])) + + def test_get_linear_constraint_references_errors(self): + with self.assertRaises(KeyError): + model = pdm.OptimizationModel(name="test_name") + model.get_linear_constraint_references("c") + + +class OptimizationModelVariablesTest(parameterized.TestCase): + _variable_indices = ( + pd.Index(range(3)), + pd.Index(range(5), name="i"), + pd.MultiIndex.from_product(((1, 2), ("a", "b", "c")), names=["i", "j"]), + pd.MultiIndex.from_product((("a", "b"), (1, 2, 3))), + ) + _bounds = ( + lambda index: (-math.inf, -10.5), + lambda index: (-math.inf, -1), + lambda index: (-math.inf, 0), + lambda index: (-math.inf, 10), + lambda index: (-math.inf, math.inf), + lambda index: (-10, -1.1), + lambda index: (-10, 0), + lambda index: (-10, -10), + lambda index: (-10, 3), + lambda index: (-9, math.inf), + lambda index: (-1, 1), + lambda index: (0, 0), + lambda index: (0, 1), + lambda index: (0, math.inf), + lambda index: (1, 1), + lambda index: (1, 10.1), + lambda index: (1, math.inf), + lambda index: (100.1, math.inf), + # pylint: disable=g-long-lambda + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(-10.5, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(-1, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(0, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(10, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: (pd.Series(-10, index=index), pd.Series(-1.1, index=index)), + lambda index: (pd.Series(-10, index=index), pd.Series(0, index=index)), + lambda index: (pd.Series(-10, index=index), pd.Series(-10, index=index)), + lambda index: (pd.Series(-10, index=index), pd.Series(3, index=index)), + lambda index: ( + pd.Series(-9, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: (pd.Series(-1, index=index), pd.Series(1, index=index)), + lambda index: (pd.Series(0, index=index), pd.Series(0, index=index)), + lambda index: (pd.Series(0, index=index), pd.Series(1, index=index)), + lambda index: ( + pd.Series(0, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: (pd.Series(1, index=index), pd.Series(1, index=index)), + lambda index: (pd.Series(1, index=index), pd.Series(10.1, index=index)), + lambda index: ( + pd.Series(1, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: ( + pd.Series(100.1, index=index), + pd.Series(math.inf, index=index), + ), + ) + _is_integer = ( + lambda index: False, + lambda index: True, + lambda index: pd.Series(False, index=index), + lambda index: pd.Series(True, index=index), + ) + + @parameterized.product( + index=_variable_indices, bounds=_bounds, is_integer=_is_integer + ) + def test_create_variables(self, index, bounds, is_integer): + model = pdm.OptimizationModel(name="test_name") + variables = model.create_variables( + name="test_variable", + index=index, + lower_bound=bounds(index)[0], + upper_bound=bounds(index)[1], + is_integer=is_integer(index), + ) + self.assertLen(variables, len(index)) + self.assertLen(set(variables), len(index)) + for i in index: + self.assertEqual(repr(variables[i]), f"test_variable[{i}]") + + @parameterized.named_parameters( + dict(testcase_name="all", variable_name=None, variable_count=3 + 5), + dict(testcase_name="x", variable_name="x", variable_count=3), + dict(testcase_name="y", variable_name="y", variable_count=5), + ) + def test_get_variables(self, variable_name, variable_count): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index(range(3))) + model.create_variables(name="y", index=pd.Index(range(5))) + for variables, expected_count in ( + (model.get_variables(), 3 + 5), + (model.get_variables(variable_name), variable_count), + (model.get_variables(name=variable_name), variable_count), + ): + self.assertIsInstance(variables, pd.Index) + self.assertLen(variables, expected_count) + + @parameterized.product( + index=_variable_indices, bounds=_bounds, is_integer=_is_integer + ) + def test_get_variable_lower_bounds(self, index, bounds, is_integer): + lower_bound, upper_bound = bounds(index) + model = pdm.OptimizationModel(name="test_name") + model.create_variables( + name="x", + index=index, + lower_bound=lower_bound, + upper_bound=upper_bound, + is_integer=is_integer(index), + ) + model.create_variables( + name="y", + index=index, + lower_bound=lower_bound, + upper_bound=upper_bound, + is_integer=is_integer(index), + ) + for lower_bounds in ( + model.get_variable_lower_bounds(model.get_variables("x")), + model.get_variable_lower_bounds(model.get_variables("y")), + model.get_variable_lower_bounds(model.get_variable_references("x")), + model.get_variable_lower_bounds(model.get_variable_references("y")), + ): + self.assertSequenceAlmostEqual( + lower_bounds, + pdm._convert_to_series_and_validate_index(lower_bound, index), + ) + self.assertSequenceAlmostEqual( + model.get_variable_lower_bounds(), + pd.concat( + [ + model.get_variable_lower_bounds(model.get_variables("x")), + model.get_variable_lower_bounds(model.get_variables("y")), + ] + ), + ) + for variables in (model.get_variables("x"), model.get_variables("y")): + lower_bounds = model.get_variable_lower_bounds(variables) + self.assertSequenceAlmostEqual(lower_bounds.index, variables) + for variables in ( + model.get_variable_references("x"), + model.get_variable_references("y"), + ): + lower_bounds = model.get_variable_lower_bounds(variables) + self.assertSequenceAlmostEqual(lower_bounds.index, variables.index) + + @parameterized.named_parameters( + dict(testcase_name="x", variable_name="x", variable_count=3), + dict(testcase_name="y", variable_name="y", variable_count=5), + ) + def test_get_variable_references(self, variable_name, variable_count): + model = pdm.OptimizationModel(name="test_name") + model.create_variables(name="x", index=pd.Index(range(3))) + model.create_variables(name="y", index=pd.Index(range(5))) + self.assertLen(model.get_variables(), 3 + 5) + for variables in ( + model.get_variable_references(variable_name), + model.get_variable_references(name=variable_name), + ): + self.assertLen(variables, variable_count) + + @parameterized.product( + index=_variable_indices, bounds=_bounds, is_integer=_is_integer + ) + def test_get_variable_upper_bounds(self, index, bounds, is_integer): + lower_bound, upper_bound = bounds(index) + model = pdm.OptimizationModel(name="test_name") + model.create_variables( + name="x", + index=index, + lower_bound=lower_bound, + upper_bound=upper_bound, + is_integer=is_integer(index), + ) + model.create_variables( + name="y", + index=index, + lower_bound=lower_bound, + upper_bound=upper_bound, + is_integer=is_integer(index), + ) + for upper_bounds in ( + model.get_variable_upper_bounds(model.get_variables("x")), + model.get_variable_upper_bounds(model.get_variables("y")), + model.get_variable_upper_bounds(model.get_variable_references("x")), + model.get_variable_upper_bounds(model.get_variable_references("y")), + ): + self.assertSequenceAlmostEqual( + upper_bounds, + pdm._convert_to_series_and_validate_index(upper_bound, index), + ) + self.assertSequenceAlmostEqual( + model.get_variable_upper_bounds(), + pd.concat( + [ + model.get_variable_upper_bounds(model.get_variables("x")), + model.get_variable_upper_bounds(model.get_variables("y")), + ] + ), + ) + for variables in (model.get_variables("x"), model.get_variables("y")): + upper_bounds = model.get_variable_upper_bounds(variables) + self.assertSequenceAlmostEqual(upper_bounds.index, variables) + for variables in ( + model.get_variable_references("x"), + model.get_variable_references("y"), + ): + upper_bounds = model.get_variable_upper_bounds(variables) + self.assertSequenceAlmostEqual(upper_bounds.index, variables.index) + + +class OptimizationModelLinearConstraintsTest(parameterized.TestCase): + constraint_test_cases = [ + # pylint: disable=g-long-lambda + dict( + testcase_name="True", + name="true", + bounded_exprs=lambda x, y: True, + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="False", + name="false", + bounded_exprs=lambda x, y: False, + constraint_count=1, + lower_bounds=[1], + upper_bounds=[1], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] <= 1.5", + name="x0_le_c", + bounded_exprs=lambda x, y: x[0] <= 1.5, + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[1.5], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] == 1", + name="x0_eq_c", + bounded_exprs=lambda x, y: x[0] == 1, + constraint_count=1, + lower_bounds=[1], + upper_bounds=[1], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] >= -1", + name="x0_ge_c", + bounded_exprs=lambda x, y: x[0] >= -1, + constraint_count=1, + lower_bounds=[-1], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="-1.5 <= x[0]", + name="c_le_x0", + bounded_exprs=lambda x, y: -1.5 <= x[0], + constraint_count=1, + lower_bounds=[-1.5], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="0 == x[0]", + name="c_eq_x0", + bounded_exprs=lambda x, y: 0 == x[0], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="10 >= x[0]", + name="c_ge_x0", + bounded_exprs=lambda x, y: 10 >= x[0], + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[10], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] <= x[0]", + name="x0_le_x0", + bounded_exprs=lambda x, y: x[0] <= x[0], + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 0}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] == x[0]", + name="x0_eq_x0", + bounded_exprs=lambda x, y: x[0] == x[0], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 0}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] >= x[0]", + name="x0_ge_x0", + bounded_exprs=lambda x, y: x[0] >= x[0], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 0}], + expression_offsets=[0], + ), + dict( + # x[0] - x[0] <= 3 + testcase_name="x[0] - 1 <= x[0] + 2", + name="x0c_le_x0c", + bounded_exprs=lambda x, y: pd.Series(x[0] - 1 <= x[0] + 2), + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[3], + expression_terms=lambda x, y: [{x[0]: 0}], + expression_offsets=[0], + ), + dict( + # x[0] - x[0] == 3 + testcase_name="x[0] - 1 == x[0] + 2", + name="x0c_eq_x0c", + bounded_exprs=lambda x, y: pd.Series(x[0] - 1 == x[0] + 2), + constraint_count=1, + lower_bounds=[3], + upper_bounds=[3], + expression_terms=lambda x, y: [{x[0]: 0}], + expression_offsets=[0], + ), + dict( + # x[0] - x[0] >= 3 + testcase_name="x[0] - 1 >= x[0] + 2", + name="x0c_ge_x0c", + bounded_exprs=lambda x, y: pd.Series(x[0] - 1 >= x[0] + 2), + constraint_count=1, + lower_bounds=[3], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 0}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] <= x[1]", + name="x0_le_x1", + bounded_exprs=lambda x, y: x[0] <= x[1], + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] == x[1]", + name="x0_eq_x1", + bounded_exprs=lambda x, y: x[0] == x[1], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] >= x[1]", + name="x0_ge_x1", + bounded_exprs=lambda x, y: x[0] >= x[1], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + # x[0] - x[1] <= -3 + testcase_name="x[0] + 1 <= x[1] - 2", + name="x0c_le_x1c", + bounded_exprs=lambda x, y: x[0] + 1 <= x[1] - 2, + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[-3], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + # x[0] - x[1] == -3 + testcase_name="x[0] + 1 == x[1] - 2", + name="x0c_eq_x1c", + bounded_exprs=lambda x, y: x[0] + 1 == x[1] - 2, + constraint_count=1, + lower_bounds=[-3], + upper_bounds=[-3], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + # x[0] - x[1] >= -3 + testcase_name="x[0] + 1 >= x[1] - 2", + name="x0c_ge_x1c", + bounded_exprs=lambda x, y: pd.Series(x[0] + 1 >= x[1] - 2), + constraint_count=1, + lower_bounds=[-3], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + testcase_name="x <= 0", + name="x_le_c", + bounded_exprs=lambda x, y: x.apply(lambda expr: expr <= 0), + constraint_count=3, + lower_bounds=[-math.inf] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [{xi: 1} for xi in x], + expression_offsets=[0] * 3, + ), + dict( + testcase_name="x >= 0", + name="x_ge_c", + bounded_exprs=lambda x, y: x.apply(lambda expr: expr >= 0), + constraint_count=3, + lower_bounds=[0] * 3, + upper_bounds=[math.inf] * 3, + expression_terms=lambda x, y: [{xi: 1} for xi in x], + expression_offsets=[0] * 3, + ), + dict( + testcase_name="x == 0", + name="x_eq_c", + bounded_exprs=lambda x, y: x.apply(lambda expr: expr == 0), + constraint_count=3, + lower_bounds=[0] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [{xi: 1} for xi in x], + expression_offsets=[0] * 3, + ), + dict( + testcase_name="y == 0", + name="y_eq_c", + bounded_exprs=(lambda x, y: y.apply(lambda expr: expr == 0)), + constraint_count=2 * 3, + lower_bounds=[0] * 2 * 3, + upper_bounds=[0] * 2 * 3, + expression_terms=lambda x, y: [{yi: 1} for yi in y], + expression_offsets=[0] * 3 * 2, + ), + dict( + testcase_name='y.groupby("i").sum() == 0', + name="ygroupbyi_eq_c", + bounded_exprs=( + lambda x, y: y.groupby("i").sum().apply(lambda expr: expr == 0) + ), + constraint_count=2, + lower_bounds=[0] * 2, + upper_bounds=[0] * 2, + expression_terms=lambda x, y: [ + {y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1}, + {y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1}, + ], + expression_offsets=[0] * 2, + ), + dict( + testcase_name='y.groupby("j").sum() == 0', + name="ygroupbyj_eq_c", + bounded_exprs=( + lambda x, y: y.groupby("j").sum().apply(lambda expr: expr == 0) + ), + constraint_count=3, + lower_bounds=[0] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [ + {y[1, "a"]: 1, y[2, "a"]: 1}, + {y[1, "b"]: 1, y[2, "b"]: 1}, + {y[1, "c"]: 1, y[2, "c"]: 1}, + ], + expression_offsets=[0] * 3, + ), + dict( + testcase_name='3 * x + y.groupby("i").sum() <= 0', + name="broadcast_align_fill", + bounded_exprs=( + lambda x, y: (3 * x) + .add(y.groupby("i").sum(), fill_value=0) + .apply(lambda expr: expr <= 0) + ), + constraint_count=3, + lower_bounds=[-math.inf] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [ + {x[0]: 3}, + {x[1]: 3, y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1}, + {x[2]: 3, y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1}, + ], + expression_offsets=[0] * 3, + ), + ] + + def create_test_model(self, name, bounded_exprs): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables( + name="x", + index=pd.Index(range(3), name="i"), + ) + y = model.create_variables( + name="y", + index=pd.MultiIndex.from_product( + ((1, 2), ("a", "b", "c")), names=["i", "j"] + ), + ) + model.create_linear_constraints(name=name, bounded_exprs=bounded_exprs(x, y)) + return model + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "constraint_count", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraints( + self, + name, + bounded_exprs, + constraint_count, + ): + model = self.create_test_model(name, bounded_exprs) + for linear_constraints, expected_count in ( + (model.get_linear_constraints(), constraint_count), + (model.get_linear_constraints(name), constraint_count), + (model.get_linear_constraints(name), constraint_count), + ): + self.assertIsInstance(linear_constraints, pd.Index) + self.assertLen(linear_constraints, expected_count) + + def test_get_linear_constraints_empty(self): + linear_constraints = pdm.OptimizationModel( + name="test_name" + ).get_linear_constraints() + self.assertIsInstance(linear_constraints, pd.Index) + self.assertEmpty(linear_constraints) + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "constraint_count", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraint_references( + self, + name, + bounded_exprs, + constraint_count, + ): + model = self.create_test_model(name, bounded_exprs) + for linear_constraints, expected_count in ( + (model.get_linear_constraint_references(name=name), constraint_count), + (model.get_linear_constraint_references(name=name), constraint_count), + ): + self.assertIsInstance(linear_constraints, pd.Series) + self.assertLen(linear_constraints, expected_count) + for i in linear_constraints.index: + self.assertEqual(repr(linear_constraints[i]), f"{name}[{i}]") + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "lower_bounds", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraint_lower_bounds( + self, + name, + bounded_exprs, + lower_bounds, + ): + model = self.create_test_model(name, bounded_exprs) + for linear_constraint_lower_bounds in ( + model.get_linear_constraint_lower_bounds(), + model.get_linear_constraint_lower_bounds(model.get_linear_constraints()), + model.get_linear_constraint_lower_bounds( + model.get_linear_constraint_references(name) + ), + ): + self.assertSequenceAlmostEqual(linear_constraint_lower_bounds, lower_bounds) + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "upper_bounds", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraint_upper_bounds( + self, + name, + bounded_exprs, + upper_bounds, + ): + model = self.create_test_model(name, bounded_exprs) + for linear_constraint_upper_bounds in ( + model.get_linear_constraint_upper_bounds(), + model.get_linear_constraint_upper_bounds(model.get_linear_constraints()), + model.get_linear_constraint_upper_bounds( + model.get_linear_constraint_references(name) + ), + ): + self.assertSequenceAlmostEqual(linear_constraint_upper_bounds, upper_bounds) + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "expression_terms", + "expression_offsets", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraint_expressions( + self, + name, + bounded_exprs, + expression_terms, + expression_offsets, + ): + model = self.create_test_model(name, bounded_exprs) + x = model.get_variable_references(name="x") + y = model.get_variable_references(name="y") + for linear_constraint_expressions in ( + model.get_linear_constraint_expressions(), + model.get_linear_constraint_expressions(model.get_linear_constraints()), + model.get_linear_constraint_expressions( + model.get_linear_constraint_references(name) + ), + ): + expr_terms = expression_terms(x, y) + self.assertLen(linear_constraint_expressions, len(expr_terms)) + for expr, expr_term in zip(linear_constraint_expressions, expr_terms): + self.assertDictEqual(expr._terms, expr_term) + self.assertSequenceAlmostEqual( + [expr._offset for expr in linear_constraint_expressions], + expression_offsets, + ) + + +class OptimizationModelObjectiveTest(parameterized.TestCase): + _expressions = ( + lambda x, y: -3, + lambda x, y: 0, + lambda x, y: 10, + lambda x, y: x[0], + lambda x, y: x[1], + lambda x, y: x[2], + lambda x, y: y[0], + lambda x, y: y[1], + lambda x, y: x[0] + 5, + lambda x, y: -3 + y[1], + lambda x, y: 3 * x[0], + lambda x, y: x[0] * 3 * 5 - 3, + lambda x, y: x.sum(), + lambda x, y: 101 + 2 * 3 * x.sum(), + lambda x, y: x.sum() * 2, + lambda x, y: sum(y), + lambda x, y: x.sum() + 2 * y.sum() + 3, + ) + _variable_indices = ( + pd.Index(range(3)), + pd.Index(range(3), name="i"), + pd.Index(range(10), name="i"), + ) + + def assertLinearExpressionAlmostEqual( + self, + expr1: pdm._LinearExpression, + expr2: pdm._LinearExpression, + ) -> None: + """Test that the two linear expressions are almost equal.""" + for variable, coeff in expr1._terms.items(): + self.assertAlmostEqual(expr2._terms.get(variable, 0), coeff) + for variable, coeff in expr2._terms.items(): + self.assertAlmostEqual(expr1._terms.get(variable, 0), coeff) + self.assertAlmostEqual(expr1._offset, expr2._offset) + + @parameterized.product( + expression=_expressions, + variable_indices=_variable_indices, + sense=(pdm.ObjectiveSense.MINIMIZE, pdm.ObjectiveSense.MAXIMIZE), + ) + def test_set_objective( + self, + expression: Callable[[pd.Series, pd.Series], pdm._LinearType], + variable_indices: pd.Index, + sense: pdm.ObjectiveSense, + ): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables(name="x", index=variable_indices) + y = model.create_variables(name="y", index=variable_indices) + objective_expression = pdm._as_flat_linear_expression(expression(x, y)) + model.set_objective(expression=objective_expression, sense=sense) + self.assertEqual(model.get_objective_sense(), sense) + got_objective_expression = model.get_objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, objective_expression + ) + + def test_set_new_objective(self): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables(name="x", index=pd.Index(range(3))) + old_objective_expression = 1 + new_objective_expression = pdm._as_flat_linear_expression(x.sum() - 2.3) + + # Set and check for old objective. + model.set_objective( + expression=old_objective_expression, sense=pdm.ObjectiveSense.MAXIMIZE + ) + self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MAXIMIZE) + got_objective_expression = model.get_objective_expression() + for var_coeff in got_objective_expression._terms.values(): + self.assertAlmostEqual(var_coeff, 0) + self.assertAlmostEqual(got_objective_expression._offset, 1) + + # Set to a new objective and check that it is different. + model.set_objective( + expression=new_objective_expression, sense=pdm.ObjectiveSense.MINIMIZE + ) + self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MINIMIZE) + got_objective_expression = model.get_objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, new_objective_expression + ) + + @parameterized.product( + expression=_expressions, + variable_indices=_variable_indices, + ) + def test_minimize( + self, + expression: Callable[[pd.Series, pd.Series], pdm._LinearType], + variable_indices: pd.Index, + ): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables(name="x", index=variable_indices) + y = model.create_variables(name="y", index=variable_indices) + objective_expression = pdm._as_flat_linear_expression(expression(x, y)) + model.minimize(expression=objective_expression) + self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MINIMIZE) + got_objective_expression = model.get_objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, objective_expression + ) + + @parameterized.product( + expression=_expressions, + variable_indices=_variable_indices, + ) + def test_maximize( + self, + expression: Callable[[pd.Series, pd.Series], float], + variable_indices: pd.Index, + ): + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables(name="x", index=variable_indices) + y = model.create_variables(name="y", index=variable_indices) + objective_expression = pdm._as_flat_linear_expression(expression(x, y)) + model.maximize(expression=objective_expression) + self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MAXIMIZE) + got_objective_expression = model.get_objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, objective_expression + ) + + +class OptimizationModelProtoTest(absltest.TestCase): + def test_to_proto(self): + expected = linear_solver_pb2.MPModelProto() + text_format.Parse( + """ + name: "test_name" + maximize: true + objective_offset: 0 + variable { + lower_bound: 0 + upper_bound: 1000 + objective_coefficient: 1 + is_integer: false + name: "x[0]" + } + variable { + lower_bound: 0 + upper_bound: 1000 + objective_coefficient: 1 + is_integer: false + name: "x[1]" + } + constraint { + var_index: 0 + coefficient: 1 + lower_bound: -inf + upper_bound: 10 + name: "Ct[0]" + } + constraint { + var_index: 1 + coefficient: 1 + lower_bound: -inf + upper_bound: 10 + name: "Ct[1]" + } + """, + expected, + ) + model = pdm.OptimizationModel(name="test_name") + x = model.create_variables("x", pd.Index(range(2)), 0, 1000) + model.create_linear_constraints("Ct", x.apply(lambda expr: expr <= 10)) + model.maximize(expression=x.sum()) + self.assertEqual(str(expected), str(model.to_proto())) + + +class SolverTest(parameterized.TestCase): + _solvers = ( + { + "type": pdm.SolverType.CP_SAT, + "options": pdm.SolveOptions(), + "is_integer": True, # CP-SAT supports only pure integer variables. + }, + { + "type": pdm.SolverType.GLOP, + "options": pdm.SolveOptions( + # Disable GLOP's presolve to correctly trigger unboundedness: + # https://github.com/google/or-tools/issues/3319 + solver_specific_parameters="use_preprocessing: False" + ), + "is_integer": False, # GLOP does not properly support integers. + }, + { + "type": pdm.SolverType.SCIP, + "options": pdm.SolveOptions(), + "is_integer": False, + }, + { + "type": pdm.SolverType.SCIP, + "options": pdm.SolveOptions(), + "is_integer": True, + }, + ) + _variable_indices = ( + pd.Index(range(0)), # No variables. + pd.Index(range(1)), # Single variable. + pd.Index(range(3)), # Multiple variables. + ) + _variable_bounds = (-1, 0, 10.1) + _solve_statuses = ( + pdm.SolveStatus.OPTIMAL, + pdm.SolveStatus.INFEASIBLE, + pdm.SolveStatus.UNBOUNDED, + ) + _set_objectives = (True, False) + _objective_senses = ( + pdm.ObjectiveSense.MAXIMIZE, + pdm.ObjectiveSense.MINIMIZE, + ) + _objective_expressions = ( + lambda x: x.sum(), + lambda x: x.sum() + 5.2, + lambda x: -10.1, + lambda x: 0, + ) + + def _create_model( + self, + variable_indices: pd.Index = pd.Index(range(3)), + variable_bound: float = 0, + is_integer: bool = False, + solve_status: pdm.SolveStatus = pdm.SolveStatus.OPTIMAL, + set_objective: bool = True, + objective_sense: pdm.ObjectiveSense = pdm.ObjectiveSense.MAXIMIZE, + objective_expression: Callable[[pd.Series], float] = lambda x: x.sum(), + ) -> pdm.OptimizationModel: + """Constructs an optimization problem. + + It has the following formulation: + + ``` + objective_sense (MAXIMIZE / MINIMIZE) objective_expression(x) + satisfying constraints + (if solve_status != UNBOUNDED and objective_sense == MAXIMIZE) + x[variable_indices] <= variable_bound + (if solve_status != UNBOUNDED and objective_sense == MINIMIZE) + x[variable_indices] >= variable_bound + x[variable_indices] is_integer + False (if solve_status == INFEASIBLE) + ``` + + Args: + variable_indices (pd.Index): The indices of the variable(s). + variable_bound (float): The upper- or lower-bound(s) of the variable(s). + is_integer (bool): Whether the variables should be integer. + solve_status (pdm.SolveStatus): The solve status to target. + set_objective (bool): Whether to set the objective of the model. + objective_sense (pdm.ObjectiveSense): MAXIMIZE or MINIMIZE, + objective_expression (Callable[[pd.Series], float]): The expression to + maximize or minimize if set_objective=True. + + Returns: + pdm.OptimizationModel: The resulting problem. + """ + model = pdm.OptimizationModel(name="test") + # Variable(s) + x = model.create_variables( + name="x", + index=pd.Index(variable_indices), + is_integer=is_integer, + ) + # Constraint(s) + if solve_status == pdm.SolveStatus.INFEASIBLE: + # Force infeasibility here to test that we get pd.NA later. + model.create_linear_constraints("bool", False) + elif solve_status != pdm.SolveStatus.UNBOUNDED: + if objective_sense == pdm.ObjectiveSense.MAXIMIZE: + model.create_linear_constraints( + "upper_bound", x.apply(lambda xi: xi <= variable_bound) + ) + elif objective_sense == pdm.ObjectiveSense.MINIMIZE: + model.create_linear_constraints( + "lower_bound", x.apply(lambda xi: xi >= variable_bound) + ) + # Objective + if set_objective: + model.set_objective( + expression=objective_expression(x), sense=objective_sense + ) + return model + + @parameterized.product( + solver=_solvers, + variable_indices=_variable_indices, + variable_bound=_variable_bounds, + solve_status=_solve_statuses, + set_objective=_set_objectives, + objective_sense=_objective_senses, + objective_expression=_objective_expressions, + ) + def test_solve_status( + self, + solver: dict[str, Union[pdm.SolverType, pdm.SolveOptions, bool]], + variable_indices: pd.Index, + variable_bound: float, + solve_status: pdm.SolveStatus, + set_objective: bool, + objective_sense: pdm.ObjectiveSense, + objective_expression: Callable[[pd.Series], float], + ): + model = self._create_model( + variable_indices=variable_indices, + variable_bound=variable_bound, + is_integer=solver["is_integer"], + solve_status=solve_status, + set_objective=set_objective, + objective_sense=objective_sense, + objective_expression=objective_expression, + ) + solve_result = pdm.Solver(solver_type=solver["type"]).solve( + model=model, options=solver["options"] + ) + + # pylint: disable=g-explicit-length-test + # (we disable explicit-length-test here because `variable_indices: pd.Index` + # evaluates to an ambiguous boolean value.) + if len(variable_indices) > 0: # Test cases with >=1 variable. + self.assertNotEmpty(variable_indices) + if ( + isinstance( + objective_expression(model.get_variable_references("x")), + (int, float), + ) + and solve_status != pdm.SolveStatus.INFEASIBLE + ): + # Feasibility implies optimality when objective is a constant term. + self.assertEqual(solve_result.get_status(), pdm.SolveStatus.OPTIMAL) + elif not set_objective and solve_status != pdm.SolveStatus.INFEASIBLE: + # Feasibility implies optimality when objective is not set. + self.assertEqual(solve_result.get_status(), pdm.SolveStatus.OPTIMAL) + elif ( + solver["type"] == pdm.SolverType.CP_SAT + and solve_result.get_status() == pdm.SolveStatus.UNKNOWN + ): + # CP_SAT returns unknown for some of the infeasible and unbounded cases. + self.assertIn( + solve_status, + (pdm.SolveStatus.INFEASIBLE, pdm.SolveStatus.UNBOUNDED), + ) + else: + self.assertEqual(solve_result.get_status(), solve_status) + elif solve_status == pdm.SolveStatus.UNBOUNDED: + # Unbounded problems are optimal when there are no variables. + self.assertEqual(solve_result.get_status(), pdm.SolveStatus.OPTIMAL) + else: + self.assertEqual(solve_result.get_status(), solve_status) + + @parameterized.product( + solver=_solvers, + variable_indices=_variable_indices, + variable_bound=_variable_bounds, + solve_status=_solve_statuses, + set_objective=_set_objectives, + objective_sense=_objective_senses, + objective_expression=_objective_expressions, + ) + def test_get_variable_values( + self, + solver: dict[str, Union[pdm.SolverType, pdm.SolveOptions, bool]], + variable_indices: pd.Index, + variable_bound: float, + solve_status: pdm.SolveStatus, + set_objective: bool, + objective_sense: pdm.ObjectiveSense, + objective_expression: Callable[[pd.Series], float], + ): + model = self._create_model( + variable_indices=variable_indices, + variable_bound=variable_bound, + is_integer=solver["is_integer"], + solve_status=solve_status, + set_objective=set_objective, + objective_sense=objective_sense, + objective_expression=objective_expression, + ) + solve_result = pdm.Solver(solver_type=solver["type"]).solve( + model=model, options=solver["options"] + ) + for variables in ( + None, # We get all variables when none is specified. + model.get_variables()[:2], # We can filter to a subset (pd.Index). + model.get_variable_references("x")[:2], # It works for pd.Series. + ): + variable_values = solve_result.get_variable_values(variables) + # Test the type of `variable_values` (we always get pd.Series) + self.assertIsInstance(variable_values, pd.Series) + # Test the index of `variable_values` (match the input variables [if any]) + self.assertSequenceAlmostEqual( + variable_values.index, + pdm._get_index(model._get_variables(variables)), + ) + if solve_result.get_status() not in ( + pdm.SolveStatus.OPTIMAL, + pdm.SolveStatus.FEASIBLE, + ): + # self.assertSequenceAlmostEqual does not work here because we cannot do + # equality comparison for NA values (NAs will propagate and we will get + # 'TypeError: boolean value of NA is ambiguous') + for variable_value in variable_values: + self.assertTrue(pd.isna(variable_value)) + elif set_objective and not isinstance( + objective_expression(model.get_variable_references("x")), + (int, float), + ): + # The variable values are only well-defined when the objective is set + # and depends on the variable(s). + if not solver["is_integer"]: + self.assertSequenceAlmostEqual( + variable_values, [variable_bound] * len(variable_values) + ) + elif objective_sense == pdm.ObjectiveSense.MAXIMIZE: + self.assertTrue(solver["is_integer"]) # Assert a known assumption. + self.assertSequenceAlmostEqual( + variable_values, + [math.floor(variable_bound)] * len(variable_values), + ) + else: + self.assertTrue(solver["is_integer"]) # Assert a known assumption. + self.assertEqual(objective_sense, pdm.ObjectiveSense.MINIMIZE) + self.assertSequenceAlmostEqual( + variable_values, + [math.ceil(variable_bound)] * len(variable_values), + ) + + @parameterized.product( + solver=_solvers, + variable_indices=_variable_indices, + variable_bound=_variable_bounds, + solve_status=_solve_statuses, + set_objective=_set_objectives, + objective_sense=_objective_senses, + objective_expression=_objective_expressions, + ) + def test_get_objective_value( + self, + solver: dict[str, Union[pdm.SolverType, pdm.SolveOptions, bool]], + variable_indices: pd.Index, + variable_bound: float, + solve_status: pdm.SolveStatus, + set_objective: bool, + objective_sense: pdm.ObjectiveSense, + objective_expression: Callable[[pd.Series], float], + ): + model = self._create_model( + variable_indices=variable_indices, + variable_bound=variable_bound, + is_integer=solver["is_integer"], + solve_status=solve_status, + set_objective=set_objective, + objective_sense=objective_sense, + objective_expression=objective_expression, + ) + solve_result = pdm.Solver(solver_type=solver["type"]).solve( + model=model, options=solver["options"] + ) + + # Test objective value + if solve_result.get_status() not in ( + pdm.SolveStatus.OPTIMAL, + pdm.SolveStatus.FEASIBLE, + ): + self.assertTrue(pd.isna(solve_result.get_objective_value())) + return + if set_objective: + variable_values = solve_result.get_variable_values(model.get_variables()) + self.assertAlmostEqual( + solve_result.get_objective_value(), + objective_expression(variable_values), + ) + else: + self.assertAlmostEqual(solve_result.get_objective_value(), 0) + + +if __name__ == "__main__": + absltest.main() diff --git a/ortools/linear_solver/python/pywrap_model_builder_helper.cc b/ortools/linear_solver/python/pywrap_model_builder_helper.cc index 8cbd7d5dd0..1f2aba2ed1 100644 --- a/ortools/linear_solver/python/pywrap_model_builder_helper.cc +++ b/ortools/linear_solver/python/pywrap_model_builder_helper.cc @@ -36,6 +36,7 @@ #include "pybind11/pybind11.h" #include "pybind11/pytypes.h" #include "pybind11/stl.h" +#include "pybind11_protobuf/native_proto_caster.h" using ::Eigen::SparseMatrix; using ::Eigen::VectorXd; @@ -52,6 +53,10 @@ using ::operations_research::SolveStatus; namespace py = pybind11; using ::py::arg; +const MPModelProto& ToMPModelProto(ModelBuilderHelper* helper) { + return helper->model(); +} + // TODO(user): The interface uses serialized protos because of issues building // pybind11_protobuf. See // https://github.com/protocolbuffers/protobuf/issues/9464. After @@ -146,6 +151,10 @@ std::vector> SortedGroupedTerms( } PYBIND11_MODULE(pywrap_model_builder_helper, m) { + pybind11_protobuf::ImportNativeProtoCasters(); + + m.def("to_mpmodel_proto", &ToMPModelProto, arg("helper")); + py::class_(m, "MPModelExportOptions") .def(py::init<>()) .def_readwrite("obfuscate", &MPModelExportOptions::obfuscate) diff --git a/ortools/linear_solver/python/pywraplp_test.py b/ortools/linear_solver/python/pywraplp_test.py old mode 100755 new mode 100644 index fa6e3fc367..64caec4d37 --- a/ortools/linear_solver/python/pywraplp_test.py +++ b/ortools/linear_solver/python/pywraplp_test.py @@ -11,6 +11,7 @@ # 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. + """Simple unit tests for python/linear_solver.i. Not exhaustive.""" import unittest @@ -41,11 +42,10 @@ constraint { class PyWrapLp(unittest.TestCase): - def test_proto(self): input_proto = linear_solver_pb2.MPModelProto() text_format.Merge(TEXT_MODEL, input_proto) - solver = pywraplp.Solver.CreateSolver('CBC') + solver = pywraplp.Solver.CreateSolver("CBC") if not solver: return # For now, create the model from the proto by parsing the proto @@ -61,23 +61,23 @@ class PyWrapLp(unittest.TestCase): self.assertEqual(solution.best_objective_bound, 3.0) def test_external_api(self): - solver = pywraplp.Solver.CreateSolver('GLOP') + solver = pywraplp.Solver.CreateSolver("GLOP") infinity = solver.Infinity() infinity2 = solver.infinity() self.assertEqual(infinity, infinity2) # x1, x2 and x3 are continuous non-negative variables. - x1 = solver.NumVar(0.0, infinity, 'x1') - x2 = solver.NumVar(0.0, infinity, 'x2') - x3 = solver.NumVar(0.0, infinity, 'x3') + x1 = solver.NumVar(0.0, infinity, "x1") + x2 = solver.NumVar(0.0, infinity, "x2") + x3 = solver.NumVar(0.0, infinity, "x3") self.assertEqual(x1.Lb(), 0) self.assertEqual(x1.Ub(), infinity) self.assertFalse(x1.Integer()) solver.Maximize(10 * x1 + 6 * x2 + 4 * x3 + 5) self.assertEqual(solver.Objective().Offset(), 5) - c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, 'ConstraintName0') + c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) sum_of_vars = sum([x1, x2, x3]) - solver.Add(sum_of_vars <= 100.0, 'OtherConstraintName') + solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") self.assertEqual(c1.Lb(), -infinity) self.assertEqual(c1.Ub(), 300) c1.SetLb(-100000) @@ -94,5 +94,5 @@ class PyWrapLp(unittest.TestCase): self.assertAlmostEqual(c0.DualValue(), 0.6666666666666667) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/ortools/linear_solver/samples/assignment_groups_mip.py b/ortools/linear_solver/samples/assignment_groups_mip.py old mode 100755 new mode 100644 index 8ae7a72198..c9797ab6d8 --- a/ortools/linear_solver/samples/assignment_groups_mip.py +++ b/ortools/linear_solver/samples/assignment_groups_mip.py @@ -70,7 +70,7 @@ def main(): # Solver. # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: return # [END solver] @@ -82,31 +82,30 @@ def main(): x = {} for worker in range(num_workers): for task in range(num_tasks): - x[worker, task] = solver.BoolVar(f'x[{worker},{task}]') + x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") # [END variables] # Constraints # [START constraints] # The total size of the tasks each worker takes on is at most total_size_max. for worker in range(num_workers): - solver.Add( - solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) + solver.Add(solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) # Each task is assigned to exactly one worker. for task in range(num_tasks): - solver.Add( - solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) + solver.Add(solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) # [END constraints] # [START assignments] # Create variables for each worker, indicating whether they work on some task. work = {} for worker in range(num_workers): - work[worker] = solver.BoolVar(f'work[{worker}]') + work[worker] = solver.BoolVar(f"work[{worker}]") for worker in range(num_workers): - solver.Add(work[worker] == solver.Sum( - [x[worker, task] for task in range(num_tasks)])) + solver.Add( + work[worker] == solver.Sum([x[worker, task] for task in range(num_tasks)]) + ) # Group1 constraint_g1 = solver.Constraint(1, 1) @@ -116,7 +115,7 @@ def main(): constraint = solver.Constraint(0, 1) constraint.SetCoefficient(work[group1[i][0]], 1) constraint.SetCoefficient(work[group1[i][1]], 1) - p = solver.BoolVar(f'g1_p{i}') + p = solver.BoolVar(f"g1_p{i}") constraint.SetCoefficient(p, -2) constraint_g1.SetCoefficient(p, 1) @@ -129,7 +128,7 @@ def main(): constraint = solver.Constraint(0, 1) constraint.SetCoefficient(work[group2[i][0]], 1) constraint.SetCoefficient(work[group2[i][1]], 1) - p = solver.BoolVar(f'g2_p{i}') + p = solver.BoolVar(f"g2_p{i}") constraint.SetCoefficient(p, -2) constraint_g2.SetCoefficient(p, 1) @@ -142,7 +141,7 @@ def main(): constraint = solver.Constraint(0, 1) constraint.SetCoefficient(work[group3[i][0]], 1) constraint.SetCoefficient(work[group3[i][1]], 1) - p = solver.BoolVar(f'g3_p{i}') + p = solver.BoolVar(f"g3_p{i}") constraint.SetCoefficient(p, -2) constraint_g3.SetCoefficient(p, 1) @@ -165,17 +164,19 @@ def main(): # Print solution. # [START print_solution] if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f'Total cost = {solver.Objective().Value()}\n') + print(f"Total cost = {solver.Objective().Value()}\n") for worker in range(num_workers): for task in range(num_tasks): if x[worker, task].solution_value() > 0.5: - print(f'Worker {worker} assigned to task {task}.' + - f' Cost: {costs[worker][task]}') + print( + f"Worker {worker} assigned to task {task}." + + f" Cost: {costs[worker][task]}" + ) else: - print('No solution found.') + print("No solution found.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_mb.py b/ortools/linear_solver/samples/assignment_mb.py index 1ed8c19613..1d60227ef8 100644 --- a/ortools/linear_solver/samples/assignment_mb.py +++ b/ortools/linear_solver/samples/assignment_mb.py @@ -11,6 +11,7 @@ # 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. + """MIP example that solves an assignment problem.""" # [START program] # [START import] @@ -23,13 +24,15 @@ from ortools.linear_solver.python import model_builder def main(): # Data # [START data_model] - costs = np.array([ - [90, 80, 75, 70], - [35, 85, 55, 65], - [125, 95, 90, 95], - [45, 110, 95, 115], - [50, 100, 90, 100], - ]) + costs = np.array( + [ + [90, 80, 75, 70], + [35, 85, 55, 65], + [125, 95, 90, 95], + [45, 110, 95, 115], + [50, 100, 90, 100], + ] + ) num_workers, num_tasks = costs.shape # [END data_model] @@ -42,7 +45,9 @@ def main(): # [START variables] # x[i, j] is an array of 0-1 variables, which will be 1 # if worker i is assigned to task j. - x = model.new_bool_var_array(shape=[num_workers, num_tasks], name='x') # pytype: disable=wrong-arg-types # numpy-scalars + x = model.new_bool_var_array( + shape=[num_workers, num_tasks], name="x" + ) # pytype: disable=wrong-arg-types # numpy-scalars # [END variables] # Constraints @@ -63,26 +68,27 @@ def main(): # [START solve] # Create the solver with the CP-SAT backend, and solve the model. - solver = model_builder.ModelSolver('sat') + solver = model_builder.ModelSolver("sat") status = solver.solve(model) # [END solve] # Print solution. # [START print_solution] - if (status == model_builder.SolveStatus.OPTIMAL or - status == model_builder.SolveStatus.FEASIBLE): - print(f'Total cost = {solver.objective_value}\n') + if ( + status == model_builder.SolveStatus.OPTIMAL + or status == model_builder.SolveStatus.FEASIBLE + ): + print(f"Total cost = {solver.objective_value}\n") for i in range(num_workers): for j in range(num_tasks): # Test if x[i,j] is 1 (with tolerance for floating point arithmetic). if solver.value(x[i, j]) > 0.5: - print(f'Worker {i} assigned to task {j}.' + - f' Cost: {costs[i][j]}') + print(f"Worker {i} assigned to task {j}." + f" Cost: {costs[i][j]}") else: - print('No solution found.') + print("No solution found.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_mip.py b/ortools/linear_solver/samples/assignment_mip.py old mode 100755 new mode 100644 index 2a94f82acf..d30d3222dc --- a/ortools/linear_solver/samples/assignment_mip.py +++ b/ortools/linear_solver/samples/assignment_mip.py @@ -11,6 +11,7 @@ # 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. + """MIP example that solves an assignment problem.""" # [START program] # [START import] @@ -35,7 +36,7 @@ def main(): # Solver # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: return @@ -48,7 +49,7 @@ def main(): x = {} for i in range(num_workers): for j in range(num_tasks): - x[i, j] = solver.IntVar(0, 1, '') + x[i, j] = solver.IntVar(0, 1, "") # [END variables] # Constraints @@ -79,18 +80,17 @@ def main(): # Print solution. # [START print_solution] if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f'Total cost = {solver.Objective().Value()}\n') + print(f"Total cost = {solver.Objective().Value()}\n") for i in range(num_workers): for j in range(num_tasks): # Test if x[i,j] is 1 (with tolerance for floating point arithmetic). if x[i, j].solution_value() > 0.5: - print(f'Worker {i} assigned to task {j}.' + - f' Cost: {costs[i][j]}') + print(f"Worker {i} assigned to task {j}." + f" Cost: {costs[i][j]}") else: - print('No solution found.') + print("No solution found.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_task_sizes_mip.py b/ortools/linear_solver/samples/assignment_task_sizes_mip.py old mode 100755 new mode 100644 index 6d213e124f..a1d53a0f3b --- a/ortools/linear_solver/samples/assignment_task_sizes_mip.py +++ b/ortools/linear_solver/samples/assignment_task_sizes_mip.py @@ -45,7 +45,7 @@ def main(): # Solver # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: return @@ -58,7 +58,7 @@ def main(): x = {} for worker in range(num_workers): for task in range(num_tasks): - x[worker, task] = solver.BoolVar(f'x[{worker},{task}]') + x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") # [END variables] # Constraints @@ -66,14 +66,15 @@ def main(): # The total size of the tasks each worker takes on is at most total_size_max. for worker in range(num_workers): solver.Add( - solver.Sum([ - task_sizes[task] * x[worker, task] for task in range(num_tasks) - ]) <= total_size_max) + solver.Sum( + [task_sizes[task] * x[worker, task] for task in range(num_tasks)] + ) + <= total_size_max + ) # Each task is assigned to exactly one worker. for task in range(num_tasks): - solver.Add( - solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) + solver.Add(solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) # [END constraints] # Objective @@ -93,17 +94,19 @@ def main(): # Print solution. # [START print_solution] if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f'Total cost = {solver.Objective().Value()}\n') + print(f"Total cost = {solver.Objective().Value()}\n") for worker in range(num_workers): for task in range(num_tasks): if x[worker, task].solution_value() > 0.5: - print(f'Worker {worker} assigned to task {task}.' + - f' Cost: {costs[worker][task]}') + print( + f"Worker {worker} assigned to task {task}." + + f" Cost: {costs[worker][task]}" + ) else: - print('No solution found.') + print("No solution found.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_teams_mip.py b/ortools/linear_solver/samples/assignment_teams_mip.py old mode 100755 new mode 100644 index 792f82cc41..3e9e616b78 --- a/ortools/linear_solver/samples/assignment_teams_mip.py +++ b/ortools/linear_solver/samples/assignment_teams_mip.py @@ -42,7 +42,7 @@ def main(): # Solver # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: return # [END solver] @@ -54,20 +54,18 @@ def main(): x = {} for worker in range(num_workers): for task in range(num_tasks): - x[worker, task] = solver.BoolVar(f'x[{worker},{task}]') + x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") # [END variables] # Constraints # [START constraints] # Each worker is assigned at most 1 task. for worker in range(num_workers): - solver.Add( - solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) + solver.Add(solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) # Each task is assigned to exactly one worker. for task in range(num_tasks): - solver.Add( - solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) + solver.Add(solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) # Each team takes at most two tasks. team1_tasks = [] @@ -100,18 +98,20 @@ def main(): # Print solution. # [START print_solution] if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f'Total cost = {solver.Objective().Value()}\n') + print(f"Total cost = {solver.Objective().Value()}\n") for worker in range(num_workers): for task in range(num_tasks): if x[worker, task].solution_value() > 0.5: - print(f'Worker {worker} assigned to task {task}.' + - f' Cost = {costs[worker][task]}') + print( + f"Worker {worker} assigned to task {task}." + + f" Cost = {costs[worker][task]}" + ) else: - print('No solution found.') - print(f'Time = {solver.WallTime()} ms') + print("No solution found.") + print(f"Time = {solver.WallTime()} ms") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/basic_example.py b/ortools/linear_solver/samples/basic_example.py old mode 100755 new mode 100644 index c86a269972..fc48b93788 --- a/ortools/linear_solver/samples/basic_example.py +++ b/ortools/linear_solver/samples/basic_example.py @@ -11,37 +11,37 @@ # 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. + """Minimal example to call the GLOP solver.""" # [START program] # [START import] from ortools.linear_solver import pywraplp -from ortools.init import pywrapinit # [END import] def main(): # [START solver] # Create the linear solver with the GLOP backend. - solver = pywraplp.Solver.CreateSolver('GLOP') + solver = pywraplp.Solver.CreateSolver("GLOP") if not solver: return # [END solver] # [START variables] # Create the variables x and y. - x = solver.NumVar(0, 1, 'x') - y = solver.NumVar(0, 2, 'y') + x = solver.NumVar(0, 1, "x") + y = solver.NumVar(0, 2, "y") - print('Number of variables =', solver.NumVariables()) + print("Number of variables =", solver.NumVariables()) # [END variables] # [START constraints] # Create a linear constraint, 0 <= x + y <= 2. - ct = solver.Constraint(0, 2, 'ct') + ct = solver.Constraint(0, 2, "ct") ct.SetCoefficient(x, 1) ct.SetCoefficient(y, 1) - print('Number of constraints =', solver.NumConstraints()) + print("Number of constraints =", solver.NumConstraints()) # [END constraints] # [START objective] @@ -57,19 +57,13 @@ def main(): # [END solve] # [START print_solution] - print('Solution:') - print('Objective value =', objective.Value()) - print('x =', x.solution_value()) - print('y =', y.solution_value()) + print("Solution:") + print("Objective value =", objective.Value()) + print("x =", x.solution_value()) + print("y =", y.solution_value()) # [END print_solution] -if __name__ == '__main__': - pywrapinit.CppBridge.InitLogging('basic_example.py') - cpp_flags = pywrapinit.CppFlags() - cpp_flags.logtostderr = True - cpp_flags.log_prefix = False - pywrapinit.CppBridge.SetFlags(cpp_flags) - +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/bin_packing_mb.py b/ortools/linear_solver/samples/bin_packing_mb.py index 1fd0e0890d..bae4d746d4 100644 --- a/ortools/linear_solver/samples/bin_packing_mb.py +++ b/ortools/linear_solver/samples/bin_packing_mb.py @@ -11,6 +11,7 @@ # 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. + """Solve a simple bin packing problem using a MIP solver.""" # [START program] # [START import] @@ -26,10 +27,10 @@ def create_data_model(): """Create the data for the example.""" data = {} weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30] - data['weights'] = weights - data['items'] = list(range(len(weights))) - data['bins'] = data['items'] - data['bin_capacity'] = 100 + data["weights"] = weights + data["items"] = list(range(len(weights))) + data["bins"] = data["items"] + data["bin_capacity"] = 100 return data # [END data_model] @@ -38,8 +39,8 @@ def create_data_model(): def main(): # [START data] data = create_data_model() - num_items = len(data['items']) - num_bins = len(data['bins']) + num_items = len(data["items"]) + num_bins = len(data["bins"]) # [END data] # [END program_part1] @@ -52,22 +53,25 @@ def main(): # [START variables] # Variables # x[i, j] = 1 if item i is packed in bin j. - x = model.new_bool_var_array(shape=[num_items, num_bins], name='x') # pytype: disable=wrong-arg-types # numpy-scalars + x = model.new_bool_var_array( + shape=[num_items, num_bins], name="x" + ) # pytype: disable=wrong-arg-types # numpy-scalars # y[j] = 1 if bin j is used. - y = model.new_bool_var_array(shape=[num_bins], name='y') # pytype: disable=wrong-arg-types # numpy-scalars + y = model.new_bool_var_array( + shape=[num_bins], name="y" + ) # pytype: disable=wrong-arg-types # numpy-scalars # [END variables] # [START constraints] # Constraints # Each item must be in exactly one bin. - for i in data['items']: + for i in data["items"]: model.add(np.sum(x[i, :]) == 1) # The amount packed in each bin cannot exceed its capacity. - for j in data['bins']: - model.add( - np.dot(x[:, j], data['weights']) <= data['bin_capacity'] * y[j]) + for j in data["bins"]: + model.add(np.dot(x[:, j], data["weights"]) <= data["bin_capacity"] * y[j]) # [END constraints] # [START objective] @@ -77,36 +81,36 @@ def main(): # [START solve] # Create the solver with the CP-SAT backend, and solve the model. - solver = model_builder.ModelSolver('sat') + solver = model_builder.ModelSolver("sat") status = solver.solve(model) # [END solve] # [START print_solution] if status == model_builder.SolveStatus.OPTIMAL: - num_bins = 0. - for j in data['bins']: + num_bins = 0.0 + for j in data["bins"]: if solver.value(y[j]) == 1: bin_items = [] bin_weight = 0 - for i in data['items']: + for i in data["items"]: if solver.value(x[i, j]) > 0: bin_items.append(i) - bin_weight += data['weights'][i] + bin_weight += data["weights"][i] if bin_weight > 0: num_bins += 1 - print('Bin number', j) - print(' Items packed:', bin_items) - print(' Total weight:', bin_weight) + print("Bin number", j) + print(" Items packed:", bin_items) + print(" Total weight:", bin_weight) print() print() - print('Number of bins used:', num_bins) - print('Time = ', solver.wall_time, ' seconds') + print("Number of bins used:", num_bins) + print("Time = ", solver.wall_time, " seconds") else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program_part2] # [END program] diff --git a/ortools/linear_solver/samples/bin_packing_mip.py b/ortools/linear_solver/samples/bin_packing_mip.py index ae93acf751..77468e917c 100755 --- a/ortools/linear_solver/samples/bin_packing_mip.py +++ b/ortools/linear_solver/samples/bin_packing_mip.py @@ -11,6 +11,7 @@ # 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. + """Solve a simple bin packing problem using a MIP solver.""" # [START program] # [START import] @@ -24,10 +25,10 @@ def create_data_model(): """Create the data for the example.""" data = {} weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30] - data['weights'] = weights - data['items'] = list(range(len(weights))) - data['bins'] = data['items'] - data['bin_capacity'] = 100 + data["weights"] = weights + data["items"] = list(range(len(weights))) + data["bins"] = data["items"] + data["bin_capacity"] = 100 return data # [END data_model] @@ -41,7 +42,7 @@ def main(): # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: return @@ -52,32 +53,33 @@ def main(): # Variables # x[i, j] = 1 if item i is packed in bin j. x = {} - for i in data['items']: - for j in data['bins']: - x[(i, j)] = solver.IntVar(0, 1, 'x_%i_%i' % (i, j)) + for i in data["items"]: + for j in data["bins"]: + x[(i, j)] = solver.IntVar(0, 1, "x_%i_%i" % (i, j)) # y[j] = 1 if bin j is used. y = {} - for j in data['bins']: - y[j] = solver.IntVar(0, 1, 'y[%i]' % j) + for j in data["bins"]: + y[j] = solver.IntVar(0, 1, "y[%i]" % j) # [END variables] # [START constraints] # Constraints # Each item must be in exactly one bin. - for i in data['items']: - solver.Add(sum(x[i, j] for j in data['bins']) == 1) + for i in data["items"]: + solver.Add(sum(x[i, j] for j in data["bins"]) == 1) # The amount packed in each bin cannot exceed its capacity. - for j in data['bins']: + for j in data["bins"]: solver.Add( - sum(x[(i, j)] * data['weights'][i] for i in data['items']) <= y[j] * - data['bin_capacity']) + sum(x[(i, j)] * data["weights"][i] for i in data["items"]) + <= y[j] * data["bin_capacity"] + ) # [END constraints] # [START objective] # Objective: minimize the number of bins used. - solver.Minimize(solver.Sum([y[j] for j in data['bins']])) + solver.Minimize(solver.Sum([y[j] for j in data["bins"]])) # [END objective] # [START solve] @@ -87,29 +89,29 @@ def main(): # [START print_solution] if status == pywraplp.Solver.OPTIMAL: num_bins = 0 - for j in data['bins']: + for j in data["bins"]: if y[j].solution_value() == 1: bin_items = [] bin_weight = 0 - for i in data['items']: + for i in data["items"]: if x[i, j].solution_value() > 0: bin_items.append(i) - bin_weight += data['weights'][i] + bin_weight += data["weights"][i] if bin_items: num_bins += 1 - print('Bin number', j) - print(' Items packed:', bin_items) - print(' Total weight:', bin_weight) + print("Bin number", j) + print(" Items packed:", bin_items) + print(" Total weight:", bin_weight) print() print() - print('Number of bins used:', num_bins) - print('Time = ', solver.WallTime(), ' milliseconds') + print("Number of bins used:", num_bins) + print("Time = ", solver.WallTime(), " milliseconds") else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program_part2] # [END program] diff --git a/ortools/linear_solver/samples/integer_programming_example.py b/ortools/linear_solver/samples/integer_programming_example.py old mode 100755 new mode 100644 index 077a684fc4..07d473cdee --- a/ortools/linear_solver/samples/integer_programming_example.py +++ b/ortools/linear_solver/samples/integer_programming_example.py @@ -11,6 +11,7 @@ # 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. + """Small example to illustrate solving a MIP problem.""" # [START program] # [START import] @@ -22,16 +23,16 @@ def IntegerProgrammingExample(): """Integer programming sample.""" # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: return # [END solver] # [START variables] # x, y, and z are non-negative integer variables. - x = solver.IntVar(0.0, solver.infinity(), 'x') - y = solver.IntVar(0.0, solver.infinity(), 'y') - z = solver.IntVar(0.0, solver.infinity(), 'z') + x = solver.IntVar(0.0, solver.infinity(), "x") + y = solver.IntVar(0.0, solver.infinity(), "y") + z = solver.IntVar(0.0, solver.infinity(), "z") # [END variables] # [START constraints] @@ -67,11 +68,11 @@ def IntegerProgrammingExample(): # [START print_solution] solver.Solve() # Print the objective value of the solution. - print('Maximum objective function value = %d' % solver.Objective().Value()) + print("Maximum objective function value = %d" % solver.Objective().Value()) print() # Print the value of each variable in the solution. for variable in [x, y, z]: - print('%s = %d' % (variable.name(), variable.solution_value())) + print("%s = %d" % (variable.name(), variable.solution_value())) # [END print_solution] diff --git a/ortools/linear_solver/samples/linear_programming_example.py b/ortools/linear_solver/samples/linear_programming_example.py old mode 100755 new mode 100644 index de36dfbfe8..3eaeeeb531 --- a/ortools/linear_solver/samples/linear_programming_example.py +++ b/ortools/linear_solver/samples/linear_programming_example.py @@ -11,6 +11,7 @@ # 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. + """Linear optimization example.""" # [START program] # [START import] @@ -22,17 +23,17 @@ def LinearProgrammingExample(): """Linear programming sample.""" # Instantiate a Glop solver, naming it LinearExample. # [START solver] - solver = pywraplp.Solver.CreateSolver('GLOP') + solver = pywraplp.Solver.CreateSolver("GLOP") if not solver: return # [END solver] # Create the two variables and let them take on any non-negative value. # [START variables] - x = solver.NumVar(0, solver.infinity(), 'x') - y = solver.NumVar(0, solver.infinity(), 'y') + x = solver.NumVar(0, solver.infinity(), "x") + y = solver.NumVar(0, solver.infinity(), "y") - print('Number of variables =', solver.NumVariables()) + print("Number of variables =", solver.NumVariables()) # [END variables] # [START constraints] @@ -45,7 +46,7 @@ def LinearProgrammingExample(): # Constraint 2: x - y <= 2. solver.Add(x - y <= 2.0) - print('Number of constraints =', solver.NumConstraints()) + print("Number of constraints =", solver.NumConstraints()) # [END constraints] # [START objective] @@ -60,18 +61,18 @@ def LinearProgrammingExample(): # [START print_solution] if status == pywraplp.Solver.OPTIMAL: - print('Solution:') - print('Objective value =', solver.Objective().Value()) - print('x =', x.solution_value()) - print('y =', y.solution_value()) + print("Solution:") + print("Objective value =", solver.Objective().Value()) + print("x =", x.solution_value()) + print("y =", y.solution_value()) else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] # [START advanced] - print('\nAdvanced usage:') - print('Problem solved in %f milliseconds' % solver.wall_time()) - print('Problem solved in %d iterations' % solver.iterations()) + print("\nAdvanced usage:") + print("Problem solved in %f milliseconds" % solver.wall_time()) + print("Problem solved in %d iterations" % solver.iterations()) # [END advanced] diff --git a/ortools/linear_solver/samples/mip_var_array.py b/ortools/linear_solver/samples/mip_var_array.py old mode 100755 new mode 100644 index 038b22bd41..3bd69325f2 --- a/ortools/linear_solver/samples/mip_var_array.py +++ b/ortools/linear_solver/samples/mip_var_array.py @@ -11,6 +11,7 @@ # 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. + """MIP example that uses a variable array.""" # [START program] # [START import] @@ -23,16 +24,16 @@ from ortools.linear_solver import pywraplp def create_data_model(): """Stores the data for the problem.""" data = {} - data['constraint_coeffs'] = [ + data["constraint_coeffs"] = [ [5, 7, 9, 2, 1], [18, 4, -9, 10, 12], [4, 7, 3, 8, 5], [5, 13, 16, 3, -7], ] - data['bounds'] = [250, 285, 211, 315] - data['obj_coeffs'] = [7, 8, 2, 9, 6] - data['num_vars'] = 5 - data['num_constraints'] = 4 + data["bounds"] = [250, 285, 211, 315] + data["obj_coeffs"] = [7, 8, 2, 9, 6] + data["num_vars"] = 5 + data["num_constraints"] = 4 return data # [END data_model] @@ -45,7 +46,7 @@ def main(): # [END program_part1] # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if not solver: return # [END solver] @@ -54,17 +55,17 @@ def main(): # [START variables] infinity = solver.infinity() x = {} - for j in range(data['num_vars']): - x[j] = solver.IntVar(0, infinity, 'x[%i]' % j) - print('Number of variables =', solver.NumVariables()) + for j in range(data["num_vars"]): + x[j] = solver.IntVar(0, infinity, "x[%i]" % j) + print("Number of variables =", solver.NumVariables()) # [END variables] # [START constraints] - for i in range(data['num_constraints']): - constraint = solver.RowConstraint(0, data['bounds'][i], '') - for j in range(data['num_vars']): - constraint.SetCoefficient(x[j], data['constraint_coeffs'][i][j]) - print('Number of constraints =', solver.NumConstraints()) + for i in range(data["num_constraints"]): + constraint = solver.RowConstraint(0, data["bounds"][i], "") + for j in range(data["num_vars"]): + constraint.SetCoefficient(x[j], data["constraint_coeffs"][i][j]) + print("Number of constraints =", solver.NumConstraints()) # In Python, you can also set the constraints as follows. # for i in range(data['num_constraints']): # constraint_expr = \ @@ -74,8 +75,8 @@ def main(): # [START objective] objective = solver.Objective() - for j in range(data['num_vars']): - objective.SetCoefficient(x[j], data['obj_coeffs'][j]) + for j in range(data["num_vars"]): + objective.SetCoefficient(x[j], data["obj_coeffs"][j]) objective.SetMaximization() # In Python, you can also set the objective as follows. # obj_expr = [data['obj_coeffs'][j] * x[j] for j in range(data['num_vars'])] @@ -88,19 +89,19 @@ def main(): # [START print_solution] if status == pywraplp.Solver.OPTIMAL: - print('Objective value =', solver.Objective().Value()) - for j in range(data['num_vars']): - print(x[j].name(), ' = ', x[j].solution_value()) + print("Objective value =", solver.Objective().Value()) + for j in range(data["num_vars"]): + print(x[j].name(), " = ", x[j].solution_value()) print() - print('Problem solved in %f milliseconds' % solver.wall_time()) - print('Problem solved in %d iterations' % solver.iterations()) - print('Problem solved in %d branch-and-bound nodes' % solver.nodes()) + print("Problem solved in %f milliseconds" % solver.wall_time()) + print("Problem solved in %d iterations" % solver.iterations()) + print("Problem solved in %d branch-and-bound nodes" % solver.nodes()) else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program_part2] # [END program] diff --git a/ortools/linear_solver/samples/multiple_knapsack_mip.py b/ortools/linear_solver/samples/multiple_knapsack_mip.py old mode 100755 new mode 100644 index b9e2cbc1df..d20f5ce363 --- a/ortools/linear_solver/samples/multiple_knapsack_mip.py +++ b/ortools/linear_solver/samples/multiple_knapsack_mip.py @@ -22,26 +22,22 @@ from ortools.linear_solver import pywraplp def main(): # [START data] data = {} - data['weights'] = [ - 48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36 - ] - data['values'] = [ - 10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25 - ] - assert len(data['weights']) == len(data['values']) - data['num_items'] = len(data['weights']) - data['all_items'] = range(data['num_items']) + data["weights"] = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36] + data["values"] = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25] + assert len(data["weights"]) == len(data["values"]) + data["num_items"] = len(data["weights"]) + data["all_items"] = range(data["num_items"]) - data['bin_capacities'] = [100, 100, 100, 100, 100] - data['num_bins'] = len(data['bin_capacities']) - data['all_bins'] = range(data['num_bins']) + data["bin_capacities"] = [100, 100, 100, 100, 100] + data["num_bins"] = len(data["bin_capacities"]) + data["all_bins"] = range(data["num_bins"]) # [END data] # Create the mip solver with the SCIP backend. # [START solver] - solver = pywraplp.Solver.CreateSolver('SCIP') + solver = pywraplp.Solver.CreateSolver("SCIP") if solver is None: - print('SCIP solver unavailable.') + print("SCIP solver unavailable.") return # [END solver] @@ -49,31 +45,32 @@ def main(): # [START variables] # x[i, b] = 1 if item i is packed in bin b. x = {} - for i in data['all_items']: - for b in data['all_bins']: - x[i, b] = solver.BoolVar(f'x_{i}_{b}') + for i in data["all_items"]: + for b in data["all_bins"]: + x[i, b] = solver.BoolVar(f"x_{i}_{b}") # [END variables] # Constraints. # [START constraints] # Each item is assigned to at most one bin. - for i in data['all_items']: - solver.Add(sum(x[i, b] for b in data['all_bins']) <= 1) + for i in data["all_items"]: + solver.Add(sum(x[i, b] for b in data["all_bins"]) <= 1) # The amount packed in each bin cannot exceed its capacity. - for b in data['all_bins']: + for b in data["all_bins"]: solver.Add( - sum(x[i, b] * data['weights'][i] - for i in data['all_items']) <= data['bin_capacities'][b]) + sum(x[i, b] * data["weights"][i] for i in data["all_items"]) + <= data["bin_capacities"][b] + ) # [END constraints] # Objective. # [START objective] # Maximize total value of packed items. objective = solver.Objective() - for i in data['all_items']: - for b in data['all_bins']: - objective.SetCoefficient(x[i, b], data['values'][i]) + for i in data["all_items"]: + for b in data["all_bins"]: + objective.SetCoefficient(x[i, b], data["values"][i]) objective.SetMaximization() # [END objective] @@ -83,28 +80,28 @@ def main(): # [START print_solution] if status == pywraplp.Solver.OPTIMAL: - print(f'Total packed value: {objective.Value()}') + print(f"Total packed value: {objective.Value()}") total_weight = 0 - for b in data['all_bins']: - print(f'Bin {b}') + for b in data["all_bins"]: + print(f"Bin {b}") bin_weight = 0 bin_value = 0 - for i in data['all_items']: + for i in data["all_items"]: if x[i, b].solution_value() > 0: print( f"Item {i} weight: {data['weights'][i]} value: {data['values'][i]}" ) - bin_weight += data['weights'][i] - bin_value += data['values'][i] - print(f'Packed bin weight: {bin_weight}') - print(f'Packed bin value: {bin_value}\n') + bin_weight += data["weights"][i] + bin_value += data["values"][i] + print(f"Packed bin weight: {bin_weight}") + print(f"Packed bin value: {bin_value}\n") total_weight += bin_weight - print(f'Total packed weight: {total_weight}') + print(f"Total packed weight: {total_weight}") else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/simple_lp_program.py b/ortools/linear_solver/samples/simple_lp_program.py old mode 100755 new mode 100644 index 3b87b2a835..047d4b462e --- a/ortools/linear_solver/samples/simple_lp_program.py +++ b/ortools/linear_solver/samples/simple_lp_program.py @@ -11,6 +11,7 @@ # 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. + """Minimal example to call the GLOP solver.""" # [START program] # [START import] @@ -21,7 +22,7 @@ from ortools.linear_solver import pywraplp def main(): # [START solver] # Create the linear solver with the GLOP backend. - solver = pywraplp.Solver.CreateSolver('GLOP') + solver = pywraplp.Solver.CreateSolver("GLOP") if not solver: return # [END solver] @@ -29,10 +30,10 @@ def main(): # [START variables] infinity = solver.infinity() # Create the variables x and y. - x = solver.NumVar(0.0, infinity, 'x') - y = solver.NumVar(0.0, infinity, 'y') + x = solver.NumVar(0.0, infinity, "x") + y = solver.NumVar(0.0, infinity, "y") - print('Number of variables =', solver.NumVariables()) + print("Number of variables =", solver.NumVariables()) # [END variables] # [START constraints] @@ -42,7 +43,7 @@ def main(): # x <= 3.5. solver.Add(x <= 3.5) - print('Number of constraints =', solver.NumConstraints()) + print("Number of constraints =", solver.NumConstraints()) # [END constraints] # [START objective] @@ -51,27 +52,27 @@ def main(): # [END objective] # [START solve] - print(f'Solving with {solver.SolverVersion()}') + print(f"Solving with {solver.SolverVersion()}") status = solver.Solve() # [END solve] # [START print_solution] if status == pywraplp.Solver.OPTIMAL: - print('Solution:') - print('Objective value =', solver.Objective().Value()) - print('x =', x.solution_value()) - print('y =', y.solution_value()) + print("Solution:") + print("Objective value =", solver.Objective().Value()) + print("x =", x.solution_value()) + print("y =", y.solution_value()) else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] # [START advanced] - print('\nAdvanced usage:') - print('Problem solved in %f milliseconds' % solver.wall_time()) - print('Problem solved in %d iterations' % solver.iterations()) + print("\nAdvanced usage:") + print("Problem solved in %f milliseconds" % solver.wall_time()) + print("Problem solved in %d iterations" % solver.iterations()) # [END advanced] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/simple_lp_program_mb.py b/ortools/linear_solver/samples/simple_lp_program_mb.py index d7f483dbe8..12c7ed287e 100644 --- a/ortools/linear_solver/samples/simple_lp_program_mb.py +++ b/ortools/linear_solver/samples/simple_lp_program_mb.py @@ -29,10 +29,14 @@ def main(): # [START variables] # Create the variables x and y. - x = model.new_num_var(0.0, math.inf, 'x') # pytype: disable=wrong-arg-types # numpy-scalars - y = model.new_num_var(0.0, math.inf, 'y') # pytype: disable=wrong-arg-types # numpy-scalars + x = model.new_num_var( + 0.0, math.inf, "x" + ) # pytype: disable=wrong-arg-types # numpy-scalars + y = model.new_num_var( + 0.0, math.inf, "y" + ) # pytype: disable=wrong-arg-types # numpy-scalars - print('Number of variables =', model.num_variables) + print("Number of variables =", model.num_variables) # [END variables] # [START constraints] @@ -42,7 +46,7 @@ def main(): # x <= 3.5. model.add(x <= 3.5) - print('Number of constraints =', model.num_constraints) + print("Number of constraints =", model.num_constraints) # [END constraints] # [START objective] @@ -52,29 +56,29 @@ def main(): # [START solve] # Create the solver with the GLOP backend, and solve the model. - solver = model_builder.ModelSolver('glop') + solver = model_builder.ModelSolver("glop") status = solver.solve(model) # [END solve] # [START print_solution] if status == model_builder.SolveStatus.OPTIMAL: - print('Solution:') - print('Objective value =', solver.objective_value) - print('x =', solver.value(x)) - print('y =', solver.value(y)) + print("Solution:") + print("Objective value =", solver.objective_value) + print("x =", solver.value(x)) + print("y =", solver.value(y)) - print('dual_value(ct) =', solver.dual_value(ct)) - print('reduced_cost(x) =', solver.reduced_cost(x)) + print("dual_value(ct) =", solver.dual_value(ct)) + print("reduced_cost(x) =", solver.reduced_cost(x)) else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] # [START advanced] - print('\nAdvanced usage:') - print('Problem solved in %f seconds' % solver.wall_time) + print("\nAdvanced usage:") + print("Problem solved in %f seconds" % solver.wall_time) # [END advanced] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/simple_mip_program.py b/ortools/linear_solver/samples/simple_mip_program.py old mode 100755 new mode 100644 index f656c380f2..9f2c09a1e1 --- a/ortools/linear_solver/samples/simple_mip_program.py +++ b/ortools/linear_solver/samples/simple_mip_program.py @@ -11,6 +11,7 @@ # 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. + """Integer programming examples that show how to use the APIs.""" # [START program] # [START import] @@ -21,7 +22,7 @@ from ortools.linear_solver import pywraplp def main(): # [START solver] # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver('SAT') + solver = pywraplp.Solver.CreateSolver("SAT") if not solver: return # [END solver] @@ -29,10 +30,10 @@ def main(): # [START variables] infinity = solver.infinity() # x and y are integer non-negative variables. - x = solver.IntVar(0.0, infinity, 'x') - y = solver.IntVar(0.0, infinity, 'y') + x = solver.IntVar(0.0, infinity, "x") + y = solver.IntVar(0.0, infinity, "y") - print('Number of variables =', solver.NumVariables()) + print("Number of variables =", solver.NumVariables()) # [END variables] # [START constraints] @@ -42,7 +43,7 @@ def main(): # x <= 3.5. solver.Add(x <= 3.5) - print('Number of constraints =', solver.NumConstraints()) + print("Number of constraints =", solver.NumConstraints()) # [END constraints] # [START objective] @@ -51,28 +52,28 @@ def main(): # [END objective] # [START solve] - print(f'Solving with {solver.SolverVersion()}') + print(f"Solving with {solver.SolverVersion()}") status = solver.Solve() # [END solve] # [START print_solution] if status == pywraplp.Solver.OPTIMAL: - print('Solution:') - print('Objective value =', solver.Objective().Value()) - print('x =', x.solution_value()) - print('y =', y.solution_value()) + print("Solution:") + print("Objective value =", solver.Objective().Value()) + print("x =", x.solution_value()) + print("y =", y.solution_value()) else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] # [START advanced] - print('\nAdvanced usage:') - print('Problem solved in %f milliseconds' % solver.wall_time()) - print('Problem solved in %d iterations' % solver.iterations()) - print('Problem solved in %d branch-and-bound nodes' % solver.nodes()) + print("\nAdvanced usage:") + print("Problem solved in %f milliseconds" % solver.wall_time()) + print("Problem solved in %d iterations" % solver.iterations()) + print("Problem solved in %d branch-and-bound nodes" % solver.nodes()) # [END advanced] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/simple_mip_program_mb.py b/ortools/linear_solver/samples/simple_mip_program_mb.py index f0e41bcda0..bc5e61f6d8 100644 --- a/ortools/linear_solver/samples/simple_mip_program_mb.py +++ b/ortools/linear_solver/samples/simple_mip_program_mb.py @@ -29,10 +29,14 @@ def main(): # [START variables] # x and y are integer non-negative variables. - x = model.new_int_var(0.0, math.inf, 'x') # pytype: disable=wrong-arg-types # numpy-scalars - y = model.new_int_var(0.0, math.inf, 'y') # pytype: disable=wrong-arg-types # numpy-scalars + x = model.new_int_var( + 0.0, math.inf, "x" + ) # pytype: disable=wrong-arg-types # numpy-scalars + y = model.new_int_var( + 0.0, math.inf, "y" + ) # pytype: disable=wrong-arg-types # numpy-scalars - print('Number of variables =', model.num_variables) + print("Number of variables =", model.num_variables) # [END variables] # [START constraints] @@ -42,7 +46,7 @@ def main(): # x <= 3.5. model.add(x <= 3.5) - print('Number of constraints =', model.num_constraints) + print("Number of constraints =", model.num_constraints) # [END constraints] # [START objective] @@ -52,26 +56,26 @@ def main(): # [START solve] # Create the solver with the SCIP backend, and solve the model. - solver = model_builder.ModelSolver('scip') + solver = model_builder.ModelSolver("scip") status = solver.solve(model) # [END solve] # [START print_solution] if status == model_builder.SolveStatus.OPTIMAL: - print('Solution:') - print('Objective value =', solver.objective_value) - print('x =', solver.value(x)) - print('y =', solver.value(y)) + print("Solution:") + print("Objective value =", solver.objective_value) + print("x =", solver.value(x)) + print("y =", solver.value(y)) else: - print('The problem does not have an optimal solution.') + print("The problem does not have an optimal solution.") # [END print_solution] # [START advanced] - print('\nAdvanced usage:') - print('Problem solved in %f seconds' % solver.wall_time) + print("\nAdvanced usage:") + print("Problem solved in %f seconds" % solver.wall_time) # [END advanced] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program] diff --git a/ortools/linear_solver/samples/stigler_diet.py b/ortools/linear_solver/samples/stigler_diet.py index e34c4a5ac7..39eb3e3efe 100755 --- a/ortools/linear_solver/samples/stigler_diet.py +++ b/ortools/linear_solver/samples/stigler_diet.py @@ -29,15 +29,15 @@ def main(): # [START data_model] # Nutrient minimums. nutrients = [ - ['Calories (kcal)', 3], - ['Protein (g)', 70], - ['Calcium (g)', 0.8], - ['Iron (mg)', 12], - ['Vitamin A (KIU)', 5], - ['Vitamin B1 (mg)', 1.8], - ['Vitamin B2 (mg)', 2.7], - ['Niacin (mg)', 18], - ['Vitamin C (mg)', 75], + ["Calories (kcal)", 3], + ["Protein (g)", 70], + ["Calcium (g)", 0.8], + ["Iron (mg)", 12], + ["Vitamin A (KIU)", 5], + ["Vitamin B1 (mg)", 1.8], + ["Vitamin B2 (mg)", 2.7], + ["Niacin (mg)", 18], + ["Vitamin C (mg)", 75], ] # Commodity, Unit, 1939 price (cents), Calories (kcal), Protein (g), @@ -45,187 +45,179 @@ def main(): # Niacin (mg), Vitamin C (mg) data = [ [ - 'Wheat Flour (Enriched)', '10 lb.', 36, 44.7, 1411, 2, 365, 0, 55.4, - 33.3, 441, 0 + "Wheat Flour (Enriched)", + "10 lb.", + 36, + 44.7, + 1411, + 2, + 365, + 0, + 55.4, + 33.3, + 441, + 0, ], - ['Macaroni', '1 lb.', 14.1, 11.6, 418, 0.7, 54, 0, 3.2, 1.9, 68, 0], + ["Macaroni", "1 lb.", 14.1, 11.6, 418, 0.7, 54, 0, 3.2, 1.9, 68, 0], [ - 'Wheat Cereal (Enriched)', '28 oz.', 24.2, 11.8, 377, 14.4, 175, 0, - 14.4, 8.8, 114, 0 + "Wheat Cereal (Enriched)", + "28 oz.", + 24.2, + 11.8, + 377, + 14.4, + 175, + 0, + 14.4, + 8.8, + 114, + 0, ], - ['Corn Flakes', '8 oz.', 7.1, 11.4, 252, 0.1, 56, 0, 13.5, 2.3, 68, 0], + ["Corn Flakes", "8 oz.", 7.1, 11.4, 252, 0.1, 56, 0, 13.5, 2.3, 68, 0], + ["Corn Meal", "1 lb.", 4.6, 36.0, 897, 1.7, 99, 30.9, 17.4, 7.9, 106, 0], + ["Hominy Grits", "24 oz.", 8.5, 28.6, 680, 0.8, 80, 0, 10.6, 1.6, 110, 0], + ["Rice", "1 lb.", 7.5, 21.2, 460, 0.6, 41, 0, 2, 4.8, 60, 0], + ["Rolled Oats", "1 lb.", 7.1, 25.3, 907, 5.1, 341, 0, 37.1, 8.9, 64, 0], [ - 'Corn Meal', '1 lb.', 4.6, 36.0, 897, 1.7, 99, 30.9, 17.4, 7.9, 106, - 0 + "White Bread (Enriched)", + "1 lb.", + 7.9, + 15.0, + 488, + 2.5, + 115, + 0, + 13.8, + 8.5, + 126, + 0, + ], + ["Whole Wheat Bread", "1 lb.", 9.1, 12.2, 484, 2.7, 125, 0, 13.9, 6.4, 160, 0], + ["Rye Bread", "1 lb.", 9.1, 12.4, 439, 1.1, 82, 0, 9.9, 3, 66, 0], + ["Pound Cake", "1 lb.", 24.8, 8.0, 130, 0.4, 31, 18.9, 2.8, 3, 17, 0], + ["Soda Crackers", "1 lb.", 15.1, 12.5, 288, 0.5, 50, 0, 0, 0, 0, 0], + ["Milk", "1 qt.", 11, 6.1, 310, 10.5, 18, 16.8, 4, 16, 7, 177], + [ + "Evaporated Milk (can)", + "14.5 oz.", + 6.7, + 8.4, + 422, + 15.1, + 9, + 26, + 3, + 23.5, + 11, + 60, + ], + ["Butter", "1 lb.", 30.8, 10.8, 9, 0.2, 3, 44.2, 0, 0.2, 2, 0], + ["Oleomargarine", "1 lb.", 16.1, 20.6, 17, 0.6, 6, 55.8, 0.2, 0, 0, 0], + ["Eggs", "1 doz.", 32.6, 2.9, 238, 1.0, 52, 18.6, 2.8, 6.5, 1, 0], + ["Cheese (Cheddar)", "1 lb.", 24.2, 7.4, 448, 16.4, 19, 28.1, 0.8, 10.3, 4, 0], + ["Cream", "1/2 pt.", 14.1, 3.5, 49, 1.7, 3, 16.9, 0.6, 2.5, 0, 17], + ["Peanut Butter", "1 lb.", 17.9, 15.7, 661, 1.0, 48, 0, 9.6, 8.1, 471, 0], + ["Mayonnaise", "1/2 pt.", 16.7, 8.6, 18, 0.2, 8, 2.7, 0.4, 0.5, 0, 0], + ["Crisco", "1 lb.", 20.3, 20.1, 0, 0, 0, 0, 0, 0, 0, 0], + ["Lard", "1 lb.", 9.8, 41.7, 0, 0, 0, 0.2, 0, 0.5, 5, 0], + ["Sirloin Steak", "1 lb.", 39.6, 2.9, 166, 0.1, 34, 0.2, 2.1, 2.9, 69, 0], + ["Round Steak", "1 lb.", 36.4, 2.2, 214, 0.1, 32, 0.4, 2.5, 2.4, 87, 0], + ["Rib Roast", "1 lb.", 29.2, 3.4, 213, 0.1, 33, 0, 0, 2, 0, 0], + ["Chuck Roast", "1 lb.", 22.6, 3.6, 309, 0.2, 46, 0.4, 1, 4, 120, 0], + ["Plate", "1 lb.", 14.6, 8.5, 404, 0.2, 62, 0, 0.9, 0, 0, 0], + ["Liver (Beef)", "1 lb.", 26.8, 2.2, 333, 0.2, 139, 169.2, 6.4, 50.8, 316, 525], + ["Leg of Lamb", "1 lb.", 27.6, 3.1, 245, 0.1, 20, 0, 2.8, 3.9, 86, 0], + ["Lamb Chops (Rib)", "1 lb.", 36.6, 3.3, 140, 0.1, 15, 0, 1.7, 2.7, 54, 0], + ["Pork Chops", "1 lb.", 30.7, 3.5, 196, 0.2, 30, 0, 17.4, 2.7, 60, 0], + ["Pork Loin Roast", "1 lb.", 24.2, 4.4, 249, 0.3, 37, 0, 18.2, 3.6, 79, 0], + ["Bacon", "1 lb.", 25.6, 10.4, 152, 0.2, 23, 0, 1.8, 1.8, 71, 0], + ["Ham, smoked", "1 lb.", 27.4, 6.7, 212, 0.2, 31, 0, 9.9, 3.3, 50, 0], + ["Salt Pork", "1 lb.", 16, 18.8, 164, 0.1, 26, 0, 1.4, 1.8, 0, 0], + ["Roasting Chicken", "1 lb.", 30.3, 1.8, 184, 0.1, 30, 0.1, 0.9, 1.8, 68, 46], + ["Veal Cutlets", "1 lb.", 42.3, 1.7, 156, 0.1, 24, 0, 1.4, 2.4, 57, 0], + ["Salmon, Pink (can)", "16 oz.", 13, 5.8, 705, 6.8, 45, 3.5, 1, 4.9, 209, 0], + ["Apples", "1 lb.", 4.4, 5.8, 27, 0.5, 36, 7.3, 3.6, 2.7, 5, 544], + ["Bananas", "1 lb.", 6.1, 4.9, 60, 0.4, 30, 17.4, 2.5, 3.5, 28, 498], + ["Lemons", "1 doz.", 26, 1.0, 21, 0.5, 14, 0, 0.5, 0, 4, 952], + ["Oranges", "1 doz.", 30.9, 2.2, 40, 1.1, 18, 11.1, 3.6, 1.3, 10, 1998], + ["Green Beans", "1 lb.", 7.1, 2.4, 138, 3.7, 80, 69, 4.3, 5.8, 37, 862], + ["Cabbage", "1 lb.", 3.7, 2.6, 125, 4.0, 36, 7.2, 9, 4.5, 26, 5369], + ["Carrots", "1 bunch", 4.7, 2.7, 73, 2.8, 43, 188.5, 6.1, 4.3, 89, 608], + ["Celery", "1 stalk", 7.3, 0.9, 51, 3.0, 23, 0.9, 1.4, 1.4, 9, 313], + ["Lettuce", "1 head", 8.2, 0.4, 27, 1.1, 22, 112.4, 1.8, 3.4, 11, 449], + ["Onions", "1 lb.", 3.6, 5.8, 166, 3.8, 59, 16.6, 4.7, 5.9, 21, 1184], + ["Potatoes", "15 lb.", 34, 14.3, 336, 1.8, 118, 6.7, 29.4, 7.1, 198, 2522], + ["Spinach", "1 lb.", 8.1, 1.1, 106, 0, 138, 918.4, 5.7, 13.8, 33, 2755], + ["Sweet Potatoes", "1 lb.", 5.1, 9.6, 138, 2.7, 54, 290.7, 8.4, 5.4, 83, 1912], + ["Peaches (can)", "No. 2 1/2", 16.8, 3.7, 20, 0.4, 10, 21.5, 0.5, 1, 31, 196], + ["Pears (can)", "No. 2 1/2", 20.4, 3.0, 8, 0.3, 8, 0.8, 0.8, 0.8, 5, 81], + ["Pineapple (can)", "No. 2 1/2", 21.3, 2.4, 16, 0.4, 8, 2, 2.8, 0.8, 7, 399], + ["Asparagus (can)", "No. 2", 27.7, 0.4, 33, 0.3, 12, 16.3, 1.4, 2.1, 17, 272], + ["Green Beans (can)", "No. 2", 10, 1.0, 54, 2, 65, 53.9, 1.6, 4.3, 32, 431], + ["Pork and Beans (can)", "16 oz.", 7.1, 7.5, 364, 4, 134, 3.5, 8.3, 7.7, 56, 0], + ["Corn (can)", "No. 2", 10.4, 5.2, 136, 0.2, 16, 12, 1.6, 2.7, 42, 218], + ["Peas (can)", "No. 2", 13.8, 2.3, 136, 0.6, 45, 34.9, 4.9, 2.5, 37, 370], + ["Tomatoes (can)", "No. 2", 8.6, 1.3, 63, 0.7, 38, 53.2, 3.4, 2.5, 36, 1253], + [ + "Tomato Soup (can)", + "10 1/2 oz.", + 7.6, + 1.6, + 71, + 0.6, + 43, + 57.9, + 3.5, + 2.4, + 67, + 862, + ], + ["Peaches, Dried", "1 lb.", 15.7, 8.5, 87, 1.7, 173, 86.8, 1.2, 4.3, 55, 57], + ["Prunes, Dried", "1 lb.", 9, 12.8, 99, 2.5, 154, 85.7, 3.9, 4.3, 65, 257], + ["Raisins, Dried", "15 oz.", 9.4, 13.5, 104, 2.5, 136, 4.5, 6.3, 1.4, 24, 136], + ["Peas, Dried", "1 lb.", 7.9, 20.0, 1367, 4.2, 345, 2.9, 28.7, 18.4, 162, 0], + [ + "Lima Beans, Dried", + "1 lb.", + 8.9, + 17.4, + 1055, + 3.7, + 459, + 5.1, + 26.9, + 38.2, + 93, + 0, ], [ - 'Hominy Grits', '24 oz.', 8.5, 28.6, 680, 0.8, 80, 0, 10.6, 1.6, - 110, 0 - ], - ['Rice', '1 lb.', 7.5, 21.2, 460, 0.6, 41, 0, 2, 4.8, 60, 0], - ['Rolled Oats', '1 lb.', 7.1, 25.3, 907, 5.1, 341, 0, 37.1, 8.9, 64, 0], - [ - 'White Bread (Enriched)', '1 lb.', 7.9, 15.0, 488, 2.5, 115, 0, - 13.8, 8.5, 126, 0 - ], - [ - 'Whole Wheat Bread', '1 lb.', 9.1, 12.2, 484, 2.7, 125, 0, 13.9, - 6.4, 160, 0 - ], - ['Rye Bread', '1 lb.', 9.1, 12.4, 439, 1.1, 82, 0, 9.9, 3, 66, 0], - ['Pound Cake', '1 lb.', 24.8, 8.0, 130, 0.4, 31, 18.9, 2.8, 3, 17, 0], - ['Soda Crackers', '1 lb.', 15.1, 12.5, 288, 0.5, 50, 0, 0, 0, 0, 0], - ['Milk', '1 qt.', 11, 6.1, 310, 10.5, 18, 16.8, 4, 16, 7, 177], - [ - 'Evaporated Milk (can)', '14.5 oz.', 6.7, 8.4, 422, 15.1, 9, 26, 3, - 23.5, 11, 60 - ], - ['Butter', '1 lb.', 30.8, 10.8, 9, 0.2, 3, 44.2, 0, 0.2, 2, 0], - ['Oleomargarine', '1 lb.', 16.1, 20.6, 17, 0.6, 6, 55.8, 0.2, 0, 0, 0], - ['Eggs', '1 doz.', 32.6, 2.9, 238, 1.0, 52, 18.6, 2.8, 6.5, 1, 0], - [ - 'Cheese (Cheddar)', '1 lb.', 24.2, 7.4, 448, 16.4, 19, 28.1, 0.8, - 10.3, 4, 0 - ], - ['Cream', '1/2 pt.', 14.1, 3.5, 49, 1.7, 3, 16.9, 0.6, 2.5, 0, 17], - [ - 'Peanut Butter', '1 lb.', 17.9, 15.7, 661, 1.0, 48, 0, 9.6, 8.1, - 471, 0 - ], - ['Mayonnaise', '1/2 pt.', 16.7, 8.6, 18, 0.2, 8, 2.7, 0.4, 0.5, 0, 0], - ['Crisco', '1 lb.', 20.3, 20.1, 0, 0, 0, 0, 0, 0, 0, 0], - ['Lard', '1 lb.', 9.8, 41.7, 0, 0, 0, 0.2, 0, 0.5, 5, 0], - [ - 'Sirloin Steak', '1 lb.', 39.6, 2.9, 166, 0.1, 34, 0.2, 2.1, 2.9, - 69, 0 - ], - ['Round Steak', '1 lb.', 36.4, 2.2, 214, 0.1, 32, 0.4, 2.5, 2.4, 87, 0], - ['Rib Roast', '1 lb.', 29.2, 3.4, 213, 0.1, 33, 0, 0, 2, 0, 0], - ['Chuck Roast', '1 lb.', 22.6, 3.6, 309, 0.2, 46, 0.4, 1, 4, 120, 0], - ['Plate', '1 lb.', 14.6, 8.5, 404, 0.2, 62, 0, 0.9, 0, 0, 0], - [ - 'Liver (Beef)', '1 lb.', 26.8, 2.2, 333, 0.2, 139, 169.2, 6.4, 50.8, - 316, 525 - ], - ['Leg of Lamb', '1 lb.', 27.6, 3.1, 245, 0.1, 20, 0, 2.8, 3.9, 86, 0], - [ - 'Lamb Chops (Rib)', '1 lb.', 36.6, 3.3, 140, 0.1, 15, 0, 1.7, 2.7, - 54, 0 - ], - ['Pork Chops', '1 lb.', 30.7, 3.5, 196, 0.2, 30, 0, 17.4, 2.7, 60, 0], - [ - 'Pork Loin Roast', '1 lb.', 24.2, 4.4, 249, 0.3, 37, 0, 18.2, 3.6, - 79, 0 - ], - ['Bacon', '1 lb.', 25.6, 10.4, 152, 0.2, 23, 0, 1.8, 1.8, 71, 0], - ['Ham, smoked', '1 lb.', 27.4, 6.7, 212, 0.2, 31, 0, 9.9, 3.3, 50, 0], - ['Salt Pork', '1 lb.', 16, 18.8, 164, 0.1, 26, 0, 1.4, 1.8, 0, 0], - [ - 'Roasting Chicken', '1 lb.', 30.3, 1.8, 184, 0.1, 30, 0.1, 0.9, 1.8, - 68, 46 - ], - ['Veal Cutlets', '1 lb.', 42.3, 1.7, 156, 0.1, 24, 0, 1.4, 2.4, 57, 0], - [ - 'Salmon, Pink (can)', '16 oz.', 13, 5.8, 705, 6.8, 45, 3.5, 1, 4.9, - 209, 0 - ], - ['Apples', '1 lb.', 4.4, 5.8, 27, 0.5, 36, 7.3, 3.6, 2.7, 5, 544], - ['Bananas', '1 lb.', 6.1, 4.9, 60, 0.4, 30, 17.4, 2.5, 3.5, 28, 498], - ['Lemons', '1 doz.', 26, 1.0, 21, 0.5, 14, 0, 0.5, 0, 4, 952], - ['Oranges', '1 doz.', 30.9, 2.2, 40, 1.1, 18, 11.1, 3.6, 1.3, 10, 1998], - ['Green Beans', '1 lb.', 7.1, 2.4, 138, 3.7, 80, 69, 4.3, 5.8, 37, 862], - ['Cabbage', '1 lb.', 3.7, 2.6, 125, 4.0, 36, 7.2, 9, 4.5, 26, 5369], - ['Carrots', '1 bunch', 4.7, 2.7, 73, 2.8, 43, 188.5, 6.1, 4.3, 89, 608], - ['Celery', '1 stalk', 7.3, 0.9, 51, 3.0, 23, 0.9, 1.4, 1.4, 9, 313], - ['Lettuce', '1 head', 8.2, 0.4, 27, 1.1, 22, 112.4, 1.8, 3.4, 11, 449], - ['Onions', '1 lb.', 3.6, 5.8, 166, 3.8, 59, 16.6, 4.7, 5.9, 21, 1184], - [ - 'Potatoes', '15 lb.', 34, 14.3, 336, 1.8, 118, 6.7, 29.4, 7.1, 198, - 2522 - ], - ['Spinach', '1 lb.', 8.1, 1.1, 106, 0, 138, 918.4, 5.7, 13.8, 33, 2755], - [ - 'Sweet Potatoes', '1 lb.', 5.1, 9.6, 138, 2.7, 54, 290.7, 8.4, 5.4, - 83, 1912 - ], - [ - 'Peaches (can)', 'No. 2 1/2', 16.8, 3.7, 20, 0.4, 10, 21.5, 0.5, 1, - 31, 196 - ], - [ - 'Pears (can)', 'No. 2 1/2', 20.4, 3.0, 8, 0.3, 8, 0.8, 0.8, 0.8, 5, - 81 - ], - [ - 'Pineapple (can)', 'No. 2 1/2', 21.3, 2.4, 16, 0.4, 8, 2, 2.8, 0.8, - 7, 399 - ], - [ - 'Asparagus (can)', 'No. 2', 27.7, 0.4, 33, 0.3, 12, 16.3, 1.4, 2.1, - 17, 272 - ], - [ - 'Green Beans (can)', 'No. 2', 10, 1.0, 54, 2, 65, 53.9, 1.6, 4.3, - 32, 431 - ], - [ - 'Pork and Beans (can)', '16 oz.', 7.1, 7.5, 364, 4, 134, 3.5, 8.3, - 7.7, 56, 0 - ], - ['Corn (can)', 'No. 2', 10.4, 5.2, 136, 0.2, 16, 12, 1.6, 2.7, 42, 218], - [ - 'Peas (can)', 'No. 2', 13.8, 2.3, 136, 0.6, 45, 34.9, 4.9, 2.5, 37, - 370 - ], - [ - 'Tomatoes (can)', 'No. 2', 8.6, 1.3, 63, 0.7, 38, 53.2, 3.4, 2.5, - 36, 1253 - ], - [ - 'Tomato Soup (can)', '10 1/2 oz.', 7.6, 1.6, 71, 0.6, 43, 57.9, 3.5, - 2.4, 67, 862 - ], - [ - 'Peaches, Dried', '1 lb.', 15.7, 8.5, 87, 1.7, 173, 86.8, 1.2, 4.3, - 55, 57 - ], - [ - 'Prunes, Dried', '1 lb.', 9, 12.8, 99, 2.5, 154, 85.7, 3.9, 4.3, 65, - 257 - ], - [ - 'Raisins, Dried', '15 oz.', 9.4, 13.5, 104, 2.5, 136, 4.5, 6.3, 1.4, - 24, 136 - ], - [ - 'Peas, Dried', '1 lb.', 7.9, 20.0, 1367, 4.2, 345, 2.9, 28.7, 18.4, - 162, 0 - ], - [ - 'Lima Beans, Dried', '1 lb.', 8.9, 17.4, 1055, 3.7, 459, 5.1, 26.9, - 38.2, 93, 0 - ], - [ - 'Navy Beans, Dried', '1 lb.', 5.9, 26.9, 1691, 11.4, 792, 0, 38.4, - 24.6, 217, 0 - ], - ['Coffee', '1 lb.', 22.4, 0, 0, 0, 0, 0, 4, 5.1, 50, 0], - ['Tea', '1/4 lb.', 17.4, 0, 0, 0, 0, 0, 0, 2.3, 42, 0], - ['Cocoa', '8 oz.', 8.6, 8.7, 237, 3, 72, 0, 2, 11.9, 40, 0], - ['Chocolate', '8 oz.', 16.2, 8.0, 77, 1.3, 39, 0, 0.9, 3.4, 14, 0], - ['Sugar', '10 lb.', 51.7, 34.9, 0, 0, 0, 0, 0, 0, 0, 0], - ['Corn Syrup', '24 oz.', 13.7, 14.7, 0, 0.5, 74, 0, 0, 0, 5, 0], - ['Molasses', '18 oz.', 13.6, 9.0, 0, 10.3, 244, 0, 1.9, 7.5, 146, 0], - [ - 'Strawberry Preserves', '1 lb.', 20.5, 6.4, 11, 0.4, 7, 0.2, 0.2, - 0.4, 3, 0 + "Navy Beans, Dried", + "1 lb.", + 5.9, + 26.9, + 1691, + 11.4, + 792, + 0, + 38.4, + 24.6, + 217, + 0, ], + ["Coffee", "1 lb.", 22.4, 0, 0, 0, 0, 0, 4, 5.1, 50, 0], + ["Tea", "1/4 lb.", 17.4, 0, 0, 0, 0, 0, 0, 2.3, 42, 0], + ["Cocoa", "8 oz.", 8.6, 8.7, 237, 3, 72, 0, 2, 11.9, 40, 0], + ["Chocolate", "8 oz.", 16.2, 8.0, 77, 1.3, 39, 0, 0.9, 3.4, 14, 0], + ["Sugar", "10 lb.", 51.7, 34.9, 0, 0, 0, 0, 0, 0, 0, 0], + ["Corn Syrup", "24 oz.", 13.7, 14.7, 0, 0.5, 74, 0, 0, 0, 5, 0], + ["Molasses", "18 oz.", 13.6, 9.0, 0, 10.3, 244, 0, 1.9, 7.5, 146, 0], + ["Strawberry Preserves", "1 lb.", 20.5, 6.4, 11, 0.4, 7, 0.2, 0.2, 0.4, 3, 0], ] # [END data_model] # [START solver] # Instantiate a Glop solver and naming it. - solver = pywraplp.Solver.CreateSolver('GLOP') + solver = pywraplp.Solver.CreateSolver("GLOP") if not solver: return # [END solver] @@ -234,7 +226,7 @@ def main(): # Declare an array to hold our variables. foods = [solver.NumVar(0.0, solver.infinity(), item[0]) for item in data] - print('Number of variables =', solver.NumVariables()) + print("Number of variables =", solver.NumVariables()) # [END variables] # [START constraints] @@ -245,7 +237,7 @@ def main(): for j, item in enumerate(data): constraints[i].SetCoefficient(foods[j], item[i + 3]) - print('Number of constraints =', solver.NumConstraints()) + print("Number of constraints =", solver.NumConstraints()) # [END constraints] # [START objective] @@ -263,36 +255,37 @@ def main(): # [START print_solution] # Check that the problem has an optimal solution. if status != solver.OPTIMAL: - print('The problem does not have an optimal solution!') + print("The problem does not have an optimal solution!") if status == solver.FEASIBLE: - print('A potentially suboptimal solution was found.') + print("A potentially suboptimal solution was found.") else: - print('The solver could not solve the problem.') + print("The solver could not solve the problem.") exit(1) # Display the amounts (in dollars) to purchase of each food. nutrients_result = [0] * len(nutrients) - print('\nAnnual Foods:') + print("\nAnnual Foods:") for i, food in enumerate(foods): if food.solution_value() > 0.0: - print('{}: ${}'.format(data[i][0], 365. * food.solution_value())) + print("{}: ${}".format(data[i][0], 365.0 * food.solution_value())) for j, _ in enumerate(nutrients): nutrients_result[j] += data[i][j + 3] * food.solution_value() - print('\nOptimal annual price: ${:.4f}'.format(365. * objective.Value())) + print("\nOptimal annual price: ${:.4f}".format(365.0 * objective.Value())) - print('\nNutrients per day:') + print("\nNutrients per day:") for i, nutrient in enumerate(nutrients): - print('{}: {:.2f} (min {})'.format(nutrient[0], nutrients_result[i], - nutrient[1])) + print( + "{}: {:.2f} (min {})".format(nutrient[0], nutrients_result[i], nutrient[1]) + ) # [END print_solution] # [START advanced] - print('\nAdvanced usage:') - print('Problem solved in ', solver.wall_time(), ' milliseconds') - print('Problem solved in ', solver.iterations(), ' iterations') + print("\nAdvanced usage:") + print("Problem solved in ", solver.wall_time(), " milliseconds") + print("Problem solved in ", solver.iterations(), " iterations") # [END advanced] -if __name__ == '__main__': +if __name__ == "__main__": main() # [END program]