revisit model_builder python API; append ::mb to the c++ part of model_builder

This commit is contained in:
Laurent Perron
2025-01-08 22:53:25 +01:00
parent 9982c872c2
commit 352c75041e
8 changed files with 1719 additions and 1104 deletions

View File

@@ -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):

View File

@@ -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)

View File

@@ -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])

View File

@@ -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(