2022-06-17 08:40:20 +02:00
|
|
|
# Copyright 2010-2022 Google LLC
|
2022-03-23 17:47:22 +01:00
|
|
|
# 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.
|
2023-11-06 15:20:03 +01:00
|
|
|
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Methods for building and solving model_builder models.
|
|
|
|
|
|
|
|
|
|
The following two sections describe the main
|
|
|
|
|
methods for building and solving those models.
|
|
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
* [`Model`](#model_builder.Model): Methods for creating
|
2022-03-23 17:47:22 +01:00
|
|
|
models, including variables and constraints.
|
2023-11-06 16:16:38 +01:00
|
|
|
* [`Solver`](#model_builder.Solver): Methods for solving
|
2022-03-23 17:47:22 +01:00
|
|
|
a model and evaluating solutions.
|
|
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
Additional methods for solving Model models:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
* [`Constraint`](#model_builder.Constraint): A few utility methods for modifying
|
2023-11-06 16:16:38 +01:00
|
|
|
constraints created by `Model`.
|
2022-03-23 17:47:22 +01:00
|
|
|
* [`LinearExpr`](#model_builder.LinearExpr): Methods for creating constraints
|
|
|
|
|
and the objective from large arrays of coefficients.
|
|
|
|
|
|
|
|
|
|
Other methods and functions listed are primarily used for developing OR-Tools,
|
|
|
|
|
rather than for solving specific optimization problems.
|
|
|
|
|
"""
|
2023-11-06 16:16:38 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
import abc
|
|
|
|
|
import dataclasses
|
2022-03-23 17:47:22 +01:00
|
|
|
import math
|
2023-02-28 10:55:51 +04:00
|
|
|
import numbers
|
2023-07-09 13:54:23 +02:00
|
|
|
import typing
|
2023-07-27 08:50:52 -07:00
|
|
|
from typing import Callable, List, Optional, Sequence, Tuple, Union, cast
|
2023-07-03 14:30:27 +02:00
|
|
|
|
2023-01-19 16:09:45 +01:00
|
|
|
import numpy as np
|
2023-02-28 10:55:51 +04:00
|
|
|
from numpy import typing as npt
|
2023-07-09 13:54:23 +02:00
|
|
|
import pandas as pd
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
from ortools.linear_solver import linear_solver_pb2
|
2022-09-12 11:28:52 +02:00
|
|
|
from ortools.linear_solver.python import model_builder_helper as mbh
|
2023-07-03 14:30:27 +02:00
|
|
|
from ortools.linear_solver.python import model_builder_numbers as mbn
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
# Custom types.
|
2023-07-09 13:54:23 +02:00
|
|
|
NumberT = Union[int, float, numbers.Real, np.number]
|
|
|
|
|
IntegerT = Union[int, numbers.Integral, np.integer]
|
2023-06-28 15:57:32 +02:00
|
|
|
LinearExprT = Union["LinearExpr", NumberT]
|
2023-07-09 13:54:23 +02:00
|
|
|
ConstraintT = Union["_BoundedLinearExpr", bool]
|
|
|
|
|
_IndexOrSeries = Union[pd.Index, pd.Series]
|
|
|
|
|
_VariableOrConstraint = Union["LinearConstraint", "Variable"]
|
2023-06-28 15:57:32 +02:00
|
|
|
|
2022-03-23 17:47:22 +01:00
|
|
|
# Forward solve statuses.
|
2023-07-03 14:30:27 +02:00
|
|
|
SolveStatus = mbh.SolveStatus
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-27 08:50:52 -07:00
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
class LinearExpr(metaclass=abc.ABCMeta):
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Holds an linear expression.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
A linear expression is built from constants and variables.
|
|
|
|
|
For example, `x + 2.0 * (y - z + 1.0)`.
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
Linear expressions are used in Model models in constraints and in the
|
2023-06-28 15:57:32 +02:00
|
|
|
objective:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
* You can define linear constraints as in:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
```
|
|
|
|
|
model.add(x + 2 * y <= 5.0)
|
|
|
|
|
model.add(sum(array_of_vars) == 5.0)
|
|
|
|
|
```
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
* In Model, the objective is a linear expression:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
```
|
|
|
|
|
model.minimize(x + 2.0 * y + z)
|
|
|
|
|
```
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
* 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:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
```
|
|
|
|
|
model.minimize(model_builder.LinearExpr.sum(expressions))
|
|
|
|
|
model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
|
|
|
|
|
```
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
2023-05-24 15:33:27 +02:00
|
|
|
def sum( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
2023-11-06 15:20:03 +01:00
|
|
|
cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0
|
|
|
|
|
) -> LinearExprT:
|
2023-02-28 10:55:51 +04:00
|
|
|
"""Creates `sum(expressions) + constant`.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
It can perform simple simplifications and returns different objects,
|
|
|
|
|
including the input.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Args:
|
|
|
|
|
expressions: a sequence of linear expressions or constants.
|
|
|
|
|
constant: a numerical constant.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Returns:
|
|
|
|
|
a LinearExpr instance or a numerical constant.
|
|
|
|
|
"""
|
2023-07-03 14:30:27 +02:00
|
|
|
checked_constant: np.double = mbn.assert_is_a_number(constant)
|
2023-02-28 10:55:51 +04:00
|
|
|
if not expressions:
|
|
|
|
|
return checked_constant
|
2023-07-03 14:30:27 +02:00
|
|
|
if len(expressions) == 1 and mbn.is_zero(checked_constant):
|
2022-03-23 17:47:22 +01:00
|
|
|
return expressions[0]
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
return LinearExpr.weighted_sum(
|
|
|
|
|
expressions, np.ones(len(expressions)), constant=checked_constant
|
|
|
|
|
)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
2023-05-24 15:33:27 +02:00
|
|
|
def weighted_sum( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
2023-02-28 10:55:51 +04:00
|
|
|
cls,
|
|
|
|
|
expressions: Sequence[LinearExprT],
|
|
|
|
|
coefficients: Sequence[NumberT],
|
|
|
|
|
*,
|
|
|
|
|
constant: NumberT = 0.0,
|
2023-07-27 08:50:52 -07:00
|
|
|
) -> Union[NumberT, "_LinearExpression"]:
|
2023-02-28 10:55:51 +04:00
|
|
|
"""Creates `sum(expressions[i] * coefficients[i]) + constant`.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
It can perform simple simplifications and returns different object,
|
|
|
|
|
including the input.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Args:
|
|
|
|
|
expressions: a sequence of linear expressions or constants.
|
|
|
|
|
coefficients: a sequence of numerical constants.
|
|
|
|
|
constant: a numerical constant.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Returns:
|
2023-07-27 08:50:52 -07:00
|
|
|
a _LinearExpression instance or a numerical constant.
|
2023-06-28 15:57:32 +02:00
|
|
|
"""
|
2023-02-28 10:55:51 +04:00
|
|
|
if len(expressions) != len(coefficients):
|
|
|
|
|
raise ValueError(
|
2023-06-28 15:57:32 +02:00
|
|
|
"LinearExpr.weighted_sum: expressions and coefficients have"
|
2023-11-06 15:20:03 +01:00
|
|
|
" different lengths"
|
|
|
|
|
)
|
2023-07-03 14:30:27 +02:00
|
|
|
checked_constant: np.double = mbn.assert_is_a_number(constant)
|
2023-02-28 10:55:51 +04:00
|
|
|
if not expressions:
|
|
|
|
|
return checked_constant
|
2023-11-06 15:20:03 +01:00
|
|
|
return _sum_as_flat_linear_expression(
|
|
|
|
|
to_process=list(zip(expressions, coefficients)), offset=checked_constant
|
|
|
|
|
)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
@classmethod
|
2023-05-24 15:33:27 +02:00
|
|
|
def term( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
2023-02-28 10:55:51 +04:00
|
|
|
cls,
|
|
|
|
|
expression: LinearExprT,
|
|
|
|
|
coefficient: NumberT,
|
|
|
|
|
*,
|
|
|
|
|
constant: NumberT = 0.0,
|
|
|
|
|
) -> LinearExprT:
|
|
|
|
|
"""Creates `expression * coefficient + constant`.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
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.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Returns:
|
|
|
|
|
a LinearExpr instance or a numerical constant.
|
|
|
|
|
"""
|
2023-07-03 14:30:27 +02:00
|
|
|
checked_coefficient: np.double = mbn.assert_is_a_number(coefficient)
|
|
|
|
|
checked_constant: np.double = mbn.assert_is_a_number(constant)
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-03 14:30:27 +02:00
|
|
|
if mbn.is_zero(checked_coefficient):
|
2023-02-28 10:55:51 +04:00
|
|
|
return checked_constant
|
2023-07-03 14:30:27 +02:00
|
|
|
if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant):
|
2023-02-28 10:55:51 +04:00
|
|
|
return expression
|
2023-07-03 14:30:27 +02:00
|
|
|
if mbn.is_a_number(expression):
|
2023-11-06 15:20:03 +01:00
|
|
|
return np.double(expression) * checked_coefficient + checked_constant
|
2023-07-09 13:54:23 +02:00
|
|
|
if isinstance(expression, LinearExpr):
|
2023-11-06 15:20:03 +01:00
|
|
|
return _as_flat_linear_expression(
|
|
|
|
|
expression * checked_coefficient + checked_constant
|
|
|
|
|
)
|
|
|
|
|
raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}")
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
|
return object.__hash__(self)
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __add__(self, arg: LinearExprT) -> "_Sum":
|
|
|
|
|
return _Sum(self, arg)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __radd__(self, arg: LinearExprT) -> "_Sum":
|
2022-03-29 17:59:36 +02:00
|
|
|
return self.__add__(arg)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __sub__(self, arg: LinearExprT) -> "_Sum":
|
|
|
|
|
return _Sum(self, -arg)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __rsub__(self, arg: LinearExprT) -> "_Sum":
|
|
|
|
|
return _Sum(-self, arg)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __mul__(self, arg: NumberT) -> "_Product":
|
|
|
|
|
return _Product(self, arg)
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __rmul__(self, arg: NumberT) -> "_Product":
|
2022-03-28 16:42:03 +02:00
|
|
|
return self.__mul__(arg)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __truediv__(self, coeff: NumberT) -> "_Product":
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__mul__(1.0 / coeff)
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __neg__(self) -> "_Product":
|
|
|
|
|
return _Product(self, -1)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
def __bool__(self):
|
2023-11-06 15:20:03 +01:00
|
|
|
raise NotImplementedError(f"Cannot use a LinearExpr {self} as a Boolean value")
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __eq__(self, arg: LinearExprT) -> "BoundedLinearExpression":
|
|
|
|
|
return BoundedLinearExpression(self - arg, 0, 0)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression":
|
2023-11-06 15:20:03 +01:00
|
|
|
return BoundedLinearExpression(
|
|
|
|
|
self - arg, 0, math.inf
|
|
|
|
|
) # pytype: disable=wrong-arg-types # numpy-scalars
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression":
|
2023-11-06 15:20:03 +01:00
|
|
|
return BoundedLinearExpression(
|
|
|
|
|
self - arg, -math.inf, 0
|
|
|
|
|
) # pytype: disable=wrong-arg-types # numpy-scalars
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Variable(LinearExpr):
|
2023-01-04 08:19:04 +01:00
|
|
|
"""A variable (continuous or integral).
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
A Variable is an object that can take on any integer value within defined
|
|
|
|
|
ranges. Variables appear in constraint like:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
x + y >= 5
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
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.
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
2023-07-03 14:30:27 +02:00
|
|
|
helper: mbh.ModelBuilderHelper,
|
2023-02-28 10:55:51 +04:00
|
|
|
lb: NumberT,
|
|
|
|
|
ub: Optional[NumberT],
|
|
|
|
|
is_integral: Optional[bool],
|
|
|
|
|
name: Optional[str],
|
|
|
|
|
):
|
2023-11-06 16:16:38 +01:00
|
|
|
"""See Model.new_var below."""
|
2023-02-28 10:55:51 +04:00
|
|
|
LinearExpr.__init__(self)
|
2023-07-03 14:30:27 +02:00
|
|
|
self.__helper: mbh.ModelBuilderHelper = helper
|
2022-03-23 17:47:22 +01:00
|
|
|
# Python do not support multiple __init__ methods.
|
2023-11-06 16:16:38 +01:00
|
|
|
# This method is only called from the Model class.
|
2022-03-23 17:47:22 +01:00
|
|
|
# We hack the parameter to support the two cases:
|
|
|
|
|
# case 1:
|
|
|
|
|
# helper is a ModelBuilderHelper, lb is a double value, ub is a double
|
|
|
|
|
# value, is_integral is a Boolean value, and name is a string.
|
|
|
|
|
# case 2:
|
|
|
|
|
# helper is a ModelBuilderHelper, lb is an index (int), ub is None,
|
|
|
|
|
# is_integral is None, and name is None.
|
2023-07-03 14:30:27 +02:00
|
|
|
if mbn.is_integral(lb) and ub is None and is_integral is None:
|
2023-02-28 10:55:51 +04:00
|
|
|
self.__index: np.int32 = np.int32(lb)
|
2023-07-03 14:30:27 +02:00
|
|
|
self.__helper: mbh.ModelBuilderHelper = helper
|
2022-03-23 17:47:22 +01:00
|
|
|
else:
|
2023-02-28 10:55:51 +04:00
|
|
|
index: np.int32 = helper.add_var()
|
|
|
|
|
self.__index: np.int32 = np.int32(index)
|
2023-07-03 14:30:27 +02:00
|
|
|
self.__helper: mbh.ModelBuilderHelper = helper
|
2022-03-26 17:00:47 +01:00
|
|
|
helper.set_var_lower_bound(index, lb)
|
|
|
|
|
helper.set_var_upper_bound(index, ub)
|
|
|
|
|
helper.set_var_integrality(index, is_integral)
|
2022-03-23 17:47:22 +01:00
|
|
|
if name:
|
2022-03-26 17:00:47 +01:00
|
|
|
helper.set_var_name(index, name)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def index(self) -> np.int32:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the index of the variable in the helper."""
|
|
|
|
|
return self.__index
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-07-03 14:30:27 +02:00
|
|
|
def helper(self) -> mbh.ModelBuilderHelper:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the underlying ModelBuilderHelper."""
|
|
|
|
|
return self.__helper
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def is_equal_to(self, other: LinearExprT) -> bool:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns true if self == other in the python sense."""
|
|
|
|
|
if not isinstance(other, Variable):
|
|
|
|
|
return False
|
2023-02-28 10:55:51 +04:00
|
|
|
return self.index == other.index and self.helper == other.helper
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __str__(self) -> str:
|
2023-07-09 13:54:23 +02:00
|
|
|
return self.name
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __repr__(self) -> str:
|
2023-07-09 13:54:23 +02:00
|
|
|
return self.__str__()
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def name(self) -> str:
|
|
|
|
|
"""Returns the name of the variable."""
|
2023-07-09 13:54:23 +02:00
|
|
|
var_name = self.__helper.var_name(self.__index)
|
|
|
|
|
if var_name:
|
|
|
|
|
return var_name
|
|
|
|
|
return f"variable#{self.index}"
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@name.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def name(self, name: str) -> None:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Sets the name of the variable."""
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_var_name(self.__index, name)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
|
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def lower_bound(self) -> np.double:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Returns the lower bound of the variable."""
|
|
|
|
|
return self.__helper.var_lower_bound(self.__index)
|
|
|
|
|
|
|
|
|
|
@lower_bound.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def lower_bound(self, bound: NumberT) -> None:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Sets the lower bound of the variable."""
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_var_lower_bound(self.__index, bound)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
|
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def upper_bound(self) -> np.double:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Returns the upper bound of the variable."""
|
|
|
|
|
return self.__helper.var_upper_bound(self.__index)
|
|
|
|
|
|
|
|
|
|
@upper_bound.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def upper_bound(self, bound: NumberT) -> None:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Sets the upper bound of the variable."""
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_var_upper_bound(self.__index, bound)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
|
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def is_integral(self) -> bool:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Returns whether the variable is integral."""
|
|
|
|
|
return self.__helper.var_is_integral(self.__index)
|
|
|
|
|
|
|
|
|
|
@is_integral.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def integrality(self, is_integral: bool) -> None:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Sets the integrality of the variable."""
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_var_integrality(self.__index, is_integral)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
|
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def objective_coefficient(self) -> NumberT:
|
2022-03-24 16:48:24 +01:00
|
|
|
return self.__helper.var_objective_coefficient(self.__index)
|
|
|
|
|
|
|
|
|
|
@objective_coefficient.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def objective_coefficient(self, coeff: NumberT) -> None:
|
|
|
|
|
self.__helper.set_var_objective_coefficient(self.__index, coeff)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __eq__(self, arg: Optional[LinearExprT]) -> ConstraintT:
|
2022-03-29 17:59:36 +02:00
|
|
|
if arg is None:
|
|
|
|
|
return False
|
|
|
|
|
if isinstance(arg, Variable):
|
2023-07-09 13:54:23 +02:00
|
|
|
return VarEqVar(self, arg)
|
2023-11-06 15:20:03 +01:00
|
|
|
return BoundedLinearExpression(
|
|
|
|
|
self - arg, 0.0, 0.0
|
|
|
|
|
) # pytype: disable=wrong-arg-types # numpy-scalars
|
2022-03-29 17:59:36 +02:00
|
|
|
|
2022-03-28 16:42:03 +02:00
|
|
|
def __hash__(self):
|
|
|
|
|
return hash((self.__helper, self.__index))
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
class _BoundedLinearExpr(metaclass=abc.ABCMeta):
|
|
|
|
|
"""Interface for types that can build bounded linear (boolean) expressions.
|
2023-03-05 08:18:45 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Classes derived from _BoundedLinearExpr are used to build linear constraints
|
|
|
|
|
to be satisfied.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
* BoundedLinearExpression: a linear expression with upper and lower bounds.
|
|
|
|
|
* VarEqVar: an equality comparison between two variables.
|
|
|
|
|
"""
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
@abc.abstractmethod
|
2023-11-06 15:20:03 +01:00
|
|
|
def _add_linear_constraint(
|
|
|
|
|
self, helper: mbh.ModelBuilderHelper, name: str
|
|
|
|
|
) -> "LinearConstraint":
|
2023-07-09 13:54:23 +02:00
|
|
|
"""Creates a new linear constraint in the helper.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Args:
|
|
|
|
|
helper (mbh.ModelBuilderHelper): The helper to create the constraint.
|
|
|
|
|
name (str): The name of the linear constraint.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Returns:
|
|
|
|
|
LinearConstraint: A reference to the linear constraint in the helper.
|
|
|
|
|
"""
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
@abc.abstractmethod
|
|
|
|
|
def _add_enforced_linear_constraint(
|
|
|
|
|
self,
|
|
|
|
|
helper: mbh.ModelBuilderHelper,
|
|
|
|
|
var: Variable,
|
|
|
|
|
value: bool,
|
|
|
|
|
name: str,
|
|
|
|
|
) -> "EnforcedLinearConstraint":
|
|
|
|
|
"""Creates a new enforced linear constraint in the helper.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
helper (mbh.ModelBuilderHelper): The helper to create the constraint.
|
|
|
|
|
var (Variable): The indicator variable of the constraint.
|
|
|
|
|
value (bool): The indicator value of the constraint.
|
|
|
|
|
name (str): The name of the linear constraint.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Enforced LinearConstraint: A reference to the linear constraint in the
|
|
|
|
|
helper.
|
|
|
|
|
"""
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-27 08:50:52 -07:00
|
|
|
def _add_linear_constraint_to_helper(
|
|
|
|
|
bounded_expr: Union[bool, _BoundedLinearExpr],
|
2023-07-09 13:54:23 +02:00
|
|
|
helper: mbh.ModelBuilderHelper,
|
2023-07-27 08:50:52 -07:00
|
|
|
name: Optional[str],
|
2023-07-09 13:54:23 +02:00
|
|
|
):
|
|
|
|
|
"""Creates a new linear constraint in the helper.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
It handles boolean values (which might arise in the construction of
|
|
|
|
|
BoundedLinearExpressions).
|
2023-01-19 16:09:45 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Args:
|
2023-07-27 08:50:52 -07:00
|
|
|
bounded_expr: The bounded expression used to create the constraint.
|
2023-07-09 13:54:23 +02:00
|
|
|
helper: The helper to create the constraint.
|
|
|
|
|
name: The name of the constraint to be created.
|
2022-03-28 16:42:03 +02:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Returns:
|
|
|
|
|
LinearConstraint: a constraint in the helper corresponding to the input.
|
2022-03-28 16:42:03 +02:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Raises:
|
|
|
|
|
TypeError: If constraint is an invalid type.
|
|
|
|
|
"""
|
2023-07-27 08:50:52 -07:00
|
|
|
if isinstance(bounded_expr, bool):
|
2023-07-09 13:54:23 +02:00
|
|
|
c = LinearConstraint(helper)
|
2023-07-27 08:50:52 -07:00
|
|
|
if name is not None:
|
|
|
|
|
helper.set_constraint_name(c.index, name)
|
|
|
|
|
if bounded_expr:
|
|
|
|
|
# constraint that is always feasible: 0.0 <= nothing <= 0.0
|
|
|
|
|
helper.set_constraint_lower_bound(c.index, 0.0)
|
|
|
|
|
helper.set_constraint_upper_bound(c.index, 0.0)
|
2022-03-28 16:42:03 +02:00
|
|
|
else:
|
2023-07-27 08:50:52 -07:00
|
|
|
# constraint that is always infeasible: +oo <= nothing <= -oo
|
|
|
|
|
helper.set_constraint_lower_bound(c.index, 1)
|
2023-07-09 13:54:23 +02:00
|
|
|
helper.set_constraint_upper_bound(c.index, -1)
|
|
|
|
|
return c
|
2023-07-27 08:50:52 -07:00
|
|
|
if isinstance(bounded_expr, _BoundedLinearExpr):
|
2023-07-09 13:54:23 +02:00
|
|
|
# pylint: disable=protected-access
|
2023-07-27 08:50:52 -07:00
|
|
|
return bounded_expr._add_linear_constraint(helper, name)
|
|
|
|
|
raise TypeError("invalid type={}".format(type(bounded_expr)))
|
2022-03-28 16:42:03 +02:00
|
|
|
|
|
|
|
|
|
2023-11-05 09:51:40 +01:00
|
|
|
def _add_enforced_linear_constraint_to_helper(
|
|
|
|
|
bounded_expr: Union[bool, _BoundedLinearExpr],
|
2023-11-06 16:16:38 +01:00
|
|
|
helper: mbh.ModelBuilderHelper,
|
2023-11-06 15:20:03 +01:00
|
|
|
var: Variable,
|
|
|
|
|
value: bool,
|
2023-11-05 09:51:40 +01:00
|
|
|
name: Optional[str],
|
|
|
|
|
):
|
|
|
|
|
"""Creates a new enforced linear constraint in the helper.
|
|
|
|
|
|
|
|
|
|
It handles boolean values (which might arise in the construction of
|
|
|
|
|
BoundedLinearExpressions).
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
bounded_expr: The bounded expression used to create the constraint.
|
2023-11-06 18:08:34 +01:00
|
|
|
helper: The helper to create the constraint.
|
2023-11-06 15:20:03 +01:00
|
|
|
var: the variable used in the indicator
|
|
|
|
|
value: the value used in the indicator
|
2023-11-05 09:51:40 +01:00
|
|
|
name: The name of the constraint to be created.
|
|
|
|
|
|
|
|
|
|
Returns:
|
2023-11-06 15:20:03 +01:00
|
|
|
EnforcedLinearConstraint: a constraint in the helper corresponding to the
|
|
|
|
|
input.
|
2023-11-05 09:51:40 +01:00
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
TypeError: If constraint is an invalid type.
|
|
|
|
|
"""
|
|
|
|
|
if isinstance(bounded_expr, bool):
|
|
|
|
|
c = EnforcedLinearConstraint(helper)
|
2023-11-06 15:20:03 +01:00
|
|
|
c.indicator_variable = var
|
|
|
|
|
c.indicator_value = value
|
2023-11-05 09:51:40 +01:00
|
|
|
if name is not None:
|
|
|
|
|
helper.set_enforced_constraint_name(c.index, name)
|
|
|
|
|
if bounded_expr:
|
|
|
|
|
# constraint that is always feasible: 0.0 <= nothing <= 0.0
|
|
|
|
|
helper.set_enforced_constraint_lower_bound(c.index, 0.0)
|
|
|
|
|
helper.set_enforced_constraint_upper_bound(c.index, 0.0)
|
|
|
|
|
else:
|
|
|
|
|
# constraint that is always infeasible: +oo <= nothing <= -oo
|
|
|
|
|
helper.set_enforced_constraint_lower_bound(c.index, 1)
|
|
|
|
|
helper.set_enforced_constraint_upper_bound(c.index, -1)
|
|
|
|
|
return c
|
|
|
|
|
if isinstance(bounded_expr, _BoundedLinearExpr):
|
|
|
|
|
# pylint: disable=protected-access
|
2023-11-06 15:20:03 +01:00
|
|
|
return bounded_expr._add_enforced_linear_constraint(helper, var, value, name)
|
2023-11-05 09:51:40 +01:00
|
|
|
raise TypeError("invalid type={}".format(type(bounded_expr)))
|
|
|
|
|
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
@dataclasses.dataclass(repr=False, eq=False, frozen=True)
|
|
|
|
|
class VarEqVar(_BoundedLinearExpr):
|
|
|
|
|
"""Represents var == var."""
|
2022-03-28 16:42:03 +02:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
__slots__ = ("left", "right")
|
2022-03-28 16:42:03 +02:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
left: Variable
|
|
|
|
|
right: Variable
|
2022-03-28 16:42:03 +02:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __str__(self):
|
|
|
|
|
return f"{self.left} == {self.right}"
|
2022-03-28 16:42:03 +02:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __repr__(self):
|
|
|
|
|
return self.__str__()
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __bool__(self) -> bool:
|
|
|
|
|
return hash(self.left) == hash(self.right)
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def _add_linear_constraint(
|
|
|
|
|
self, helper: mbh.ModelBuilderHelper, name: str
|
|
|
|
|
) -> "LinearConstraint":
|
2023-07-09 13:54:23 +02:00
|
|
|
c = LinearConstraint(helper)
|
|
|
|
|
helper.set_constraint_lower_bound(c.index, 0.0)
|
|
|
|
|
helper.set_constraint_upper_bound(c.index, 0.0)
|
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
helper.add_term_to_constraint(c.index, self.left.index, 1.0)
|
|
|
|
|
helper.add_term_to_constraint(c.index, self.right.index, -1.0)
|
|
|
|
|
# pylint: enable=protected-access
|
|
|
|
|
helper.set_constraint_name(c.index, name)
|
|
|
|
|
return c
|
|
|
|
|
|
2023-11-05 09:51:40 +01:00
|
|
|
def _add_enforced_linear_constraint(
|
2023-11-06 15:20:03 +01:00
|
|
|
self,
|
|
|
|
|
helper: mbh.ModelBuilderHelper,
|
2023-11-06 18:08:34 +01:00
|
|
|
var: Variable,
|
2023-11-06 15:20:03 +01:00
|
|
|
value: bool,
|
|
|
|
|
name: str,
|
|
|
|
|
) -> "EnforcedLinearConstraint":
|
|
|
|
|
"""Adds an enforced linear constraint to the model."""
|
2023-11-05 09:51:40 +01:00
|
|
|
c = EnforcedLinearConstraint(helper)
|
2023-11-06 15:20:03 +01:00
|
|
|
c.indicator_variable = var
|
|
|
|
|
c.indicator_value = value
|
2023-11-05 09:51:40 +01:00
|
|
|
helper.set_enforced_constraint_lower_bound(c.index, 0.0)
|
|
|
|
|
helper.set_enforced_constraint_upper_bound(c.index, 0.0)
|
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
helper.add_term_to_enforced_constraint(c.index, self.left.index, 1.0)
|
|
|
|
|
helper.add_term_to_enforced_constraint(c.index, self.right.index, -1.0)
|
|
|
|
|
# pylint: enable=protected-access
|
|
|
|
|
helper.set_enforced_constraint_name(c.index, name)
|
|
|
|
|
return c
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
|
|
|
|
|
class BoundedLinearExpression(_BoundedLinearExpr):
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Represents a linear constraint: `lb <= linear expression <= ub`.
|
|
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
The only use of this class is to be added to the Model through
|
|
|
|
|
`Model.add(bounded expression)`, as in:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
model.Add(x + 2 * y -1 >= z)
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT):
|
|
|
|
|
self.__expr: LinearExprT = expr
|
2023-07-03 14:30:27 +02:00
|
|
|
self.__lb: np.double = mbn.assert_is_a_number(lb)
|
|
|
|
|
self.__ub: np.double = mbn.assert_is_a_number(ub)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __str__(self) -> str:
|
2022-03-23 17:47:22 +01:00
|
|
|
if self.__lb > -math.inf and self.__ub < math.inf:
|
|
|
|
|
if self.__lb == self.__ub:
|
2023-11-06 18:08:34 +01:00
|
|
|
return f"{self.__expr} == {self.__lb}"
|
2022-03-23 17:47:22 +01:00
|
|
|
else:
|
2023-11-06 18:08:34 +01:00
|
|
|
return f"{self.__lb} <= {self.__expr} <= {self.__ub}"
|
2022-03-23 17:47:22 +01:00
|
|
|
elif self.__lb > -math.inf:
|
2023-11-06 18:08:34 +01:00
|
|
|
return f"{self.__expr} >= {self.__lb}"
|
2022-03-23 17:47:22 +01:00
|
|
|
elif self.__ub < math.inf:
|
2023-11-06 18:08:34 +01:00
|
|
|
return f"{self.__expr} <= {self.__ub}"
|
2022-03-23 17:47:22 +01:00
|
|
|
else:
|
2023-11-06 18:08:34 +01:00
|
|
|
return f"{self.__expr} free"
|
2023-07-09 13:54:23 +02:00
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return self.__str__()
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def expression(self) -> LinearExprT:
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__expr
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def lower_bound(self) -> np.double:
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__lb
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def upper_bound(self) -> np.double:
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__ub
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __bool__(self) -> bool:
|
2022-03-23 17:47:22 +01:00
|
|
|
raise NotImplementedError(
|
2023-11-06 15:20:03 +01:00
|
|
|
f"Cannot use a BoundedLinearExpression {self} as a Boolean value"
|
|
|
|
|
)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def _add_linear_constraint(
|
|
|
|
|
self, helper: mbh.ModelBuilderHelper, name: Optional[str]
|
|
|
|
|
) -> "LinearConstraint":
|
2023-07-09 13:54:23 +02:00
|
|
|
c = LinearConstraint(helper)
|
2023-07-27 08:50:52 -07:00
|
|
|
flat_expr = _as_flat_linear_expression(self.__expr)
|
2023-07-09 13:54:23 +02:00
|
|
|
# pylint: disable=protected-access
|
2023-11-06 15:20:03 +01:00
|
|
|
helper.add_terms_to_constraint(
|
|
|
|
|
c.index, flat_expr._variable_indices, flat_expr._coefficients
|
|
|
|
|
)
|
|
|
|
|
helper.set_constraint_lower_bound(c.index, self.__lb - flat_expr._offset)
|
|
|
|
|
helper.set_constraint_upper_bound(c.index, self.__ub - flat_expr._offset)
|
2023-07-09 13:54:23 +02:00
|
|
|
# pylint: enable=protected-access
|
2023-07-27 08:50:52 -07:00
|
|
|
if name is not None:
|
|
|
|
|
helper.set_constraint_name(c.index, name)
|
2023-07-09 13:54:23 +02:00
|
|
|
return c
|
|
|
|
|
|
2023-11-05 09:51:40 +01:00
|
|
|
def _add_enforced_linear_constraint(
|
2023-11-06 15:20:03 +01:00
|
|
|
self,
|
|
|
|
|
helper: mbh.ModelBuilderHelper,
|
2023-11-07 14:07:43 +01:00
|
|
|
var: Variable,
|
2023-11-06 15:20:03 +01:00
|
|
|
value: bool,
|
|
|
|
|
name: Optional[str],
|
|
|
|
|
) -> "EnforcedLinearConstraint":
|
|
|
|
|
"""Adds an enforced linear constraint to the model."""
|
2023-11-05 09:51:40 +01:00
|
|
|
c = EnforcedLinearConstraint(helper)
|
2023-11-06 15:20:03 +01:00
|
|
|
c.indicator_variable = var
|
|
|
|
|
c.indicator_value = value
|
2023-11-05 09:51:40 +01:00
|
|
|
flat_expr = _as_flat_linear_expression(self.__expr)
|
|
|
|
|
# pylint: disable=protected-access
|
2023-11-06 15:20:03 +01:00
|
|
|
helper.add_terms_to_enforced_constraint(
|
|
|
|
|
c.index, flat_expr._variable_indices, flat_expr._coefficients
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
helper.set_enforced_constraint_lower_bound(
|
2023-11-06 15:20:03 +01:00
|
|
|
c.index, self.__lb - flat_expr._offset
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
helper.set_enforced_constraint_upper_bound(
|
2023-11-06 15:20:03 +01:00
|
|
|
c.index, self.__ub - flat_expr._offset
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
# pylint: enable=protected-access
|
|
|
|
|
if name is not None:
|
|
|
|
|
helper.set_enforced_constraint_name(c.index, name)
|
|
|
|
|
return c
|
|
|
|
|
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-01-19 16:09:45 +01:00
|
|
|
class LinearConstraint:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Stores a linear equation.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Example:
|
|
|
|
|
x = model.new_num_var(0, 10, 'x')
|
|
|
|
|
y = model.new_num_var(0, 10, 'y')
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
linear_constraint = model.add(x + 2 * y == 5)
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def __init__(
|
|
|
|
|
self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None
|
|
|
|
|
):
|
2023-07-09 13:54:23 +02:00
|
|
|
if index is None:
|
|
|
|
|
self.__index = helper.add_linear_constraint()
|
|
|
|
|
else:
|
|
|
|
|
self.__index = index
|
2023-07-03 14:30:27 +02:00
|
|
|
self.__helper: mbh.ModelBuilderHelper = helper
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-07-09 13:54:23 +02:00
|
|
|
def index(self) -> IntegerT:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the index of the constraint in the helper."""
|
|
|
|
|
return self.__index
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-07-03 14:30:27 +02:00
|
|
|
def helper(self) -> mbh.ModelBuilderHelper:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Returns the ModelBuilderHelper instance."""
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__helper
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def lower_bound(self) -> np.double:
|
2022-03-24 16:48:24 +01:00
|
|
|
return self.__helper.constraint_lower_bound(self.__index)
|
|
|
|
|
|
|
|
|
|
@lower_bound.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def lower_bound(self, bound: NumberT) -> None:
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_constraint_lower_bound(self.__index, bound)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
|
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def upper_bound(self) -> np.double:
|
2022-03-24 16:48:24 +01:00
|
|
|
return self.__helper.constraint_upper_bound(self.__index)
|
|
|
|
|
|
|
|
|
|
@upper_bound.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def upper_bound(self, bound: NumberT) -> None:
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_constraint_upper_bound(self.__index, bound)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
|
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def name(self) -> str:
|
2023-07-09 13:54:23 +02:00
|
|
|
constraint_name = self.__helper.constraint_name(self.__index)
|
|
|
|
|
if constraint_name:
|
|
|
|
|
return constraint_name
|
|
|
|
|
return f"linear_constraint#{self.__index}"
|
2022-03-24 16:48:24 +01:00
|
|
|
|
|
|
|
|
@name.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def name(self, name: str) -> None:
|
2022-03-26 17:00:47 +01:00
|
|
|
return self.__helper.set_constraint_name(self.__index, name)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-07-27 08:50:52 -07:00
|
|
|
def is_always_false(self) -> bool:
|
|
|
|
|
"""Returns True if the constraint is always false.
|
|
|
|
|
|
|
|
|
|
Usually, it means that it was created by model.add(False)
|
|
|
|
|
"""
|
|
|
|
|
return self.lower_bound > self.upper_bound
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def __str__(self):
|
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
2023-07-27 08:50:52 -07:00
|
|
|
return (
|
|
|
|
|
f"LinearConstraint({self.name}, lb={self.lower_bound},"
|
|
|
|
|
f" ub={self.upper_bound},"
|
|
|
|
|
f" var_indices={self.helper.constraint_var_indices(self.index)},"
|
|
|
|
|
f" coefficients={self.helper.constraint_coefficients(self.index)})"
|
|
|
|
|
)
|
2023-07-09 13:54:23 +02:00
|
|
|
|
2023-07-20 15:13:56 -07:00
|
|
|
def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
|
|
|
|
|
"""Sets the coefficient of the variable in the constraint."""
|
2023-07-27 08:50:52 -07:00
|
|
|
if self.is_always_false():
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Constraint {self.index} is always false and cannot be modified"
|
|
|
|
|
)
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
|
2023-11-05 09:51:40 +01:00
|
|
|
|
|
|
|
|
def add_term(self, var: Variable, coeff: NumberT) -> None:
|
|
|
|
|
"""Adds var * coeff to the constraint."""
|
|
|
|
|
if self.is_always_false():
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Constraint {self.index} is always false and cannot be modified"
|
|
|
|
|
)
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)
|
2023-11-05 09:51:40 +01:00
|
|
|
|
2023-11-05 12:41:22 +01:00
|
|
|
def clear_terms(self) -> None:
|
|
|
|
|
"""Clear all terms of the constraint."""
|
|
|
|
|
self.__helper.clear_constraint_terms(self.__index)
|
|
|
|
|
|
2023-11-05 09:51:40 +01:00
|
|
|
|
|
|
|
|
class EnforcedLinearConstraint:
|
|
|
|
|
"""Stores an enforced linear equation, also name indicator constraint.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
x = model.new_num_var(0, 10, 'x')
|
|
|
|
|
y = model.new_num_var(0, 10, 'y')
|
|
|
|
|
z = model.new_bool_var('z')
|
|
|
|
|
|
|
|
|
|
enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
|
|
|
|
|
"""
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def __init__(
|
|
|
|
|
self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None
|
|
|
|
|
):
|
2023-11-05 09:51:40 +01:00
|
|
|
if index is None:
|
|
|
|
|
self.__index = helper.add_enforced_linear_constraint()
|
|
|
|
|
else:
|
2023-11-05 14:40:40 +01:00
|
|
|
if not helper.is_enforced_linear_constraint(index):
|
|
|
|
|
raise ValueError(
|
2023-11-06 15:20:03 +01:00
|
|
|
f"the given index {index} does not refer to an enforced linear"
|
|
|
|
|
" constraint"
|
2023-11-05 14:40:40 +01:00
|
|
|
)
|
|
|
|
|
|
2023-11-05 09:51:40 +01:00
|
|
|
self.__index = index
|
|
|
|
|
self.__helper: mbh.ModelBuilderHelper = helper
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def index(self) -> IntegerT:
|
|
|
|
|
"""Returns the index of the constraint in the helper."""
|
|
|
|
|
return self.__index
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def helper(self) -> mbh.ModelBuilderHelper:
|
|
|
|
|
"""Returns the ModelBuilderHelper instance."""
|
|
|
|
|
return self.__helper
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def lower_bound(self) -> np.double:
|
|
|
|
|
return self.__helper.enforced_constraint_lower_bound(self.__index)
|
|
|
|
|
|
|
|
|
|
@lower_bound.setter
|
|
|
|
|
def lower_bound(self, bound: NumberT) -> None:
|
|
|
|
|
self.__helper.set_enforced_constraint_lower_bound(self.__index, bound)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def upper_bound(self) -> np.double:
|
|
|
|
|
return self.__helper.enforced_constraint_upper_bound(self.__index)
|
|
|
|
|
|
|
|
|
|
@upper_bound.setter
|
|
|
|
|
def upper_bound(self, bound: NumberT) -> None:
|
|
|
|
|
self.__helper.set_enforced_constraint_upper_bound(self.__index, bound)
|
|
|
|
|
|
|
|
|
|
@property
|
2023-11-06 15:20:03 +01:00
|
|
|
def indicator_variable(self) -> "Variable":
|
|
|
|
|
enforcement_var_index = (
|
|
|
|
|
self.__helper.enforced_constraint_indicator_variable_index(self.__index)
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
return Variable(self.__helper, enforcement_var_index, None, None, None)
|
|
|
|
|
|
|
|
|
|
@indicator_variable.setter
|
2023-11-06 15:20:03 +01:00
|
|
|
def indicator_variable(self, var: "Variable") -> None:
|
2023-11-05 09:51:40 +01:00
|
|
|
self.__helper.set_enforced_constraint_indicator_variable_index(
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__index, var.index
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def indicator_value(self) -> bool:
|
|
|
|
|
return self.__helper.enforced_constraint_indicator_value(self.__index)
|
|
|
|
|
|
|
|
|
|
@indicator_value.setter
|
|
|
|
|
def indicator_value(self, value: bool) -> None:
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.set_enforced_constraint_indicator_value(self.__index, value)
|
2023-11-05 09:51:40 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def name(self) -> str:
|
|
|
|
|
constraint_name = self.__helper.enforced_constraint_name(self.__index)
|
|
|
|
|
if constraint_name:
|
|
|
|
|
return constraint_name
|
|
|
|
|
return f"enforced_linear_constraint#{self.__index}"
|
|
|
|
|
|
|
|
|
|
@name.setter
|
|
|
|
|
def name(self, name: str) -> None:
|
|
|
|
|
return self.__helper.set_enforced_constraint_name(self.__index, name)
|
|
|
|
|
|
|
|
|
|
def is_always_false(self) -> bool:
|
|
|
|
|
"""Returns True if the constraint is always false.
|
|
|
|
|
|
|
|
|
|
Usually, it means that it was created by model.add(False)
|
|
|
|
|
"""
|
|
|
|
|
return self.lower_bound > self.upper_bound
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return (
|
|
|
|
|
f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound},"
|
|
|
|
|
f" ub={self.upper_bound},"
|
|
|
|
|
f" var_indices={self.helper.enforced_constraint_var_indices(self.index)},"
|
|
|
|
|
f" coefficients={self.helper.enforced_constraint_coefficients(self.index)},"
|
|
|
|
|
f" indicator_variable={self.indicator_variable}"
|
2023-11-06 15:20:03 +01:00
|
|
|
f" indicator_value={self.indicator_value})"
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
|
|
|
|
|
def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
|
|
|
|
|
"""Sets the coefficient of the variable in the constraint."""
|
|
|
|
|
if self.is_always_false():
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Constraint {self.index} is always false and cannot be modified"
|
|
|
|
|
)
|
|
|
|
|
self.__helper.set_enforced_constraint_coefficient(
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__index, var.index, coeff
|
|
|
|
|
)
|
2023-07-20 15:13:56 -07:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def add_term(self, var: Variable, coeff: NumberT) -> None:
|
2023-07-20 15:13:56 -07:00
|
|
|
"""Adds var * coeff to the constraint."""
|
2023-07-27 08:50:52 -07:00
|
|
|
if self.is_always_false():
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Constraint {self.index} is always false and cannot be modified"
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
self.__helper.safe_add_term_to_enforced_constraint(
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__index, var.index, coeff
|
|
|
|
|
)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-11-05 12:41:22 +01:00
|
|
|
def clear_terms(self) -> None:
|
|
|
|
|
"""Clear all terms of the constraint."""
|
|
|
|
|
self.__helper.clear_enforced_constraint_terms(self.__index)
|
|
|
|
|
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
class Model:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Methods for building a linear model.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Methods beginning with:
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
* ```new_``` create integer, boolean, or interval variables.
|
|
|
|
|
* ```add_``` create new constraints and add them to the model.
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
def __init__(self):
|
2023-07-03 14:30:27 +02:00
|
|
|
self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper()
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
def clone(self) -> "Model":
|
2023-10-24 11:41:30 +02:00
|
|
|
"""Returns a clone of the current model."""
|
2023-11-06 16:16:38 +01:00
|
|
|
clone = Model()
|
2023-10-24 11:41:30 +02:00
|
|
|
clone.helper.overwrite_model(self.helper)
|
|
|
|
|
return clone
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
@typing.overload
|
2023-11-06 15:20:03 +01:00
|
|
|
def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index:
|
2023-07-09 13:54:23 +02:00
|
|
|
...
|
|
|
|
|
|
|
|
|
|
@typing.overload
|
|
|
|
|
def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
def _get_linear_constraints(
|
2023-11-06 15:20:03 +01:00
|
|
|
self, constraints: Optional[_IndexOrSeries] = None
|
|
|
|
|
) -> _IndexOrSeries:
|
2023-07-09 13:54:23 +02:00
|
|
|
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(
|
2023-11-06 15:20:03 +01:00
|
|
|
self, variables: Optional[_IndexOrSeries] = None
|
|
|
|
|
) -> _IndexOrSeries:
|
2023-07-09 13:54:23 +02:00
|
|
|
if variables is None:
|
|
|
|
|
return self.get_variables()
|
|
|
|
|
return variables
|
|
|
|
|
|
|
|
|
|
def get_linear_constraints(self) -> pd.Index:
|
|
|
|
|
"""Gets all linear constraints in the model."""
|
|
|
|
|
return pd.Index(
|
2023-11-06 15:20:03 +01:00
|
|
|
[self.linear_constraint_from_index(i) for i in range(self.num_constraints)],
|
2023-07-09 13:54:23 +02:00
|
|
|
name="linear_constraint",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_linear_constraint_expressions(
|
2023-11-06 15:20:03 +01:00
|
|
|
self, constraints: Optional[_IndexOrSeries] = None
|
|
|
|
|
) -> pd.Series:
|
2023-07-09 13:54:23 +02:00
|
|
|
"""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.
|
|
|
|
|
|
|
|
|
|
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
|
2023-11-06 15:20:03 +01:00
|
|
|
sum(
|
|
|
|
|
coeff * Variable(self.__helper, var_id, None, None, None)
|
2023-07-09 13:54:23 +02:00
|
|
|
for var_id, coeff in zip(
|
|
|
|
|
c.helper.constraint_var_indices(c.index),
|
|
|
|
|
c.helper.constraint_coefficients(c.index),
|
2023-11-06 15:20:03 +01:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
),
|
2023-07-09 13:54:23 +02:00
|
|
|
values=self._get_linear_constraints(constraints),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_linear_constraint_lower_bounds(
|
2023-11-06 15:20:03 +01:00
|
|
|
self, constraints: Optional[_IndexOrSeries] = None
|
|
|
|
|
) -> pd.Series:
|
2023-07-09 13:54:23 +02:00
|
|
|
"""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.
|
|
|
|
|
|
|
|
|
|
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_upper_bounds(
|
2023-11-06 15:20:03 +01:00
|
|
|
self, constraints: Optional[_IndexOrSeries] = None
|
|
|
|
|
) -> pd.Series:
|
2023-07-09 13:54:23 +02:00
|
|
|
"""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.
|
|
|
|
|
|
|
|
|
|
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_variables(self) -> pd.Index:
|
|
|
|
|
"""Gets all variables in the model."""
|
|
|
|
|
return pd.Index(
|
|
|
|
|
[self.var_from_index(i) for i in range(self.num_variables)],
|
|
|
|
|
name="variable",
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def get_variable_lower_bounds(
|
|
|
|
|
self, variables: Optional[_IndexOrSeries] = None
|
|
|
|
|
) -> pd.Series:
|
2023-07-09 13:54:23 +02:00
|
|
|
"""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.
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def get_variable_upper_bounds(
|
|
|
|
|
self, variables: Optional[_IndexOrSeries] = None
|
|
|
|
|
) -> pd.Series:
|
2023-07-09 13:54:23 +02:00
|
|
|
"""Gets the upper bounds of all variables in the set.
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
|
2022-03-23 17:47:22 +01:00
|
|
|
# Integer variable.
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def new_var(
|
|
|
|
|
self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
|
|
|
|
|
) -> Variable:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Create an integer variable with domain [lb, ub].
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
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.
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Returns:
|
|
|
|
|
a variable whose domain is [lb, ub].
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
return Variable(self.__helper, lb, ub, is_integer, name)
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def new_int_var(
|
|
|
|
|
self, lb: NumberT, ub: NumberT, name: Optional[str] = None
|
|
|
|
|
) -> Variable:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Create an integer variable with domain [lb, ub].
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Args:
|
|
|
|
|
lb: Lower bound of the variable.
|
|
|
|
|
ub: Upper bound of the variable.
|
|
|
|
|
name: The name of the variable.
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Returns:
|
|
|
|
|
a variable whose domain is [lb, ub].
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
return self.new_var(lb, ub, True, name)
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def new_num_var(
|
|
|
|
|
self, lb: NumberT, ub: NumberT, name: Optional[str] = None
|
|
|
|
|
) -> Variable:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Create an integer variable with domain [lb, ub].
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Args:
|
|
|
|
|
lb: Lower bound of the variable.
|
|
|
|
|
ub: Upper bound of the variable.
|
|
|
|
|
name: The name of the variable.
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Returns:
|
|
|
|
|
a variable whose domain is [lb, ub].
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
return self.new_var(lb, ub, False, name)
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def new_bool_var(self, name: Optional[str] = None) -> Variable:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Creates a 0-1 variable with the given name."""
|
2023-11-06 15:20:03 +01:00
|
|
|
return self.new_var(
|
|
|
|
|
0, 1, True, name
|
|
|
|
|
) # pytype: disable=wrong-arg-types # numpy-scalars
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def new_constant(self, value: NumberT) -> Variable:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Declares a constant variable."""
|
2023-02-28 10:55:51 +04:00
|
|
|
return self.new_var(value, value, False, None)
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def new_var_series(
|
2023-02-28 10:55:51 +04:00
|
|
|
self,
|
2023-07-09 13:54:23 +02:00
|
|
|
name: str,
|
|
|
|
|
index: pd.Index,
|
|
|
|
|
lower_bounds: Union[NumberT, pd.Series] = -math.inf,
|
|
|
|
|
upper_bounds: Union[NumberT, pd.Series] = math.inf,
|
|
|
|
|
is_integral: Union[bool, pd.Series] = False,
|
|
|
|
|
) -> pd.Series:
|
|
|
|
|
"""Creates a series of (scalar-valued) variables with the given name.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Args:
|
|
|
|
|
name (str): Required. The name of the variable set.
|
|
|
|
|
index (pd.Index): Required. The index to use for the variable set.
|
|
|
|
|
lower_bounds (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_bounds (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_integral (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.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Returns:
|
|
|
|
|
pd.Series: The variable set indexed by its corresponding dimensions.
|
|
|
|
|
|
|
|
|
|
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))
|
2023-11-06 15:20:03 +01:00
|
|
|
if (
|
|
|
|
|
mbn.is_a_number(lower_bounds)
|
|
|
|
|
and mbn.is_a_number(upper_bounds)
|
|
|
|
|
and lower_bounds > upper_bounds
|
|
|
|
|
):
|
2023-07-09 13:54:23 +02:00
|
|
|
raise ValueError(
|
2023-11-06 15:20:03 +01:00
|
|
|
"lower_bound={} is greater than upper_bound={} for variable set={}".format(
|
|
|
|
|
lower_bounds, upper_bounds, name
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if (
|
|
|
|
|
isinstance(is_integral, bool)
|
|
|
|
|
and is_integral
|
|
|
|
|
and mbn.is_a_number(lower_bounds)
|
|
|
|
|
and mbn.is_a_number(upper_bounds)
|
|
|
|
|
and math.isfinite(lower_bounds)
|
|
|
|
|
and math.isfinite(upper_bounds)
|
|
|
|
|
and math.ceil(lower_bounds) > math.floor(upper_bounds)
|
|
|
|
|
):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds))
|
|
|
|
|
+ " is greater than floor("
|
|
|
|
|
+ "upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds))
|
|
|
|
|
+ " for variable set={}".format(name)
|
|
|
|
|
)
|
|
|
|
|
lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
|
|
|
|
|
upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
|
|
|
|
|
is_integrals = _convert_to_series_and_validate_index(is_integral, index)
|
2023-07-09 13:54:23 +02:00
|
|
|
return pd.Series(
|
|
|
|
|
index=index,
|
|
|
|
|
data=[
|
|
|
|
|
# pylint: disable=g-complex-comprehension
|
|
|
|
|
Variable(
|
|
|
|
|
helper=self.__helper,
|
|
|
|
|
name=f"{name}[{i}]",
|
|
|
|
|
lb=lower_bounds[i],
|
|
|
|
|
ub=upper_bounds[i],
|
|
|
|
|
is_integral=is_integrals[i],
|
2023-11-06 15:20:03 +01:00
|
|
|
)
|
|
|
|
|
for i in index
|
2023-07-09 13:54:23 +02:00
|
|
|
],
|
2023-06-28 15:57:32 +02:00
|
|
|
)
|
2023-01-19 16:09:45 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def new_num_var_series(
|
2023-02-28 10:55:51 +04:00
|
|
|
self,
|
2023-07-09 13:54:23 +02:00
|
|
|
name: str,
|
|
|
|
|
index: pd.Index,
|
|
|
|
|
lower_bounds: Union[NumberT, pd.Series] = -math.inf,
|
|
|
|
|
upper_bounds: Union[NumberT, pd.Series] = math.inf,
|
|
|
|
|
) -> pd.Series:
|
|
|
|
|
"""Creates a series of continuous variables with the given name.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Args:
|
|
|
|
|
name (str): Required. The name of the variable set.
|
|
|
|
|
index (pd.Index): Required. The index to use for the variable set.
|
|
|
|
|
lower_bounds (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_bounds (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.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Returns:
|
|
|
|
|
pd.Series: The variable set indexed by its corresponding dimensions.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
"""
|
2023-11-06 15:20:03 +01:00
|
|
|
return self.new_var_series(name, index, lower_bounds, upper_bounds, False)
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def new_int_var_series(
|
|
|
|
|
self,
|
|
|
|
|
name: str,
|
|
|
|
|
index: pd.Index,
|
|
|
|
|
lower_bounds: Union[NumberT, pd.Series] = -math.inf,
|
|
|
|
|
upper_bounds: Union[NumberT, pd.Series] = math.inf,
|
|
|
|
|
) -> pd.Series:
|
|
|
|
|
"""Creates a series of integer variables with the given name.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Args:
|
|
|
|
|
name (str): Required. The name of the variable set.
|
|
|
|
|
index (pd.Index): Required. The index to use for the variable set.
|
|
|
|
|
lower_bounds (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_bounds (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.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Returns:
|
|
|
|
|
pd.Series: The variable set indexed by its corresponding dimensions.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
"""
|
2023-11-06 15:20:03 +01:00
|
|
|
return self.new_var_series(name, index, lower_bounds, upper_bounds, True)
|
2023-01-19 16:09:45 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def new_bool_var_series(
|
2023-02-28 10:55:51 +04:00
|
|
|
self,
|
2023-07-09 13:54:23 +02:00
|
|
|
name: str,
|
|
|
|
|
index: pd.Index,
|
|
|
|
|
) -> pd.Series:
|
|
|
|
|
"""Creates a series of Boolean variables with the given name.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
name (str): Required. The name of the variable set.
|
|
|
|
|
index (pd.Index): Required. The index to use for the variable set.
|
2023-02-28 10:55:51 +04:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
Returns:
|
|
|
|
|
pd.Series: The variable set indexed by its corresponding dimensions.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
"""
|
|
|
|
|
return self.new_var_series(name, index, 0, 1, True)
|
2023-02-28 10:55:51 +04:00
|
|
|
|
|
|
|
|
def var_from_index(self, index: IntegerT) -> Variable:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Rebuilds a variable object from the model and its index."""
|
|
|
|
|
return Variable(self.__helper, index, None, None, None)
|
|
|
|
|
|
|
|
|
|
# Linear constraints.
|
|
|
|
|
|
2023-05-24 15:33:27 +02:00
|
|
|
def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
2023-02-28 10:55:51 +04:00
|
|
|
self,
|
|
|
|
|
linear_expr: LinearExprT,
|
|
|
|
|
lb: NumberT = -math.inf,
|
|
|
|
|
ub: NumberT = math.inf,
|
|
|
|
|
name: Optional[str] = None,
|
|
|
|
|
) -> LinearConstraint:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
|
|
|
|
|
ct = LinearConstraint(self.__helper)
|
2023-02-28 10:55:51 +04:00
|
|
|
if name:
|
|
|
|
|
self.__helper.set_constraint_name(ct.index, name)
|
2023-07-03 14:30:27 +02:00
|
|
|
if mbn.is_a_number(linear_expr):
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
|
|
|
|
|
self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
|
2023-02-28 10:55:51 +04:00
|
|
|
elif isinstance(linear_expr, Variable):
|
|
|
|
|
self.__helper.set_constraint_lower_bound(ct.index, lb)
|
|
|
|
|
self.__helper.set_constraint_upper_bound(ct.index, ub)
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
|
2023-07-09 13:54:23 +02:00
|
|
|
elif isinstance(linear_expr, LinearExpr):
|
|
|
|
|
flat_expr = _as_flat_linear_expression(linear_expr)
|
|
|
|
|
# pylint: disable=protected-access
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
|
|
|
|
|
self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
|
|
|
|
|
self.__helper.add_terms_to_constraint(
|
|
|
|
|
ct.index, flat_expr._variable_indices, flat_expr._coefficients
|
|
|
|
|
)
|
2022-03-23 17:47:22 +01:00
|
|
|
else:
|
|
|
|
|
raise TypeError(
|
2023-11-06 16:16:38 +01:00
|
|
|
f"Not supported: Model.add_linear_constraint({linear_expr})"
|
2023-11-06 15:20:03 +01:00
|
|
|
f" with type {type(linear_expr)}"
|
|
|
|
|
)
|
2022-03-23 17:47:22 +01:00
|
|
|
return ct
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def add(
|
|
|
|
|
self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None
|
|
|
|
|
) -> Union[LinearConstraint, pd.Series]:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Adds a `BoundedLinearExpression` to the model.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Args:
|
|
|
|
|
ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
|
|
|
|
|
name: An optional name.
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Returns:
|
|
|
|
|
An instance of the `Constraint` class.
|
2023-07-27 08:50:52 -07:00
|
|
|
|
|
|
|
|
Note that a special treatment is done when the argument does not contain any
|
|
|
|
|
variable, and thus evaluates to True or False.
|
|
|
|
|
|
|
|
|
|
model.add(True) will create a constraint 0 <= empty sum <= 0
|
|
|
|
|
|
|
|
|
|
model.add(False) will create a constraint inf <= empty sum <= -inf
|
|
|
|
|
|
|
|
|
|
you can check the if a constraint is always false (lb=inf, ub=-inf) by
|
|
|
|
|
calling LinearConstraint.is_always_false()
|
2023-06-28 15:57:32 +02:00
|
|
|
"""
|
2023-07-27 08:50:52 -07:00
|
|
|
if isinstance(ct, _BoundedLinearExpr):
|
|
|
|
|
return ct._add_linear_constraint(self.__helper, name)
|
|
|
|
|
elif isinstance(ct, bool):
|
|
|
|
|
return _add_linear_constraint_to_helper(ct, self.__helper, name)
|
2023-07-09 13:54:23 +02:00
|
|
|
elif isinstance(ct, pd.Series):
|
|
|
|
|
return pd.Series(
|
|
|
|
|
index=ct.index,
|
|
|
|
|
data=[
|
2023-11-06 15:20:03 +01:00
|
|
|
_add_linear_constraint_to_helper(
|
|
|
|
|
expr, self.__helper, f"{name}[{i}]"
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
for (i, expr) in zip(ct.index, ct)
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
else:
|
2023-11-06 16:16:38 +01:00
|
|
|
raise TypeError("Not supported: Model.add(" + str(ct) + ")")
|
2023-11-05 09:51:40 +01:00
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
|
2023-11-05 14:40:40 +01:00
|
|
|
"""Rebuilds a linear constraint object from the model and its index."""
|
|
|
|
|
return LinearConstraint(self.__helper, index)
|
|
|
|
|
|
2023-11-05 09:51:40 +01:00
|
|
|
# EnforcedLinear constraints.
|
|
|
|
|
|
|
|
|
|
def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
|
|
|
|
self,
|
|
|
|
|
linear_expr: LinearExprT,
|
2023-11-06 15:20:03 +01:00
|
|
|
ivar: "Variable",
|
2023-11-05 09:51:40 +01:00
|
|
|
ivalue: bool,
|
|
|
|
|
lb: NumberT = -math.inf,
|
|
|
|
|
ub: NumberT = math.inf,
|
|
|
|
|
name: Optional[str] = None,
|
|
|
|
|
) -> EnforcedLinearConstraint:
|
|
|
|
|
"""Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name."""
|
|
|
|
|
ct = EnforcedLinearConstraint(self.__helper)
|
|
|
|
|
ct.indicator_variable = ivar
|
|
|
|
|
ct.indicator_value = ivalue
|
|
|
|
|
if name:
|
|
|
|
|
self.__helper.set_constraint_name(ct.index, name)
|
|
|
|
|
if mbn.is_a_number(linear_expr):
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
|
|
|
|
|
self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
|
2023-11-05 09:51:40 +01:00
|
|
|
elif isinstance(linear_expr, Variable):
|
|
|
|
|
self.__helper.set_constraint_lower_bound(ct.index, lb)
|
|
|
|
|
self.__helper.set_constraint_upper_bound(ct.index, ub)
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
|
2023-11-05 09:51:40 +01:00
|
|
|
elif isinstance(linear_expr, LinearExpr):
|
|
|
|
|
flat_expr = _as_flat_linear_expression(linear_expr)
|
|
|
|
|
# pylint: disable=protected-access
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
|
|
|
|
|
self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
|
|
|
|
|
self.__helper.add_terms_to_constraint(
|
|
|
|
|
ct.index, flat_expr._variable_indices, flat_expr._coefficients
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
else:
|
|
|
|
|
raise TypeError(
|
2023-11-06 15:20:03 +01:00
|
|
|
"Not supported:"
|
2023-11-06 16:16:38 +01:00
|
|
|
f" Model.add_enforced_linear_constraint({linear_expr}) with"
|
2023-11-06 15:20:03 +01:00
|
|
|
f" type {type(linear_expr)}"
|
|
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
return ct
|
|
|
|
|
|
|
|
|
|
def add_enforced(
|
|
|
|
|
self,
|
|
|
|
|
ct: Union[ConstraintT, pd.Series],
|
2023-11-06 16:16:38 +01:00
|
|
|
var: Union[Variable, pd.Series],
|
2023-11-06 15:20:03 +01:00
|
|
|
value: Union[bool, pd.Series],
|
|
|
|
|
name: Optional[str] = None,
|
2023-11-05 09:51:40 +01:00
|
|
|
) -> Union[EnforcedLinearConstraint, pd.Series]:
|
|
|
|
|
"""Adds a `ivar == ivalue => BoundedLinearExpression` to the model.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
|
2023-11-06 15:20:03 +01:00
|
|
|
var: The indicator variable
|
|
|
|
|
value: the indicator value
|
2023-11-05 09:51:40 +01:00
|
|
|
name: An optional name.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
An instance of the `Constraint` class.
|
|
|
|
|
|
|
|
|
|
Note that a special treatment is done when the argument does not contain any
|
|
|
|
|
variable, and thus evaluates to True or False.
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
|
|
|
|
|
sum <= 0
|
2023-11-05 09:51:40 +01:00
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
model.add_enforced(False, var, value) will create a constraint inf <=
|
|
|
|
|
empty sum <= -inf
|
2023-11-05 09:51:40 +01:00
|
|
|
|
|
|
|
|
you can check the if a constraint is always false (lb=inf, ub=-inf) by
|
|
|
|
|
calling EnforcedLinearConstraint.is_always_false()
|
|
|
|
|
"""
|
|
|
|
|
if isinstance(ct, _BoundedLinearExpr):
|
2023-11-06 15:20:03 +01:00
|
|
|
return ct._add_enforced_linear_constraint(self.__helper, var, value, name)
|
|
|
|
|
elif (
|
|
|
|
|
isinstance(ct, bool)
|
|
|
|
|
and isinstance(var, Variable)
|
|
|
|
|
and isinstance(value, bool)
|
|
|
|
|
):
|
2023-11-05 09:51:40 +01:00
|
|
|
return _add_enforced_linear_constraint_to_helper(
|
2023-11-06 16:16:38 +01:00
|
|
|
ct, self.__helper, var, value, name
|
2023-11-06 15:20:03 +01:00
|
|
|
)
|
2023-11-05 09:51:40 +01:00
|
|
|
elif isinstance(ct, pd.Series):
|
2023-11-06 15:20:03 +01:00
|
|
|
ivar_series = _convert_to_var_series_and_validate_index(var, ct.index)
|
|
|
|
|
ivalue_series = _convert_to_series_and_validate_index(value, ct.index)
|
2023-11-05 09:51:40 +01:00
|
|
|
return pd.Series(
|
|
|
|
|
index=ct.index,
|
|
|
|
|
data=[
|
|
|
|
|
_add_enforced_linear_constraint_to_helper(
|
2023-11-06 15:20:03 +01:00
|
|
|
expr,
|
|
|
|
|
self.__helper,
|
|
|
|
|
ivar_series[i],
|
|
|
|
|
ivalue_series[i],
|
|
|
|
|
f"{name}[{i}]",
|
|
|
|
|
)
|
|
|
|
|
for (i, expr) in zip(ct.index, ct)
|
2023-07-09 13:54:23 +02:00
|
|
|
],
|
|
|
|
|
)
|
2022-03-23 17:47:22 +01:00
|
|
|
else:
|
2023-11-06 16:16:38 +01:00
|
|
|
raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")")
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-05 14:40:40 +01:00
|
|
|
def enforced_linear_constraint_from_index(
|
2023-11-06 15:20:03 +01:00
|
|
|
self, index: IntegerT
|
|
|
|
|
) -> EnforcedLinearConstraint:
|
2023-11-05 14:40:40 +01:00
|
|
|
"""Rebuilds an enforced linear constraint object from the model and its index."""
|
|
|
|
|
return EnforcedLinearConstraint(self.__helper, index)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
# Objective.
|
2023-02-28 10:55:51 +04:00
|
|
|
def minimize(self, linear_expr: LinearExprT) -> None:
|
2023-11-04 20:58:00 +01:00
|
|
|
"""Minimize the given objective."""
|
2022-03-23 17:47:22 +01:00
|
|
|
self.__optimize(linear_expr, False)
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def maximize(self, linear_expr: LinearExprT) -> None:
|
2023-11-04 20:58:00 +01:00
|
|
|
"""Maximize the given objective."""
|
2022-03-23 17:47:22 +01:00
|
|
|
self.__optimize(linear_expr, True)
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Defines the objective."""
|
2022-04-02 23:26:17 +02:00
|
|
|
self.helper.clear_objective()
|
2023-02-28 10:55:51 +04:00
|
|
|
self.__helper.set_maximize(maximize)
|
2023-07-03 14:30:27 +02:00
|
|
|
if mbn.is_a_number(linear_expr):
|
2023-02-28 10:55:51 +04:00
|
|
|
self.helper.set_objective_offset(linear_expr)
|
|
|
|
|
elif isinstance(linear_expr, Variable):
|
|
|
|
|
self.helper.set_var_objective_coefficient(linear_expr.index, 1.0)
|
2023-07-09 13:54:23 +02:00
|
|
|
elif isinstance(linear_expr, LinearExpr):
|
|
|
|
|
flat_expr = _as_flat_linear_expression(linear_expr)
|
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
self.helper.set_objective_offset(flat_expr._offset)
|
2023-11-06 15:20:03 +01:00
|
|
|
self.helper.set_objective_coefficients(
|
|
|
|
|
flat_expr._variable_indices, flat_expr._coefficients
|
|
|
|
|
)
|
2022-03-23 17:47:22 +01:00
|
|
|
else:
|
2023-11-06 16:16:38 +01:00
|
|
|
raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})")
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def objective_offset(self) -> np.double:
|
2023-11-04 20:58:00 +01:00
|
|
|
"""Returns the fixed offset of the objective."""
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__helper.objective_offset()
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@objective_offset.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def objective_offset(self, value: NumberT) -> None:
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_objective_offset(value)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def objective_expression(self) -> "_LinearExpression":
|
2023-11-04 20:58:00 +01:00
|
|
|
"""Returns the expression to optimize."""
|
2023-07-09 13:54:23 +02:00
|
|
|
return _as_flat_linear_expression(
|
2023-11-06 15:20:03 +01:00
|
|
|
sum(
|
|
|
|
|
variable * self.__helper.var_objective_coefficient(variable.index)
|
|
|
|
|
for variable in self.get_variables()
|
|
|
|
|
if self.__helper.var_objective_coefficient(variable.index) != 0.0
|
|
|
|
|
)
|
|
|
|
|
+ self.__helper.objective_offset()
|
|
|
|
|
)
|
2023-07-09 13:54:23 +02:00
|
|
|
|
2023-11-04 20:58:00 +01:00
|
|
|
# Hints.
|
|
|
|
|
def clear_hints(self):
|
|
|
|
|
"""Clear all solution hints."""
|
2023-11-05 09:51:40 +01:00
|
|
|
self.__helper.clear_hints()
|
2023-11-04 20:58:00 +01:00
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
def add_hint(self, var: Variable, value: NumberT) -> None:
|
2023-11-04 20:58:00 +01:00
|
|
|
"""Add var == value as a hint to the model.
|
|
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
Args:
|
2023-11-06 15:20:03 +01:00
|
|
|
var: The variable of the hint
|
|
|
|
|
value: The value of the hint
|
|
|
|
|
|
2023-11-04 20:58:00 +01:00
|
|
|
Note that variables must not appear more than once in the list of hints.
|
|
|
|
|
"""
|
|
|
|
|
self.__helper.add_hint(var.index, value)
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
# Input/Output
|
2023-02-28 10:55:51 +04:00
|
|
|
def export_to_lp_string(self, obfuscate: bool = False) -> str:
|
2023-07-03 14:30:27 +02:00
|
|
|
options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
|
2022-03-24 16:48:24 +01:00
|
|
|
options.obfuscate = obfuscate
|
2022-03-26 17:00:47 +01:00
|
|
|
return self.__helper.export_to_lp_string(options)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def export_to_mps_string(self, obfuscate: bool = False) -> str:
|
2023-07-03 14:30:27 +02:00
|
|
|
options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
|
2022-03-24 16:48:24 +01:00
|
|
|
options.obfuscate = obfuscate
|
2022-03-26 17:00:47 +01:00
|
|
|
return self.__helper.export_to_mps_string(options)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def export_to_proto(self) -> linear_solver_pb2.MPModelProto:
|
|
|
|
|
"""Exports the optimization model to a ProtoBuf format."""
|
|
|
|
|
return mbh.to_mpmodel_proto(self.__helper)
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def import_from_mps_string(self, mps_string: str) -> bool:
|
2023-11-19 07:22:44 +01:00
|
|
|
"""Loads the a model from an MPS string."""
|
2022-03-26 17:00:47 +01:00
|
|
|
return self.__helper.import_from_mps_string(mps_string)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def import_from_mps_file(self, mps_file: str) -> bool:
|
2023-11-19 07:22:44 +01:00
|
|
|
"""Loads the a model from an MPS file."""
|
2022-03-26 17:00:47 +01:00
|
|
|
return self.__helper.import_from_mps_file(mps_file)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def import_from_lp_string(self, lp_string: str) -> bool:
|
2023-11-19 07:22:44 +01:00
|
|
|
"""Loads the a model from an LP string."""
|
2022-03-26 17:00:47 +01:00
|
|
|
return self.__helper.import_from_lp_string(lp_string)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def import_from_lp_file(self, lp_file: str) -> bool:
|
2023-11-19 07:22:44 +01:00
|
|
|
"""Loads the a model from an LP file."""
|
2022-03-26 17:00:47 +01:00
|
|
|
return self.__helper.import_from_lp_file(lp_file)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-11-19 07:22:44 +01:00
|
|
|
def import_from_proto_file(self, proto_file: str) -> bool:
|
|
|
|
|
"""Loads the a model from an proto file."""
|
|
|
|
|
return self.__helper.load_model_from_file(proto_file)
|
|
|
|
|
|
|
|
|
|
def export_to_proto_file(self, proto_file: str) -> bool:
|
|
|
|
|
"""Write a model to a proto file."""
|
|
|
|
|
return self.__helper.write_model_to_file(proto_file)
|
|
|
|
|
|
2023-11-05 14:40:40 +01:00
|
|
|
# Model getters and Setters
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def num_variables(self) -> int:
|
|
|
|
|
"""Returns the number of variables in the model."""
|
|
|
|
|
return self.__helper.num_variables()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def num_constraints(self) -> int:
|
|
|
|
|
"""The number of constraints in the model."""
|
|
|
|
|
return self.__helper.num_constraints()
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def name(self) -> str:
|
2023-11-04 20:58:00 +01:00
|
|
|
"""The name of the model."""
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__helper.name()
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@name.setter
|
2023-02-28 10:55:51 +04:00
|
|
|
def name(self, name: str):
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__helper.set_name(name)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
@property
|
2023-07-03 14:30:27 +02:00
|
|
|
def helper(self) -> mbh.ModelBuilderHelper:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the model builder helper."""
|
|
|
|
|
return self.__helper
|
|
|
|
|
|
|
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
class Solver:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Main solver class.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
The purpose of this class is to search for a solution to the model provided
|
|
|
|
|
to the solve() method.
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
Once solve() is called, this class allows inspecting the solution found
|
|
|
|
|
with the value() method, as well as general statistics about the solve
|
|
|
|
|
procedure.
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def __init__(self, solver_name: str):
|
2023-11-06 15:20:03 +01:00
|
|
|
self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name)
|
2023-02-28 10:55:51 +04:00
|
|
|
self.log_callback: Optional[Callable[[str], None]] = None
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def solver_is_supported(self) -> bool:
|
2022-03-26 17:00:47 +01:00
|
|
|
"""Checks whether the requested solver backend was found."""
|
|
|
|
|
return self.__solve_helper.solver_is_supported()
|
|
|
|
|
|
2022-03-24 16:48:24 +01:00
|
|
|
# Solver backend and parameters.
|
2023-02-28 10:55:51 +04:00
|
|
|
def set_time_limit_in_seconds(self, limit: NumberT) -> None:
|
2022-03-24 16:48:24 +01:00
|
|
|
"""Sets a time limit for the solve() call."""
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__solve_helper.set_time_limit_in_seconds(limit)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def set_solver_specific_parameters(self, parameters: str) -> None:
|
2022-03-25 15:12:19 +01:00
|
|
|
"""Sets parameters specific to the solver backend."""
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__solve_helper.set_solver_specific_parameters(parameters)
|
2022-03-24 16:48:24 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def enable_output(self, enabled: bool) -> None:
|
2022-03-25 15:12:19 +01:00
|
|
|
"""Controls the solver backend logs."""
|
2022-10-11 13:36:35 +02:00
|
|
|
self.__solve_helper.enable_output(enabled)
|
2022-03-25 15:12:19 +01:00
|
|
|
|
2023-11-06 16:16:38 +01:00
|
|
|
def solve(self, model: Model) -> SolveStatus:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Solves a problem and passes each solution to the callback if not null."""
|
|
|
|
|
if self.log_callback is not None:
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__solve_helper.set_log_callback(self.log_callback)
|
2022-04-04 13:05:53 +02:00
|
|
|
else:
|
2022-04-04 15:20:57 +02:00
|
|
|
self.__solve_helper.clear_log_callback()
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__solve_helper.solve(model.helper)
|
2022-04-02 23:26:17 +02:00
|
|
|
return SolveStatus(self.__solve_helper.status())
|
2022-03-23 17:47:22 +01:00
|
|
|
|
|
|
|
|
def stop_search(self):
|
|
|
|
|
"""Stops the current search asynchronously."""
|
2022-03-26 17:00:47 +01:00
|
|
|
self.__solve_helper.interrupt_solve()
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def value(self, expr: LinearExprT) -> np.double:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the value of a linear expression after solve."""
|
2023-07-09 13:54:23 +02:00
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return pd.NA
|
2023-07-03 14:30:27 +02:00
|
|
|
if mbn.is_a_number(expr):
|
2023-02-28 10:55:51 +04:00
|
|
|
return expr
|
|
|
|
|
elif isinstance(expr, Variable):
|
|
|
|
|
return self.__solve_helper.var_value(expr.index)
|
2023-07-09 13:54:23 +02:00
|
|
|
elif isinstance(expr, LinearExpr):
|
|
|
|
|
flat_expr = _as_flat_linear_expression(expr)
|
|
|
|
|
return self.__solve_helper.expression_value(
|
2023-07-27 08:50:52 -07:00
|
|
|
flat_expr._variable_indices,
|
|
|
|
|
flat_expr._coefficients,
|
|
|
|
|
flat_expr._offset,
|
2023-07-09 13:54:23 +02:00
|
|
|
)
|
2023-02-28 10:55:51 +04:00
|
|
|
else:
|
2023-11-06 15:20:03 +01:00
|
|
|
raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}")
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
def values(self, variables: _IndexOrSeries) -> pd.Series:
|
|
|
|
|
"""Returns the values of the input variables.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
variables (Union[pd.Index, pd.Series]): The set of variables from which to
|
|
|
|
|
get the values.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
pd.Series: The values of all variables in the set.
|
|
|
|
|
"""
|
|
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return _attribute_series(func=lambda v: pd.NA, values=variables)
|
|
|
|
|
return _attribute_series(
|
|
|
|
|
func=lambda v: self.__solve_helper.var_value(v.index),
|
|
|
|
|
values=variables,
|
|
|
|
|
)
|
2023-07-20 08:52:32 -07:00
|
|
|
|
|
|
|
|
def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series:
|
|
|
|
|
"""Returns the reduced cost of the input variables.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
variables (Union[pd.Index, pd.Series]): The set of variables from which to
|
|
|
|
|
get the values.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
pd.Series: The reduced cost of all variables in the set.
|
|
|
|
|
"""
|
|
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return _attribute_series(func=lambda v: pd.NA, values=variables)
|
|
|
|
|
return _attribute_series(
|
|
|
|
|
func=lambda v: self.__solve_helper.reduced_cost(v.index),
|
|
|
|
|
values=variables,
|
|
|
|
|
)
|
2023-07-09 13:54:23 +02:00
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def reduced_cost(self, var: Variable) -> np.double:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the reduced cost of a linear expression after solve."""
|
2023-07-09 13:54:23 +02:00
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return pd.NA
|
2022-03-24 16:48:24 +01:00
|
|
|
return self.__solve_helper.reduced_cost(var.index)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-07-20 14:01:10 -07:00
|
|
|
def dual_values(self, constraints: _IndexOrSeries) -> pd.Series:
|
|
|
|
|
"""Returns the dual values of the input constraints.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
constraints (Union[pd.Index, pd.Series]): The set of constraints from
|
2023-11-06 15:20:03 +01:00
|
|
|
which to get the dual values.
|
2023-07-20 14:01:10 -07:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
pd.Series: The dual_values of all constraints in the set.
|
|
|
|
|
"""
|
|
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return _attribute_series(func=lambda v: pd.NA, values=constraints)
|
|
|
|
|
return _attribute_series(
|
|
|
|
|
func=lambda v: self.__solve_helper.dual_value(v.index),
|
|
|
|
|
values=constraints,
|
|
|
|
|
)
|
|
|
|
|
|
2023-02-28 10:55:51 +04:00
|
|
|
def dual_value(self, ct: LinearConstraint) -> np.double:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the dual value of a linear constraint after solve."""
|
2023-07-09 13:54:23 +02:00
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return pd.NA
|
2022-03-24 16:48:24 +01:00
|
|
|
return self.__solve_helper.dual_value(ct.index)
|
2022-03-23 17:47:22 +01:00
|
|
|
|
2023-03-03 12:12:37 +04:00
|
|
|
def activity(self, ct: LinearConstraint) -> np.double:
|
|
|
|
|
"""Returns the activity of a linear constraint after solve."""
|
2023-07-09 13:54:23 +02:00
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return pd.NA
|
2023-03-03 12:12:37 +04:00
|
|
|
return self.__solve_helper.activity(ct.index)
|
|
|
|
|
|
2022-03-26 17:00:47 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def objective_value(self) -> np.double:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the value of the objective after solve."""
|
2023-07-09 13:54:23 +02:00
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return pd.NA
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__solve_helper.objective_value()
|
|
|
|
|
|
2022-03-26 17:00:47 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def best_objective_bound(self) -> np.double:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns the best lower (upper) bound found when min(max)imizing."""
|
2023-07-09 13:54:23 +02:00
|
|
|
if not self.__solve_helper.has_solution():
|
|
|
|
|
return pd.NA
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__solve_helper.best_objective_bound()
|
|
|
|
|
|
2022-03-26 17:00:47 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def status_string(self) -> str:
|
2022-03-23 17:47:22 +01:00
|
|
|
"""Returns additional information of the last solve.
|
|
|
|
|
|
2023-06-28 15:57:32 +02:00
|
|
|
It can describe why the model is invalid.
|
|
|
|
|
"""
|
2022-03-23 17:47:22 +01:00
|
|
|
return self.__solve_helper.status_string()
|
|
|
|
|
|
2022-03-25 15:12:19 +01:00
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def wall_time(self) -> np.double:
|
2022-03-25 15:12:19 +01:00
|
|
|
return self.__solve_helper.wall_time()
|
|
|
|
|
|
|
|
|
|
@property
|
2023-02-28 10:55:51 +04:00
|
|
|
def user_time(self) -> np.double:
|
2022-03-25 15:12:19 +01:00
|
|
|
return self.__solve_helper.user_time()
|
2023-07-09 13:54:23 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# The maximum number of terms to display in a linear expression's repr.
|
|
|
|
|
_MAX_LINEAR_EXPRESSION_REPR_TERMS = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass(repr=False, eq=False, frozen=True)
|
|
|
|
|
class _LinearExpression(LinearExpr):
|
|
|
|
|
"""For variables x, an expression: offset + sum_{i in I} coeff_i * x_i."""
|
|
|
|
|
|
2023-07-27 08:50:52 -07:00
|
|
|
__slots__ = ("_variable_indices", "_coefficients", "_offset", "_helper")
|
2023-07-09 13:54:23 +02:00
|
|
|
|
2023-07-27 08:50:52 -07:00
|
|
|
_variable_indices: npt.NDArray[np.int32]
|
|
|
|
|
_coefficients: npt.NDArray[np.double]
|
2023-07-09 13:54:23 +02:00
|
|
|
_offset: float
|
2023-07-27 08:50:52 -07:00
|
|
|
_helper: Optional[mbh.ModelBuilderHelper]
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def variable_indices(self) -> npt.NDArray[np.int32]:
|
|
|
|
|
return self._variable_indices
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def coefficients(self) -> npt.NDArray[np.double]:
|
|
|
|
|
return self._coefficients
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def constant(self) -> float:
|
|
|
|
|
return self._offset
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def helper(self) -> Optional[mbh.ModelBuilderHelper]:
|
|
|
|
|
return self._helper
|
2023-07-09 13:54:23 +02:00
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return self.__str__()
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-07-27 08:50:52 -07:00
|
|
|
if self._helper is None:
|
|
|
|
|
return str(self._offset)
|
|
|
|
|
|
2023-07-09 13:54:23 +02:00
|
|
|
result = []
|
2023-07-27 08:50:52 -07:00
|
|
|
for index, coeff in zip(self.variable_indices, self.coefficients):
|
|
|
|
|
if len(result) >= _MAX_LINEAR_EXPRESSION_REPR_TERMS:
|
2023-07-09 13:54:23 +02:00
|
|
|
result.append(" + ...")
|
|
|
|
|
break
|
2023-07-27 08:50:52 -07:00
|
|
|
var_name = Variable(self._helper, index, None, None, None).name
|
|
|
|
|
if not result and mbn.is_one(coeff):
|
|
|
|
|
result.append(var_name)
|
|
|
|
|
elif not result and mbn.is_minus_one(coeff):
|
|
|
|
|
result.append(f"-{var_name}")
|
|
|
|
|
elif not result:
|
|
|
|
|
result.append(f"{coeff} * {var_name}")
|
|
|
|
|
elif mbn.is_one(coeff):
|
|
|
|
|
result.append(f" + {var_name}")
|
|
|
|
|
elif mbn.is_minus_one(coeff):
|
|
|
|
|
result.append(f" - {var_name}")
|
|
|
|
|
elif coeff > 0.0:
|
|
|
|
|
result.append(f" + {coeff} * {var_name}")
|
|
|
|
|
elif coeff < 0.0:
|
|
|
|
|
result.append(f" - {-coeff} * {var_name}")
|
|
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
|
return f"{self.constant}"
|
|
|
|
|
if self.constant > 0:
|
|
|
|
|
result.append(f" + {self.constant}")
|
|
|
|
|
elif self.constant < 0:
|
|
|
|
|
result.append(f" - {-self.constant}")
|
2023-07-09 13:54:23 +02:00
|
|
|
return "".join(result)
|
|
|
|
|
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def _sum_as_flat_linear_expression(
|
|
|
|
|
to_process: List[Tuple[LinearExprT, float]], offset: float = 0.0
|
|
|
|
|
) -> _LinearExpression:
|
2023-07-27 08:50:52 -07:00
|
|
|
"""Creates a _LinearExpression as the sum of terms."""
|
|
|
|
|
indices = []
|
|
|
|
|
coeffs = []
|
|
|
|
|
helper = None
|
2023-07-09 13:54:23 +02:00
|
|
|
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):
|
2023-07-27 08:50:52 -07:00
|
|
|
indices.append([expr.index])
|
|
|
|
|
coeffs.append([coeff])
|
|
|
|
|
if helper is None:
|
|
|
|
|
helper = expr.helper
|
2023-07-21 16:42:55 -07:00
|
|
|
elif mbn.is_a_number(expr):
|
|
|
|
|
offset += coeff * cast(NumberT, expr)
|
2023-07-09 13:54:23 +02:00
|
|
|
elif isinstance(expr, _Product):
|
|
|
|
|
to_process.append((expr._expression, coeff * expr._coefficient))
|
|
|
|
|
elif isinstance(expr, _LinearExpression):
|
|
|
|
|
offset += coeff * expr._offset
|
2023-07-27 08:50:52 -07:00
|
|
|
if expr._helper is not None:
|
|
|
|
|
indices.append(expr.variable_indices)
|
|
|
|
|
coeffs.append(np.multiply(expr.coefficients, coeff))
|
|
|
|
|
if helper is None:
|
|
|
|
|
helper = expr._helper
|
2023-07-09 13:54:23 +02:00
|
|
|
else:
|
2023-11-06 15:20:03 +01:00
|
|
|
raise TypeError(
|
|
|
|
|
"Unrecognized linear expression: " + str(expr) + f" {type(expr)}"
|
|
|
|
|
)
|
2023-07-27 08:50:52 -07:00
|
|
|
|
|
|
|
|
if helper is not None:
|
|
|
|
|
all_indices: npt.NDArray[np.int32] = np.concatenate(indices, axis=0)
|
|
|
|
|
all_coeffs: npt.NDArray[np.double] = np.concatenate(coeffs, axis=0)
|
|
|
|
|
sorted_indices, sorted_coefficients = helper.sort_and_regroup_terms(
|
2023-11-06 15:20:03 +01:00
|
|
|
all_indices, all_coeffs
|
|
|
|
|
)
|
|
|
|
|
return _LinearExpression(sorted_indices, sorted_coefficients, offset, helper)
|
2023-07-27 08:50:52 -07:00
|
|
|
else:
|
|
|
|
|
assert not indices
|
|
|
|
|
assert not coeffs
|
|
|
|
|
return _LinearExpression(
|
|
|
|
|
_variable_indices=np.zeros(dtype=np.int32, shape=[0]),
|
|
|
|
|
_coefficients=np.zeros(dtype=np.double, shape=[0]),
|
|
|
|
|
_offset=offset,
|
|
|
|
|
_helper=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _as_flat_linear_expression(base_expr: LinearExprT) -> _LinearExpression:
|
|
|
|
|
"""Converts floats, ints and Linear objects to a LinearExpression."""
|
|
|
|
|
if isinstance(base_expr, _LinearExpression):
|
|
|
|
|
return base_expr
|
2023-11-06 15:20:03 +01:00
|
|
|
return _sum_as_flat_linear_expression(to_process=[(base_expr, 1.0)], offset=0.0)
|
2023-07-09 13:54:23 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass(repr=False, eq=False, frozen=True)
|
|
|
|
|
class _Sum(LinearExpr):
|
|
|
|
|
"""Represents the (deferred) sum of two expressions."""
|
|
|
|
|
|
|
|
|
|
__slots__ = ("_left", "_right")
|
|
|
|
|
|
|
|
|
|
_left: LinearExprT
|
|
|
|
|
_right: LinearExprT
|
|
|
|
|
|
|
|
|
|
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(LinearExpr):
|
|
|
|
|
"""Represents the (deferred) product of an expression by a constant."""
|
|
|
|
|
|
|
|
|
|
__slots__ = ("_expression", "_coefficient")
|
|
|
|
|
|
|
|
|
|
_expression: LinearExpr
|
|
|
|
|
_coefficient: NumberT
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return self.__str__()
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return str(_as_flat_linear_expression(self))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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], NumberT],
|
|
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def _convert_to_series_and_validate_index(
|
|
|
|
|
value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index
|
|
|
|
|
) -> pd.Series:
|
2023-07-09 13:54:23 +02:00
|
|
|
"""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.
|
|
|
|
|
"""
|
2023-07-21 16:42:55 -07:00
|
|
|
if mbn.is_a_number(value_or_series) or isinstance(value_or_series, bool):
|
2023-07-09 13:54:23 +02:00
|
|
|
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
|
2023-11-05 10:06:20 +01:00
|
|
|
|
|
|
|
|
|
2023-11-06 15:20:03 +01:00
|
|
|
def _convert_to_var_series_and_validate_index(
|
|
|
|
|
var_or_series: Union["Variable", pd.Series], index: pd.Index
|
|
|
|
|
) -> pd.Series:
|
2023-11-05 10:06:20 +01:00
|
|
|
"""Returns a pd.Series of the given index with the corresponding values.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
var_or_series: the variables 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(var_or_series, Variable):
|
|
|
|
|
result = pd.Series(data=var_or_series, index=index)
|
|
|
|
|
elif isinstance(var_or_series, pd.Series):
|
|
|
|
|
if var_or_series.index.equals(index):
|
|
|
|
|
result = var_or_series
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("index does not match")
|
|
|
|
|
else:
|
|
|
|
|
raise TypeError("invalid type={}".format(type(var_or_series)))
|
|
|
|
|
return result
|
2023-11-06 16:16:38 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# Compatibility.
|
|
|
|
|
ModelBuilder = Model
|
|
|
|
|
ModelSolver = Solver
|