revisit model_builder python API; append ::mb to the c++ part of model_builder
This commit is contained in:
@@ -32,386 +32,35 @@ Other methods and functions listed are primarily used for developing OR-Tools,
|
||||
rather than for solving specific optimization problems.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import dataclasses
|
||||
import math
|
||||
import numbers
|
||||
import typing
|
||||
from typing import Callable, List, Optional, Sequence, Tuple, Union, cast
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
from numpy import typing as npt
|
||||
import pandas as pd
|
||||
|
||||
from ortools.linear_solver import linear_solver_pb2
|
||||
from ortools.linear_solver.python import model_builder_helper as mbh
|
||||
from ortools.linear_solver.python import model_builder_numbers as mbn
|
||||
|
||||
|
||||
# Custom types.
|
||||
NumberT = Union[int, float, numbers.Real, np.number]
|
||||
IntegerT = Union[int, numbers.Integral, np.integer]
|
||||
LinearExprT = Union["LinearExpr", NumberT]
|
||||
ConstraintT = Union["_BoundedLinearExpr", bool]
|
||||
LinearExprT = Union[mbh.LinearExpr, NumberT]
|
||||
ConstraintT = Union[mbh.BoundedLinearExpression, bool]
|
||||
_IndexOrSeries = Union[pd.Index, pd.Series]
|
||||
_VariableOrConstraint = Union["LinearConstraint", "Variable"]
|
||||
_VariableOrConstraint = Union["LinearConstraint", mbh.Variable]
|
||||
|
||||
# Forward solve statuses.
|
||||
BoundedLinearExpression = mbh.BoundedLinearExpression
|
||||
LinearExpr = mbh.LinearExpr
|
||||
SolveStatus = mbh.SolveStatus
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
class LinearExpr(metaclass=abc.ABCMeta):
|
||||
"""Holds an linear expression.
|
||||
|
||||
A linear expression is built from constants and variables.
|
||||
For example, `x + 2.0 * (y - z + 1.0)`.
|
||||
|
||||
Linear expressions are used in Model models in constraints and in the
|
||||
objective:
|
||||
|
||||
* You can define linear constraints as in:
|
||||
|
||||
```
|
||||
model.add(x + 2 * y <= 5.0)
|
||||
model.add(sum(array_of_vars) == 5.0)
|
||||
```
|
||||
|
||||
* In Model, the objective is a linear expression:
|
||||
|
||||
```
|
||||
model.minimize(x + 2.0 * y + z)
|
||||
```
|
||||
|
||||
* For large arrays, using the LinearExpr class is faster that using the python
|
||||
`sum()` function. You can create constraints and the objective from lists of
|
||||
linear expressions or coefficients as follows:
|
||||
|
||||
```
|
||||
model.minimize(model_builder.LinearExpr.sum(expressions))
|
||||
model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
|
||||
```
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sum( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
||||
cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0
|
||||
) -> LinearExprT:
|
||||
"""Creates `sum(expressions) + constant`.
|
||||
|
||||
It can perform simple simplifications and returns different objects,
|
||||
including the input.
|
||||
|
||||
Args:
|
||||
expressions: a sequence of linear expressions or constants.
|
||||
constant: a numerical constant.
|
||||
|
||||
Returns:
|
||||
a LinearExpr instance or a numerical constant.
|
||||
"""
|
||||
checked_constant: np.double = mbn.assert_is_a_number(constant)
|
||||
if not expressions:
|
||||
return checked_constant
|
||||
if len(expressions) == 1 and mbn.is_zero(checked_constant):
|
||||
return expressions[0]
|
||||
|
||||
return LinearExpr.weighted_sum(
|
||||
expressions, np.ones(len(expressions)), constant=checked_constant
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def weighted_sum( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
||||
cls,
|
||||
expressions: Sequence[LinearExprT],
|
||||
coefficients: Sequence[NumberT],
|
||||
*,
|
||||
constant: NumberT = 0.0,
|
||||
) -> Union[NumberT, "_LinearExpression"]:
|
||||
"""Creates `sum(expressions[i] * coefficients[i]) + constant`.
|
||||
|
||||
It can perform simple simplifications and returns different object,
|
||||
including the input.
|
||||
|
||||
Args:
|
||||
expressions: a sequence of linear expressions or constants.
|
||||
coefficients: a sequence of numerical constants.
|
||||
constant: a numerical constant.
|
||||
|
||||
Returns:
|
||||
a _LinearExpression instance or a numerical constant.
|
||||
"""
|
||||
if len(expressions) != len(coefficients):
|
||||
raise ValueError(
|
||||
"LinearExpr.weighted_sum: expressions and coefficients have"
|
||||
" different lengths"
|
||||
)
|
||||
checked_constant: np.double = mbn.assert_is_a_number(constant)
|
||||
if not expressions:
|
||||
return checked_constant
|
||||
return _sum_as_flat_linear_expression(
|
||||
to_process=list(zip(expressions, coefficients)), offset=checked_constant
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def term( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
||||
cls,
|
||||
expression: LinearExprT,
|
||||
coefficient: NumberT,
|
||||
*,
|
||||
constant: NumberT = 0.0,
|
||||
) -> LinearExprT:
|
||||
"""Creates `expression * coefficient + constant`.
|
||||
|
||||
It can perform simple simplifications and returns different object,
|
||||
including the input.
|
||||
Args:
|
||||
expression: a linear expression or a constant.
|
||||
coefficient: a numerical constant.
|
||||
constant: a numerical constant.
|
||||
|
||||
Returns:
|
||||
a LinearExpr instance or a numerical constant.
|
||||
"""
|
||||
checked_coefficient: np.double = mbn.assert_is_a_number(coefficient)
|
||||
checked_constant: np.double = mbn.assert_is_a_number(constant)
|
||||
|
||||
if mbn.is_zero(checked_coefficient):
|
||||
return checked_constant
|
||||
if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant):
|
||||
return expression
|
||||
if mbn.is_a_number(expression):
|
||||
return np.double(expression) * checked_coefficient + checked_constant
|
||||
if isinstance(expression, LinearExpr):
|
||||
return _as_flat_linear_expression(
|
||||
expression * checked_coefficient + checked_constant
|
||||
)
|
||||
raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}")
|
||||
|
||||
def __hash__(self):
|
||||
return object.__hash__(self)
|
||||
|
||||
def __add__(self, arg: LinearExprT) -> "_Sum":
|
||||
return _Sum(self, arg)
|
||||
|
||||
def __radd__(self, arg: LinearExprT) -> "_Sum":
|
||||
return self.__add__(arg)
|
||||
|
||||
def __sub__(self, arg: LinearExprT) -> "_Sum":
|
||||
return _Sum(self, -arg)
|
||||
|
||||
def __rsub__(self, arg: LinearExprT) -> "_Sum":
|
||||
return _Sum(-self, arg)
|
||||
|
||||
def __mul__(self, arg: NumberT) -> "_Product":
|
||||
return _Product(self, arg)
|
||||
|
||||
def __rmul__(self, arg: NumberT) -> "_Product":
|
||||
return self.__mul__(arg)
|
||||
|
||||
def __truediv__(self, coeff: NumberT) -> "_Product":
|
||||
return self.__mul__(1.0 / coeff)
|
||||
|
||||
def __neg__(self) -> "_Product":
|
||||
return _Product(self, -1)
|
||||
|
||||
def __bool__(self):
|
||||
raise NotImplementedError(f"Cannot use a LinearExpr {self} as a Boolean value")
|
||||
|
||||
def __eq__(self, arg: LinearExprT) -> "BoundedLinearExpression":
|
||||
return BoundedLinearExpression(self - arg, 0, 0)
|
||||
|
||||
def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression":
|
||||
return BoundedLinearExpression(
|
||||
self - arg, 0, math.inf
|
||||
) # pytype: disable=wrong-arg-types # numpy-scalars
|
||||
|
||||
def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression":
|
||||
return BoundedLinearExpression(
|
||||
self - arg, -math.inf, 0
|
||||
) # pytype: disable=wrong-arg-types # numpy-scalars
|
||||
|
||||
|
||||
class Variable(LinearExpr):
|
||||
"""A variable (continuous or integral).
|
||||
|
||||
A Variable is an object that can take on any integer value within defined
|
||||
ranges. Variables appear in constraint like:
|
||||
|
||||
x + y >= 5
|
||||
|
||||
Solving a model is equivalent to finding, for each variable, a single value
|
||||
from the set of initial values (called the initial domain), such that the
|
||||
model is feasible, or optimal if you provided an objective function.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
helper: mbh.ModelBuilderHelper,
|
||||
lb: NumberT,
|
||||
ub: Optional[NumberT],
|
||||
is_integral: Optional[bool],
|
||||
name: Optional[str],
|
||||
) -> None:
|
||||
"""See Model.new_var below."""
|
||||
LinearExpr.__init__(self)
|
||||
self.__helper: mbh.ModelBuilderHelper = helper
|
||||
# Python do not support multiple __init__ methods.
|
||||
# This method is only called from the Model class.
|
||||
# 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.
|
||||
if mbn.is_integral(lb) and ub is None and is_integral is None:
|
||||
self.__index: np.int32 = np.int32(lb)
|
||||
self.__helper: mbh.ModelBuilderHelper = helper
|
||||
else:
|
||||
index: np.int32 = helper.add_var()
|
||||
self.__index: np.int32 = np.int32(index)
|
||||
self.__helper: mbh.ModelBuilderHelper = helper
|
||||
helper.set_var_lower_bound(index, lb)
|
||||
helper.set_var_upper_bound(index, ub)
|
||||
helper.set_var_integrality(index, is_integral)
|
||||
if name:
|
||||
helper.set_var_name(index, name)
|
||||
|
||||
@property
|
||||
def index(self) -> np.int32:
|
||||
"""Returns the index of the variable in the helper."""
|
||||
return self.__index
|
||||
|
||||
@property
|
||||
def helper(self) -> mbh.ModelBuilderHelper:
|
||||
"""Returns the underlying ModelBuilderHelper."""
|
||||
return self.__helper
|
||||
|
||||
def is_equal_to(self, other: LinearExprT) -> bool:
|
||||
"""Returns true if self == other in the python sense."""
|
||||
if not isinstance(other, Variable):
|
||||
return False
|
||||
return self.index == other.index and self.helper == other.helper
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Returns the name of the variable."""
|
||||
var_name = self.__helper.var_name(self.__index)
|
||||
if var_name:
|
||||
return var_name
|
||||
return f"variable#{self.index}"
|
||||
|
||||
@name.setter
|
||||
def name(self, name: str) -> None:
|
||||
"""Sets the name of the variable."""
|
||||
self.__helper.set_var_name(self.__index, name)
|
||||
|
||||
@property
|
||||
def lower_bound(self) -> np.double:
|
||||
"""Returns the lower bound of the variable."""
|
||||
return self.__helper.var_lower_bound(self.__index)
|
||||
|
||||
@lower_bound.setter
|
||||
def lower_bound(self, bound: NumberT) -> None:
|
||||
"""Sets the lower bound of the variable."""
|
||||
self.__helper.set_var_lower_bound(self.__index, bound)
|
||||
|
||||
@property
|
||||
def upper_bound(self) -> np.double:
|
||||
"""Returns the upper bound of the variable."""
|
||||
return self.__helper.var_upper_bound(self.__index)
|
||||
|
||||
@upper_bound.setter
|
||||
def upper_bound(self, bound: NumberT) -> None:
|
||||
"""Sets the upper bound of the variable."""
|
||||
self.__helper.set_var_upper_bound(self.__index, bound)
|
||||
|
||||
@property
|
||||
def is_integral(self) -> bool:
|
||||
"""Returns whether the variable is integral."""
|
||||
return self.__helper.var_is_integral(self.__index)
|
||||
|
||||
@is_integral.setter
|
||||
def integrality(self, is_integral: bool) -> None:
|
||||
"""Sets the integrality of the variable."""
|
||||
self.__helper.set_var_integrality(self.__index, is_integral)
|
||||
|
||||
@property
|
||||
def objective_coefficient(self) -> NumberT:
|
||||
return self.__helper.var_objective_coefficient(self.__index)
|
||||
|
||||
@objective_coefficient.setter
|
||||
def objective_coefficient(self, coeff: NumberT) -> None:
|
||||
self.__helper.set_var_objective_coefficient(self.__index, coeff)
|
||||
|
||||
def __eq__(self, arg: Optional[LinearExprT]) -> ConstraintT:
|
||||
if arg is None:
|
||||
return False
|
||||
if isinstance(arg, Variable):
|
||||
return VarEqVar(self, arg)
|
||||
return BoundedLinearExpression(
|
||||
self - arg, 0.0, 0.0
|
||||
) # pytype: disable=wrong-arg-types # numpy-scalars
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.__helper, self.__index))
|
||||
|
||||
|
||||
class _BoundedLinearExpr(metaclass=abc.ABCMeta):
|
||||
"""Interface for types that can build bounded linear (boolean) expressions.
|
||||
|
||||
Classes derived from _BoundedLinearExpr are used to build linear constraints
|
||||
to be satisfied.
|
||||
|
||||
* BoundedLinearExpression: a linear expression with upper and lower bounds.
|
||||
* VarEqVar: an equality comparison between two variables.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _add_linear_constraint(
|
||||
self, helper: mbh.ModelBuilderHelper, name: str
|
||||
) -> "LinearConstraint":
|
||||
"""Creates a new linear constraint in the helper.
|
||||
|
||||
Args:
|
||||
helper (mbh.ModelBuilderHelper): The helper to create the constraint.
|
||||
name (str): The name of the linear constraint.
|
||||
|
||||
Returns:
|
||||
LinearConstraint: A reference to the linear constraint in the helper.
|
||||
"""
|
||||
|
||||
@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.
|
||||
"""
|
||||
Variable = mbh.Variable
|
||||
|
||||
|
||||
def _add_linear_constraint_to_helper(
|
||||
bounded_expr: Union[bool, _BoundedLinearExpr],
|
||||
bounded_expr: Union[bool, mbh.BoundedLinearExpression],
|
||||
helper: mbh.ModelBuilderHelper,
|
||||
name: Optional[str],
|
||||
):
|
||||
@@ -448,14 +97,21 @@ def _add_linear_constraint_to_helper(
|
||||
helper.set_constraint_lower_bound(c.index, 1)
|
||||
helper.set_constraint_upper_bound(c.index, -1)
|
||||
return c
|
||||
if isinstance(bounded_expr, _BoundedLinearExpr):
|
||||
if isinstance(bounded_expr, mbh.BoundedLinearExpression):
|
||||
c = LinearConstraint(helper)
|
||||
# pylint: disable=protected-access
|
||||
return bounded_expr._add_linear_constraint(helper, name)
|
||||
helper.add_terms_to_constraint(c.index, bounded_expr.vars, bounded_expr.coeffs)
|
||||
helper.set_constraint_lower_bound(c.index, bounded_expr.lower_bound)
|
||||
helper.set_constraint_upper_bound(c.index, bounded_expr.upper_bound)
|
||||
# pylint: enable=protected-access
|
||||
if name is not None:
|
||||
helper.set_constraint_name(c.index, name)
|
||||
return c
|
||||
raise TypeError("invalid type={}".format(type(bounded_expr)))
|
||||
|
||||
|
||||
def _add_enforced_linear_constraint_to_helper(
|
||||
bounded_expr: Union[bool, _BoundedLinearExpr],
|
||||
bounded_expr: Union[bool, mbh.BoundedLinearExpression],
|
||||
helper: mbh.ModelBuilderHelper,
|
||||
var: Variable,
|
||||
value: bool,
|
||||
@@ -502,153 +158,20 @@ def _add_enforced_linear_constraint_to_helper(
|
||||
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
|
||||
return bounded_expr._add_enforced_linear_constraint(helper, var, value, name)
|
||||
raise TypeError("invalid type={}".format(type(bounded_expr)))
|
||||
|
||||
|
||||
@dataclasses.dataclass(repr=False, eq=False, frozen=True)
|
||||
class VarEqVar(_BoundedLinearExpr):
|
||||
"""Represents var == var."""
|
||||
|
||||
__slots__ = ("left", "right")
|
||||
|
||||
left: Variable
|
||||
right: Variable
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.left} == {self.right}"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return hash(self.left) == hash(self.right)
|
||||
|
||||
def _add_linear_constraint(
|
||||
self, helper: mbh.ModelBuilderHelper, name: str
|
||||
) -> "LinearConstraint":
|
||||
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
|
||||
|
||||
def _add_enforced_linear_constraint(
|
||||
self,
|
||||
helper: mbh.ModelBuilderHelper,
|
||||
var: Variable,
|
||||
value: bool,
|
||||
name: str,
|
||||
) -> "EnforcedLinearConstraint":
|
||||
"""Adds an enforced linear constraint to the model."""
|
||||
if isinstance(bounded_expr, mbh.BoundedLinearExpression):
|
||||
c = EnforcedLinearConstraint(helper)
|
||||
c.indicator_variable = var
|
||||
c.indicator_value = value
|
||||
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
|
||||
|
||||
|
||||
class BoundedLinearExpression(_BoundedLinearExpr):
|
||||
"""Represents a linear constraint: `lb <= linear expression <= ub`.
|
||||
|
||||
The only use of this class is to be added to the Model through
|
||||
`Model.add(bounded expression)`, as in:
|
||||
|
||||
model.Add(x + 2 * y -1 >= z)
|
||||
"""
|
||||
|
||||
def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT) -> None:
|
||||
self.__expr: LinearExprT = expr
|
||||
self.__lb: np.double = mbn.assert_is_a_number(lb)
|
||||
self.__ub: np.double = mbn.assert_is_a_number(ub)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.__lb > -math.inf and self.__ub < math.inf:
|
||||
if self.__lb == self.__ub:
|
||||
return f"{self.__expr} == {self.__lb}"
|
||||
else:
|
||||
return f"{self.__lb} <= {self.__expr} <= {self.__ub}"
|
||||
elif self.__lb > -math.inf:
|
||||
return f"{self.__expr} >= {self.__lb}"
|
||||
elif self.__ub < math.inf:
|
||||
return f"{self.__expr} <= {self.__ub}"
|
||||
else:
|
||||
return f"{self.__expr} free"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@property
|
||||
def expression(self) -> LinearExprT:
|
||||
return self.__expr
|
||||
|
||||
@property
|
||||
def lower_bound(self) -> np.double:
|
||||
return self.__lb
|
||||
|
||||
@property
|
||||
def upper_bound(self) -> np.double:
|
||||
return self.__ub
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
raise NotImplementedError(
|
||||
f"Cannot use a BoundedLinearExpression {self} as a Boolean value"
|
||||
helper.add_terms_to_enforced_constraint(
|
||||
c.index, bounded_expr.vars, bounded_expr.coeffs
|
||||
)
|
||||
|
||||
def _add_linear_constraint(
|
||||
self, helper: mbh.ModelBuilderHelper, name: Optional[str]
|
||||
) -> "LinearConstraint":
|
||||
c = LinearConstraint(helper)
|
||||
flat_expr = _as_flat_linear_expression(self.__expr)
|
||||
# pylint: disable=protected-access
|
||||
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)
|
||||
# pylint: enable=protected-access
|
||||
helper.set_enforced_constraint_lower_bound(c.index, bounded_expr.lower_bound)
|
||||
helper.set_enforced_constraint_upper_bound(c.index, bounded_expr.upper_bound)
|
||||
if name is not None:
|
||||
helper.set_constraint_name(c.index, name)
|
||||
return c
|
||||
|
||||
def _add_enforced_linear_constraint(
|
||||
self,
|
||||
helper: mbh.ModelBuilderHelper,
|
||||
var: Variable,
|
||||
value: bool,
|
||||
name: Optional[str],
|
||||
) -> "EnforcedLinearConstraint":
|
||||
"""Adds an enforced linear constraint to the model."""
|
||||
c = EnforcedLinearConstraint(helper)
|
||||
c.indicator_variable = var
|
||||
c.indicator_value = value
|
||||
flat_expr = _as_flat_linear_expression(self.__expr)
|
||||
# pylint: disable=protected-access
|
||||
helper.add_terms_to_enforced_constraint(
|
||||
c.index, flat_expr._variable_indices, flat_expr._coefficients
|
||||
)
|
||||
helper.set_enforced_constraint_lower_bound(
|
||||
c.index, self.__lb - flat_expr._offset
|
||||
)
|
||||
helper.set_enforced_constraint_upper_bound(
|
||||
c.index, self.__ub - flat_expr._offset
|
||||
)
|
||||
# pylint: enable=protected-access
|
||||
if name is not None:
|
||||
helper.set_enforced_constraint_name(c.index, name)
|
||||
return c
|
||||
raise TypeError("invalid type={}".format(type(bounded_expr)))
|
||||
|
||||
|
||||
class LinearConstraint:
|
||||
@@ -683,6 +206,9 @@ class LinearConstraint:
|
||||
self.__helper: mbh.ModelBuilderHelper = helper
|
||||
self.__is_under_specified = is_under_specified
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.__helper, self.__index))
|
||||
|
||||
@property
|
||||
def index(self) -> IntegerT:
|
||||
"""Returns the index of the constraint in the helper."""
|
||||
@@ -837,7 +363,7 @@ class EnforcedLinearConstraint:
|
||||
enforcement_var_index = (
|
||||
self.__helper.enforced_constraint_indicator_variable_index(self.__index)
|
||||
)
|
||||
return Variable(self.__helper, enforcement_var_index, None, None, None)
|
||||
return Variable(self.__helper, enforcement_var_index)
|
||||
|
||||
@indicator_variable.setter
|
||||
def indicator_variable(self, var: "Variable") -> None:
|
||||
@@ -984,15 +510,14 @@ class Model:
|
||||
"""
|
||||
return _attribute_series(
|
||||
# pylint: disable=g-long-lambda
|
||||
func=lambda c: _as_flat_linear_expression(
|
||||
func=lambda c: mbh.FlatExpression(
|
||||
# pylint: disable=g-complex-comprehension
|
||||
sum(
|
||||
coeff * Variable(self.__helper, var_id, None, None, None)
|
||||
for var_id, coeff in zip(
|
||||
c.helper.constraint_var_indices(c.index),
|
||||
c.helper.constraint_coefficients(c.index),
|
||||
)
|
||||
)
|
||||
[
|
||||
Variable(self.__helper, var_id)
|
||||
for var_id in c.helper.constraint_var_indices(c.index)
|
||||
],
|
||||
c.helper.constraint_coefficients(c.index),
|
||||
0.0,
|
||||
),
|
||||
values=self._get_linear_constraints(constraints),
|
||||
)
|
||||
@@ -1221,11 +746,11 @@ class Model:
|
||||
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],
|
||||
self.__helper,
|
||||
lower_bounds[i],
|
||||
upper_bounds[i],
|
||||
is_integrals[i],
|
||||
f"{name}[{i}]",
|
||||
)
|
||||
for i in index
|
||||
],
|
||||
@@ -1318,7 +843,7 @@ class Model:
|
||||
|
||||
def var_from_index(self, index: IntegerT) -> Variable:
|
||||
"""Rebuilds a variable object from the model and its index."""
|
||||
return Variable(self.__helper, index, None, None, None)
|
||||
return Variable(self.__helper, index)
|
||||
|
||||
# Linear constraints.
|
||||
|
||||
@@ -1336,17 +861,13 @@ class Model:
|
||||
if mbn.is_a_number(linear_expr):
|
||||
self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
|
||||
self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
|
||||
elif isinstance(linear_expr, Variable):
|
||||
self.__helper.set_constraint_lower_bound(ct.index, lb)
|
||||
self.__helper.set_constraint_upper_bound(ct.index, ub)
|
||||
self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
|
||||
elif isinstance(linear_expr, LinearExpr):
|
||||
flat_expr = _as_flat_linear_expression(linear_expr)
|
||||
flat_expr = mbh.FlatExpression(linear_expr)
|
||||
# pylint: disable=protected-access
|
||||
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.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
|
||||
ct.index, flat_expr.vars, flat_expr.coeffs
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
@@ -1381,8 +902,8 @@ class Model:
|
||||
you can check the if a constraint is under specified by reading the
|
||||
`LinearConstraint.is_under_specified` property.
|
||||
"""
|
||||
if isinstance(ct, _BoundedLinearExpr):
|
||||
return ct._add_linear_constraint(self.__helper, name)
|
||||
if isinstance(ct, mbh.BoundedLinearExpression):
|
||||
return _add_linear_constraint_to_helper(ct, self.__helper, name)
|
||||
elif isinstance(ct, bool):
|
||||
return _add_linear_constraint_to_helper(ct, self.__helper, name)
|
||||
elif isinstance(ct, pd.Series):
|
||||
@@ -1402,7 +923,7 @@ class Model:
|
||||
"""Rebuilds a linear constraint object from the model and its index."""
|
||||
return LinearConstraint(self.__helper, index=index)
|
||||
|
||||
# EnforcedLinear constraints.
|
||||
# Enforced Linear constraints.
|
||||
|
||||
def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars
|
||||
self,
|
||||
@@ -1422,18 +943,12 @@ class Model:
|
||||
if mbn.is_a_number(linear_expr):
|
||||
self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
|
||||
self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
|
||||
elif isinstance(linear_expr, Variable):
|
||||
self.__helper.set_constraint_lower_bound(ct.index, lb)
|
||||
self.__helper.set_constraint_upper_bound(ct.index, ub)
|
||||
self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
|
||||
elif isinstance(linear_expr, LinearExpr):
|
||||
flat_expr = _as_flat_linear_expression(linear_expr)
|
||||
flat_expr = mbh.FlatExpression(linear_expr)
|
||||
# pylint: disable=protected-access
|
||||
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
|
||||
)
|
||||
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, flat_expr.coeffs)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Not supported:"
|
||||
@@ -1472,8 +987,10 @@ class Model:
|
||||
you can check the if a constraint is always false (lb=inf, ub=-inf) by
|
||||
calling EnforcedLinearConstraint.is_always_false()
|
||||
"""
|
||||
if isinstance(ct, _BoundedLinearExpr):
|
||||
return ct._add_enforced_linear_constraint(self.__helper, var, value, name)
|
||||
if isinstance(ct, mbh.BoundedLinearExpression): # IMPLEMENTME
|
||||
return _add_enforced_linear_constraint_to_helper(
|
||||
ct, self.__helper, var, value, name
|
||||
)
|
||||
elif (
|
||||
isinstance(ct, bool)
|
||||
and isinstance(var, Variable)
|
||||
@@ -1525,12 +1042,11 @@ class Model:
|
||||
elif isinstance(linear_expr, Variable):
|
||||
self.helper.set_var_objective_coefficient(linear_expr.index, 1.0)
|
||||
elif isinstance(linear_expr, LinearExpr):
|
||||
flat_expr = _as_flat_linear_expression(linear_expr)
|
||||
flat_expr = mbh.FlatExpression(linear_expr)
|
||||
# pylint: disable=protected-access
|
||||
self.helper.set_objective_offset(flat_expr._offset)
|
||||
self.helper.set_objective_coefficients(
|
||||
flat_expr._variable_indices, flat_expr._coefficients
|
||||
)
|
||||
self.helper.set_objective_offset(flat_expr.offset)
|
||||
var_indices = [var.index for var in flat_expr.vars]
|
||||
self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs)
|
||||
else:
|
||||
raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})")
|
||||
|
||||
@@ -1543,15 +1059,17 @@ class Model:
|
||||
def objective_offset(self, value: NumberT) -> None:
|
||||
self.__helper.set_objective_offset(value)
|
||||
|
||||
def objective_expression(self) -> "_LinearExpression":
|
||||
def objective_expression(self) -> "LinearExpr":
|
||||
"""Returns the expression to optimize."""
|
||||
return _as_flat_linear_expression(
|
||||
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()
|
||||
variables: list[Variable] = []
|
||||
coefficients: list[numbers.Real] = []
|
||||
for variable in self.get_variables():
|
||||
coeff = self.__helper.var_objective_coefficient(variable.index)
|
||||
if coeff != 0.0:
|
||||
variables.append(variable)
|
||||
coefficients.append(coeff)
|
||||
return mbh.FlatExpression(
|
||||
variables, coefficients, self.__helper.objective_offset()
|
||||
)
|
||||
|
||||
# Hints.
|
||||
@@ -1712,15 +1230,8 @@ class Solver:
|
||||
return pd.NA
|
||||
if mbn.is_a_number(expr):
|
||||
return expr
|
||||
elif isinstance(expr, Variable):
|
||||
return self.__solve_helper.var_value(expr.index)
|
||||
elif isinstance(expr, LinearExpr):
|
||||
flat_expr = _as_flat_linear_expression(expr)
|
||||
return self.__solve_helper.expression_value(
|
||||
flat_expr._variable_indices,
|
||||
flat_expr._coefficients,
|
||||
flat_expr._offset,
|
||||
)
|
||||
return self.__solve_helper.expression_value(expr)
|
||||
else:
|
||||
raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}")
|
||||
|
||||
@@ -1742,7 +1253,7 @@ class Solver:
|
||||
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),
|
||||
func=lambda v: self.__solve_helper.variable_value(v.index),
|
||||
values=variables,
|
||||
)
|
||||
|
||||
@@ -1839,164 +1350,6 @@ class Solver:
|
||||
return self.__solve_helper.user_time()
|
||||
|
||||
|
||||
# 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."""
|
||||
|
||||
__slots__ = ("_variable_indices", "_coefficients", "_offset", "_helper")
|
||||
|
||||
_variable_indices: npt.NDArray[np.int32]
|
||||
_coefficients: npt.NDArray[np.double]
|
||||
_offset: float
|
||||
_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
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
if self._helper is None:
|
||||
return str(self._offset)
|
||||
|
||||
result = []
|
||||
for index, coeff in zip(self.variable_indices, self.coefficients):
|
||||
if len(result) >= _MAX_LINEAR_EXPRESSION_REPR_TERMS:
|
||||
result.append(" + ...")
|
||||
break
|
||||
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}")
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def _sum_as_flat_linear_expression(
|
||||
to_process: List[Tuple[LinearExprT, float]], offset: float = 0.0
|
||||
) -> _LinearExpression:
|
||||
"""Creates a _LinearExpression as the sum of terms."""
|
||||
indices = []
|
||||
coeffs = []
|
||||
helper = None
|
||||
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):
|
||||
indices.append([expr.index])
|
||||
coeffs.append([coeff])
|
||||
if helper is None:
|
||||
helper = expr.helper
|
||||
elif mbn.is_a_number(expr):
|
||||
offset += coeff * cast(NumberT, expr)
|
||||
elif isinstance(expr, _Product):
|
||||
to_process.append((expr._expression, coeff * expr._coefficient))
|
||||
elif isinstance(expr, _LinearExpression):
|
||||
offset += coeff * expr._offset
|
||||
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
|
||||
else:
|
||||
raise TypeError(
|
||||
"Unrecognized linear expression: " + str(expr) + f" {type(expr)}"
|
||||
)
|
||||
|
||||
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(
|
||||
all_indices, all_coeffs
|
||||
)
|
||||
return _LinearExpression(sorted_indices, sorted_coefficients, offset, helper)
|
||||
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
|
||||
return _sum_as_flat_linear_expression(to_process=[(base_expr, 1.0)], offset=0.0)
|
||||
|
||||
|
||||
@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):
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
#include "ortools/linear_solver/wrappers/model_builder_helper.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <complex>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -28,12 +27,15 @@
|
||||
|
||||
#include "Eigen/Core"
|
||||
#include "Eigen/SparseCore"
|
||||
#include "absl/hash/hash.h"
|
||||
#include "absl/log/check.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/linear_solver/linear_solver.pb.h"
|
||||
#include "ortools/linear_solver/model_exporter.h"
|
||||
#include "pybind11/cast.h"
|
||||
#include "pybind11/eigen.h"
|
||||
#include "pybind11/pybind11.h"
|
||||
#include "pybind11/pytypes.h"
|
||||
@@ -42,19 +44,30 @@
|
||||
|
||||
using ::Eigen::SparseMatrix;
|
||||
using ::Eigen::VectorXd;
|
||||
using ::operations_research::ModelBuilderHelper;
|
||||
using ::operations_research::ModelSolverHelper;
|
||||
using ::operations_research::MPConstraintProto;
|
||||
using ::operations_research::MPModelExportOptions;
|
||||
using ::operations_research::MPModelProto;
|
||||
using ::operations_research::MPModelRequest;
|
||||
using ::operations_research::MPSolutionResponse;
|
||||
using ::operations_research::MPVariableProto;
|
||||
using ::operations_research::SolveStatus;
|
||||
using ::operations_research::mb::AffineExpr;
|
||||
using ::operations_research::mb::BoundedLinearExpression;
|
||||
using ::operations_research::mb::ExprOrValue;
|
||||
using ::operations_research::mb::FlatExpression;
|
||||
using ::operations_research::mb::LinearExpr;
|
||||
using ::operations_research::mb::ModelBuilderHelper;
|
||||
using ::operations_research::mb::ModelSolverHelper;
|
||||
using ::operations_research::mb::SolveStatus;
|
||||
using ::operations_research::mb::Variable;
|
||||
|
||||
namespace py = pybind11;
|
||||
using ::py::arg;
|
||||
|
||||
void ThrowError(PyObject* py_exception, const std::string& message) {
|
||||
PyErr_SetString(py_exception, message.c_str());
|
||||
throw py::error_already_set();
|
||||
}
|
||||
|
||||
const MPModelProto& ToMPModelProto(ModelBuilderHelper* helper) {
|
||||
return helper->model();
|
||||
}
|
||||
@@ -153,9 +166,306 @@ std::vector<std::pair<int, double>> SortedGroupedTerms(
|
||||
return terms;
|
||||
}
|
||||
|
||||
LinearExpr* SafeWeightedSum(const std::vector<LinearExpr*>& exprs,
|
||||
const std::vector<double>& coeffs,
|
||||
double constant = 0.0) {
|
||||
if (exprs.size() != coeffs.size()) {
|
||||
ThrowError(PyExc_ValueError,
|
||||
"The number of expressions and coefficients must match.");
|
||||
}
|
||||
return LinearExpr::WeightedSum(exprs, coeffs, constant);
|
||||
}
|
||||
|
||||
LinearExpr* SafeMixedWeightedSum(const std::vector<ExprOrValue>& exprs,
|
||||
const std::vector<double>& coeffs,
|
||||
double constant = 0.0) {
|
||||
if (exprs.size() != coeffs.size()) {
|
||||
ThrowError(PyExc_ValueError,
|
||||
"The number of expressions and coefficients must match.");
|
||||
}
|
||||
return LinearExpr::MixedWeightedSum(exprs, coeffs, constant);
|
||||
}
|
||||
|
||||
const char* kLinearExprClassDoc = R"doc(
|
||||
Holds an linear expression.
|
||||
|
||||
A linear expression is built from constants and variables.
|
||||
For example, `x + 2.0 * (y - z + 1.0)`.
|
||||
|
||||
Linear expressions are used in Model models in constraints and in the objective:
|
||||
|
||||
* You can define linear constraints as in:
|
||||
|
||||
```
|
||||
model.add(x + 2 * y <= 5.0)
|
||||
model.add(sum(array_of_vars) == 5.0)
|
||||
```
|
||||
|
||||
* In Model, the objective is a linear expression:
|
||||
|
||||
```
|
||||
model.minimize(x + 2.0 * y + z)
|
||||
```
|
||||
|
||||
* For large arrays, using the LinearExpr class is faster that using the python
|
||||
`sum()` function. You can create constraints and the objective from lists of
|
||||
linear expressions or coefficients as follows:
|
||||
|
||||
```
|
||||
model.minimize(model_builder.LinearExpr.sum(expressions))
|
||||
model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
|
||||
```
|
||||
)doc";
|
||||
|
||||
const char* kVarClassDoc = R"doc(A variable (continuous or integral).
|
||||
|
||||
A Variable is an object that can take on any integer value within defined
|
||||
ranges. Variables appear in constraint like:
|
||||
|
||||
x + y >= 5
|
||||
|
||||
Solving a model is equivalent to finding, for each variable, a single value
|
||||
from the set of initial values (called the initial domain), such that the
|
||||
model is feasible, or optimal if you provided an objective function.
|
||||
)doc";
|
||||
|
||||
PYBIND11_MODULE(model_builder_helper, m) {
|
||||
pybind11_protobuf::ImportNativeProtoCasters();
|
||||
|
||||
py::class_<ExprOrValue>(m, "ExprOrValue")
|
||||
.def(py::init<double>())
|
||||
.def(py::init<int64_t>())
|
||||
.def(py::init<LinearExpr*>())
|
||||
.def_readonly("double_value", &ExprOrValue::value)
|
||||
.def_readonly("expr", &ExprOrValue::expr);
|
||||
|
||||
py::implicitly_convertible<double, ExprOrValue>();
|
||||
py::implicitly_convertible<int, ExprOrValue>();
|
||||
py::implicitly_convertible<LinearExpr*, ExprOrValue>();
|
||||
|
||||
py::class_<LinearExpr>(m, "LinearExpr", kLinearExprClassDoc)
|
||||
// We make sure to keep the order of the overloads: LinearExpr* before
|
||||
// ExprOrValue as this is faster to parse and type check.
|
||||
.def_static("sum", (&LinearExpr::Sum), arg("exprs"), py::kw_only(),
|
||||
arg("constant") = 0.0, "Creates `sum(exprs) + constant`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def_static("sum", &LinearExpr::MixedSum, arg("exprs"), py::kw_only(),
|
||||
arg("constant") = 0.0, "Creates `sum(exprs) + constant`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def_static("weighted_sum", &SafeWeightedSum, arg("exprs"), arg("coeffs"),
|
||||
py::kw_only(), arg("constant") = 0.0,
|
||||
"Creates `sum(expressions[i] * coefficients[i]) + constant`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def_static("weighted_sum", &SafeMixedWeightedSum, arg("exprs"),
|
||||
arg("coeffs"), py::kw_only(), arg("constant") = 0.0,
|
||||
"Creates `sum(expressions[i] * coefficients[i]) + constant`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def_static("term", &LinearExpr::Term, arg("expr").none(false),
|
||||
arg("coeff"), "Returns expr * coeff.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
// Compatibility layer.
|
||||
.def_static("term", &LinearExpr::Affine, arg("expr").none(false),
|
||||
arg("coeff"), py::kw_only(), py::arg("constant"),
|
||||
"Returns expr * coeff [+ constant].",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def_static("term", &LinearExpr::AffineCst, arg("value"), arg("coeff"),
|
||||
py::kw_only(), py::arg("constant"),
|
||||
"Returns value * coeff [+ constant].",
|
||||
py::return_value_policy::automatic)
|
||||
.def_static("affine", &LinearExpr::Affine, arg("expr").none(false),
|
||||
arg("coeff"), arg("constant") = 0.0,
|
||||
"Returns expr * coeff + constant.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def_static("affine", &LinearExpr::AffineCst, arg("value"), arg("coeff"),
|
||||
arg("constant") = 0.0, "Returns value * coeff + constant.",
|
||||
py::return_value_policy::automatic)
|
||||
.def_static("constant", &LinearExpr::Constant, arg("value"),
|
||||
"Returns a constant linear expression.",
|
||||
py::return_value_policy::automatic)
|
||||
// Methods.
|
||||
.def("__str__", &LinearExpr::ToString)
|
||||
.def("__repr__", &LinearExpr::DebugString)
|
||||
// Operators.
|
||||
// Note that we keep the 3 APIS (expr, int, double) instead of using an
|
||||
// ExprOrValue argument as this is more efficient.
|
||||
.def("__add__", &LinearExpr::Add, arg("other").none(false),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>(),
|
||||
py::keep_alive<0, 2>())
|
||||
.def("__add__", &LinearExpr::AddFloat, arg("cst"),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__radd__", &LinearExpr::AddFloat, arg("cst"),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__sub__", &LinearExpr::Sub, arg("other").none(false),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>(),
|
||||
py::keep_alive<0, 2>())
|
||||
.def("__sub__", &LinearExpr::SubFloat, arg("cst"),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__rsub__", &LinearExpr::RSubFloat, arg("cst"),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__mul__", &LinearExpr::MulFloat, arg("cst"),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__rmul__", &LinearExpr::MulFloat, arg("cst"),
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def(
|
||||
"__truediv__",
|
||||
[](LinearExpr* self, double cst) {
|
||||
if (cst == 0.0) {
|
||||
ThrowError(PyExc_ZeroDivisionError,
|
||||
"Division by zero is not supported.");
|
||||
}
|
||||
return self->MulFloat(1.0 / cst);
|
||||
},
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__neg__", &LinearExpr::Neg, py::return_value_policy::automatic,
|
||||
py::keep_alive<0, 1>())
|
||||
// Comparison operators.
|
||||
.def("__eq__", &LinearExpr::Eq, arg("other").none(false),
|
||||
"Creates the constraint `self == other`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>(),
|
||||
py::keep_alive<0, 2>())
|
||||
.def("__eq__", &LinearExpr::EqCst, arg("cst"),
|
||||
"Creates the constraint `self == cst`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__le__", &LinearExpr::Le, arg("other").none(false),
|
||||
"Creates the constraint `self <= other`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>(),
|
||||
py::keep_alive<0, 2>())
|
||||
.def("__le__", &LinearExpr::LeCst, arg("cst"),
|
||||
"Creates the constraint `self <= cst`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
.def("__ge__", &LinearExpr::Ge, arg("other").none(false),
|
||||
"Creates the constraint `self >= other`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>(),
|
||||
py::keep_alive<0, 2>())
|
||||
.def("__ge__", &LinearExpr::GeCst, arg("cst"),
|
||||
"Creates the constraint `self >= cst`.",
|
||||
py::return_value_policy::automatic, py::keep_alive<0, 1>())
|
||||
// Disable other operators as they are not supported.
|
||||
.def("__floordiv__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
"calling // on a linear expression is not supported.");
|
||||
})
|
||||
.def("__mod__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
"calling %% on a linear expression is not supported.");
|
||||
})
|
||||
.def("__pow__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
"calling ** on a linear expression is not supported.");
|
||||
})
|
||||
.def("__lshift__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(
|
||||
PyExc_NotImplementedError,
|
||||
"calling left shift on a linear expression is not supported");
|
||||
})
|
||||
.def("__rshift__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(
|
||||
PyExc_NotImplementedError,
|
||||
"calling right shift on a linear expression is not supported");
|
||||
})
|
||||
.def("__and__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
"calling and on a linear expression is not supported");
|
||||
})
|
||||
.def("__or__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
"calling or on a linear expression is not supported");
|
||||
})
|
||||
.def("__xor__",
|
||||
[](LinearExpr* /*self*/, ExprOrValue /*other*/) {
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
"calling xor on a linear expression is not supported");
|
||||
})
|
||||
.def("__abs__",
|
||||
[](LinearExpr* /*self*/) {
|
||||
ThrowError(
|
||||
PyExc_NotImplementedError,
|
||||
"calling abs() on a linear expression is not supported.");
|
||||
})
|
||||
.def("__bool__", [](LinearExpr* /*self*/) {
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
"Evaluating a LinearExpr instance as a Boolean is "
|
||||
"not supported.");
|
||||
});
|
||||
|
||||
// Expose Internal classes, mostly for testing.
|
||||
py::class_<FlatExpression, LinearExpr>(m, "FlatExpression")
|
||||
.def(py::init<const LinearExpr*>())
|
||||
.def(py::init<const LinearExpr*, const LinearExpr*>())
|
||||
.def(py::init<const std::vector<const Variable*>&,
|
||||
const std::vector<double>&, double>(),
|
||||
py::keep_alive<1, 2>())
|
||||
.def(py::init<double>())
|
||||
.def_property_readonly("vars", &FlatExpression::vars)
|
||||
.def("variable_indices", &FlatExpression::VarIndices)
|
||||
.def_property_readonly("coeffs", &FlatExpression::coeffs)
|
||||
.def_property_readonly("offset", &FlatExpression::offset);
|
||||
|
||||
py::class_<AffineExpr, LinearExpr>(m, "AffineExpr")
|
||||
.def(py::init<LinearExpr*, double, double>())
|
||||
.def_property_readonly("expression", &AffineExpr ::expression)
|
||||
.def_property_readonly("coefficient", &AffineExpr::coefficient)
|
||||
.def_property_readonly("offset", &AffineExpr::offset);
|
||||
|
||||
py::class_<Variable, LinearExpr>(m, "Variable", kVarClassDoc)
|
||||
.def(py::init<ModelBuilderHelper*, int>())
|
||||
.def(py::init<ModelBuilderHelper*, double, double, bool>())
|
||||
.def(py::init<ModelBuilderHelper*, double, double, bool, std::string>())
|
||||
.def(py::init<ModelBuilderHelper*, int64_t, int64_t, bool>())
|
||||
.def(py::init<ModelBuilderHelper*, int64_t, int64_t, bool, std::string>())
|
||||
.def_property_readonly("index", &Variable::index,
|
||||
"The index of the variable in the model.")
|
||||
.def_property_readonly("helper", &Variable::helper,
|
||||
"The ModelBuilderHelper instance.")
|
||||
.def_property("name", &Variable::name, &Variable::SetName,
|
||||
"The name of the variable in the model.")
|
||||
.def_property("lower_bound", &Variable::lower_bounds,
|
||||
&Variable::SetLowerBound)
|
||||
.def_property("upper_bound", &Variable::upper_bound,
|
||||
&Variable::SetUpperBound)
|
||||
.def_property("is_integral", &Variable::is_integral,
|
||||
&Variable::SetIsIntegral)
|
||||
.def_property("objective_coefficient", &Variable::objective_coefficient,
|
||||
&Variable::SetObjectiveCoefficient)
|
||||
.def("__str__", &Variable::ToString)
|
||||
.def("__repr__", &Variable::DebugString)
|
||||
.def("__hash__", [](const Variable& self) {
|
||||
return absl::HashOf(std::make_tuple(self.helper(), self.index()));
|
||||
});
|
||||
|
||||
py::class_<BoundedLinearExpression>(m, "BoundedLinearExpression")
|
||||
.def(py::init<const LinearExpr*, double, double>())
|
||||
.def(py::init<const LinearExpr*, const LinearExpr*, double, double>())
|
||||
.def(py::init<const LinearExpr*, int64_t, int64_t>())
|
||||
.def(py::init<const LinearExpr*, const LinearExpr*, int64_t, int64_t>())
|
||||
.def_property_readonly("vars", &BoundedLinearExpression::vars)
|
||||
.def_property_readonly("coeffs", &BoundedLinearExpression::coeffs)
|
||||
.def_property_readonly("lower_bound",
|
||||
&BoundedLinearExpression::lower_bound)
|
||||
.def_property_readonly("upper_bound",
|
||||
&BoundedLinearExpression::upper_bound)
|
||||
.def("__bool__",
|
||||
[](const BoundedLinearExpression& self) {
|
||||
bool result;
|
||||
if (self.CastToBool(&result)) return result;
|
||||
ThrowError(PyExc_NotImplementedError,
|
||||
absl::StrCat("Evaluating a BoundedLinearExpression '",
|
||||
self.ToString(),
|
||||
"'instance as a Boolean is "
|
||||
"not supported.")
|
||||
.c_str());
|
||||
return false;
|
||||
})
|
||||
.def("__str__", &BoundedLinearExpression::ToString)
|
||||
.def("__repr__", &BoundedLinearExpression::DebugString);
|
||||
|
||||
m.def("to_mpmodel_proto", &ToMPModelProto, arg("helper"));
|
||||
|
||||
py::class_<MPModelExportOptions>(m, "MPModelExportOptions")
|
||||
@@ -314,11 +624,11 @@ PYBIND11_MODULE(model_builder_helper, m) {
|
||||
arg("ct_index"), arg("var_index"), arg("coeff"))
|
||||
.def("add_terms_to_constraint",
|
||||
[](ModelBuilderHelper* helper, int ct_index,
|
||||
const std::vector<int>& indices,
|
||||
const std::vector<const Variable*>& vars,
|
||||
const std::vector<double>& coefficients) {
|
||||
for (const auto& [i, c] :
|
||||
SortedGroupedTerms(indices, coefficients)) {
|
||||
helper->AddConstraintTerm(ct_index, i, c);
|
||||
for (int i = 0; i < vars.size(); ++i) {
|
||||
helper->AddConstraintTerm(ct_index, vars[i]->index(),
|
||||
coefficients[i]);
|
||||
}
|
||||
})
|
||||
.def("safe_add_term_to_constraint",
|
||||
@@ -354,11 +664,11 @@ PYBIND11_MODULE(model_builder_helper, m) {
|
||||
arg("var_index"), arg("coeff"))
|
||||
.def("add_terms_to_enforced_constraint",
|
||||
[](ModelBuilderHelper* helper, int ct_index,
|
||||
const std::vector<int>& indices,
|
||||
const std::vector<const Variable*>& vars,
|
||||
const std::vector<double>& coefficients) {
|
||||
for (const auto& [i, c] :
|
||||
SortedGroupedTerms(indices, coefficients)) {
|
||||
helper->AddEnforcedConstraintTerm(ct_index, i, c);
|
||||
for (int i = 0; i < vars.size(); ++i) {
|
||||
helper->AddEnforcedConstraintTerm(ct_index, vars[i]->index(),
|
||||
coefficients[i]);
|
||||
}
|
||||
})
|
||||
.def("safe_add_term_to_enforced_constraint",
|
||||
@@ -402,22 +712,7 @@ PYBIND11_MODULE(model_builder_helper, m) {
|
||||
.def("objective_offset", &ModelBuilderHelper::ObjectiveOffset)
|
||||
.def("clear_hints", &ModelBuilderHelper::ClearHints)
|
||||
.def("add_hint", &ModelBuilderHelper::AddHint, arg("var_index"),
|
||||
arg("var_value"))
|
||||
.def("sort_and_regroup_terms",
|
||||
[](ModelBuilderHelper* helper, py::array_t<int> indices,
|
||||
py::array_t<double> coefficients) {
|
||||
const std::vector<std::pair<int, double>> terms =
|
||||
SortedGroupedTerms(indices, coefficients);
|
||||
std::vector<int> sorted_indices;
|
||||
std::vector<double> sorted_coefficients;
|
||||
sorted_indices.reserve(terms.size());
|
||||
sorted_coefficients.reserve(terms.size());
|
||||
for (const auto& [i, c] : terms) {
|
||||
sorted_indices.push_back(i);
|
||||
sorted_coefficients.push_back(c);
|
||||
}
|
||||
return std::make_pair(sorted_indices, sorted_coefficients);
|
||||
});
|
||||
arg("var_value"));
|
||||
|
||||
py::enum_<SolveStatus>(m, "SolveStatus")
|
||||
.value("OPTIMAL", SolveStatus::OPTIMAL)
|
||||
@@ -483,7 +778,16 @@ PYBIND11_MODULE(model_builder_helper, m) {
|
||||
.def("user_time", &ModelSolverHelper::user_time)
|
||||
.def("objective_value", &ModelSolverHelper::objective_value)
|
||||
.def("best_objective_bound", &ModelSolverHelper::best_objective_bound)
|
||||
.def("var_value", &ModelSolverHelper::variable_value, arg("var_index"))
|
||||
.def("variable_value", &ModelSolverHelper::variable_value,
|
||||
arg("var_index"))
|
||||
.def("expression_value",
|
||||
[](const ModelSolverHelper& helper, LinearExpr* expr) {
|
||||
if (!helper.has_response()) {
|
||||
throw std::logic_error(
|
||||
"Accessing a solution value when none has been found.");
|
||||
}
|
||||
return helper.expression_value(expr);
|
||||
})
|
||||
.def("reduced_cost", &ModelSolverHelper::reduced_cost, arg("var_index"))
|
||||
.def("dual_value", &ModelSolverHelper::dual_value, arg("ct_index"))
|
||||
.def("activity", &ModelSolverHelper::activity, arg("ct_index"))
|
||||
@@ -500,20 +804,6 @@ PYBIND11_MODULE(model_builder_helper, m) {
|
||||
}
|
||||
return vec;
|
||||
})
|
||||
.def("expression_value",
|
||||
[](const ModelSolverHelper& helper, const std::vector<int>& indices,
|
||||
const std::vector<double>& coefficients, double constant) {
|
||||
if (!helper.has_response()) {
|
||||
throw std::logic_error(
|
||||
"Accessing a solution value when none has been found.");
|
||||
}
|
||||
const MPSolutionResponse& response = helper.response();
|
||||
for (int i = 0; i < indices.size(); ++i) {
|
||||
constant +=
|
||||
response.variable_value(indices[i]) * coefficients[i];
|
||||
}
|
||||
return constant;
|
||||
})
|
||||
.def("reduced_costs",
|
||||
[](const ModelSolverHelper& helper) {
|
||||
if (!helper.has_response()) {
|
||||
@@ -539,4 +829,4 @@ PYBIND11_MODULE(model_builder_helper, m) {
|
||||
}
|
||||
return vec;
|
||||
});
|
||||
}
|
||||
} // NOLINT(readability/fn_size)
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for model_builder_helper."""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import threading
|
||||
@@ -97,7 +95,7 @@ class PywrapModelBuilderHelperTest(absltest.TestCase):
|
||||
linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL,
|
||||
)
|
||||
self.assertAlmostEqual(solver.objective_value(), 1.0)
|
||||
self.assertAlmostEqual(solver.var_value(0), 1.0)
|
||||
self.assertAlmostEqual(solver.variable_value(0), 1.0)
|
||||
values = solver.variable_values()
|
||||
self.assertEqual(1, len(values))
|
||||
self.assertAlmostEqual(1.0, values[0])
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for ModelBuilder."""
|
||||
|
||||
import math
|
||||
from typing import Any, Callable, Dict, Mapping, Union
|
||||
|
||||
@@ -28,15 +26,18 @@ import os
|
||||
from google.protobuf import text_format
|
||||
from ortools.linear_solver import linear_solver_pb2
|
||||
from ortools.linear_solver.python import model_builder as mb
|
||||
from ortools.linear_solver.python import model_builder_helper as mbh
|
||||
|
||||
|
||||
def build_dict(expr: mb.LinearExprT) -> Dict[mb.Variable, float]:
|
||||
def build_dict(expr: mb.LinearExprT) -> Dict[mbh.Variable, float]:
|
||||
res = {}
|
||||
for i, c in zip(expr.variable_indices, expr.coefficients):
|
||||
if not c:
|
||||
flat_expr = mbh.FlatExpression(expr)
|
||||
print(f"expr = {expr} flat_expr = {flat_expr}", flush=True)
|
||||
for var, coeff in zip(flat_expr.vars, flat_expr.coeffs):
|
||||
print(f"process {var} * {coeff}", flush=True)
|
||||
if not coeff:
|
||||
continue
|
||||
var = mb.Variable(expr.helper, lb=i, ub=None, is_integral=None, name=None)
|
||||
res[var] = c
|
||||
res[var] = coeff
|
||||
return res
|
||||
|
||||
|
||||
@@ -208,66 +209,75 @@ ENDATA
|
||||
t = model.new_int_var(3, 10, "t")
|
||||
|
||||
e1 = mb.LinearExpr.sum([x, y, z])
|
||||
flat_e1 = mbh.FlatExpression(e1)
|
||||
expected_vars = np.array([0, 1, 2], dtype=np.int32)
|
||||
np_testing.assert_array_equal(expected_vars, e1.variable_indices)
|
||||
np_testing.assert_array_equal(expected_vars, flat_e1.variable_indices())
|
||||
np_testing.assert_array_equal(
|
||||
np.array([1, 1, 1], dtype=np.double), e1.coefficients
|
||||
np.array([1, 1, 1], dtype=np.double), flat_e1.coeffs
|
||||
)
|
||||
self.assertEqual(e1.constant, 0.0)
|
||||
self.assertEqual(e1.__str__(), "x + y + z")
|
||||
self.assertEqual(flat_e1.offset, 0.0)
|
||||
self.assertEqual(e1.__str__(), "(x + y + z)")
|
||||
|
||||
e2 = mb.LinearExpr.sum([e1, 4.0])
|
||||
np_testing.assert_array_equal(expected_vars, e2.variable_indices)
|
||||
flat_e2 = mbh.FlatExpression(e2)
|
||||
np_testing.assert_array_equal(expected_vars, flat_e2.variable_indices())
|
||||
np_testing.assert_array_equal(
|
||||
np.array([1, 1, 1], dtype=np.double), e2.coefficients
|
||||
np.array([1, 1, 1], dtype=np.double), flat_e2.coeffs
|
||||
)
|
||||
self.assertEqual(e2.constant, 4.0)
|
||||
self.assertEqual(e2.__str__(), "x + y + z + 4.0")
|
||||
self.assertEqual(flat_e2.offset, 4.0)
|
||||
self.assertEqual(e2.__str__(), "((x + y + z) + 4)")
|
||||
self.assertEqual(flat_e2.__str__(), "x + y + z + 4")
|
||||
|
||||
e3 = mb.LinearExpr.term(e2, 2)
|
||||
np_testing.assert_array_equal(expected_vars, e3.variable_indices)
|
||||
flat_e3 = mbh.FlatExpression(e3)
|
||||
np_testing.assert_array_equal(expected_vars, flat_e3.variable_indices())
|
||||
np_testing.assert_array_equal(
|
||||
np.array([2, 2, 2], dtype=np.double), e3.coefficients
|
||||
np.array([2, 2, 2], dtype=np.double), flat_e3.coeffs
|
||||
)
|
||||
self.assertEqual(e3.constant, 8.0)
|
||||
self.assertEqual(e3.__str__(), "2.0 * x + 2.0 * y + 2.0 * z + 8.0")
|
||||
self.assertEqual(flat_e3.offset, 8.0)
|
||||
self.assertEqual(e3.__str__(), "(2 * ((x + y + z) + 4))")
|
||||
self.assertEqual(flat_e3.__str__(), "2 * x + 2 * y + 2 * z + 8")
|
||||
|
||||
e4 = mb.LinearExpr.weighted_sum([x, t], [-1, 1], constant=2)
|
||||
flat_e4 = mbh.FlatExpression(e4)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([0, 3], dtype=np.int32), e4.variable_indices
|
||||
np.array([0, 3], dtype=np.int32), flat_e4.variable_indices()
|
||||
)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([-1, 1], dtype=np.double), e4.coefficients
|
||||
np.array([-1, 1], dtype=np.double), flat_e4.coeffs
|
||||
)
|
||||
self.assertEqual(e4.constant, 2.0)
|
||||
self.assertEqual(e4.__str__(), "-x + t + 2.0")
|
||||
self.assertEqual(flat_e4.offset, 2.0)
|
||||
self.assertEqual(e4.__str__(), "(-x + t + 2)")
|
||||
|
||||
e4b = mb.LinearExpr.weighted_sum([e4 * 3], [1])
|
||||
flat_e4b = mbh.FlatExpression(e4b)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([0, 3], dtype=np.int32), e4b.variable_indices
|
||||
np.array([0, 3], dtype=np.int32), flat_e4b.variable_indices()
|
||||
)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([-3, 3], dtype=np.double), e4b.coefficients
|
||||
np.array([-3, 3], dtype=np.double), flat_e4b.coeffs
|
||||
)
|
||||
self.assertEqual(e4b.constant, 6.0)
|
||||
self.assertEqual(e4b.__str__(), "-3.0 * x + 3.0 * t + 6.0")
|
||||
self.assertEqual(flat_e4b.offset, 6.0)
|
||||
self.assertEqual(e4b.__str__(), "(3 * (-x + t + 2))")
|
||||
|
||||
e5 = mb.LinearExpr.sum([e1, -3, e4])
|
||||
flat_e5 = mbh.FlatExpression(e5)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([1, 2, 3], dtype=np.int32), e5.variable_indices
|
||||
np.array([1, 2, 3], dtype=np.int32), flat_e5.variable_indices()
|
||||
)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([1, 1, 1], dtype=np.double), e5.coefficients
|
||||
np.array([1, 1, 1], dtype=np.double), flat_e5.coeffs
|
||||
)
|
||||
self.assertEqual(e5.constant, -1.0)
|
||||
self.assertEqual(e5.__str__(), "y + z + t - 1.0")
|
||||
self.assertEqual(flat_e5.offset, -1.0)
|
||||
self.assertEqual(flat_e5.__str__(), "y + z + t - 1")
|
||||
|
||||
e6 = mb.LinearExpr.term(x, 2.0, constant=1.0)
|
||||
flat_e6 = mbh.FlatExpression(e6)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([0], dtype=np.int32), e6.variable_indices
|
||||
np.array([0], dtype=np.int32), flat_e6.variable_indices()
|
||||
)
|
||||
np_testing.assert_array_equal(np.array([2], dtype=np.double), e6.coefficients)
|
||||
self.assertEqual(e6.constant, 1.0)
|
||||
np_testing.assert_array_equal(np.array([2], dtype=np.double), flat_e6.coeffs)
|
||||
self.assertEqual(flat_e6.offset, 1.0)
|
||||
|
||||
e7 = mb.LinearExpr.term(x, 1.0, constant=0.0)
|
||||
self.assertEqual(x, e7)
|
||||
@@ -278,8 +288,8 @@ ENDATA
|
||||
e9 = mb.LinearExpr.term(x * 2 + 3, 1, constant=0)
|
||||
e10 = mb.LinearExpr.term(x, 2, constant=3)
|
||||
self.assertEqual(
|
||||
str(mb._as_flat_linear_expression(e9)),
|
||||
str(mb._as_flat_linear_expression(e10)),
|
||||
str(mbh.FlatExpression(e9)),
|
||||
str(mbh.FlatExpression(e10)),
|
||||
)
|
||||
|
||||
def test_variables(self):
|
||||
@@ -377,14 +387,6 @@ ENDATA
|
||||
status = solver.solve(model)
|
||||
self.assertEqual(mb.SolveStatus.OPTIMAL, status)
|
||||
|
||||
def test_vareqvar(self):
|
||||
model = mb.Model()
|
||||
x = model.new_int_var(0.0, 4.0, "x")
|
||||
y = model.new_int_var(0.0, 4.0, "y")
|
||||
ct = x == y
|
||||
self.assertEqual(ct.left.index, x.index)
|
||||
self.assertEqual(ct.right.index, y.index)
|
||||
|
||||
def test_create_false_ct(self):
|
||||
# Create the model.
|
||||
model = mb.Model()
|
||||
@@ -424,7 +426,7 @@ class InternalHelperTest(absltest.TestCase):
|
||||
def test_anonymous_variables(self):
|
||||
helper = mb.Model().helper
|
||||
index = helper.add_var()
|
||||
variable = mb.Variable(helper, index, None, None, None)
|
||||
variable = mb.Variable(helper, index)
|
||||
self.assertEqual(variable.name, f"variable#{index}")
|
||||
|
||||
def test_anonymous_constraints(self):
|
||||
@@ -452,178 +454,175 @@ class LinearBaseTest(parameterized.TestCase):
|
||||
dict(
|
||||
testcase_name="x[0]",
|
||||
expr=lambda x, y: x[0],
|
||||
expected_repr="x[0]",
|
||||
expected_str="x[0]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x[1]",
|
||||
expr=lambda x, y: x[1],
|
||||
expected_repr="x[1]",
|
||||
expected_str="x[1]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x[2]",
|
||||
expr=lambda x, y: x[2],
|
||||
expected_repr="x[2]",
|
||||
expected_str="x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="y[0]",
|
||||
expr=lambda x, y: y[0],
|
||||
expected_repr="y[0]",
|
||||
expected_str="y[0]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="y[4]",
|
||||
expr=lambda x, y: y[4],
|
||||
expected_repr="y[4]",
|
||||
expected_str="y[4]",
|
||||
),
|
||||
# Sum
|
||||
dict(
|
||||
testcase_name="x[0] + 5",
|
||||
expr=lambda x, y: x[0] + 5,
|
||||
expected_repr="x[0] + 5.0",
|
||||
expected_str="x[0] + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x[0] - 5",
|
||||
expr=lambda x, y: x[0] - 5,
|
||||
expected_repr="x[0] - 5.0",
|
||||
expected_str="x[0] - 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="5 - x[0]",
|
||||
expr=lambda x, y: 5 - x[0],
|
||||
expected_repr="-x[0] + 5.0",
|
||||
expected_str="-x[0] + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="5 + x[0]",
|
||||
expr=lambda x, y: 5 + x[0],
|
||||
expected_repr="x[0] + 5.0",
|
||||
expected_str="x[0] + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x[0] + y[0]",
|
||||
expr=lambda x, y: x[0] + y[0],
|
||||
expected_repr="x[0] + y[0]",
|
||||
expected_str="x[0] + y[0]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x[0] + y[0] + 5",
|
||||
expr=lambda x, y: x[0] + y[0] + 5,
|
||||
expected_repr="x[0] + y[0] + 5.0",
|
||||
expected_str="x[0] + y[0] + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="5 + x[0] + y[0]",
|
||||
expr=lambda x, y: 5 + x[0] + y[0],
|
||||
expected_repr="x[0] + y[0] + 5.0",
|
||||
expected_str="x[0] + y[0] + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="5 + x[0] - x[0]",
|
||||
expr=lambda x, y: 5 + x[0] - x[0],
|
||||
expected_repr="5.0",
|
||||
expected_str="5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="5 + x[0] - y[0]",
|
||||
expr=lambda x, y: 5 + x[0] - y[0],
|
||||
expected_repr="x[0] - y[0] + 5.0",
|
||||
expected_str="x[0] - y[0] + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x.sum()",
|
||||
expr=lambda x, y: x.sum(),
|
||||
expected_repr="x[0] + x[1] + x[2]",
|
||||
expected_str="x[0] + x[1] + x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x.add(y, fill_value=0).sum() + 5",
|
||||
expr=lambda x, y: x.add(y, fill_value=0).sum() + 5,
|
||||
expected_repr="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5.0",
|
||||
expected_str="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="sum(x, y + 5)",
|
||||
expr=lambda x, y: mb.LinearExpr.sum([x.sum(), y.sum() + 5]),
|
||||
expected_repr="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5.0",
|
||||
expected_str="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5",
|
||||
),
|
||||
# Product
|
||||
dict(
|
||||
testcase_name="- x.sum()",
|
||||
expr=lambda x, y: -x.sum(),
|
||||
expected_repr="-x[0] - x[1] - x[2]",
|
||||
expected_str="-x[0] - x[1] - x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="5 - x.sum()",
|
||||
expr=lambda x, y: 5 - x.sum(),
|
||||
expected_repr="-x[0] - x[1] - x[2] + 5.0",
|
||||
expected_str="-x[0] - x[1] - x[2] + 5",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x.sum() / 2.0",
|
||||
expr=lambda x, y: x.sum() / 2.0,
|
||||
expected_repr="0.5 * x[0] + 0.5 * x[1] + 0.5 * x[2]",
|
||||
testcase_name="x.sum() / 2",
|
||||
expr=lambda x, y: x.sum() / 2,
|
||||
expected_str="0.5 * x[0] + 0.5 * x[1] + 0.5 * x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="(3 * x).sum()",
|
||||
expr=lambda x, y: (3 * x).sum(),
|
||||
expected_repr="3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
|
||||
expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="(x * 3).sum()",
|
||||
expr=lambda x, y: (x * 3).sum(),
|
||||
expected_repr="3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
|
||||
expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="x.sum() * 3",
|
||||
expr=lambda x, y: x.sum() * 3,
|
||||
expected_repr="3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
|
||||
expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="3 * x.sum()",
|
||||
expr=lambda x, y: 3 * x.sum(),
|
||||
expected_repr="3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
|
||||
expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="0 * x.sum() + y.sum()",
|
||||
expr=lambda x, y: 0 * x.sum() + y.sum(),
|
||||
expected_repr="y[0] + y[1] + y[2] + y[3] + y[4]",
|
||||
expected_str="y[0] + y[1] + y[2] + y[3] + y[4]",
|
||||
),
|
||||
# LinearExpression
|
||||
dict(
|
||||
testcase_name="_as_flat_linear_expression(x.sum())",
|
||||
expr=lambda x, y: mb._as_flat_linear_expression(x.sum()),
|
||||
expected_repr="x[0] + x[1] + x[2]",
|
||||
testcase_name="FlatExpression(x.sum())",
|
||||
expr=lambda x, y: mbh.FlatExpression(x.sum()),
|
||||
expected_str="x[0] + x[1] + x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name=(
|
||||
"_as_flat_linear_expression(_as_flat_linear_expression(x.sum()))"
|
||||
),
|
||||
testcase_name="FlatExpression(FlatExpression(x.sum()))",
|
||||
# pylint: disable=g-long-lambda
|
||||
expr=lambda x, y: mb._as_flat_linear_expression(
|
||||
mb._as_flat_linear_expression(x.sum())
|
||||
),
|
||||
expected_repr="x[0] + x[1] + x[2]",
|
||||
expr=lambda x, y: mbh.FlatExpression(mbh.FlatExpression(x.sum())),
|
||||
expected_str="x[0] + x[1] + x[2]",
|
||||
),
|
||||
dict(
|
||||
testcase_name="""_as_flat_linear_expression(sum([
|
||||
_as_flat_linear_expression(x.sum()),
|
||||
_as_flat_linear_expression(x.sum()),
|
||||
testcase_name="""FlatExpression(sum([
|
||||
FlatExpression(x.sum()),
|
||||
FlatExpression(x.sum()),
|
||||
]))""",
|
||||
# pylint: disable=g-long-lambda
|
||||
expr=lambda x, y: mb._as_flat_linear_expression(
|
||||
expr=lambda x, y: mbh.FlatExpression(
|
||||
sum(
|
||||
[
|
||||
mb._as_flat_linear_expression(x.sum()),
|
||||
mb._as_flat_linear_expression(x.sum()),
|
||||
mbh.FlatExpression(x.sum()),
|
||||
mbh.FlatExpression(x.sum()),
|
||||
]
|
||||
)
|
||||
),
|
||||
expected_repr="2.0 * x[0] + 2.0 * x[1] + 2.0 * x[2]",
|
||||
expected_str="2 * x[0] + 2 * x[1] + 2 * x[2]",
|
||||
),
|
||||
)
|
||||
def test_repr(self, expr, expected_repr):
|
||||
def test_str(self, expr, expected_str):
|
||||
x = self.x
|
||||
y = self.y
|
||||
self.assertEqual(repr(expr(x, y)), expected_repr)
|
||||
self.assertEqual(str(mbh.FlatExpression(expr(x, y))), expected_str)
|
||||
|
||||
|
||||
class LinearBaseErrorsTest(absltest.TestCase):
|
||||
|
||||
def test_unknown_linear_type(self):
|
||||
with self.assertRaisesRegex(TypeError, r"Unrecognized linear expression"):
|
||||
with self.assertRaises(TypeError):
|
||||
|
||||
class UnknownLinearType(mb.LinearExpr):
|
||||
pass
|
||||
def __init__(self):
|
||||
mb.LinearExpr.__init__(self)
|
||||
|
||||
mb._as_flat_linear_expression(UnknownLinearType())
|
||||
mbh.FlatExpression(UnknownLinearType())
|
||||
|
||||
def test_division_by_zero(self):
|
||||
with self.assertRaises(ZeroDivisionError):
|
||||
@@ -632,7 +631,7 @@ class LinearBaseErrorsTest(absltest.TestCase):
|
||||
print(x / 0)
|
||||
|
||||
def test_boolean_expression(self):
|
||||
with self.assertRaisesRegex(NotImplementedError, r"Cannot use a LinearExpr"):
|
||||
with self.assertRaisesRegex(NotImplementedError, r"instance as a Boolean"):
|
||||
model = mb.Model()
|
||||
x = model.new_var_series(name="x", index=pd.Index(range(1)))
|
||||
bool(x.sum())
|
||||
@@ -688,28 +687,26 @@ class BoundedLinearBaseTest(parameterized.TestCase):
|
||||
lambda lhs, rhs: lhs >= rhs,
|
||||
),
|
||||
)
|
||||
def test_repr(self, lhs, rhs, op):
|
||||
def test_str(self, lhs, rhs, op):
|
||||
x = self.x
|
||||
y = self.y
|
||||
l: mb.LinearExprT = lhs(x, y)
|
||||
r: mb.LinearExprT = rhs(x, y)
|
||||
result = op(l, r)
|
||||
if isinstance(l, mb.LinearExpr) or isinstance(r, mb.LinearExpr):
|
||||
self.assertIsInstance(result, mb._BoundedLinearExpr)
|
||||
self.assertIn("=", repr(result), msg="is one of ==, <=, or >=")
|
||||
self.assertIsInstance(result, mbh.BoundedLinearExpression)
|
||||
self.assertIn("=", str(result), msg="is one of ==, <=, or >=")
|
||||
else:
|
||||
self.assertIsInstance(result, bool)
|
||||
|
||||
def test_doublesided_bounded_expressions(self):
|
||||
x = self.x
|
||||
self.assertEqual(
|
||||
"0.0 <= x[0] <= 1.0", repr(mb.BoundedLinearExpression(x[0], 0, 1))
|
||||
)
|
||||
self.assertEqual("0 <= x[0] <= 1", str(mb.BoundedLinearExpression(x[0], 0, 1)))
|
||||
|
||||
def test_free_bounded_expressions(self):
|
||||
self.assertEqual(
|
||||
"x[0] free",
|
||||
repr(mb.BoundedLinearExpression(self.x[0], -math.inf, math.inf)),
|
||||
"-inf <= x[0] <= inf",
|
||||
str(mb.BoundedLinearExpression(self.x[0], -math.inf, math.inf)),
|
||||
)
|
||||
|
||||
def test_var_eq_var_as_bool(self):
|
||||
@@ -734,8 +731,16 @@ class BoundedLinearBaseTest(parameterized.TestCase):
|
||||
|
||||
class BoundedLinearBaseErrorsTest(absltest.TestCase):
|
||||
|
||||
def test_single_var_bounded_linear_expression_as_bool(self):
|
||||
with self.assertRaisesRegex(
|
||||
NotImplementedError, "Evaluating a BoundedLinearExpression"
|
||||
):
|
||||
model = mb.Model()
|
||||
x = model.new_bool_var(name="x")
|
||||
bool(mb.BoundedLinearExpression(x, 0, 1))
|
||||
|
||||
def test_bounded_linear_expression_as_bool(self):
|
||||
with self.assertRaisesRegex(NotImplementedError, "Boolean value"):
|
||||
with self.assertRaisesRegex(TypeError, "incompatible constructor arguments"):
|
||||
model = mb.Model()
|
||||
x = model.new_var_series(name="x", index=pd.Index(range(1)))
|
||||
bool(mb.BoundedLinearExpression(x, 0, 1))
|
||||
@@ -896,7 +901,7 @@ class ModelBuilderVariablesTest(parameterized.TestCase):
|
||||
self.assertLen(variables, len(index))
|
||||
self.assertLen(set(variables), len(index))
|
||||
for i in index:
|
||||
self.assertEqual(repr(variables[i]), f"test_variable[{i}]")
|
||||
self.assertEqual(variables[i].name, f"test_variable[{i}]")
|
||||
|
||||
@parameterized.product(
|
||||
index=_variable_indices, bounds=_bounds, is_integer=_is_integer
|
||||
@@ -1440,7 +1445,7 @@ class ModelBuilderLinearConstraintsTest(parameterized.TestCase):
|
||||
for expr, expr_term in zip(linear_constraint_expressions, expr_terms):
|
||||
self.assertDictEqual(build_dict(expr), expr_term)
|
||||
self.assertSequenceAlmostEqual(
|
||||
[expr._offset for expr in linear_constraint_expressions],
|
||||
[expr.offset for expr in linear_constraint_expressions],
|
||||
expression_offsets,
|
||||
)
|
||||
|
||||
@@ -1473,19 +1478,22 @@ class ModelBuilderObjectiveTest(parameterized.TestCase):
|
||||
|
||||
def assertLinearExpressionAlmostEqual(
|
||||
self,
|
||||
expr1: mb._LinearExpression,
|
||||
expr2: mb._LinearExpression,
|
||||
expr1: mbh.LinearExpr,
|
||||
expr2: mbh.LinearExpr,
|
||||
) -> None:
|
||||
"""Test that the two linear expressions are almost equal."""
|
||||
self.assertEqual(len(expr1.variable_indices), len(expr2.variable_indices))
|
||||
if len(expr1.variable_indices) > 0: # pylint: disable=g-explicit-length-test
|
||||
self.assertSequenceEqual(expr1.variable_indices, expr2.variable_indices)
|
||||
flat_expr1 = mbh.FlatExpression(expr1)
|
||||
flat_expr2 = mbh.FlatExpression(expr2)
|
||||
self.assertEqual(len(flat_expr1.vars), len(flat_expr2.vars))
|
||||
if len(flat_expr1.vars) > 0: # pylint: disable=g-explicit-length-test
|
||||
self.assertSequenceEqual(flat_expr1.vars, flat_expr2.vars)
|
||||
self.assertSequenceAlmostEqual(
|
||||
expr1.coefficients, expr2.coefficients, places=5
|
||||
flat_expr1.coeffs, flat_expr2.coeffs, places=5
|
||||
)
|
||||
else:
|
||||
self.assertEmpty(expr2.coefficients)
|
||||
self.assertAlmostEqual(expr1._offset, expr2._offset)
|
||||
self.assertEmpty(flat_expr1.coeffs)
|
||||
self.assertEmpty(flat_expr2.coeffs)
|
||||
self.assertAlmostEqual(flat_expr1.offset, flat_expr2.offset)
|
||||
|
||||
@parameterized.product(
|
||||
expression=_expressions,
|
||||
@@ -1501,7 +1509,8 @@ class ModelBuilderObjectiveTest(parameterized.TestCase):
|
||||
model = mb.Model()
|
||||
x = model.new_var_series(name="x", index=variable_indices)
|
||||
y = model.new_var_series(name="y", index=variable_indices)
|
||||
objective_expression = mb._as_flat_linear_expression(expression(x, y))
|
||||
objective_expression = expression(x, y)
|
||||
print(f"objective_expression: {objective_expression}")
|
||||
if is_maximize:
|
||||
model.maximize(objective_expression)
|
||||
else:
|
||||
@@ -1515,14 +1524,14 @@ class ModelBuilderObjectiveTest(parameterized.TestCase):
|
||||
model = mb.Model()
|
||||
x = model.new_var_series(name="x", index=pd.Index(range(3)))
|
||||
old_objective_expression = 1
|
||||
new_objective_expression = mb._as_flat_linear_expression(x.sum() - 2.3)
|
||||
new_objective_expression = x.sum() - 2.3
|
||||
|
||||
# Set and check for old objective.
|
||||
model.maximize(old_objective_expression)
|
||||
got_objective_expression = model.objective_expression()
|
||||
for var_coeff in got_objective_expression.coefficients:
|
||||
self.assertAlmostEqual(var_coeff, 0)
|
||||
self.assertAlmostEqual(got_objective_expression._offset, 1)
|
||||
flat_got_objective_expression = mbh.FlatExpression(model.objective_expression())
|
||||
self.assertEmpty(flat_got_objective_expression.vars)
|
||||
self.assertEmpty(flat_got_objective_expression.coeffs)
|
||||
self.assertAlmostEqual(flat_got_objective_expression.offset, 1)
|
||||
|
||||
# Set to a new objective and check that it is different.
|
||||
model.minimize(new_objective_expression)
|
||||
@@ -1543,7 +1552,7 @@ class ModelBuilderObjectiveTest(parameterized.TestCase):
|
||||
model = mb.Model()
|
||||
x = model.new_var_series(name="x", index=variable_indices)
|
||||
y = model.new_var_series(name="y", index=variable_indices)
|
||||
objective_expression = mb._as_flat_linear_expression(expression(x, y))
|
||||
objective_expression = mbh.FlatExpression(expression(x, y))
|
||||
model.minimize(objective_expression)
|
||||
got_objective_expression = model.objective_expression()
|
||||
self.assertLinearExpressionAlmostEqual(
|
||||
@@ -1562,7 +1571,7 @@ class ModelBuilderObjectiveTest(parameterized.TestCase):
|
||||
model = mb.Model()
|
||||
x = model.new_var_series(name="x", index=variable_indices)
|
||||
y = model.new_var_series(name="y", index=variable_indices)
|
||||
objective_expression = mb._as_flat_linear_expression(expression(x, y))
|
||||
objective_expression = mbh.FlatExpression(expression(x, y))
|
||||
model.maximize(objective_expression)
|
||||
got_objective_expression = model.objective_expression()
|
||||
self.assertLinearExpressionAlmostEqual(
|
||||
|
||||
Reference in New Issue
Block a user