14"""Methods for building and solving model_builder models.
16The following two sections describe the main
17methods for building and solving those models.
19* [`Model`](#model_builder.Model): Methods for creating
20models, including variables and constraints.
21* [`Solver`](#model_builder.Solver): Methods for solving
22a model and evaluating solutions.
24Additional methods for solving Model models:
26* [`Constraint`](#model_builder.Constraint): A few utility methods for modifying
27 constraints created by `Model`.
28* [`LinearExpr`](#model_builder.LinearExpr): Methods for creating constraints
29 and the objective from large arrays of coefficients.
31Other methods and functions listed are primarily used for developing OR-Tools,
32rather than for solving specific optimization problems.
40from typing
import Callable, List, Optional, Sequence, Tuple, Union, cast
43from numpy
import typing
as npt
52NumberT = Union[int, float, numbers.Real, np.number]
53IntegerT = Union[int, numbers.Integral, np.integer]
54LinearExprT = Union[
"LinearExpr", NumberT]
55ConstraintT = Union[
"_BoundedLinearExpr", bool]
56_IndexOrSeries = Union[pd.Index, pd.Series]
57_VariableOrConstraint = Union[
"LinearConstraint",
"Variable"]
60SolveStatus = mbh.SolveStatus
66 """Holds an linear expression.
68 A linear expression is built from constants and variables.
69 For example, `x + 2.0 * (y - z + 1.0)`.
71 Linear expressions are used in Model models in constraints and in the
74 * You can define linear constraints as in:
77 model.add(x + 2 * y <= 5.0)
78 model.add(sum(array_of_vars) == 5.0)
81 * In Model, the objective is a linear expression:
84 model.minimize(x + 2.0 * y + z)
87 * For large arrays, using the LinearExpr class is faster that using the python
88 `sum()` function. You can create constraints and the objective from lists of
89 linear expressions or coefficients as follows:
92 model.minimize(model_builder.LinearExpr.sum(expressions))
93 model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
99 cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0
101 """Creates `sum(expressions) + constant`.
103 It can perform simple simplifications and returns different objects,
107 expressions: a sequence of linear expressions or constants.
108 constant: a numerical constant.
111 a LinearExpr instance or a numerical constant.
113 checked_constant: np.double = mbn.assert_is_a_number(constant)
115 return checked_constant
116 if len(expressions) == 1
and mbn.is_zero(checked_constant):
117 return expressions[0]
119 return LinearExpr.weighted_sum(
120 expressions, np.ones(len(expressions)), constant=checked_constant
126 expressions: Sequence[LinearExprT],
127 coefficients: Sequence[NumberT],
129 constant: NumberT = 0.0,
130 ) -> Union[NumberT,
"_LinearExpression"]:
131 """Creates `sum(expressions[i] * coefficients[i]) + constant`.
133 It can perform simple simplifications and returns different object,
137 expressions: a sequence of linear expressions or constants.
138 coefficients: a sequence of numerical constants.
139 constant: a numerical constant.
142 a _LinearExpression instance or a numerical constant.
144 if len(expressions) != len(coefficients):
146 "LinearExpr.weighted_sum: expressions and coefficients have"
149 checked_constant: np.double = mbn.assert_is_a_number(constant)
151 return checked_constant
153 to_process=list(zip(expressions, coefficients)), offset=checked_constant
159 expression: LinearExprT,
160 coefficient: NumberT,
162 constant: NumberT = 0.0,
164 """Creates `expression * coefficient + constant`.
166 It can perform simple simplifications and returns different object,
169 expression: a linear expression or a constant.
170 coefficient: a numerical constant.
171 constant: a numerical constant.
174 a LinearExpr instance or a numerical constant.
176 checked_coefficient: np.double = mbn.assert_is_a_number(coefficient)
177 checked_constant: np.double = mbn.assert_is_a_number(constant)
179 if mbn.is_zero(checked_coefficient):
180 return checked_constant
181 if mbn.is_one(checked_coefficient)
and mbn.is_zero(checked_constant):
183 if mbn.is_a_number(expression):
184 return np.double(expression) * checked_coefficient + checked_constant
185 if isinstance(expression, LinearExpr):
187 expression * checked_coefficient + checked_constant
189 raise TypeError(f
"Unknown expression {expression!r} of type {type(expression)}")
192 return object.__hash__(self)
194 def __add__(self, arg: LinearExprT) ->
"_Sum":
195 return _Sum(self, arg)
200 def __sub__(self, arg: LinearExprT) ->
"_Sum":
201 return _Sum(self, -arg)
204 return _Sum(-self, arg)
206 def __mul__(self, arg: NumberT) ->
"_Product":
213 return self.
__mul__(1.0 / coeff)
219 raise NotImplementedError(f
"Cannot use a LinearExpr {self} as a Boolean value")
221 def __eq__(self, arg: LinearExprT) ->
"BoundedLinearExpression":
224 def __ge__(self, arg: LinearExprT) ->
"BoundedLinearExpression":
226 self - arg, 0, math.inf
229 def __le__(self, arg: LinearExprT) ->
"BoundedLinearExpression":
231 self - arg, -math.inf, 0
236 """A variable (continuous or integral).
238 A Variable is an object that can take on any integer value within defined
239 ranges. Variables appear in constraint like:
243 Solving a model is equivalent to finding, for each variable, a single value
244 from the set of initial values (called the initial domain), such that the
245 model is feasible, or optimal if you provided an objective function.
250 helper: mbh.ModelBuilderHelper,
252 ub: Optional[NumberT],
253 is_integral: Optional[bool],
256 """See Model.new_var below."""
257 LinearExpr.__init__(self)
258 self.
__helper: mbh.ModelBuilderHelper = helper
268 if mbn.is_integral(lb)
and ub
is None and is_integral
is None:
269 self.
__index: np.int32 = np.int32(lb)
270 self.
__helper: mbh.ModelBuilderHelper = helper
272 index: np.int32 = helper.add_var()
273 self.
__index: np.int32 = np.int32(index)
274 self.
__helper: mbh.ModelBuilderHelper = helper
275 helper.set_var_lower_bound(index, lb)
276 helper.set_var_upper_bound(index, ub)
277 helper.set_var_integrality(index, is_integral)
279 helper.set_var_name(index, name)
283 """Returns the index of the variable in the helper."""
287 def helper(self) -> mbh.ModelBuilderHelper:
288 """Returns the underlying ModelBuilderHelper."""
292 """Returns true if self == other in the python sense."""
293 if not isinstance(other, Variable):
305 """Returns the name of the variable."""
309 return f
"variable#{self.index}"
312 def name(self, name: str) ->
None:
313 """Sets the name of the variable."""
318 """Returns the lower bound of the variable."""
323 """Sets the lower bound of the variable."""
328 """Returns the upper bound of the variable."""
333 """Sets the upper bound of the variable."""
338 """Returns whether the variable is integral."""
343 """Sets the integrality of the variable."""
350 @objective_coefficient.setter
354 def __eq__(self, arg: Optional[LinearExprT]) -> ConstraintT:
357 if isinstance(arg, Variable):
368 """Interface for types that can build bounded linear (boolean) expressions.
370 Classes derived from _BoundedLinearExpr are used to build linear constraints
373 * BoundedLinearExpression: a linear expression with upper and lower bounds.
374 * VarEqVar: an equality comparison between two variables.
379 self, helper: mbh.ModelBuilderHelper, name: str
380 ) ->
"LinearConstraint":
381 """Creates a new linear constraint in the helper.
384 helper (mbh.ModelBuilderHelper): The helper to create the constraint.
385 name (str): The name of the linear constraint.
388 LinearConstraint: A reference to the linear constraint in the helper.
394 helper: mbh.ModelBuilderHelper,
398 ) ->
"EnforcedLinearConstraint":
399 """Creates a new enforced linear constraint in the helper.
402 helper (mbh.ModelBuilderHelper): The helper to create the constraint.
403 var (Variable): The indicator variable of the constraint.
404 value (bool): The indicator value of the constraint.
405 name (str): The name of the linear constraint.
408 Enforced LinearConstraint: A reference to the linear constraint in the
414 bounded_expr: Union[bool, _BoundedLinearExpr],
415 helper: mbh.ModelBuilderHelper,
418 """Creates a new linear constraint in the helper.
420 It handles boolean values (which might arise in the construction of
421 BoundedLinearExpressions).
423 If bounded_expr is a Boolean value, the created constraint is different.
424 In that case, the constraint will be immutable and marked as under-specified.
425 It will be always feasible or infeasible whether the value is True or False.
428 bounded_expr: The bounded expression used to create the constraint.
429 helper: The helper to create the constraint.
430 name: The name of the constraint to be created.
433 LinearConstraint: a constraint in the helper corresponding to the input.
436 TypeError: If constraint is an invalid type.
438 if isinstance(bounded_expr, bool):
441 helper.set_constraint_name(c.index, name)
444 helper.set_constraint_lower_bound(c.index, 0.0)
445 helper.set_constraint_upper_bound(c.index, 0.0)
448 helper.set_constraint_lower_bound(c.index, 1)
449 helper.set_constraint_upper_bound(c.index, -1)
451 if isinstance(bounded_expr, _BoundedLinearExpr):
453 return bounded_expr._add_linear_constraint(helper, name)
454 raise TypeError(
"invalid type={}".format(type(bounded_expr)))
458 bounded_expr: Union[bool, _BoundedLinearExpr],
459 helper: mbh.ModelBuilderHelper,
464 """Creates a new enforced linear constraint in the helper.
466 It handles boolean values (which might arise in the construction of
467 BoundedLinearExpressions).
469 If bounded_expr is a Boolean value, the linear part of the constraint is
471 In that case, the constraint will be immutable and marked as under-specified.
472 Its linear part will be always feasible or infeasible whether the value is
476 bounded_expr: The bounded expression used to create the constraint.
477 helper: The helper to create the constraint.
478 var: the variable used in the indicator
479 value: the value used in the indicator
480 name: The name of the constraint to be created.
483 EnforcedLinearConstraint: a constraint in the helper corresponding to the
487 TypeError: If constraint is an invalid type.
489 if isinstance(bounded_expr, bool):
492 c.indicator_variable = var
493 c.indicator_value = value
495 helper.set_enforced_constraint_name(c.index, name)
498 helper.set_enforced_constraint_lower_bound(c.index, 0.0)
499 helper.set_enforced_constraint_upper_bound(c.index, 0.0)
502 helper.set_enforced_constraint_lower_bound(c.index, 1)
503 helper.set_enforced_constraint_upper_bound(c.index, -1)
505 if isinstance(bounded_expr, _BoundedLinearExpr):
507 return bounded_expr._add_enforced_linear_constraint(helper, var, value, name)
508 raise TypeError(
"invalid type={}".format(type(bounded_expr)))
511@dataclasses.dataclass(repr=False, eq=False, frozen=True)
513 """Represents var == var."""
515 __slots__ = (
"left",
"right")
521 return f
"{self.left} == {self.right}"
527 return hash(self.
left) == hash(self.right)
530 self, helper: mbh.ModelBuilderHelper, name: str
531 ) ->
"LinearConstraint":
533 helper.set_constraint_lower_bound(c.index, 0.0)
534 helper.set_constraint_upper_bound(c.index, 0.0)
536 helper.add_term_to_constraint(c.index, self.
left.index, 1.0)
537 helper.add_term_to_constraint(c.index, self.right.index, -1.0)
539 helper.set_constraint_name(c.index, name)
544 helper: mbh.ModelBuilderHelper,
548 ) ->
"EnforcedLinearConstraint":
549 """Adds an enforced linear constraint to the model."""
551 c.indicator_variable = var
552 c.indicator_value = value
553 helper.set_enforced_constraint_lower_bound(c.index, 0.0)
554 helper.set_enforced_constraint_upper_bound(c.index, 0.0)
556 helper.add_term_to_enforced_constraint(c.index, self.
left.index, 1.0)
557 helper.add_term_to_enforced_constraint(c.index, self.right.index, -1.0)
559 helper.set_enforced_constraint_name(c.index, name)
564 """Represents a linear constraint: `lb <= linear expression <= ub`.
566 The only use of this class is to be added to the Model through
567 `Model.add(bounded expression)`, as in:
569 model.Add(x + 2 * y -1 >= z)
572 def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT) ->
None:
573 self.
__expr: LinearExprT = expr
574 self.
__lb: np.double = mbn.assert_is_a_number(lb)
575 self.
__ub: np.double = mbn.assert_is_a_number(ub)
578 if self.
__lb > -math.inf
and self.
__ub < math.inf:
580 return f
"{self.__expr} == {self.__lb}"
582 return f
"{self.__lb} <= {self.__expr} <= {self.__ub}"
583 elif self.
__lb > -math.inf:
584 return f
"{self.__expr} >= {self.__lb}"
585 elif self.
__ub < math.inf:
586 return f
"{self.__expr} <= {self.__ub}"
588 return f
"{self.__expr} free"
606 raise NotImplementedError(
607 f
"Cannot use a BoundedLinearExpression {self} as a Boolean value"
611 self, helper: mbh.ModelBuilderHelper, name: Optional[str]
612 ) ->
"LinearConstraint":
616 helper.add_terms_to_constraint(
617 c.index, flat_expr._variable_indices, flat_expr._coefficients
619 helper.set_constraint_lower_bound(c.index, self.
__lb - flat_expr._offset)
620 helper.set_constraint_upper_bound(c.index, self.
__ub - flat_expr._offset)
623 helper.set_constraint_name(c.index, name)
628 helper: mbh.ModelBuilderHelper,
632 ) ->
"EnforcedLinearConstraint":
633 """Adds an enforced linear constraint to the model."""
635 c.indicator_variable = var
636 c.indicator_value = value
639 helper.add_terms_to_enforced_constraint(
640 c.index, flat_expr._variable_indices, flat_expr._coefficients
642 helper.set_enforced_constraint_lower_bound(
643 c.index, self.
__lb - flat_expr._offset
645 helper.set_enforced_constraint_upper_bound(
646 c.index, self.
__ub - flat_expr._offset
650 helper.set_enforced_constraint_name(c.index, name)
655 """Stores a linear equation.
658 x = model.new_num_var(0, 10, 'x')
659 y = model.new_num_var(0, 10, 'y')
661 linear_constraint = model.add(x + 2 * y == 5)
666 helper: mbh.ModelBuilderHelper,
668 index: Optional[IntegerT] =
None,
669 is_under_specified: bool =
False,
671 """LinearConstraint constructor.
674 helper: The pybind11 ModelBuilderHelper.
675 index: If specified, recreates a wrapper to an existing linear constraint.
676 is_under_specified: indicates if the constraint was created by
680 self.
__index = helper.add_linear_constraint()
683 self.
__helper: mbh.ModelBuilderHelper = helper
688 """Returns the index of the constraint in the helper."""
692 def helper(self) -> mbh.ModelBuilderHelper:
693 """Returns the ModelBuilderHelper instance."""
718 return constraint_name
719 return f
"linear_constraint#{self.__index}"
722 def name(self, name: str) ->
None:
727 """Returns True if the constraint is under specified.
729 Usually, it means that it was created by model.add(False) or model.add(True)
730 The effect is that modifying the constraint will raise an exception.
735 """Raises an exception if the constraint is under specified."""
738 f
"Constraint {self.index} is under specified and cannot be modified"
746 f
"LinearConstraint({self.name}, lb={self.lower_bound},"
747 f
" ub={self.upper_bound},"
748 f
" var_indices={self.helper.constraint_var_indices(self.index)},"
749 f
" coefficients={self.helper.constraint_coefficients(self.index)})"
753 """Sets the coefficient of the variable in the constraint."""
755 self.
__helper.set_constraint_coefficient(self.
__index, var.index, coeff)
757 def add_term(self, var: Variable, coeff: NumberT) ->
None:
758 """Adds var * coeff to the constraint."""
760 self.
__helper.safe_add_term_to_constraint(self.
__index, var.index, coeff)
763 """Clear all terms of the constraint."""
769 """Stores an enforced linear equation, also name indicator constraint.
772 x = model.new_num_var(0, 10, 'x')
773 y = model.new_num_var(0, 10, 'y')
774 z = model.new_bool_var('z')
776 enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
781 helper: mbh.ModelBuilderHelper,
783 index: Optional[IntegerT] =
None,
784 is_under_specified: bool =
False,
786 """EnforcedLinearConstraint constructor.
789 helper: The pybind11 ModelBuilderHelper.
790 index: If specified, recreates a wrapper to an existing linear constraint.
791 is_under_specified: indicates if the constraint was created by
795 self.
__index = helper.add_enforced_linear_constraint()
797 if not helper.is_enforced_linear_constraint(index):
799 f
"the given index {index} does not refer to an enforced linear"
804 self.
__helper: mbh.ModelBuilderHelper = helper
809 """Returns the index of the constraint in the helper."""
813 def helper(self) -> mbh.ModelBuilderHelper:
814 """Returns the ModelBuilderHelper instance."""
837 enforcement_var_index = (
838 self.
__helper.enforced_constraint_indicator_variable_index(self.
__index)
842 @indicator_variable.setter
844 self.
__helper.set_enforced_constraint_indicator_variable_index(
852 @indicator_value.setter
854 self.
__helper.set_enforced_constraint_indicator_value(self.
__index, value)
860 return constraint_name
861 return f
"enforced_linear_constraint#{self.__index}"
864 def name(self, name: str) ->
None:
869 """Returns True if the constraint is under specified.
871 Usually, it means that it was created by model.add(False) or model.add(True)
872 The effect is that modifying the constraint will raise an exception.
877 """Raises an exception if the constraint is under specified."""
880 f
"Constraint {self.index} is under specified and cannot be modified"
888 f
"EnforcedLinearConstraint({self.name}, lb={self.lower_bound},"
889 f
" ub={self.upper_bound},"
890 f
" var_indices={self.helper.enforced_constraint_var_indices(self.index)},"
891 f
" coefficients={self.helper.enforced_constraint_coefficients(self.index)},"
892 f
" indicator_variable={self.indicator_variable}"
893 f
" indicator_value={self.indicator_value})"
897 """Sets the coefficient of the variable in the constraint."""
899 self.
__helper.set_enforced_constraint_coefficient(
903 def add_term(self, var: Variable, coeff: NumberT) ->
None:
904 """Adds var * coeff to the constraint."""
906 self.
__helper.safe_add_term_to_enforced_constraint(
911 """Clear all terms of the constraint."""
917 """Methods for building a linear model.
919 Methods beginning with:
921 * ```new_``` create integer, boolean, or interval variables.
922 * ```add_``` create new constraints and add them to the model.
926 self.
__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper()
929 """Returns a clone of the current model."""
941 self, constraints: Optional[_IndexOrSeries] =
None
943 if constraints
is None:
954 self, variables: Optional[_IndexOrSeries] =
None
956 if variables
is None:
961 """Gets all linear constraints in the model."""
964 name=
"linear_constraint",
968 self, constraints: Optional[_IndexOrSeries] =
None
970 """Gets the expressions of all linear constraints in the set.
972 If `constraints` is a `pd.Index`, then the output will be indexed by the
973 constraints. If `constraints` is a `pd.Series` indexed by the underlying
974 dimensions, then the output will be indexed by the same underlying
978 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
979 constraints from which to get the expressions. If unspecified, all
980 linear constraints will be in scope.
983 pd.Series: The expressions of all linear constraints in the set.
991 for var_id, coeff
in zip(
992 c.helper.constraint_var_indices(c.index),
993 c.helper.constraint_coefficients(c.index),
1001 self, constraints: Optional[_IndexOrSeries] =
None
1003 """Gets the lower bounds of all linear constraints in the set.
1005 If `constraints` is a `pd.Index`, then the output will be indexed by the
1006 constraints. If `constraints` is a `pd.Series` indexed by the underlying
1007 dimensions, then the output will be indexed by the same underlying
1011 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1012 constraints from which to get the lower bounds. If unspecified, all
1013 linear constraints will be in scope.
1016 pd.Series: The lower bounds of all linear constraints in the set.
1019 func=
lambda c: c.lower_bound,
1024 self, constraints: Optional[_IndexOrSeries] =
None
1026 """Gets the upper bounds of all linear constraints in the set.
1028 If `constraints` is a `pd.Index`, then the output will be indexed by the
1029 constraints. If `constraints` is a `pd.Series` indexed by the underlying
1030 dimensions, then the output will be indexed by the same underlying
1034 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1035 constraints. If unspecified, all linear constraints will be in scope.
1038 pd.Series: The upper bounds of all linear constraints in the set.
1041 func=
lambda c: c.upper_bound,
1046 """Gets all variables in the model."""
1053 self, variables: Optional[_IndexOrSeries] =
None
1055 """Gets the lower bounds of all variables in the set.
1057 If `variables` is a `pd.Index`, then the output will be indexed by the
1058 variables. If `variables` is a `pd.Series` indexed by the underlying
1059 dimensions, then the output will be indexed by the same underlying
1063 variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1064 from which to get the lower bounds. If unspecified, all variables will
1068 pd.Series: The lower bounds of all variables in the set.
1071 func=
lambda v: v.lower_bound,
1076 self, variables: Optional[_IndexOrSeries] =
None
1078 """Gets the upper bounds of all variables in the set.
1081 variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1082 from which to get the upper bounds. If unspecified, all variables will
1086 pd.Series: The upper bounds of all variables in the set.
1089 func=
lambda v: v.upper_bound,
1096 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
1098 """Create an integer variable with domain [lb, ub].
1101 lb: Lower bound of the variable.
1102 ub: Upper bound of the variable.
1103 is_integer: Indicates if the variable must take integral values.
1104 name: The name of the variable.
1107 a variable whose domain is [lb, ub].
1113 self, lb: NumberT, ub: NumberT, name: Optional[str] =
None
1115 """Create an integer variable with domain [lb, ub].
1118 lb: Lower bound of the variable.
1119 ub: Upper bound of the variable.
1120 name: The name of the variable.
1123 a variable whose domain is [lb, ub].
1126 return self.
new_var(lb, ub,
True, name)
1129 self, lb: NumberT, ub: NumberT, name: Optional[str] =
None
1131 """Create an integer variable with domain [lb, ub].
1134 lb: Lower bound of the variable.
1135 ub: Upper bound of the variable.
1136 name: The name of the variable.
1139 a variable whose domain is [lb, ub].
1142 return self.
new_var(lb, ub,
False, name)
1145 """Creates a 0-1 variable with the given name."""
1151 """Declares a constant variable."""
1152 return self.
new_var(value, value,
False,
None)
1158 lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1159 upper_bounds: Union[NumberT, pd.Series] = math.inf,
1160 is_integral: Union[bool, pd.Series] =
False,
1162 """Creates a series of (scalar-valued) variables with the given name.
1165 name (str): Required. The name of the variable set.
1166 index (pd.Index): Required. The index to use for the variable set.
1167 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1168 variables in the set. If a `pd.Series` is passed in, it will be based on
1169 the corresponding values of the pd.Series. Defaults to -inf.
1170 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1171 variables in the set. If a `pd.Series` is passed in, it will be based on
1172 the corresponding values of the pd.Series. Defaults to +inf.
1173 is_integral (bool, pd.Series): Optional. Indicates if the variable can
1174 only take integer values. If a `pd.Series` is passed in, it will be
1175 based on the corresponding values of the pd.Series. Defaults to False.
1178 pd.Series: The variable set indexed by its corresponding dimensions.
1181 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1182 ValueError: if the `name` is not a valid identifier or already exists.
1183 ValueError: if the `lowerbound` is greater than the `upperbound`.
1184 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1185 does not match the input index.
1187 if not isinstance(index, pd.Index):
1188 raise TypeError(
"Non-index object is used as index")
1189 if not name.isidentifier():
1190 raise ValueError(
"name={} is not a valid identifier".format(name))
1192 mbn.is_a_number(lower_bounds)
1193 and mbn.is_a_number(upper_bounds)
1194 and lower_bounds > upper_bounds
1197 "lower_bound={} is greater than upper_bound={} for variable set={}".format(
1198 lower_bounds, upper_bounds, name
1202 isinstance(is_integral, bool)
1204 and mbn.is_a_number(lower_bounds)
1205 and mbn.is_a_number(upper_bounds)
1206 and math.isfinite(lower_bounds)
1207 and math.isfinite(upper_bounds)
1208 and math.ceil(lower_bounds) > math.floor(upper_bounds)
1211 "ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds))
1212 +
" is greater than floor("
1213 +
"upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds))
1214 +
" for variable set={}".format(name)
1225 name=f
"{name}[{i}]",
1228 is_integral=is_integrals[i],
1238 lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1239 upper_bounds: Union[NumberT, pd.Series] = math.inf,
1241 """Creates a series of continuous variables with the given name.
1244 name (str): Required. The name of the variable set.
1245 index (pd.Index): Required. The index to use for the variable set.
1246 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1247 variables in the set. If a `pd.Series` is passed in, it will be based on
1248 the corresponding values of the pd.Series. Defaults to -inf.
1249 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1250 variables in the set. If a `pd.Series` is passed in, it will be based on
1251 the corresponding values of the pd.Series. Defaults to +inf.
1254 pd.Series: The variable set indexed by its corresponding dimensions.
1257 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1258 ValueError: if the `name` is not a valid identifier or already exists.
1259 ValueError: if the `lowerbound` is greater than the `upperbound`.
1260 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1261 does not match the input index.
1263 return self.
new_var_series(name, index, lower_bounds, upper_bounds,
False)
1269 lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1270 upper_bounds: Union[NumberT, pd.Series] = math.inf,
1272 """Creates a series of integer variables with the given name.
1275 name (str): Required. The name of the variable set.
1276 index (pd.Index): Required. The index to use for the variable set.
1277 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1278 variables in the set. If a `pd.Series` is passed in, it will be based on
1279 the corresponding values of the pd.Series. Defaults to -inf.
1280 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1281 variables in the set. If a `pd.Series` is passed in, it will be based on
1282 the corresponding values of the pd.Series. Defaults to +inf.
1285 pd.Series: The variable set indexed by its corresponding dimensions.
1288 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1289 ValueError: if the `name` is not a valid identifier or already exists.
1290 ValueError: if the `lowerbound` is greater than the `upperbound`.
1291 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1292 does not match the input index.
1294 return self.
new_var_series(name, index, lower_bounds, upper_bounds,
True)
1301 """Creates a series of Boolean variables with the given name.
1304 name (str): Required. The name of the variable set.
1305 index (pd.Index): Required. The index to use for the variable set.
1308 pd.Series: The variable set indexed by its corresponding dimensions.
1311 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1312 ValueError: if the `name` is not a valid identifier or already exists.
1313 ValueError: if the `lowerbound` is greater than the `upperbound`.
1314 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1315 does not match the input index.
1320 """Rebuilds a variable object from the model and its index."""
1327 linear_expr: LinearExprT,
1328 lb: NumberT = -math.inf,
1329 ub: NumberT = math.inf,
1330 name: Optional[str] =
None,
1331 ) -> LinearConstraint:
1332 """Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
1335 self.
__helper.set_constraint_name(ct.index, name)
1336 if mbn.is_a_number(linear_expr):
1337 self.
__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
1338 self.
__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
1339 elif isinstance(linear_expr, Variable):
1340 self.
__helper.set_constraint_lower_bound(ct.index, lb)
1341 self.
__helper.set_constraint_upper_bound(ct.index, ub)
1342 self.
__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
1343 elif isinstance(linear_expr, LinearExpr):
1346 self.
__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
1347 self.
__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
1348 self.
__helper.add_terms_to_constraint(
1349 ct.index, flat_expr._variable_indices, flat_expr._coefficients
1353 f
"Not supported: Model.add_linear_constraint({linear_expr})"
1354 f
" with type {type(linear_expr)}"
1359 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] =
None
1360 ) -> Union[LinearConstraint, pd.Series]:
1361 """Adds a `BoundedLinearExpression` to the model.
1364 ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1365 name: An optional name.
1368 An instance of the `Constraint` class.
1370 Note that a special treatment is done when the argument does not contain any
1371 variable, and thus evaluates to True or False.
1373 `model.add(True)` will create a constraint 0 <= empty sum <= 0.
1374 The constraint will be marked as under specified, and cannot be modified
1377 `model.add(False)` will create a constraint inf <= empty sum <= -inf. The
1378 constraint will be marked as under specified, and cannot be modified
1381 you can check the if a constraint is under specified by reading the
1382 `LinearConstraint.is_under_specified` property.
1384 if isinstance(ct, _BoundedLinearExpr):
1385 return ct._add_linear_constraint(self.
__helper, name)
1386 elif isinstance(ct, bool):
1388 elif isinstance(ct, pd.Series):
1393 expr, self.
__helper, f
"{name}[{i}]"
1395 for (i, expr)
in zip(ct.index, ct)
1399 raise TypeError(
"Not supported: Model.add(" + str(ct) +
")")
1402 """Rebuilds a linear constraint object from the model and its index."""
1409 linear_expr: LinearExprT,
1412 lb: NumberT = -math.inf,
1413 ub: NumberT = math.inf,
1414 name: Optional[str] =
None,
1415 ) -> EnforcedLinearConstraint:
1416 """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name."""
1418 ct.indicator_variable = ivar
1419 ct.indicator_value = ivalue
1421 self.
__helper.set_constraint_name(ct.index, name)
1422 if mbn.is_a_number(linear_expr):
1423 self.
__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
1424 self.
__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
1425 elif isinstance(linear_expr, Variable):
1426 self.
__helper.set_constraint_lower_bound(ct.index, lb)
1427 self.
__helper.set_constraint_upper_bound(ct.index, ub)
1428 self.
__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
1429 elif isinstance(linear_expr, LinearExpr):
1432 self.
__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
1433 self.
__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
1434 self.
__helper.add_terms_to_constraint(
1435 ct.index, flat_expr._variable_indices, flat_expr._coefficients
1440 f
" Model.add_enforced_linear_constraint({linear_expr}) with"
1441 f
" type {type(linear_expr)}"
1447 ct: Union[ConstraintT, pd.Series],
1448 var: Union[Variable, pd.Series],
1449 value: Union[bool, pd.Series],
1450 name: Optional[str] =
None,
1451 ) -> Union[EnforcedLinearConstraint, pd.Series]:
1452 """Adds a `ivar == ivalue => BoundedLinearExpression` to the model.
1455 ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1456 var: The indicator variable
1457 value: the indicator value
1458 name: An optional name.
1461 An instance of the `Constraint` class.
1463 Note that a special treatment is done when the argument does not contain any
1464 variable, and thus evaluates to True or False.
1466 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
1469 model.add_enforced(False, var, value) will create a constraint inf <=
1472 you can check the if a constraint is always false (lb=inf, ub=-inf) by
1473 calling EnforcedLinearConstraint.is_always_false()
1475 if isinstance(ct, _BoundedLinearExpr):
1476 return ct._add_enforced_linear_constraint(self.
__helper, var, value, name)
1478 isinstance(ct, bool)
1479 and isinstance(var, Variable)
1480 and isinstance(value, bool)
1483 ct, self.
__helper, var, value, name
1485 elif isinstance(ct, pd.Series):
1498 for (i, expr)
in zip(ct.index, ct)
1502 raise TypeError(
"Not supported: Model.add_enforced(" + str(ct) +
")")
1505 self, index: IntegerT
1506 ) -> EnforcedLinearConstraint:
1507 """Rebuilds an enforced linear constraint object from the model and its index."""
1512 """Minimizes the given objective."""
1516 """Maximizes the given objective."""
1519 def __optimize(self, linear_expr: LinearExprT, maximize: bool) ->
None:
1520 """Defines the objective."""
1522 self.
__helper.set_maximize(maximize)
1523 if mbn.is_a_number(linear_expr):
1525 elif isinstance(linear_expr, Variable):
1526 self.
helperhelper.set_var_objective_coefficient(linear_expr.index, 1.0)
1527 elif isinstance(linear_expr, LinearExpr):
1530 self.
helperhelper.set_objective_offset(flat_expr._offset)
1532 flat_expr._variable_indices, flat_expr._coefficients
1535 raise TypeError(f
"Not supported: Model.minimize/maximize({linear_expr})")
1539 """Returns the fixed offset of the objective."""
1542 @objective_offset.setter
1544 self.
__helper.set_objective_offset(value)
1547 """Returns the expression to optimize."""
1550 variable * self.
__helper.var_objective_coefficient(variable.index)
1552 if self.
__helper.var_objective_coefficient(variable.index) != 0.0
1559 """Clears all solution hints."""
1562 def add_hint(self, var: Variable, value: NumberT) ->
None:
1563 """Adds var == value as a hint to the model.
1566 var: The variable of the hint
1567 value: The value of the hint
1569 Note that variables must not appear more than once in the list of hints.
1575 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1576 options.obfuscate = obfuscate
1580 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1581 options.obfuscate = obfuscate
1585 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1586 options.obfuscate = obfuscate
1590 """Exports the optimization model to a ProtoBuf format."""
1591 return mbh.to_mpmodel_proto(self.
__helper)
1594 """Reads a model from a MPS string."""
1598 """Reads a model from a .mps file."""
1602 """Reads a model from a LP string."""
1606 """Reads a model from a .lp file."""
1610 """Reads a model from a proto file."""
1611 return self.
__helper.read_model_from_proto_file(proto_file)
1614 """Writes a model to a proto file."""
1615 return self.
__helper.write_model_to_proto_file(proto_file)
1621 """Returns the number of variables in the model."""
1626 """The number of constraints in the model."""
1631 """The name of the model."""
1640 """Returns the model builder helper."""
1645 """Main solver class.
1647 The purpose of this class is to search for a solution to the model provided
1648 to the solve() method.
1650 Once solve() is called, this class allows inspecting the solution found
1651 with the value() method, as well as general statistics about the solve
1656 self.
__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name)
1660 """Checks whether the requested solver backend was found."""
1665 """Sets a time limit for the solve() call."""
1669 """Sets parameters specific to the solver backend."""
1673 """Controls the solver backend logs."""
1676 def solve(self, model: Model) -> SolveStatus:
1677 """Solves a problem and passes each solution to the callback if not null."""
1686 """Stops the current search asynchronously."""
1689 def value(self, expr: LinearExprT) -> np.double:
1690 """Returns the value of a linear expression after solve."""
1693 if mbn.is_a_number(expr):
1695 elif isinstance(expr, Variable):
1697 elif isinstance(expr, LinearExpr):
1700 flat_expr._variable_indices,
1701 flat_expr._coefficients,
1705 raise TypeError(f
"Unknown expression {expr!r} of type {type(expr)}")
1707 def values(self, variables: _IndexOrSeries) -> pd.Series:
1708 """Returns the values of the input variables.
1710 If `variables` is a `pd.Index`, then the output will be indexed by the
1711 variables. If `variables` is a `pd.Series` indexed by the underlying
1712 dimensions, then the output will be indexed by the same underlying
1716 variables (Union[pd.Index, pd.Series]): The set of variables from which to
1720 pd.Series: The values of all variables in the set.
1730 """Returns the reduced cost of the input variables.
1732 If `variables` is a `pd.Index`, then the output will be indexed by the
1733 variables. If `variables` is a `pd.Series` indexed by the underlying
1734 dimensions, then the output will be indexed by the same underlying
1738 variables (Union[pd.Index, pd.Series]): The set of variables from which to
1742 pd.Series: The reduced cost of all variables in the set.
1752 """Returns the reduced cost of a linear expression after solve."""
1758 """Returns the dual values of the input constraints.
1760 If `constraints` is a `pd.Index`, then the output will be indexed by the
1761 constraints. If `constraints` is a `pd.Series` indexed by the underlying
1762 dimensions, then the output will be indexed by the same underlying
1766 constraints (Union[pd.Index, pd.Series]): The set of constraints from
1767 which to get the dual values.
1770 pd.Series: The dual_values of all constraints in the set.
1780 """Returns the dual value of a linear constraint after solve."""
1786 """Returns the activity of a linear constraint after solve."""
1793 """Returns the value of the objective after solve."""
1800 """Returns the best lower (upper) bound found when min(max)imizing."""
1807 """Returns additional information of the last solve.
1809 It can describe why the model is invalid.
1823_MAX_LINEAR_EXPRESSION_REPR_TERMS = 5
1826@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1828 """For variables x, an expression: offset + sum_{i in I} coeff_i * x_i."""
1830 __slots__ = (
"_variable_indices",
"_coefficients",
"_offset",
"_helper")
1832 _variable_indices: npt.NDArray[np.int32]
1833 _coefficients: npt.NDArray[np.double]
1835 _helper: Optional[mbh.ModelBuilderHelper]
1850 def helper(self) -> Optional[mbh.ModelBuilderHelper]:
1862 if len(result) >= _MAX_LINEAR_EXPRESSION_REPR_TERMS:
1863 result.append(
" + ...")
1866 if not result
and mbn.is_one(coeff):
1867 result.append(var_name)
1868 elif not result
and mbn.is_minus_one(coeff):
1869 result.append(f
"-{var_name}")
1871 result.append(f
"{coeff} * {var_name}")
1872 elif mbn.is_one(coeff):
1873 result.append(f
" + {var_name}")
1874 elif mbn.is_minus_one(coeff):
1875 result.append(f
" - {var_name}")
1877 result.append(f
" + {coeff} * {var_name}")
1879 result.append(f
" - {-coeff} * {var_name}")
1882 return f
"{self.constant}"
1884 result.append(f
" + {self.constant}")
1886 result.append(f
" - {-self.constant}")
1887 return "".join(result)
1891 to_process: List[Tuple[LinearExprT, float]], offset: float = 0.0
1892) -> _LinearExpression:
1893 """Creates a _LinearExpression as the sum of terms."""
1898 expr, coeff = to_process.pop()
1899 if isinstance(expr, _Sum):
1900 to_process.append((expr._left, coeff))
1901 to_process.append((expr._right, coeff))
1902 elif isinstance(expr, Variable):
1903 indices.append([expr.index])
1904 coeffs.append([coeff])
1906 helper = expr.helper
1907 elif mbn.is_a_number(expr):
1908 offset += coeff * cast(NumberT, expr)
1909 elif isinstance(expr, _Product):
1910 to_process.append((expr._expression, coeff * expr._coefficient))
1911 elif isinstance(expr, _LinearExpression):
1912 offset += coeff * expr._offset
1913 if expr._helper
is not None:
1914 indices.append(expr.variable_indices)
1915 coeffs.append(np.multiply(expr.coefficients, coeff))
1917 helper = expr._helper
1920 "Unrecognized linear expression: " + str(expr) + f
" {type(expr)}"
1923 if helper
is not None:
1924 all_indices: npt.NDArray[np.int32] = np.concatenate(indices, axis=0)
1925 all_coeffs: npt.NDArray[np.double] = np.concatenate(coeffs, axis=0)
1926 sorted_indices, sorted_coefficients = helper.sort_and_regroup_terms(
1927 all_indices, all_coeffs
1934 _variable_indices=np.zeros(dtype=np.int32, shape=[0]),
1935 _coefficients=np.zeros(dtype=np.double, shape=[0]),
1942 """Converts floats, ints and Linear objects to a LinearExpression."""
1943 if isinstance(base_expr, _LinearExpression):
1948@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1950 """Represents the (deferred) sum of two expressions."""
1952 __slots__ = (
"_left",
"_right")
1964@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1966 """Represents the (deferred) product of an expression by a constant."""
1968 __slots__ = (
"_expression",
"_coefficient")
1970 _expression: LinearExpr
1971 _coefficient: NumberT
1981 """Returns the indices of `obj` as a `pd.Index`."""
1982 if isinstance(obj, pd.Series):
1989 func: Callable[[_VariableOrConstraint], NumberT],
1990 values: _IndexOrSeries,
1992 """Returns the attributes of `values`.
1995 func: The function to call for getting the attribute data.
1996 values: The values that the function will be applied (element-wise) to.
1999 pd.Series: The attribute values.
2002 data=[func(v)
for v
in values],
2008 value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index
2010 """Returns a pd.Series of the given index with the corresponding values.
2013 value_or_series: the values to be converted (if applicable).
2014 index: the index of the resulting pd.Series.
2017 pd.Series: The set of values with the given index.
2020 TypeError: If the type of `value_or_series` is not recognized.
2021 ValueError: If the index does not match.
2023 if mbn.is_a_number(value_or_series)
or isinstance(value_or_series, bool):
2024 result = pd.Series(data=value_or_series, index=index)
2025 elif isinstance(value_or_series, pd.Series):
2026 if value_or_series.index.equals(index):
2027 result = value_or_series
2029 raise ValueError(
"index does not match")
2031 raise TypeError(
"invalid type={}".format(type(value_or_series)))
2036 var_or_series: Union[
"Variable", pd.Series], index: pd.Index
2038 """Returns a pd.Series of the given index with the corresponding values.
2041 var_or_series: the variables to be converted (if applicable).
2042 index: the index of the resulting pd.Series.
2045 pd.Series: The set of values with the given index.
2048 TypeError: If the type of `value_or_series` is not recognized.
2049 ValueError: If the index does not match.
2051 if isinstance(var_or_series, Variable):
2052 result = pd.Series(data=var_or_series, index=index)
2053 elif isinstance(var_or_series, pd.Series):
2054 if var_or_series.index.equals(index):
2055 result = var_or_series
2057 raise ValueError(
"index does not match")
2059 raise TypeError(
"invalid type={}".format(type(var_or_series)))