Files
ortools-clone/ortools/model_builder/python/model_builder.py

991 lines
31 KiB
Python
Raw Normal View History

# Copyright 2010-2021 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Methods for building and solving model_builder models.
The following two sections describe the main
methods for building and solving those models.
* [`ModelBuilder`](#model_builder.ModelBuilder): Methods for creating
models, including variables and constraints.
* [`ModelSolver`](#model_builder.ModelSolver): Methods for solving
a model and evaluating solutions.
Additional methods for solving ModelBuilder models:
* [`Constraint`](#model_builder.Constraint): A few utility methods for modifying
constraints created by `ModelBuilder`.
* [`LinearExpr`](#model_builder.LinearExpr): Methods for creating constraints
and the objective from large arrays of coefficients.
Other methods and functions listed are primarily used for developing OR-Tools,
rather than for solving specific optimization problems.
"""
import math
from ortools.linear_solver import linear_solver_pb2
from ortools.model_builder.python import model_builder_helper as mbh
from ortools.model_builder.python import pywrap_model_builder_helper as pwmb
# Forward solve statuses.
OPTIMAL = linear_solver_pb2.MPSOLVER_OPTIMAL
FEASIBLE = linear_solver_pb2.MPSOLVER_FEASIBLE
INFEASIBLE = linear_solver_pb2.MPSOLVER_INFEASIBLE
UNBOUNDED = linear_solver_pb2.MPSOLVER_UNBOUNDED
ABNORMAL = linear_solver_pb2.MPSOLVER_ABNORMAL
NOT_SOLVED = linear_solver_pb2.MPSOLVER_NOT_SOLVED
MODEL_IS_VALID = linear_solver_pb2.MPSOLVER_MODEL_IS_VALID
CANCELLED_BY_USER = linear_solver_pb2.MPSOLVER_CANCELLED_BY_USER
UNKNOWN_STATUS = linear_solver_pb2.MPSOLVER_UNKNOWN_STATUS
MODEL_INVALID = linear_solver_pb2.MPSOLVER_MODEL_INVALID
class LinearExpr(object):
"""Holds an linear expression.
2022-03-24 16:48:24 +01:00
A linear expression is built from constants and variables.
For example, `x + 2.0 * (y - z + 1.0)`.
Linear expressions are used in ModelBuilder models in constraints and in the
objective:
* You can define linear constraints as in:
```
2022-03-26 17:00:47 +01:00
model.add(x + 2 * y <= 5.0)
model.add(sum(array_of_vars) == 5.0)
```
* In ModelBuilder, the objective is a linear expression:
```
2022-03-26 17:00:47 +01:00
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:
```
2022-03-26 17:00:47 +01:00
model.minimize(model_builder.LinearExpr.sum(expressions))
model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
```
"""
@classmethod
def sum(cls, expressions):
"""Creates the expression sum(expressions)."""
if len(expressions) == 1:
return expressions[0]
return _SumArray(expressions)
@classmethod
def weighted_sum(cls, expressions, coefficients):
"""Creates the expression sum(expressions[i] * coefficients[i])."""
if LinearExpr.is_empty_or_null(coefficients):
return 0
elif len(expressions) == 1:
return expressions[0] * coefficients[0]
else:
return _WeightedSum(expressions, coefficients)
@classmethod
def term(cls, expression, coefficient):
"""Creates `expression * coefficient`."""
if mbh.is_zero(coefficient):
return 0
else:
return expression * coefficient
@classmethod
def is_empty_or_null(cls, coefficients):
for c in coefficients:
if not mbh.is_zero(c):
return False
return True
def get_var_value_map(self):
"""Scans the expression. Returns (var_coef_map, constant)."""
coeffs = {}
constant = 0.0
to_process = [(self, 1.0)]
while to_process: # Flatten to avoid recursion.
expr, coeff = to_process.pop()
if mbh.is_a_number(expr):
constant += coeff * mbh.assert_is_a_number(expr)
elif isinstance(expr, _ProductCst):
2022-03-24 16:48:24 +01:00
to_process.append((expr.expression, coeff * expr.coefficient))
elif isinstance(expr, _Sum):
2022-03-24 16:48:24 +01:00
to_process.append((expr.left, coeff))
to_process.append((expr.right, coeff))
elif isinstance(expr, _SumArray):
2022-03-24 16:48:24 +01:00
for e in expr.expressions:
to_process.append((e, coeff))
2022-03-26 17:00:47 +01:00
constant += expr.constant * coeff
elif isinstance(expr, _WeightedSum):
2022-03-24 16:48:24 +01:00
for e, c in zip(expr.expressions, expr.coefficients):
to_process.append((e, coeff * c))
2022-03-24 16:48:24 +01:00
constant += expr.constant * coeff
elif isinstance(expr, Variable):
if expr in coeffs:
coeffs[expr] += coeff
else:
coeffs[expr] = coeff
else:
raise TypeError('Unrecognized linear expression: ' + str(expr))
return coeffs, constant
def __hash__(self):
return object.__hash__(self)
def __abs__(self):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __add__(self, arg):
if mbh.is_zero(arg):
return self
return _Sum(self, arg)
def __radd__(self, arg):
2022-03-28 16:42:03 +02:00
return self.__add(arg)
def __sub__(self, arg):
if mbh.is_zero(arg):
return self
return _Sum(self, -arg)
def __rsub__(self, arg):
return _Sum(-self, arg)
def __mul__(self, arg):
arg = mbh.assert_is_a_number(arg)
if mbh.is_one(arg):
return self
elif mbh.is_zero(arg):
return 0
return _ProductCst(self, arg)
def __rmul__(self, arg):
2022-03-28 16:42:03 +02:00
return self.__mul__(arg)
def __div__(self, arg):
coeff = mbh.assert_is_a_number(arg)
if coeff == 0.0:
raise ValueError(
'Cannot call the division operator with a zero divisor')
return self.__mul__(1.0 / coeff)
def __truediv__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __mod__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __pow__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __lshift__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __rshift__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __and__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __or__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __xor__(self, _):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __neg__(self):
return _ProductCst(self, -1)
def __bool__(self):
raise NotImplementedError(
2022-03-28 16:42:03 +02:00
f'Cannot use a LinearExpr {self} as an Boolean value')
def __eq__(self, arg):
if arg is None:
return False
2022-03-28 16:42:03 +02:00
if isinstance(self, Variable) and isinstance(arg, Variable):
return VarCompVar(self, arg, True)
if mbh.is_a_number(arg):
arg = mbh.assert_is_a_number(arg)
return BoundedLinearExpression(self, arg, arg)
else:
return BoundedLinearExpression(self - arg, 0, 0)
def __ge__(self, arg):
if mbh.is_a_number(arg):
arg = mbh.assert_is_a_number(arg)
return BoundedLinearExpression(self, arg, math.inf)
else:
return BoundedLinearExpression(self - arg, 0, math.inf)
def __le__(self, arg):
if mbh.is_a_number(arg):
arg = mbh.assert_is_a_number(arg)
return BoundedLinearExpression(self, -math.inf, arg)
else:
return BoundedLinearExpression(self - arg, -math.inf, 0)
def __ne__(self, arg):
2022-03-28 16:42:03 +02:00
if isinstance(self, Variable) and isinstance(arg, Variable):
return VarCompVar(self, arg, False)
return NotImplemented
def __lt__(self, arg):
2022-03-28 16:42:03 +02:00
return NotImplemented
def __gt__(self, arg):
2022-03-28 16:42:03 +02:00
return NotImplemented
class _Sum(LinearExpr):
"""Represents the sum of two LinearExprs."""
def __init__(self, left, right):
for x in [left, right]:
if not mbh.is_a_number(x) and not isinstance(x, LinearExpr):
raise TypeError('Not an linear expression: ' + str(x))
self.__left = left
self.__right = right
2022-03-24 16:48:24 +01:00
@property
def left(self):
return self.__left
2022-03-24 16:48:24 +01:00
@property
def right(self):
return self.__right
def __str__(self):
return f'({self.__left} + {self.__right})'
def __repr__(self):
return f'Sum({repr(self.__left)}, {repr(self.__right)})'
class _ProductCst(LinearExpr):
"""Represents the product of a LinearExpr by a constant."""
def __init__(self, expr, coeff):
coeff = mbh.assert_is_a_number(coeff)
if isinstance(expr, _ProductCst):
2022-03-26 17:00:47 +01:00
self.__expr = expr.expression
self.__coef = expr.coefficient * coeff
else:
self.__expr = expr
self.__coef = coeff
def __str__(self):
if self.__coef == -1:
return '-' + str(self.__expr)
else:
return '(' + str(self.__coef) + ' * ' + str(self.__expr) + ')'
def __repr__(self):
return 'ProductCst(' + repr(self.__expr) + ', ' + repr(
self.__coef) + ')'
2022-03-24 16:48:24 +01:00
@property
def coefficient(self):
return self.__coef
2022-03-24 16:48:24 +01:00
@property
def expression(self):
return self.__expr
class _SumArray(LinearExpr):
"""Represents the sum of a list of LinearExpr and a constant."""
def __init__(self, expressions, constant=0):
self.__expressions = []
self.__constant = constant
for x in expressions:
if mbh.is_a_number(x):
if mbh.is_zero(x):
continue
x = mbh.assert_is_a_number(x)
self.__constant += x
elif isinstance(x, LinearExpr):
self.__expressions.append(x)
else:
raise TypeError('Not an linear expression: ' + str(x))
def __str__(self):
if self.__constant == 0:
return '({})'.format(' + '.join(map(str, self.__expressions)))
else:
return '({} + {})'.format(' + '.join(map(str, self.__expressions)),
self.__constant)
def __repr__(self):
return 'SumArray({}, {})'.format(
', '.join(map(repr, self.__expressions)), self.__constant)
2022-03-24 16:48:24 +01:00
@property
def expressions(self):
return self.__expressions
2022-03-24 16:48:24 +01:00
@property
def constant(self):
return self.__constant
class _WeightedSum(LinearExpr):
"""Represents sum(ai * xi) + b."""
2022-03-24 16:48:24 +01:00
def __init__(self, expressions, coefficients, constant=0.0):
self.__expressions = []
self.__coefficients = []
self.__constant = constant
if len(expressions) != len(coefficients):
raise TypeError(
2022-03-26 17:00:47 +01:00
'In the LinearExpr.weighted_sum method, the expression array and the '
' coefficient array must have the same length.')
for e, c in zip(expressions, coefficients):
c = mbh.assert_is_a_number(c)
if mbh.is_zero(c):
continue
if mbh.is_a_number(e):
e = mbh.assert_is_a_number(e)
self.__constant += e * c
elif isinstance(e, LinearExpr):
self.__expressions.append(e)
self.__coefficients.append(c)
else:
raise TypeError('Not an linear expression: ' + str(e))
def __str__(self):
output = None
for expr, coeff in zip(self.__expressions, self.__coefficients):
if not output and mbh.is_one(coeff):
output = str(expr)
elif not output and mbh.is_minus_one(coeff):
output = '-' + str(expr)
elif not output:
output = '{} * {}'.format(coeff, str(expr))
elif mbh.is_one(coeff):
output += ' + {}'.format(str(expr))
elif mbh.is_minus_one(coeff):
output += ' - {}'.format(str(expr))
elif coeff > 1:
output += ' + {} * {}'.format(coeff, str(expr))
elif coeff < -1:
output += ' - {} * {}'.format(-coeff, str(expr))
if self.__constant > 0:
output += ' + {}'.format(self.__constant)
elif self.__constant < 0:
output += ' - {}'.format(-self.__constant)
if output is None:
output = '0'
return output
def __repr__(self):
return 'WeightedSum([{}], [{}], {})'.format(
', '.join(map(repr, self.__expressions)),
', '.join(map(repr, self.__coefficients)), self.__constant)
2022-03-24 16:48:24 +01:00
@property
def expressions(self):
return self.__expressions
2022-03-24 16:48:24 +01:00
@property
def coefficients(self):
return self.__coefficients
2022-03-24 16:48:24 +01:00
@property
def constant(self):
return self.__constant
class Variable(LinearExpr):
2022-03-24 16:48:24 +01:00
"""An variable (continuous or integral).
An 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, lb, ub, is_integral, name):
2022-03-24 16:48:24 +01:00
"""See ModelBuilder.new_var below."""
self.__helper = helper
# Python do not support multiple __init__ methods.
# This method is only called from the ModelBuilder 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 mbh.is_integral(lb) and ub is None and is_integral is None:
self.__index = int(lb)
self.__helper = helper
else:
2022-03-26 17:00:47 +01:00
index = helper.add_var()
self.__index = index
self.__helper = helper
2022-03-26 17:00:47 +01:00
helper.set_var_lower_bound(index, lb)
helper.set_var_upper_bound(index, ub)
helper.set_var_integrality(index, is_integral)
if name:
2022-03-26 17:00:47 +01:00
helper.set_var_name(index, name)
2022-03-24 16:48:24 +01:00
@property
def index(self):
"""Returns the index of the variable in the helper."""
return self.__index
2022-03-24 16:48:24 +01:00
@property
def helper(self):
"""Returns the underlying ModelBuilderHelper."""
return self.__helper
def is_equal_to(self, other):
"""Returns true if self == other in the python sense."""
if not isinstance(other, Variable):
return False
2022-03-24 16:48:24 +01:00
return self.index == other.index
def __str__(self):
name = self.__helper.var_name(self.__index)
if not name:
if self.__helper.VarIsInteger(self.__index):
return 'unnamed_int_var_%i' % self.__index
else:
return 'unnamed_num_var_%i' % self.__index
return name
def __repr__(self):
index = self.__index
name = self.__helper.VarName(index)
lb = self.__helper.VarLowerBound(index)
ub = self.__helper.VarUpperBound(index)
is_integer = self.__helper.VarIsInteger(index)
if name:
if is_integer:
return f'{name}(index={index}, lb={lb}, ub={ub}, integer)'
else:
return f'{name}(index={index}, lb={lb}, ub={ub})'
else:
if is_integer:
return f'unnamed_var(index={index}, lb={lb}, ub={ub}, integer)'
else:
return f'unnamed_var(index={index}, lb={lb}, ub={ub})'
2022-03-24 16:48:24 +01:00
@property
def name(self):
return self.__helper.var_name(self.__index)
2022-03-24 16:48:24 +01:00
@name.setter
def name(self, name):
"""Sets the name of the variable."""
2022-03-26 17:00:47 +01:00
self.__helper.set_var_name(self.__index, name)
2022-03-24 16:48:24 +01:00
@property
def lower_bound(self):
"""Returns the lower bound of the variable."""
return self.__helper.var_lower_bound(self.__index)
@lower_bound.setter
def lower_bound(self, bound):
"""Sets the lower bound of the variable."""
2022-03-26 17:00:47 +01:00
self.__helper.set_var_lower_bound(self.__index, bound)
2022-03-24 16:48:24 +01:00
@property
def upper_bound(self):
"""Returns the upper bound of the variable."""
return self.__helper.var_upper_bound(self.__index)
@upper_bound.setter
def upper_bound(self, bound):
"""Sets the upper bound of the variable."""
2022-03-26 17:00:47 +01:00
self.__helper.set_var_upper_bound(self.__index, bound)
2022-03-24 16:48:24 +01:00
@property
def is_integral(self):
"""Returns whether the variable is integral."""
return self.__helper.var_is_integral(self.__index)
@is_integral.setter
def integrality(self, is_integral):
"""Sets the integrality of the variable."""
2022-03-26 17:00:47 +01:00
self.__helper.set_var_integrality(self.__index, is_integral)
2022-03-24 16:48:24 +01:00
@property
def objective_coefficient(self):
return self.__helper.var_objective_coefficient(self.__index)
@objective_coefficient.setter
def objective_coefficient(self, coeff):
2022-03-26 17:00:47 +01:00
return self.__helper.set_var_objective_coefficient(self.__index, coeff)
2022-03-24 16:48:24 +01:00
2022-03-28 16:42:03 +02:00
def __hash__(self):
return hash((self.__helper, self.__index))
class VarCompVar(object):
"""Represents var == /!= var."""
def __init__(self, left, right, is_equality):
self.__left = left
self.__right = right
self.__is_equality = is_equality
def __str__(self):
if self.__is_equality:
return f'{self.__left} == {self.__right}'
else:
return f'{self.__left} == {self.__right}'
def __repr__(self):
return f'VarEqVar({self.__left}, {self.__right}, {self.__is_equality})'
@property
def left(self):
return self.__left
@property
def right(self):
return self.__left
@property
def is_equality(self):
return self.__is_equality
def __bool__(self):
return (self.__left == self.__right) == self.__is_equality
class BoundedLinearExpression(object):
"""Represents a linear constraint: `lb <= linear expression <= ub`.
The only use of this class is to be added to the ModelBuilder through
2022-03-26 17:00:47 +01:00
`ModelBuilder.add(bounded expression)`, as in:
model.Add(x + 2 * y -1 >= z)
"""
def __init__(self, expr, lb, ub):
self.__expr = expr
self.__lb = lb
self.__ub = ub
def __str__(self):
if self.__lb > -math.inf and self.__ub < math.inf:
if self.__lb == self.__ub:
return str(self.__expr) + ' == ' + str(self.__lb)
else:
return str(self.__lb) + ' <= ' + str(
self.__expr) + ' <= ' + str(self.__ub)
elif self.__lb > -math.inf:
return str(self.__expr) + ' >= ' + str(self.__lb)
elif self.__ub < math.inf:
return str(self.__expr) + ' <= ' + str(self.__ub)
else:
return 'True (unbounded expr ' + str(self.__expr) + ')'
2022-03-24 16:48:24 +01:00
@property
def expression(self):
return self.__expr
2022-03-24 16:48:24 +01:00
@property
def lower_bound(self):
return self.__lb
2022-03-24 16:48:24 +01:00
@property
def upper_bound(self):
return self.__ub
def __bool__(self):
raise NotImplementedError(
2022-03-28 16:42:03 +02:00
f'Cannot use a BoundedLinearExpression {self} as an Boolean value')
class LinearConstraint(object):
"""Stores a linear equation.
Example:
2022-03-26 17:00:47 +01:00
x = model.new_num_var(0, 10, 'x')
y = model.new_num_var(0, 10, 'y')
2022-03-26 17:00:47 +01:00
model.add(x + 2 * y == 5)
"""
def __init__(self, helper):
2022-03-26 17:00:47 +01:00
self.__index = helper.add_linear_constraint()
self.__helper = helper
2022-03-24 16:48:24 +01:00
@property
def index(self):
"""Returns the index of the constraint in the helper."""
return self.__index
2022-03-24 16:48:24 +01:00
@property
def helper(self):
2022-03-24 16:48:24 +01:00
"""Returns the ModelBuilderHelper instance."""
return self.__helper
2022-03-24 16:48:24 +01:00
@property
def lower_bound(self):
return self.__helper.constraint_lower_bound(self.__index)
@lower_bound.setter
def lower_bound(self, bound):
2022-03-26 17:00:47 +01:00
self.__helper.set_constraint_lower_bound(self.__index, bound)
2022-03-24 16:48:24 +01:00
@property
def upper_bound(self):
return self.__helper.constraint_upper_bound(self.__index)
@upper_bound.setter
def upper_bound(self, bound):
2022-03-26 17:00:47 +01:00
self.__helper.set_constraint_upper_bound(self.__index, bound)
2022-03-24 16:48:24 +01:00
@property
def name(self):
return self.__helper.constraint_name(self.__index)
@name.setter
def name(self, name):
2022-03-26 17:00:47 +01:00
return self.__helper.set_constraint_name(self.__index, name)
2022-03-24 16:48:24 +01:00
def add_term(self, var, coeff):
2022-03-26 17:00:47 +01:00
self.__helper.add_term_to_constraint(self.__index, var.index, coeff)
2022-03-24 16:48:24 +01:00
class ModelBuilder(object):
"""Methods for building a linear model.
Methods beginning with:
2022-03-24 16:48:24 +01:00
* ```new_``` create integer, boolean, or interval variables.
* ```add_``` create new constraints and add them to the model.
"""
def __init__(self):
self.__helper = pwmb.ModelBuilderHelper()
self.__constant_map = {}
# Integer variable.
def new_var(self, lb, ub, is_integer, name):
"""Create an integer variable with domain [lb, ub].
Args:
lb: Lower bound for the variable.
ub: Upper bound for the variable.
is_integer: Indicates if the variable must take integral values.
name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
"""
return Variable(self.__helper, lb, ub, is_integer, name)
def new_int_var(self, lb, ub, name):
"""Create an integer variable with domain [lb, ub].
Args:
lb: Lower bound for the variable.
ub: Upper bound for the variable.
name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
"""
return self.new_var(lb, ub, True, name)
def new_num_var(self, lb, ub, name):
"""Create an integer variable with domain [lb, ub].
Args:
lb: Lower bound for the variable.
ub: Upper bound for the variable.
name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
"""
return self.new_var(lb, ub, False, name)
def new_bool_var(self, name):
"""Creates a 0-1 variable with the given name."""
return self.new_var(0, 1, True, name)
def new_constant(self, value):
"""Declares a constant variable."""
return self.new_var(value, value, False, '')
def var_from_index(self, index):
"""Rebuilds a variable object from the model and its index."""
return Variable(self.__helper, index, None, None, None)
2022-03-26 17:00:47 +01:00
@property
def num_variables(self):
"""Returns the number of variables in the model."""
return self.__helper.num_variables()
# Linear constraints.
def add_linear_constraint(self,
linear_expr,
lb=-math.inf,
ub=math.inf,
name=None):
"""Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
ct = LinearConstraint(self.__helper)
2022-03-24 16:48:24 +01:00
index = ct.index
coeffs_map = {}
constant = 0.0
if isinstance(linear_expr, LinearExpr):
coeffs_map, constant = linear_expr.get_var_value_map()
elif mbh.is_a_number(linear_expr):
constant = mbh.assert_is_a_number(linear_expr)
else:
raise TypeError(
'Not supported: ModelBuilder.add_linear_constraint(' +
f'{lb} <= {linear_expr} <= {ub})')
for t in coeffs_map.items():
if not isinstance(t[0], Variable):
raise TypeError('Wrong argument' + str(t))
c = mbh.assert_is_a_number(t[1])
2022-03-26 17:00:47 +01:00
self.__helper.add_term_to_constraint(index, t[0].index, c)
self.__helper.set_constraint_lower_bound(index, lb - constant)
self.__helper.set_constraint_upper_bound(index, ub - constant)
if name:
2022-03-26 17:00:47 +01:00
self.__helper.set_constraint_name(index, name)
return ct
def add(self, ct, name=None):
"""Adds a `BoundedLinearExpression` to the model.
Args:
ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
name: An optional name.
Returns:
An instance of the `Constraint` class.
"""
if isinstance(ct, BoundedLinearExpression):
2022-03-24 16:48:24 +01:00
return self.add_linear_constraint(ct.expression, ct.lower_bound,
ct.upper_bound, name)
2022-03-28 16:42:03 +02:00
elif isinstance(ct, VarCompVar):
if not ct.is_equality:
raise TypeError('Not supported: ModelBuilder.Add(' + str(ct) +
')')
new_ct = LinearConstraint(self.__helper)
new_ct.lower_bound = 0.0
new_ct.upper_bound = 0.0
new_ct.add_term(ct.left, 1.0)
new_ct.add_term(ct.right, -1.0)
return new_ct
elif ct and isinstance(ct, bool):
return self.add_linear_constraint(
linear_expr=0.0) # Evaluate to True.
elif not ct and isinstance(ct, bool):
return self.add_linear_constraint(1.0, 0.0,
0.0) # Evaluate to False.
else:
raise TypeError('Not supported: ModelBuilder.Add(' + str(ct) + ')')
2022-03-26 17:00:47 +01:00
@property
def num_constraints(self):
return self.__helper.num_constraints()
# Objective.
def minimize(self, linear_expr):
self.__optimize(linear_expr, False)
def maximize(self, linear_expr):
self.__optimize(linear_expr, True)
def __optimize(self, linear_expr, maximize):
"""Defines the objective."""
coeffs_map = {}
# constant = 0.0
if isinstance(linear_expr, LinearExpr):
coeffs_map, constant = linear_expr.get_var_value_map()
elif mbh.is_a_number(linear_expr):
constant = mbh.assert_is_a_number(linear_expr)
else:
raise TypeError(
f'Not supported: ModelBuilder.minimize/maximize({linear_expr})')
for t in coeffs_map.items():
if not isinstance(t[0], Variable):
raise TypeError('Wrong argument' + str(t))
c = mbh.assert_is_a_number(t[1])
2022-03-26 17:00:47 +01:00
self.__helper.set_var_objective_coefficient(t[0].index, c)
self.__helper.set_objective_offset(constant)
self.__helper.set_maximize(maximize)
2022-03-24 16:48:24 +01:00
@property
def objective_offset(self):
return self.__helper.objective_offset()
2022-03-24 16:48:24 +01:00
@objective_offset.setter
def objective_offset(self, value):
2022-03-26 17:00:47 +01:00
self.__helper.set_objective_offset(value)
2022-03-24 16:48:24 +01:00
# Input/Output
def export_to_lp_string(self, obfuscate=False):
options = pwmb.MPModelExportOptions()
options.obfuscate = obfuscate
2022-03-26 17:00:47 +01:00
return self.__helper.export_to_lp_string(options)
2022-03-24 16:48:24 +01:00
def export_to_mps_string(self, obfuscate=False):
options = pwmb.MPModelExportOptions()
options.obfuscate = obfuscate
2022-03-26 17:00:47 +01:00
return self.__helper.export_to_mps_string(options)
2022-03-24 16:48:24 +01:00
def import_from_mps_string(self, mps_string):
2022-03-26 17:00:47 +01:00
return self.__helper.import_from_mps_string(mps_string)
2022-03-24 16:48:24 +01:00
def import_from_mps_file(self, mps_file):
2022-03-26 17:00:47 +01:00
return self.__helper.import_from_mps_file(mps_file)
2022-03-24 16:48:24 +01:00
def import_from_lp_string(self, lp_string):
2022-03-26 17:00:47 +01:00
return self.__helper.import_from_lp_string(lp_string)
2022-03-24 16:48:24 +01:00
def import_from_lp_file(self, lp_file):
2022-03-26 17:00:47 +01:00
return self.__helper.import_from_lp_file(lp_file)
# Utilities
2022-03-24 16:48:24 +01:00
@property
def name(self):
return self.__helper.name()
2022-03-24 16:48:24 +01:00
@name.setter
def name(self, name):
2022-03-26 17:00:47 +01:00
self.__helper.set_name(name)
2022-03-24 16:48:24 +01:00
@property
def helper(self):
"""Returns the model builder helper."""
return self.__helper
class ModelSolver(object):
"""Main solver class.
The purpose of this class is to search for a solution to the model provided
2022-03-26 17:00:47 +01:00
to the solve() method.
2022-03-26 17:00:47 +01:00
Once solve() is called, this class allows inspecting the solution found
with the value() method, as well as general statistics about the solve
procedure.
"""
2022-03-25 15:12:19 +01:00
def __init__(self, solver_name):
self.__solve_helper = pwmb.ModelSolverHelper(solver_name)
self.log_callback = None
2022-03-26 17:00:47 +01:00
def solver_is_supported(self):
"""Checks whether the requested solver backend was found."""
return self.__solve_helper.solver_is_supported()
2022-03-24 16:48:24 +01:00
# Solver backend and parameters.
def set_time_limit_in_seconds(self, limit):
"""Sets a time limit for the solve() call."""
2022-03-26 17:00:47 +01:00
self.__solve_helper.set_time_limit_in_seconds(limit)
2022-03-24 16:48:24 +01:00
def set_solver_specific_parameters(self, parameters):
2022-03-25 15:12:19 +01:00
"""Sets parameters specific to the solver backend."""
2022-03-26 17:00:47 +01:00
self.__solve_helper.set_solver_specific_parameters(parameters)
2022-03-24 16:48:24 +01:00
2022-03-25 15:12:19 +01:00
def enable_output(self, enabled):
"""Controls the solver backend logs."""
self.__solve_helper.EnableOutput(enabled)
def solve(self, model):
"""Solves a problem and passes each solution to the callback if not null."""
if self.log_callback is not None:
2022-03-26 17:00:47 +01:00
self.__solve_helper.set_log_callback(self.log_callback)
self.__solve_helper.solve(model.helper)
return self.__solve_helper.status()
2022-03-28 16:42:03 +02:00
def __check_has_feasible_solution(self):
"""Checks that solve has run and has found a feasible solution."""
if not self.__solve_helper.has_solution():
raise RuntimeError(
'solve() has not be called, or no solution has been found.')
def stop_search(self):
"""Stops the current search asynchronously."""
2022-03-26 17:00:47 +01:00
self.__solve_helper.interrupt_solve()
2022-03-24 16:48:24 +01:00
def value(self, var):
"""Returns the value of a linear expression after solve."""
2022-03-28 16:42:03 +02:00
self.__check_has_feasible_solution()
2022-03-24 16:48:24 +01:00
return self.__solve_helper.var_value(var.index)
2022-03-24 16:48:24 +01:00
def reduced_cost(self, var):
"""Returns the reduced cost of a linear expression after solve."""
2022-03-28 16:42:03 +02:00
self.__check_has_feasible_solution()
2022-03-24 16:48:24 +01:00
return self.__solve_helper.reduced_cost(var.index)
def dual_value(self, ct):
"""Returns the dual value of a linear constraint after solve."""
2022-03-28 16:42:03 +02:00
self.__check_has_feasible_solution()
2022-03-24 16:48:24 +01:00
return self.__solve_helper.dual_value(ct.index)
2022-03-26 17:00:47 +01:00
@property
def objective_value(self):
"""Returns the value of the objective after solve."""
2022-03-28 16:42:03 +02:00
self.__check_has_feasible_solution()
return self.__solve_helper.objective_value()
2022-03-26 17:00:47 +01:00
@property
def best_objective_bound(self):
"""Returns the best lower (upper) bound found when min(max)imizing."""
2022-03-28 16:42:03 +02:00
self.__check_has_feasible_solution()
return self.__solve_helper.best_objective_bound()
def status_name(self, status=None):
2022-03-28 16:42:03 +02:00
"""Returns the name of the status returned by solve()."""
if status is None:
status = self.__solve_helper.status()
return linear_solver_pb2.MPSolverResponseStatus.Name(status)
2022-03-26 17:00:47 +01:00
@property
def status_string(self):
"""Returns additional information of the last solve.
It can describe why the model is invalid.
"""
return self.__solve_helper.status_string()
2022-03-25 15:12:19 +01:00
@property
def wall_time(self):
return self.__solve_helper.wall_time()
@property
def user_time(self):
return self.__solve_helper.user_time()