14"""A solver independent library for modeling optimization problems.
16Example use to model the optimization problem:
22 model = mathopt.Model(name='my_model')
23 x = model.add_binary_variable(name='x')
24 y = model.add_variable(lb=0.0, ub=2.5, name='y')
25 # We can directly use linear combinations of variables ...
26 model.add_linear_constraint(x + y <= 1.5, name='c')
27 # ... or build them incrementally.
28 objective_expression = 0
29 objective_expression += 2 * x
30 objective_expression += y
31 model.maximize(objective_expression)
33 # May raise a RuntimeError on invalid input or internal solver errors.
34 result = mathopt.solve(model, mathopt.SolverType.GSCIP)
36 if result.termination.reason not in (mathopt.TerminationReason.OPTIMAL,
37 mathopt.TerminationReason.FEASIBLE):
38 raise RuntimeError(f'model failed to solve: {result.termination}')
40 print(f'Objective value: {result.objective_value()}')
41 print(f'Value for variable x: {result.variable_values()[x]}')
76StorageClass = model_storage.ModelStorageImplClass
78LinearTypes = Union[int, float,
"LinearBase"]
79QuadraticTypes = Union[int, float,
"LinearBase",
"QuadraticBase"]
80LinearTypesExceptVariable = Union[
81 float, int,
"LinearTerm",
"LinearExpression",
"LinearSum",
"LinearProduct"
84_CHAINED_COMPARISON_MESSAGE = (
85 "If you were trying to create a two-sided or "
86 "ranged linear inequality of the form `lb <= "
87 "expr <= ub`, try `(lb <= expr) <= ub` instead"
89_EXPRESSION_COMP_EXPRESSION_MESSAGE = (
90 "This error can occur when adding "
91 "inequalities of the form `(a <= b) <= "
92 "c` where (a, b, c) includes two or more"
93 " non-constant linear expressions"
101 extra_message: Optional[str] =
None,
103 """Raises TypeError on unsupported operators."""
105 f
"unsupported operand type(s) for {operator}: {lhs.__name__!r} and"
108 if extra_message
is not None:
109 message +=
"\n" + extra_message
110 raise TypeError(message)
114 raise TypeError(
"!= constraints are not supported")
118 """An inequality of the form expression <= upper_bound.
121 * expression is a linear expression, and
122 * upper_bound is a float
125 __slots__ =
"_expression",
"_upper_bound"
127 def __init__(self, expression:
"LinearBase", upper_bound: float) ->
None:
128 """Operator overloading can be used instead: e.g. `x + y <= 2.0`."""
140 def __ge__(self, lhs: float) ->
"BoundedLinearExpression":
141 if isinstance(lhs, (int, float)):
147 "__bool__ is unsupported for UpperBoundedLinearExpression"
149 + _CHAINED_COMPARISON_MESSAGE
153 return f
"{self._expression!s} <= {self._upper_bound}"
156 return f
"{self._expression!r} <= {self._upper_bound}"
160 """An inequality of the form expression >= lower_bound.
163 * expression is a linear expression, and
164 * lower_bound is a float
167 __slots__ =
"_expression",
"_lower_bound"
169 def __init__(self, expression:
"LinearBase", lower_bound: float) ->
None:
170 """Operator overloading can be used instead: e.g. `x + y >= 2.0`."""
182 def __le__(self, rhs: float) ->
"BoundedLinearExpression":
183 if isinstance(rhs, (int, float)):
189 "__bool__ is unsupported for LowerBoundedLinearExpression"
191 + _CHAINED_COMPARISON_MESSAGE
195 return f
"{self._expression!s} >= {self._lower_bound}"
198 return f
"{self._expression!r} >= {self._lower_bound}"
202 """An inequality of the form lower_bound <= expression <= upper_bound.
205 * expression is a linear expression
206 * lower_bound is a float, and
207 * upper_bound is a float
209 Note: Because of limitations related to Python's handling of chained
210 comparisons, bounded expressions cannot be directly created usign
211 overloaded comparisons as in `lower_bound <= expression <= upper_bound`.
212 One solution is to wrap one of the inequalities in parenthesis as in
213 `(lower_bound <= expression) <= upper_bound`.
216 __slots__ =
"_expression",
"_lower_bound",
"_upper_bound"
219 self, lower_bound: float, expression:
"LinearBase", upper_bound: float
239 "__bool__ is unsupported for BoundedLinearExpression"
241 + _CHAINED_COMPARISON_MESSAGE
245 return f
"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}"
248 return f
"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}"
252 """The result of the equality comparison between two Variable.
254 We use an object here to delay the evaluation of equality so that we can use
255 the operator== in two use-cases:
257 1. when the user want to test that two Variable values references the same
258 variable. This is supported by having this object support implicit
261 2. when the user want to use the equality to create a constraint of equality
262 between two variables.
265 __slots__ =
"_first_variable",
"_second_variable"
269 first_variable:
"Variable",
270 second_variable:
"Variable",
290 return f
"{self.first_variable!s} == {self._second_variable!s}"
293 return f
"{self.first_variable!r} == {self._second_variable!r}"
296BoundedLinearTypesList = (
297 LowerBoundedLinearExpression,
298 UpperBoundedLinearExpression,
299 BoundedLinearExpression,
302BoundedLinearTypes = Union[BoundedLinearTypesList]
307 """An id-ordered pair of variables used as a key for quadratic terms."""
309 __slots__ =
"_first_var",
"_second_var"
312 """Variables a and b will be ordered internally by their ids."""
326 def __eq__(self, other:
"QuadraticTermKey") -> bool:
336 return f
"{self._first_var!s} * {self._second_var!s}"
339 return f
"QuadraticTermKey({self._first_var!r}, {self._second_var!r})"
342@dataclasses.dataclass
344 """Auxiliary data class for LinearBase._flatten_once_and_add_to()."""
346 terms: DefaultDict[
"Variable", float] = dataclasses.field(
347 default_factory=
lambda: collections.defaultdict(float)
352@dataclasses.dataclass
354 """Auxiliary data class for QuadraticBase._quadratic_flatten_once_and_add_to()."""
356 quadratic_terms: DefaultDict[
"QuadraticTermKey", float] = dataclasses.field(
357 default_factory=
lambda: collections.defaultdict(float)
362 """Auxiliary to-process stack interface for LinearBase._flatten_once_and_add_to() and QuadraticBase._quadratic_flatten_once_and_add_to()."""
366 def append(self, term:
"LinearBase", scale: float) ->
None:
367 """Add a linear object and scale to the to-process stack."""
370_T = TypeVar(
"_T",
"LinearBase", Union[
"LinearBase",
"QuadraticBase"])
374 """Auxiliary data class for LinearBase._flatten_once_and_add_to()."""
376 __slots__ = (
"_queue",)
378 def __init__(self, term: _T, scale: float) ->
None:
379 self.
_queue: Deque[Tuple[_T, float]] = collections.deque([(term, scale)])
381 def append(self, term: _T, scale: float) ->
None:
384 def pop(self) -> Tuple[_T, float]:
385 return self.
_queue.popleft()
391_LinearToProcessElements = _ToProcessElementsImplementation[
"LinearBase"]
392_QuadraticToProcessElements = _ToProcessElementsImplementation[
393 Union[
"LinearBase",
"QuadraticBase"]
398 """Interface for types that can build linear expressions with +, -, * and /.
400 Classes derived from LinearBase (plus float and int scalars) are used to
401 build expression trees describing a linear expression. Operations nodes of the
402 expression tree include:
404 * LinearSum: describes a deferred sum of LinearTypes objects.
405 * LinearProduct: describes a deferred product of a scalar and a
408 Leaf nodes of the expression tree include:
410 * float and int scalars.
411 * Variable: a single variable.
412 * LinearTerm: the product of a scalar and a Variable object.
413 * LinearExpression: the sum of a scalar and LinearTerm objects.
415 LinearBase objects/expression-trees can be used directly to create
416 constraints or objective functions. However, to facilitate their inspection,
417 any LinearTypes object can be flattened to a LinearExpression
420 as_flat_linear_expression(value: LinearTypes) -> LinearExpression:
422 In addition, all LinearBase objects are immutable.
426 Using an expression tree representation instead of an eager construction of
427 LinearExpression objects reduces known inefficiencies associated with the
428 use of operator overloading to construct linear expressions. In particular, we
429 expect the runtime of as_flat_linear_expression() to be linear in the size of
430 the expression tree. Additional performance can gained by using LinearSum(c)
431 instead of sum(c) for a container c, as the latter creates len(c) LinearSum
446 processed_elements: _ProcessedElements,
447 target_stack: _ToProcessElements,
449 """Flatten one level of tree if needed and add to targets.
451 Classes derived from LinearBase only need to implement this function
452 to enable transformation to LinearExpression through
453 as_flat_linear_expression().
456 scale: multiply elements by this number when processing or adding to
458 processed_elements: where to add LinearTerms and scalars that can be
459 processed immediately.
460 target_stack: where to add LinearBase elements that are not scalars or
461 LinearTerms (i.e. elements that need further flattening).
462 Implementations should append() to this stack to avoid being recursive.
466 self, rhs: LinearTypes
468 "BoundedLinearExpression"
470 if isinstance(rhs, (int, float)):
472 if not isinstance(rhs, LinearBase):
477 self, rhs: LinearTypes
484 def __le__(self, rhs: float) ->
"UpperBoundedLinearExpression": ...
487 def __le__(self, rhs:
"LinearBase") ->
"BoundedLinearExpression": ...
490 def __le__(self, rhs:
"BoundedLinearExpression") -> NoReturn: ...
493 if isinstance(rhs, (int, float)):
495 if isinstance(rhs, LinearBase):
497 if isinstance(rhs, BoundedLinearExpression):
499 "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE
504 def __ge__(self, lhs: float) ->
"LowerBoundedLinearExpression": ...
507 def __ge__(self, lhs:
"LinearBase") ->
"BoundedLinearExpression": ...
510 def __ge__(self, lhs:
"BoundedLinearExpression") -> NoReturn: ...
513 if isinstance(lhs, (int, float)):
515 if isinstance(lhs, LinearBase):
517 if isinstance(lhs, BoundedLinearExpression):
519 ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE
523 def __add__(self, expr: LinearTypes) ->
"LinearSum":
524 if not isinstance(expr, (int, float, LinearBase)):
525 return NotImplemented
528 def __radd__(self, expr: LinearTypes) ->
"LinearSum":
529 if not isinstance(expr, (int, float, LinearBase)):
530 return NotImplemented
533 def __sub__(self, expr: LinearTypes) ->
"LinearSum":
534 if not isinstance(expr, (int, float, LinearBase)):
535 return NotImplemented
538 def __rsub__(self, expr: LinearTypes) ->
"LinearSum":
539 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
540 return NotImplemented
544 def __mul__(self, other: float) ->
"LinearProduct": ...
547 def __mul__(self, other:
"LinearBase") ->
"LinearLinearProduct": ...
550 if not isinstance(other, (int, float, LinearBase)):
551 return NotImplemented
552 if isinstance(other, LinearBase):
556 def __rmul__(self, constant: float) ->
"LinearProduct":
557 if not isinstance(constant, (int, float)):
558 return NotImplemented
563 if not isinstance(constant, (int, float)):
564 return NotImplemented
572 """Interface for types that can build quadratic expressions with +, -, * and /.
574 Classes derived from QuadraticBase and LinearBase (plus float and int scalars)
575 are used to build expression trees describing a quadratic expression.
576 Operations nodes of the expression tree include:
578 * QuadraticSum: describes a deferred sum of QuadraticTypes objects.
579 * QuadraticProduct: describes a deferred product of a scalar and a
580 QuadraticTypes object.
581 * LinearLinearProduct: describes a deferred product of two LinearTypes
584 Leaf nodes of the expression tree include:
586 * float and int scalars.
587 * Variable: a single variable.
588 * LinearTerm: the product of a scalar and a Variable object.
589 * LinearExpression: the sum of a scalar and LinearTerm objects.
590 * QuadraticTerm: the product of a scalar and two Variable objects.
591 * QuadraticExpression: the sum of a scalar, LinearTerm objects and
592 QuadraticTerm objects.
594 QuadraticBase objects/expression-trees can be used directly to create
595 objective functions. However, to facilitate their inspection, any
596 QuadraticTypes object can be flattened to a QuadraticExpression
599 as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression:
601 In addition, all QuadraticBase objects are immutable.
605 Using an expression tree representation instead of an eager construction of
606 QuadraticExpression objects reduces known inefficiencies associated with the
607 use of operator overloading to construct quadratic expressions. In particular,
608 we expect the runtime of as_flat_quadratic_expression() to be linear in the
609 size of the expression tree. Additional performance can gained by using
610 QuadraticSum(c) instead of sum(c) for a container c, as the latter creates
611 len(c) QuadraticSum objects.
625 processed_elements: _QuadraticProcessedElements,
626 target_stack: _QuadraticToProcessElements,
628 """Flatten one level of tree if needed and add to targets.
630 Classes derived from QuadraticBase only need to implement this function
631 to enable transformation to QuadraticExpression through
632 as_flat_quadratic_expression().
635 scale: multiply elements by this number when processing or adding to
637 processed_elements: where to add linear terms, quadratic terms and scalars
638 that can be processed immediately.
639 target_stack: where to add LinearBase and QuadraticBase elements that are
640 not scalars or linear terms or quadratic terms (i.e. elements that need
641 further flattening). Implementations should append() to this stack to
642 avoid being recursive.
645 def __add__(self, expr: QuadraticTypes) ->
"QuadraticSum":
646 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
647 return NotImplemented
650 def __radd__(self, expr: QuadraticTypes) ->
"QuadraticSum":
651 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
652 return NotImplemented
655 def __sub__(self, expr: QuadraticTypes) ->
"QuadraticSum":
656 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
657 return NotImplemented
660 def __rsub__(self, expr: QuadraticTypes) ->
"QuadraticSum":
661 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
662 return NotImplemented
665 def __mul__(self, other: float) ->
"QuadraticProduct":
666 if not isinstance(other, (int, float)):
667 return NotImplemented
670 def __rmul__(self, other: float) ->
"QuadraticProduct":
671 if not isinstance(other, (int, float)):
672 return NotImplemented
677 if not isinstance(constant, (int, float)):
678 return NotImplemented
686 """A decision variable for an optimization model.
688 A decision variable takes a value from a domain, either the real numbers or
689 the integers, and restricted to be in some interval [lb, ub] (where lb and ub
690 can be infinite). The case of lb == ub is allowed, this means the variable
691 must take a single value. The case of lb > ub is also allowed, this implies
692 that the problem is infeasible.
694 A Variable is configured as follows:
695 * lower_bound: a float property, lb above. Should not be NaN nor +inf.
696 * upper_bound: a float property, ub above. Should not be NaN nor -inf.
697 * integer: a bool property, if the domain is integer or continuous.
699 The name is optional, read only, and used only for debugging. Non-empty names
702 Every Variable is associated with a Model (defined below). Note that data
703 describing the variable (e.g. lower_bound) is owned by Model.storage, this
704 class is simply a reference to that data. Do not create a Variable directly,
705 use Model.add_variable() instead.
708 __slots__ =
"__weakref__",
"_model",
"_id"
710 def __init__(self, model:
"Model", vid: int) ->
None:
711 """Do not invoke directly, use Model.add_variable()."""
733 return self.
modelmodel.storage.get_variable_is_integer(self.
_id)
737 self.
modelmodel.storage.set_variable_is_integer(self.
_id, value)
752 """Returns the name, or a string containing the id if the name is empty."""
753 return self.
name if self.
name else f
"variable_{self.id}"
756 return f
"<Variable id: {self.id}, name: {self.name!r}>"
759 def __eq__(self, rhs:
"Variable") ->
"VarEqVar": ...
762 def __eq__(self, rhs: LinearTypesExceptVariable) ->
"BoundedLinearExpression": ...
765 if isinstance(rhs, Variable):
767 return super().
__eq__(rhs)
770 def __ne__(self, rhs:
"Variable") -> bool: ...
773 def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: ...
776 if isinstance(rhs, Variable):
777 return not self == rhs
784 def __mul__(self, other: float) ->
"LinearTerm": ...
787 def __mul__(self, other: Union[
"Variable",
"LinearTerm"]) ->
"QuadraticTerm": ...
790 def __mul__(self, other:
"LinearBase") ->
"LinearLinearProduct": ...
793 if not isinstance(other, (int, float, LinearBase)):
794 return NotImplemented
795 if isinstance(other, Variable):
797 if isinstance(other, LinearTerm):
801 if isinstance(other, LinearBase):
805 def __rmul__(self, constant: float) ->
"LinearTerm":
806 if not isinstance(constant, (int, float)):
807 return NotImplemented
812 if not isinstance(constant, (int, float)):
813 return NotImplemented
822 processed_elements: _ProcessedElements,
823 target_stack: _ToProcessElements,
825 processed_elements.terms[self] += scale
829 """The product of a scalar and a variable.
831 This class is immutable.
834 __slots__ =
"_variable",
"_coefficient"
836 def __init__(self, variable: Variable, coefficient: float) ->
None:
851 processed_elements: _ProcessedElements,
852 target_stack: _ToProcessElements,
857 def __mul__(self, other: float) ->
"LinearTerm": ...
860 def __mul__(self, other: Union[
"Variable",
"LinearTerm"]) ->
"QuadraticTerm": ...
863 def __mul__(self, other:
"LinearBase") ->
"LinearLinearProduct": ...
866 if not isinstance(other, (int, float, LinearBase)):
867 return NotImplemented
868 if isinstance(other, Variable):
872 if isinstance(other, LinearTerm):
877 if isinstance(other, LinearBase):
881 def __rmul__(self, constant: float) ->
"LinearTerm":
882 if not isinstance(constant, (int, float)):
883 return NotImplemented
887 if not isinstance(constant, (int, float)):
888 return NotImplemented
895 return f
"{self._coefficient} * {self._variable}"
898 return f
"LinearTerm({self._variable!r}, {self._coefficient!r})"
902 """The product of a scalar and two variables.
904 This class is immutable.
907 __slots__ =
"_key",
"_coefficient"
909 def __init__(self, key: QuadraticTermKey, coefficient: float) ->
None:
910 self.
_key: QuadraticTermKey = key
914 def key(self) -> QuadraticTermKey:
924 processed_elements: _QuadraticProcessedElements,
925 target_stack: _ToProcessElements,
929 def __mul__(self, constant: float) ->
"QuadraticTerm":
930 if not isinstance(constant, (int, float)):
931 return NotImplemented
934 def __rmul__(self, constant: float) ->
"QuadraticTerm":
935 if not isinstance(constant, (int, float)):
936 return NotImplemented
940 if not isinstance(constant, (int, float)):
941 return NotImplemented
948 return f
"{self._coefficient} * {self._key!s}"
951 return f
"QuadraticTerm({self._key!r}, {self._coefficient})"
955 """For variables x, an expression: b + sum_{i in I} a_i * x_i.
957 This class is immutable.
960 __slots__ =
"__weakref__",
"_terms",
"_offset"
963 def __init__(self, /, other: LinearTypes = 0) ->
None:
965 if isinstance(other, (int, float)):
967 self.
_terms: Mapping[Variable, float] = immutabledict.immutabledict()
973 linear, coef = to_process.pop()
974 linear._flatten_once_and_add_to(coef, processed_elements, to_process)
976 self.
_terms: Mapping[Variable, float] = immutabledict.immutabledict(
977 processed_elements.terms
979 self.
_offset = processed_elements.offset
982 def terms(self) -> Mapping[Variable, float]:
989 def evaluate(self, variable_values: Mapping[Variable, float]) -> float:
990 """Returns the value of this expression for given variable values.
992 E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then
993 evaluate(variable_values) equals 10.0.
995 See also mathopt.evaluate_expression(), which works on any type in
999 variable_values: Must contain a value for every variable in expression.
1002 The value of this expression when replacing variables by their value.
1005 for var, coef
in sorted(
1006 self.
_terms.items(), key=
lambda var_coef_pair: var_coef_pair[0].id
1008 result += coef * variable_values[var]
1014 processed_elements: _ProcessedElements,
1015 target_stack: _ToProcessElements,
1017 for var, val
in self.
_terms.items():
1018 processed_elements.terms[var] += val * scale
1019 processed_elements.offset += scale * self.
offset
1024 """Returns the name, or a string containing the id if the name is empty."""
1025 result = str(self.
offset)
1026 sorted_keys = sorted(self.
_terms.keys(), key=str)
1027 for variable
in sorted_keys:
1031 coefficient = self.
_terms[variable]
1032 if coefficient == 0.0:
1038 result += str(abs(coefficient)) +
" * " + str(variable)
1042 result = f
"LinearExpression({self.offset}, " +
"{"
1043 result +=
", ".join(
1044 f
"{variable!r}: {coefficient}"
1045 for variable, coefficient
in self.
_terms.items()
1052 """For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j.
1054 This class is immutable.
1057 __slots__ =
"__weakref__",
"_linear_terms",
"_quadratic_terms",
"_offset"
1062 if isinstance(other, (int, float)):
1066 immutabledict.immutabledict()
1075 linear_or_quadratic, coef = to_process.pop()
1076 if isinstance(linear_or_quadratic, LinearBase):
1077 linear_or_quadratic._flatten_once_and_add_to(
1078 coef, processed_elements, to_process
1081 linear_or_quadratic._quadratic_flatten_once_and_add_to(
1082 coef, processed_elements, to_process
1085 self.
_linear_terms: Mapping[Variable, float] = immutabledict.immutabledict(
1086 processed_elements.terms
1089 immutabledict.immutabledict(processed_elements.quadratic_terms)
1091 self.
_offset = processed_elements.offset
1105 def evaluate(self, variable_values: Mapping[Variable, float]) -> float:
1106 """Returns the value of this expression for given variable values.
1108 E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then
1109 evaluate(variable_values) equals 16.0.
1111 See also mathopt.evaluate_expression(), which works on any type in
1115 variable_values: Must contain a value for every variable in expression.
1118 The value of this expression when replacing variables by their value.
1121 for var, coef
in sorted(
1123 key=
lambda var_coef_pair: var_coef_pair[0].id,
1125 result += coef * variable_values[var]
1126 for key, coef
in sorted(
1128 key=
lambda quad_coef_pair: (
1129 quad_coef_pair[0].first_var.id,
1130 quad_coef_pair[0].second_var.id,
1134 coef * variable_values[key.first_var] * variable_values[key.second_var]
1141 processed_elements: _QuadraticProcessedElements,
1142 target_stack: _QuadraticToProcessElements,
1145 processed_elements.terms[var] += val * scale
1147 processed_elements.quadratic_terms[key] += val * scale
1148 processed_elements.offset += scale * self.
offset
1153 result = str(self.
offset)
1154 sorted_linear_keys = sorted(self.
_linear_terms.keys(), key=str)
1155 for variable
in sorted_linear_keys:
1160 if coefficient == 0.0:
1166 result += str(abs(coefficient)) +
" * " + str(variable)
1168 for key
in sorted_quadratic_keys:
1173 if coefficient == 0.0:
1179 result += str(abs(coefficient)) +
" * " + str(key)
1183 result = f
"QuadraticExpression({self.offset}, " +
"{"
1184 result +=
", ".join(
1185 f
"{variable!r}: {coefficient}"
1189 result +=
", ".join(
1190 f
"{key!r}: {coefficient}"
1198 """A linear constraint for an optimization model.
1200 A LinearConstraint adds the following restriction on feasible solutions to an
1202 lb <= sum_{i in I} a_i * x_i <= ub
1203 where x_i are the decision variables of the problem. lb == ub is allowed, this
1204 models the equality constraint:
1205 sum_{i in I} a_i * x_i == b
1206 lb > ub is also allowed, but the optimization problem will be infeasible.
1208 A LinearConstraint can be configured as follows:
1209 * lower_bound: a float property, lb above. Should not be NaN nor +inf.
1210 * upper_bound: a float property, ub above. Should not be NaN nor -inf.
1211 * set_coefficient() and get_coefficient(): get and set the a_i * x_i
1212 terms. The variable must be from the same model as this constraint, and
1213 the a_i must be finite and not NaN. The coefficient for any variable not
1214 set is 0.0, and setting a coefficient to 0.0 removes it from I above.
1216 The name is optional, read only, and used only for debugging. Non-empty names
1219 Every LinearConstraint is associated with a Model (defined below). Note that
1220 data describing the linear constraint (e.g. lower_bound) is owned by
1221 Model.storage, this class is simply a reference to that data. Do not create a
1222 LinearConstraint directly, use Model.add_linear_constraint() instead.
1225 __slots__ =
"__weakref__",
"_model",
"_id"
1228 """Do not invoke directly, use Model.add_linear_constraint()."""
1234 return self.
model.storage.get_linear_constraint_lb(self.
_id)
1238 self.
model.storage.set_linear_constraint_lb(self.
_id, value)
1242 return self.
model.storage.get_linear_constraint_ub(self.
_id)
1246 self.
model.storage.set_linear_constraint_ub(self.
_id, value)
1250 return self.
model.storage.get_linear_constraint_name(self.
_id)
1257 def model(self) -> "Model":
1261 self.
model.check_compatible(variable)
1262 self.
model.storage.set_linear_constraint_coefficient(
1263 self.
_id, variable.id, coefficient
1267 self.
model.check_compatible(variable)
1268 return self.
model.storage.get_linear_constraint_coefficient(
1269 self.
_id, variable.id
1272 def terms(self) -> Iterator[LinearTerm]:
1273 """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint."""
1274 for variable
in self.
model.row_nonzeros(self):
1277 coefficient=self.
model.storage.get_linear_constraint_coefficient(
1278 self.
_id, variable.id
1283 """Returns the bounded expression from lower_bound, upper_bound and terms."""
1289 """Returns the name, or a string containing the id if the name is empty."""
1290 return self.
name if self.
name else f
"linear_constraint_{self.id}"
1293 return f
"<LinearConstraint id: {self.id}, name: {self.name!r}>"
1297 """The objective for an optimization model.
1299 An objective is either of the form:
1300 min o + sum_{i in I} c_i * x_i + sum_{i, j in I, i <= j} q_i,j * x_i * x_j
1302 max o + sum_{i in I} c_i * x_i + sum_{(i, j) in Q} q_i,j * x_i * x_j
1303 where x_i are the decision variables of the problem and where all pairs (i, j)
1304 in Q satisfy i <= j. The values of o, c_i and q_i,j should be finite and not
1307 The objective can be configured as follows:
1308 * offset: a float property, o above. Should be finite and not NaN.
1309 * is_maximize: a bool property, if the objective is to maximize or minimize.
1310 * set_linear_coefficient and get_linear_coefficient control the c_i * x_i
1311 terms. The variables must be from the same model as this objective, and
1312 the c_i must be finite and not NaN. The coefficient for any variable not
1313 set is 0.0, and setting a coefficient to 0.0 removes it from I above.
1314 * set_quadratic_coefficient and get_quadratic_coefficient control the
1315 q_i,j * x_i * x_j terms. The variables must be from the same model as this
1316 objective, and the q_i,j must be finite and not NaN. The coefficient for
1317 any pair of variables not set is 0.0, and setting a coefficient to 0.0
1318 removes the associated (i,j) from Q above.
1320 Every Objective is associated with a Model (defined below). Note that data
1321 describing the objective (e.g. offset) is owned by Model.storage, this class
1322 is simply a reference to that data. Do not create an Objective directly,
1323 use Model.objective to access the objective instead.
1325 The objective will be linear if only linear coefficients are set. This can be
1326 useful to avoid solve-time errors with solvers that do not accept quadratic
1327 objectives. To facilitate this linear objective guarantee we provide three
1328 functions to add to the objective:
1329 * add(), which accepts linear or quadratic expressions,
1330 * add_quadratic(), which also accepts linear or quadratic expressions and
1331 can be used to signal a quadratic objective is possible, and
1332 * add_linear(), which only accepts linear expressions and can be used to
1333 guarantee the objective remains linear.
1336 __slots__ = (
"_model",)
1339 """Do no invoke directly, use Model.objective."""
1344 return self.
model.storage.get_is_maximize()
1348 self.
model.storage.set_is_maximize(is_maximize)
1352 return self.
model.storage.get_objective_offset()
1356 self.
model.storage.set_objective_offset(value)
1359 def model(self) -> "Model":
1363 self.
model.check_compatible(variable)
1364 self.
model.storage.set_linear_objective_coefficient(variable.id, coef)
1367 self.
model.check_compatible(variable)
1368 return self.
model.storage.get_linear_objective_coefficient(variable.id)
1371 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order."""
1372 yield from self.
model.linear_objective_terms()
1374 def add(self, objective: QuadraticTypes) ->
None:
1375 """Adds the provided expression `objective` to the objective function.
1377 To ensure the objective remains linear through type checks, use
1381 objective: the expression to add to the objective function.
1383 if isinstance(objective, (LinearBase, int, float)):
1385 elif isinstance(objective, QuadraticBase):
1389 "unsupported type in objective argument for "
1390 f
"Objective.add(): {type(objective).__name__!r}"
1394 """Adds the provided linear expression `objective` to the objective function."""
1395 if not isinstance(objective, (LinearBase, int, float)):
1397 "unsupported type in objective argument for "
1398 f
"Objective.add_linear(): {type(objective).__name__!r}"
1400 objective_expr = as_flat_linear_expression(objective)
1402 for var, coefficient
in objective_expr.terms.items():
1408 """Adds the provided quadratic expression `objective` to the objective function."""
1409 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)):
1411 "unsupported type in objective argument for "
1412 f
"Objective.add(): {type(objective).__name__!r}"
1414 objective_expr = as_flat_quadratic_expression(objective)
1416 for var, coefficient
in objective_expr.linear_terms.items():
1420 for key, coefficient
in objective_expr.quadratic_terms.items():
1429 self, first_variable: Variable, second_variable: Variable, coef: float
1431 self.
model.check_compatible(first_variable)
1432 self.
model.check_compatible(second_variable)
1433 self.
model.storage.set_quadratic_objective_coefficient(
1434 first_variable.id, second_variable.id, coef
1438 self, first_variable: Variable, second_variable: Variable
1440 self.
model.check_compatible(first_variable)
1441 self.
model.check_compatible(second_variable)
1442 return self.
model.storage.get_quadratic_objective_coefficient(
1443 first_variable.id, second_variable.id
1447 """Yields quadratic terms with nonzero objective coefficient in undefined order."""
1448 yield from self.
model.quadratic_objective_terms()
1452 raise TypeError(
"Cannot get a quadratic objective as a linear expression")
1456 return as_flat_quadratic_expression(
1463 """Clears objective coefficients and offset. Does not change direction."""
1464 self.
model.storage.clear_objective()
1468 linear_constraint: LinearConstraint
1474 """Tracks updates to an optimization model from a ModelStorage.
1476 Do not instantiate directly, instead create through
1477 ModelStorage.add_update_tracker().
1479 Querying an UpdateTracker after calling Model.remove_update_tracker will
1480 result in a model_storage.UsedUpdateTrackerAfterRemovalError.
1484 x = mod.add_variable(0.0, 1.0, True, 'x')
1485 y = mod.add_variable(0.0, 1.0, True, 'y')
1486 tracker = mod.add_update_tracker()
1487 mod.set_variable_ub(x, 3.0)
1488 tracker.export_update()
1489 => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }"
1490 mod.set_variable_ub(y, 2.0)
1491 tracker.export_update()
1492 => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }"
1493 tracker.advance_checkpoint()
1494 tracker.export_update()
1496 mod.set_variable_ub(y, 4.0)
1497 tracker.export_update()
1498 => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }"
1499 tracker.advance_checkpoint()
1500 mod.remove_update_tracker(tracker)
1503 def __init__(self, storage_update_tracker: model_storage.StorageUpdateTracker):
1504 """Do not invoke directly, use Model.add_update_tracker() instead."""
1508 """Returns changes to the model since last call to checkpoint/creation."""
1512 """Track changes to the model only after this function call."""
1517 """An optimization model.
1519 The objective function of the model can be linear or quadratic, and some
1520 solvers can only handle linear objective functions. For this reason Model has
1521 three versions of all objective setting functions:
1522 * A generic one (e.g. maximize()), which accepts linear or quadratic
1524 * a quadratic version (e.g. maximize_quadratic_objective()), which also
1525 accepts linear or quadratic expressions and can be used to signal a
1526 quadratic objective is possible, and
1527 * a linear version (e.g. maximize_linear_objective()), which only accepts
1528 linear expressions and can be used to avoid solve time errors for solvers
1529 that do not accept quadratic objectives.
1532 name: A description of the problem, can be empty.
1533 objective: A function to maximize or minimize.
1534 storage: Implementation detail, do not access directly.
1535 _variable_ids: Maps variable ids to Variable objects.
1536 _linear_constraint_ids: Maps linear constraint ids to LinearConstraint
1540 __slots__ =
"storage",
"_variable_ids",
"_linear_constraint_ids",
"_objective"
1555 weakref.WeakValueDictionary()
1558 int, LinearConstraint
1559 ] = weakref.WeakValueDictionary()
1573 lb: float = -math.inf,
1574 ub: float = math.inf,
1575 is_integer: bool =
False,
1578 """Adds a decision variable to the optimization model.
1581 lb: The new variable must take at least this value (a lower bound).
1582 ub: The new variable must be at most this value (an upper bound).
1583 is_integer: Indicates if the variable can only take integer values
1584 (otherwise, the variable can take any continuous value).
1585 name: For debugging purposes only, but nonempty names must be distinct.
1588 A reference to the new decision variable.
1591 result =
Variable(self, variable_id)
1596 self, *, lb: float = -math.inf, ub: float = math.inf, name: str =
""
1598 return self.
add_variable(lb=lb, ub=ub, is_integer=
True, name=name)
1601 return self.
add_variable(lb=0.0, ub=1.0, is_integer=
True, name=name)
1604 """Returns the Variable for the id var_id, or raises KeyError."""
1605 if not self.
storage.variable_exists(var_id):
1606 raise KeyError(f
"variable does not exist with id {var_id}")
1615 """Yields the variables in the order of creation."""
1616 for var_id
in self.
storage.get_variables():
1620 """Sets the objective to maximize the provided expression `objective`."""
1624 """Sets the objective to maximize the provided linear expression `objective`."""
1628 """Sets the objective to maximize the provided quadratic expression `objective`."""
1632 """Sets the objective to minimize the provided expression `objective`."""
1636 """Sets the objective to minimize the provided linear expression `objective`."""
1640 """Sets the objective to minimize the provided quadratic expression `objective`."""
1643 def set_objective(self, objective: QuadraticTypes, *, is_maximize: bool) ->
None:
1644 """Sets the objective to optimize the provided expression `objective`."""
1645 if isinstance(objective, (LinearBase, int, float)):
1647 elif isinstance(objective, QuadraticBase):
1651 "unsupported type in objective argument for "
1652 f
"set_objective: {type(objective).__name__!r}"
1656 self, objective: LinearTypes, *, is_maximize: bool
1658 """Sets the objective to optimize the provided linear expression `objective`."""
1659 if not isinstance(objective, (LinearBase, int, float)):
1661 "unsupported type in objective argument for "
1662 f
"set_linear_objective: {type(objective).__name__!r}"
1664 self.
storage.clear_objective()
1666 objective_expr = as_flat_linear_expression(objective)
1667 self.
_objective.offset = objective_expr.offset
1668 for var, coefficient
in objective_expr.terms.items():
1669 self.
_objective.set_linear_coefficient(var, coefficient)
1672 self, objective: QuadraticTypes, *, is_maximize: bool
1674 """Sets the objective to optimize the provided linear expression `objective`."""
1675 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)):
1677 "unsupported type in objective argument for "
1678 f
"set_quadratic_objective: {type(objective).__name__!r}"
1680 self.
storage.clear_objective()
1682 objective_expr = as_flat_quadratic_expression(objective)
1683 self.
_objective.offset = objective_expr.offset
1684 for var, coefficient
in objective_expr.linear_terms.items():
1685 self.
_objective.set_linear_coefficient(var, coefficient)
1686 for key, coefficient
in objective_expr.quadratic_terms.items():
1688 key.first_var, key.second_var, coefficient
1696 bounded_expr: Optional[Union[bool, BoundedLinearTypes]] =
None,
1698 lb: Optional[float] =
None,
1699 ub: Optional[float] =
None,
1700 expr: Optional[LinearTypes] =
None,
1702 ) -> LinearConstraint:
1703 """Adds a linear constraint to the optimization model.
1705 The simplest way to specify the constraint is by passing a one-sided or
1706 two-sided linear inequality as in:
1707 * add_linear_constraint(x + y + 1.0 <= 2.0),
1708 * add_linear_constraint(x + y >= 2.0), or
1709 * add_linear_constraint((1.0 <= x + y) <= 2.0).
1711 Note the extra parenthesis for two-sided linear inequalities, which is
1712 required due to some language limitations (see
1713 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/).
1714 If the parenthesis are omitted, a TypeError will be raised explaining the
1715 issue (if this error was not raised the first inequality would have been
1716 silently ignored because of the noted language limitations).
1718 The second way to specify the constraint is by setting lb, ub, and/o expr as
1720 * add_linear_constraint(expr=x + y + 1.0, ub=2.0),
1721 * add_linear_constraint(expr=x + y, lb=2.0),
1722 * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or
1723 * add_linear_constraint(lb=1.0).
1724 Omitting lb is equivalent to setting it to -math.inf and omiting ub is
1725 equivalent to setting it to math.inf.
1727 These two alternatives are exclusive and a combined call like:
1728 * add_linear_constraint(x + y <= 2.0, lb=1.0), or
1729 * add_linear_constraint(x + y <= 2.0, ub=math.inf)
1730 will raise a ValueError. A ValueError is also raised if expr's offset is
1734 bounded_expr: a linear inequality describing the constraint. Cannot be
1735 specified together with lb, ub, or expr.
1736 lb: The constraint's lower bound if bounded_expr is omitted (if both
1737 bounder_expr and lb are omitted, the lower bound is -math.inf).
1738 ub: The constraint's upper bound if bounded_expr is omitted (if both
1739 bounder_expr and ub are omitted, the upper bound is math.inf).
1740 expr: The constraint's linear expression if bounded_expr is omitted.
1741 name: For debugging purposes only, but nonempty names must be distinct.
1744 A reference to the new linear constraint.
1746 normalized_inequality = as_normalized_linear_inequality(
1747 bounded_expr, lb=lb, ub=ub, expr=expr
1750 normalized_inequality.lb, normalized_inequality.ub, name
1754 for var, coefficient
in normalized_inequality.coefficients.items():
1755 result.set_coefficient(var, coefficient)
1759 """Returns the LinearConstraint for the id lin_con_id."""
1760 if not self.
storage.linear_constraint_exists(lin_con_id):
1761 raise KeyError(f
"linear constraint does not exist with id {lin_con_id}")
1770 """Yields the linear constraints in the order of creation."""
1771 for lin_con_id
in self.
storage.get_linear_constraints():
1774 def row_nonzeros(self, linear_constraint: LinearConstraint) -> Iterator[Variable]:
1775 """Yields the variables with nonzero coefficient for this linear constraint."""
1776 for var_id
in self.
storage.get_variables_for_linear_constraint(
1777 linear_constraint.id
1782 """Yields the linear constraints with nonzero coefficient for this variable."""
1783 for lin_con_id
in self.
storage.get_linear_constraints_with_variable(
1789 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order."""
1790 for term
in self.
storage.get_linear_objective_coefficients():
1793 coefficient=term.coefficient,
1797 """Yields the quadratic terms with nonzero objective coefficient in undefined order."""
1798 for term
in self.
storage.get_quadratic_objective_coefficients():
1807 ) -> Iterator[LinearConstraintMatrixEntry]:
1808 """Yields the nonzero elements of the linear constraint matrix in undefined order."""
1809 for entry
in self.
storage.get_linear_constraint_matrix_entries():
1812 entry.linear_constraint_id
1815 coefficient=entry.coefficient,
1822 """Creates an UpdateTracker registered on this model to view changes."""
1826 """Stops tracker from getting updates on changes to this model.
1828 An error will be raised if tracker was not created by this Model or if
1829 tracker has been previously removed.
1831 Using (via checkpoint or update) an UpdateTracker after it has been removed
1832 will result in an error.
1835 tracker: The UpdateTracker to unregister.
1838 KeyError: The tracker was created by another model or was already removed.
1843 self, var_or_constraint: Union[Variable, LinearConstraint]
1845 """Raises a ValueError if the model of var_or_constraint is not self."""
1846 if var_or_constraint.model
is not self:
1848 f
"{var_or_constraint} is from model {var_or_constraint.model} and"
1849 f
" cannot be used with model {self}"
1856 result =
Variable(self, variable_id)
1861 self, linear_constraint_id: int
1862 ) -> LinearConstraint:
1874 """A deferred sum of LinearBase objects.
1876 LinearSum objects are automatically created when two linear objects are added
1877 and, as noted in the documentation for Linear, can reduce the inefficiencies.
1878 In particular, they are created when calling sum(iterable) when iterable is
1879 an Iterable[LinearTypes]. However, using LinearSum(iterable) instead
1880 can result in additional performance improvements:
1882 * sum(iterable): creates a nested set of LinearSum objects (e.g.
1883 `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`).
1884 * LinearSum(iterable): creates a single LinearSum that saves a tuple with
1885 all the LinearTypes objects in iterable (e.g.
1886 `LinearSum([a, b, c])` does not create additional objects).
1888 This class is immutable.
1891 __slots__ =
"__weakref__",
"_elements"
1895 def __init__(self, iterable: Iterable[LinearTypes]) ->
None:
1896 """Creates a LinearSum object. A copy of iterable is saved as a tuple."""
1900 if not isinstance(item, (LinearBase, int, float)):
1902 "unsupported type in iterable argument for "
1903 f
"LinearSum: {type(item).__name__!r}"
1913 processed_elements: _ProcessedElements,
1914 target_stack: _ToProcessElements,
1917 if isinstance(term, (int, float)):
1918 processed_elements.offset += scale * float(term)
1920 target_stack.append(term, scale)
1923 return str(as_flat_linear_expression(self))
1926 result =
"LinearSum(("
1927 result +=
", ".join(repr(linear)
for linear
in self.
_elements)
1935 """A deferred sum of QuadraticTypes objects.
1937 QuadraticSum objects are automatically created when a quadratic object is
1938 added to quadratic or linear objects and, as has performance optimizations
1939 similar to LinearSum.
1941 This class is immutable.
1944 __slots__ =
"__weakref__",
"_elements"
1948 def __init__(self, iterable: Iterable[QuadraticTypes]) ->
None:
1949 """Creates a QuadraticSum object. A copy of iterable is saved as a tuple."""
1953 if not isinstance(item, (LinearBase, QuadraticBase, int, float)):
1955 "unsupported type in iterable argument for "
1956 f
"QuadraticSum: {type(item).__name__!r}"
1966 processed_elements: _QuadraticProcessedElements,
1967 target_stack: _QuadraticToProcessElements,
1970 if isinstance(term, (int, float)):
1971 processed_elements.offset += scale * float(term)
1973 target_stack.append(term, scale)
1976 return str(as_flat_quadratic_expression(self))
1979 result =
"QuadraticSum(("
1980 result +=
", ".join(repr(element)
for element
in self.
_elements)
1986 """A deferred multiplication computation for linear expressions.
1988 This class is immutable.
1991 __slots__ =
"_scalar",
"_linear"
1993 def __init__(self, scalar: float, linear: LinearBase) ->
None:
1994 if not isinstance(scalar, (float, int)):
1996 "unsupported type for scalar argument in "
1997 f
"LinearProduct: {type(scalar).__name__!r}"
1999 if not isinstance(linear, LinearBase):
2001 "unsupported type for linear argument in "
2002 f
"LinearProduct: {type(linear).__name__!r}"
2018 processed_elements: _ProcessedElements,
2019 target_stack: _ToProcessElements,
2024 return str(as_flat_linear_expression(self))
2027 result = f
"LinearProduct({self._scalar!r}, "
2028 result += f
"{self._linear!r})"
2033 """A deferred multiplication computation for quadratic expressions.
2035 This class is immutable.
2038 __slots__ =
"_scalar",
"_quadratic"
2040 def __init__(self, scalar: float, quadratic: QuadraticBase) ->
None:
2041 if not isinstance(scalar, (float, int)):
2043 "unsupported type for scalar argument in "
2044 f
"QuadraticProduct: {type(scalar).__name__!r}"
2046 if not isinstance(quadratic, QuadraticBase):
2048 "unsupported type for linear argument in "
2049 f
"QuadraticProduct: {type(quadratic).__name__!r}"
2065 processed_elements: _QuadraticProcessedElements,
2066 target_stack: _QuadraticToProcessElements,
2071 return str(as_flat_quadratic_expression(self))
2074 return f
"QuadraticProduct({self._scalar}, {self._quadratic!r})"
2078 """A deferred multiplication of two linear expressions.
2080 This class is immutable.
2083 __slots__ =
"_first_linear",
"_second_linear"
2085 def __init__(self, first_linear: LinearBase, second_linear: LinearBase) ->
None:
2086 if not isinstance(first_linear, LinearBase):
2088 "unsupported type for first_linear argument in "
2089 f
"LinearLinearProduct: {type(first_linear).__name__!r}"
2091 if not isinstance(second_linear, LinearBase):
2093 "unsupported type for second_linear argument in "
2094 f
"LinearLinearProduct: {type(second_linear).__name__!r}"
2110 processed_elements: _QuadraticProcessedElements,
2111 target_stack: _QuadraticToProcessElements,
2115 first_expression = as_flat_linear_expression(self.
_first_linear)
2116 second_expression = as_flat_linear_expression(self.
_second_linear)
2117 processed_elements.offset += (
2118 first_expression.offset * second_expression.offset * scale
2120 for first_var, first_val
in first_expression.terms.items():
2121 processed_elements.terms[first_var] += (
2122 second_expression.offset * first_val * scale
2124 for second_var, second_val
in second_expression.terms.items():
2125 processed_elements.terms[second_var] += (
2126 first_expression.offset * second_val * scale
2129 for first_var, first_val
in first_expression.terms.items():
2130 for second_var, second_val
in second_expression.terms.items():
2131 processed_elements.quadratic_terms[
2133 ] += (first_val * second_val * scale)
2136 return str(as_flat_quadratic_expression(self))
2139 result =
"LinearLinearProduct("
2140 result += f
"{self._first_linear!r}, "
2141 result += f
"{self._second_linear!r})"
2146 """Converts floats, ints and Linear objects to a LinearExpression."""
2147 if isinstance(value, LinearExpression):
2153 """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression."""
2154 if isinstance(value, QuadraticExpression):
2159@dataclasses.dataclass
2161 """Represents an inequality lb <= expr <= ub where expr's offset is zero.
2163 The inequality is of the form:
2164 lb <= sum_{x in V} coefficients[x] * x <= ub
2165 where V is the set of keys of coefficients.
2170 coefficients: Mapping[Variable, float]
2172 def __init__(self, *, lb: float, ub: float, expr: LinearTypes) ->
None:
2173 """Raises a ValueError if expr's offset is infinite."""
2174 flat_expr = as_flat_linear_expression(expr)
2175 if math.isinf(flat_expr.offset):
2177 "Trying to create a linear constraint whose expression has an"
2180 self.
lb = lb - flat_expr.offset
2181 self.
ub = ub - flat_expr.offset
2189 bounded_expr: Optional[Union[bool, BoundedLinearTypes]] =
None,
2191 lb: Optional[float] =
None,
2192 ub: Optional[float] =
None,
2193 expr: Optional[LinearTypes] =
None,
2194) -> NormalizedLinearInequality:
2195 """Converts a linear constraint to a NormalizedLinearInequality.
2197 The simplest way to specify the constraint is by passing a one-sided or
2198 two-sided linear inequality as in:
2199 * as_normalized_linear_inequality(x + y + 1.0 <= 2.0),
2200 * as_normalized_linear_inequality(x + y >= 2.0), or
2201 * as_normalized_linear_inequality((1.0 <= x + y) <= 2.0).
2203 Note the extra parenthesis for two-sided linear inequalities, which is
2204 required due to some language limitations (see
2205 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/).
2206 If the parenthesis are omitted, a TypeError will be raised explaining the
2207 issue (if this error was not raised the first inequality would have been
2208 silently ignored because of the noted language limitations).
2210 The second way to specify the constraint is by setting lb, ub, and/o expr as
2212 * as_normalized_linear_inequality(expr=x + y + 1.0, ub=2.0),
2213 * as_normalized_linear_inequality(expr=x + y, lb=2.0),
2214 * as_normalized_linear_inequality(expr=x + y, lb=1.0, ub=2.0), or
2215 * as_normalized_linear_inequality(lb=1.0).
2216 Omitting lb is equivalent to setting it to -math.inf and omiting ub is
2217 equivalent to setting it to math.inf.
2219 These two alternatives are exclusive and a combined call like:
2220 * as_normalized_linear_inequality(x + y <= 2.0, lb=1.0), or
2221 * as_normalized_linear_inequality(x + y <= 2.0, ub=math.inf)
2222 will raise a ValueError. A ValueError is also raised if expr's offset is
2226 bounded_expr: a linear inequality describing the constraint. Cannot be
2227 specified together with lb, ub, or expr.
2228 lb: The constraint's lower bound if bounded_expr is omitted (if both
2229 bounder_expr and lb are omitted, the lower bound is -math.inf).
2230 ub: The constraint's upper bound if bounded_expr is omitted (if both
2231 bounder_expr and ub are omitted, the upper bound is math.inf).
2232 expr: The constraint's linear expression if bounded_expr is omitted.
2235 A NormalizedLinearInequality representing the linear constraint.
2237 if bounded_expr
is None:
2244 if not isinstance(expr, (LinearBase, int, float)):
2246 f
"unsupported type for expr argument: {type(expr).__name__!r}"
2250 if isinstance(bounded_expr, bool):
2252 "unsupported type for bounded_expr argument:"
2253 " bool. This error can occur when trying to add != constraints "
2254 "(which are not supported) or inequalities/equalities with constant "
2255 "left-hand-side and right-hand-side (which are redundant or make a "
2256 "model infeasible)."
2261 LowerBoundedLinearExpression,
2262 UpperBoundedLinearExpression,
2263 BoundedLinearExpression,
2268 f
"unsupported type for bounded_expr: {type(bounded_expr).__name__!r}"
2271 raise ValueError(
"lb cannot be specified together with a linear inequality")
2273 raise ValueError(
"ub cannot be specified together with a linear inequality")
2274 if expr
is not None:
2275 raise ValueError(
"expr cannot be specified together with a linear inequality")
2277 if isinstance(bounded_expr, VarEqVar):
2281 expr=bounded_expr.first_variable - bounded_expr.second_variable,
2285 bounded_expr, (LowerBoundedLinearExpression, BoundedLinearExpression)
2287 lb = bounded_expr.lower_bound
2291 bounded_expr, (UpperBoundedLinearExpression, BoundedLinearExpression)
2293 ub = bounded_expr.upper_bound