Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
model_builder.py
Go to the documentation of this file.
1# Copyright 2010-2024 Google LLC
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7#
8# Unless required by applicable law or agreed to in writing, software
9# distributed under the License is distributed on an "AS IS" BASIS,
10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11# See the License for the specific language governing permissions and
12# limitations under the License.
13
14"""Methods for building and solving model_builder models.
15
16The following two sections describe the main
17methods for building and solving those models.
18
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.
23
24Additional methods for solving Model models:
25
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.
30
31Other methods and functions listed are primarily used for developing OR-Tools,
32rather than for solving specific optimization problems.
33"""
34
35import abc
36import dataclasses
37import math
38import numbers
39import typing
40from typing import Callable, List, Optional, Sequence, Tuple, Union, cast
41
42import numpy as np
43from numpy import typing as npt
44import pandas as pd
45
46from ortools.linear_solver import linear_solver_pb2
47from ortools.linear_solver.python import model_builder_helper as mbh
48from ortools.linear_solver.python import model_builder_numbers as mbn
49
50
51# Custom types.
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"]
58
59# Forward solve statuses.
60SolveStatus = mbh.SolveStatus
61
62# pylint: disable=protected-access
63
64
65class LinearExpr(metaclass=abc.ABCMeta):
66 """Holds an linear expression.
67
68 A linear expression is built from constants and variables.
69 For example, `x + 2.0 * (y - z + 1.0)`.
70
71 Linear expressions are used in Model models in constraints and in the
72 objective:
73
74 * You can define linear constraints as in:
75
76 ```
77 model.add(x + 2 * y <= 5.0)
78 model.add(sum(array_of_vars) == 5.0)
79 ```
80
81 * In Model, the objective is a linear expression:
82
83 ```
84 model.minimize(x + 2.0 * y + z)
85 ```
86
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:
90
91 ```
92 model.minimize(model_builder.LinearExpr.sum(expressions))
93 model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
94 ```
95 """
96
97 @classmethod
98 def sum( # pytype: disable=annotation-type-mismatch # numpy-scalars
99 cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0
100 ) -> LinearExprT:
101 """Creates `sum(expressions) + constant`.
102
103 It can perform simple simplifications and returns different objects,
104 including the input.
105
106 Args:
107 expressions: a sequence of linear expressions or constants.
108 constant: a numerical constant.
109
110 Returns:
111 a LinearExpr instance or a numerical constant.
112 """
113 checked_constant: np.double = mbn.assert_is_a_number(constant)
114 if not expressions:
115 return checked_constant
116 if len(expressions) == 1 and mbn.is_zero(checked_constant):
117 return expressions[0]
118
119 return LinearExpr.weighted_sum(
120 expressions, np.ones(len(expressions)), constant=checked_constant
121 )
122
123 @classmethod
124 def weighted_sum( # pytype: disable=annotation-type-mismatch # numpy-scalars
125 cls,
126 expressions: Sequence[LinearExprT],
127 coefficients: Sequence[NumberT],
128 *,
129 constant: NumberT = 0.0,
130 ) -> Union[NumberT, "_LinearExpression"]:
131 """Creates `sum(expressions[i] * coefficients[i]) + constant`.
132
133 It can perform simple simplifications and returns different object,
134 including the input.
135
136 Args:
137 expressions: a sequence of linear expressions or constants.
138 coefficients: a sequence of numerical constants.
139 constant: a numerical constant.
140
141 Returns:
142 a _LinearExpression instance or a numerical constant.
143 """
144 if len(expressions) != len(coefficients):
145 raise ValueError(
146 "LinearExpr.weighted_sum: expressions and coefficients have"
147 " different lengths"
148 )
149 checked_constant: np.double = mbn.assert_is_a_number(constant)
150 if not expressions:
151 return checked_constant
153 to_process=list(zip(expressions, coefficients)), offset=checked_constant
154 )
155
156 @classmethod
157 def term( # pytype: disable=annotation-type-mismatch # numpy-scalars
158 cls,
159 expression: LinearExprT,
160 coefficient: NumberT,
161 *,
162 constant: NumberT = 0.0,
163 ) -> LinearExprT:
164 """Creates `expression * coefficient + constant`.
165
166 It can perform simple simplifications and returns different object,
167 including the input.
168 Args:
169 expression: a linear expression or a constant.
170 coefficient: a numerical constant.
171 constant: a numerical constant.
172
173 Returns:
174 a LinearExpr instance or a numerical constant.
175 """
176 checked_coefficient: np.double = mbn.assert_is_a_number(coefficient)
177 checked_constant: np.double = mbn.assert_is_a_number(constant)
178
179 if mbn.is_zero(checked_coefficient):
180 return checked_constant
181 if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant):
182 return expression
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
188 )
189 raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}")
190
191 def __hash__(self):
192 return object.__hash__(self)
193
194 def __add__(self, arg: LinearExprT) -> "_Sum":
195 return _Sum(self, arg)
196
197 def __radd__(self, arg: LinearExprT) -> "_Sum":
198 return self.__add__(arg)
199
200 def __sub__(self, arg: LinearExprT) -> "_Sum":
201 return _Sum(self, -arg)
202
203 def __rsub__(self, arg: LinearExprT) -> "_Sum":
204 return _Sum(-self, arg)
205
206 def __mul__(self, arg: NumberT) -> "_Product":
207 return _Product(self, arg)
208
209 def __rmul__(self, arg: NumberT) -> "_Product":
210 return self.__mul__(arg)
211
212 def __truediv__(self, coeff: NumberT) -> "_Product":
213 return self.__mul__(1.0 / coeff)
214
215 def __neg__(self) -> "_Product":
216 return _Product(self, -1)
217
218 def __bool__(self):
219 raise NotImplementedError(f"Cannot use a LinearExpr {self} as a Boolean value")
220
221 def __eq__(self, arg: LinearExprT) -> "BoundedLinearExpression":
222 return BoundedLinearExpression(self - arg, 0, 0)
223
224 def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression":
226 self - arg, 0, math.inf
227 ) # pytype: disable=wrong-arg-types # numpy-scalars
228
229 def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression":
231 self - arg, -math.inf, 0
232 ) # pytype: disable=wrong-arg-types # numpy-scalars
233
234
236 """A variable (continuous or integral).
237
238 A Variable is an object that can take on any integer value within defined
239 ranges. Variables appear in constraint like:
240
241 x + y >= 5
242
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.
246 """
247
249 self,
250 helper: mbh.ModelBuilderHelper,
251 lb: NumberT,
252 ub: Optional[NumberT],
253 is_integral: Optional[bool],
254 name: Optional[str],
255 ) -> None:
256 """See Model.new_var below."""
257 LinearExpr.__init__(self)
258 self.__helper: mbh.ModelBuilderHelper = helper
259 # Python do not support multiple __init__ methods.
260 # This method is only called from the Model class.
261 # We hack the parameter to support the two cases:
262 # case 1:
263 # helper is a ModelBuilderHelper, lb is a double value, ub is a double
264 # value, is_integral is a Boolean value, and name is a string.
265 # case 2:
266 # helper is a ModelBuilderHelper, lb is an index (int), ub is None,
267 # is_integral is None, and name is None.
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
271 else:
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)
278 if name:
279 helper.set_var_name(index, name)
280
281 @property
282 def index(self) -> np.int32:
283 """Returns the index of the variable in the helper."""
284 return self.__index
285
286 @property
287 def helper(self) -> mbh.ModelBuilderHelper:
288 """Returns the underlying ModelBuilderHelper."""
289 return self.__helper
290
291 def is_equal_to(self, other: LinearExprT) -> bool:
292 """Returns true if self == other in the python sense."""
293 if not isinstance(other, Variable):
294 return False
295 return self.indexindex == other.index and self.helper == other.helper
296
297 def __str__(self) -> str:
298 return self.namename
299
300 def __repr__(self) -> str:
301 return self.__str__()
302
303 @property
304 def name(self) -> str:
305 """Returns the name of the variable."""
306 var_name = self.__helper.var_name(self.__index)
307 if var_name:
308 return var_name
309 return f"variable#{self.index}"
310
311 @name.setter
312 def name(self, name: str) -> None:
313 """Sets the name of the variable."""
314 self.__helper.set_var_name(self.__index, name)
315
316 @property
317 def lower_bound(self) -> np.double:
318 """Returns the lower bound of the variable."""
319 return self.__helper.var_lower_bound(self.__index)
320
321 @lower_bound.setter
322 def lower_bound(self, bound: NumberT) -> None:
323 """Sets the lower bound of the variable."""
324 self.__helper.set_var_lower_bound(self.__index, bound)
325
326 @property
327 def upper_bound(self) -> np.double:
328 """Returns the upper bound of the variable."""
329 return self.__helper.var_upper_bound(self.__index)
330
331 @upper_bound.setter
332 def upper_bound(self, bound: NumberT) -> None:
333 """Sets the upper bound of the variable."""
334 self.__helper.set_var_upper_bound(self.__index, bound)
335
336 @property
337 def is_integral(self) -> bool:
338 """Returns whether the variable is integral."""
339 return self.__helper.var_is_integral(self.__index)
340
341 @is_integral.setter
342 def integrality(self, is_integral: bool) -> None:
343 """Sets the integrality of the variable."""
344 self.__helper.set_var_integrality(self.__index, is_integral)
345
346 @property
347 def objective_coefficient(self) -> NumberT:
348 return self.__helper.var_objective_coefficient(self.__index)
349
350 @objective_coefficient.setter
351 def objective_coefficient(self, coeff: NumberT) -> None:
352 self.__helper.set_var_objective_coefficient(self.__index, coeff)
353
354 def __eq__(self, arg: Optional[LinearExprT]) -> ConstraintT:
355 if arg is None:
356 return False
357 if isinstance(arg, Variable):
358 return VarEqVar(self, arg)
360 self - arg, 0.0, 0.0
361 ) # pytype: disable=wrong-arg-types # numpy-scalars
362
363 def __hash__(self):
364 return hash((self.__helper, self.__index))
365
366
367class _BoundedLinearExpr(metaclass=abc.ABCMeta):
368 """Interface for types that can build bounded linear (boolean) expressions.
369
370 Classes derived from _BoundedLinearExpr are used to build linear constraints
371 to be satisfied.
372
373 * BoundedLinearExpression: a linear expression with upper and lower bounds.
374 * VarEqVar: an equality comparison between two variables.
375 """
376
377 @abc.abstractmethod
379 self, helper: mbh.ModelBuilderHelper, name: str
380 ) -> "LinearConstraint":
381 """Creates a new linear constraint in the helper.
382
383 Args:
384 helper (mbh.ModelBuilderHelper): The helper to create the constraint.
385 name (str): The name of the linear constraint.
386
387 Returns:
388 LinearConstraint: A reference to the linear constraint in the helper.
389 """
390
391 @abc.abstractmethod
393 self,
394 helper: mbh.ModelBuilderHelper,
395 var: Variable,
396 value: bool,
397 name: str,
398 ) -> "EnforcedLinearConstraint":
399 """Creates a new enforced linear constraint in the helper.
400
401 Args:
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.
406
407 Returns:
408 Enforced LinearConstraint: A reference to the linear constraint in the
409 helper.
410 """
411
412
414 bounded_expr: Union[bool, _BoundedLinearExpr],
415 helper: mbh.ModelBuilderHelper,
416 name: Optional[str],
417):
418 """Creates a new linear constraint in the helper.
419
420 It handles boolean values (which might arise in the construction of
421 BoundedLinearExpressions).
422
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.
426
427 Args:
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.
431
432 Returns:
433 LinearConstraint: a constraint in the helper corresponding to the input.
434
435 Raises:
436 TypeError: If constraint is an invalid type.
437 """
438 if isinstance(bounded_expr, bool):
439 c = LinearConstraint(helper, is_under_specified=True)
440 if name is not None:
441 helper.set_constraint_name(c.index, name)
442 if bounded_expr:
443 # constraint that is always feasible: 0.0 <= nothing <= 0.0
444 helper.set_constraint_lower_bound(c.index, 0.0)
445 helper.set_constraint_upper_bound(c.index, 0.0)
446 else:
447 # constraint that is always infeasible: +oo <= nothing <= -oo
448 helper.set_constraint_lower_bound(c.index, 1)
449 helper.set_constraint_upper_bound(c.index, -1)
450 return c
451 if isinstance(bounded_expr, _BoundedLinearExpr):
452 # pylint: disable=protected-access
453 return bounded_expr._add_linear_constraint(helper, name)
454 raise TypeError("invalid type={}".format(type(bounded_expr)))
455
456
458 bounded_expr: Union[bool, _BoundedLinearExpr],
459 helper: mbh.ModelBuilderHelper,
460 var: Variable,
461 value: bool,
462 name: Optional[str],
463):
464 """Creates a new enforced linear constraint in the helper.
465
466 It handles boolean values (which might arise in the construction of
467 BoundedLinearExpressions).
468
469 If bounded_expr is a Boolean value, the linear part of the constraint is
470 different.
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
473 True or False.
474
475 Args:
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.
481
482 Returns:
483 EnforcedLinearConstraint: a constraint in the helper corresponding to the
484 input.
485
486 Raises:
487 TypeError: If constraint is an invalid type.
488 """
489 if isinstance(bounded_expr, bool):
490 # TODO(user): create indicator variable assignment instead ?
491 c = EnforcedLinearConstraint(helper, is_under_specified=True)
492 c.indicator_variable = var
493 c.indicator_value = value
494 if name is not None:
495 helper.set_enforced_constraint_name(c.index, name)
496 if bounded_expr:
497 # constraint that is always feasible: 0.0 <= nothing <= 0.0
498 helper.set_enforced_constraint_lower_bound(c.index, 0.0)
499 helper.set_enforced_constraint_upper_bound(c.index, 0.0)
500 else:
501 # constraint that is always infeasible: +oo <= nothing <= -oo
502 helper.set_enforced_constraint_lower_bound(c.index, 1)
503 helper.set_enforced_constraint_upper_bound(c.index, -1)
504 return c
505 if isinstance(bounded_expr, _BoundedLinearExpr):
506 # pylint: disable=protected-access
507 return bounded_expr._add_enforced_linear_constraint(helper, var, value, name)
508 raise TypeError("invalid type={}".format(type(bounded_expr)))
509
510
511@dataclasses.dataclass(repr=False, eq=False, frozen=True)
513 """Represents var == var."""
514
515 __slots__ = ("left", "right")
516
517 left: Variable
518 right: Variable
519
520 def __str__(self):
521 return f"{self.left} == {self.right}"
522
523 def __repr__(self):
524 return self.__str__()
525
526 def __bool__(self) -> bool:
527 return hash(self.left) == hash(self.right)
528
530 self, helper: mbh.ModelBuilderHelper, name: str
531 ) -> "LinearConstraint":
532 c = LinearConstraint(helper)
533 helper.set_constraint_lower_bound(c.index, 0.0)
534 helper.set_constraint_upper_bound(c.index, 0.0)
535 # pylint: disable=protected-access
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)
538 # pylint: enable=protected-access
539 helper.set_constraint_name(c.index, name)
540 return c
541
543 self,
544 helper: mbh.ModelBuilderHelper,
545 var: Variable,
546 value: bool,
547 name: str,
548 ) -> "EnforcedLinearConstraint":
549 """Adds an enforced linear constraint to the model."""
550 c = EnforcedLinearConstraint(helper)
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)
555 # pylint: disable=protected-access
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)
558 # pylint: enable=protected-access
559 helper.set_enforced_constraint_name(c.index, name)
560 return c
561
562
564 """Represents a linear constraint: `lb <= linear expression <= ub`.
565
566 The only use of this class is to be added to the Model through
567 `Model.add(bounded expression)`, as in:
568
569 model.Add(x + 2 * y -1 >= z)
570 """
571
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)
576
577 def __str__(self) -> str:
578 if self.__lb > -math.inf and self.__ub < math.inf:
579 if self.__lb == self.__ub:
580 return f"{self.__expr} == {self.__lb}"
581 else:
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}"
587 else:
588 return f"{self.__expr} free"
589
590 def __repr__(self):
591 return self.__str__()
592
593 @property
594 def expression(self) -> LinearExprT:
595 return self.__expr
596
597 @property
598 def lower_bound(self) -> np.double:
599 return self.__lb
600
601 @property
602 def upper_bound(self) -> np.double:
603 return self.__ub
604
605 def __bool__(self) -> bool:
606 raise NotImplementedError(
607 f"Cannot use a BoundedLinearExpression {self} as a Boolean value"
608 )
609
611 self, helper: mbh.ModelBuilderHelper, name: Optional[str]
612 ) -> "LinearConstraint":
613 c = LinearConstraint(helper)
614 flat_expr = _as_flat_linear_expression(self.__expr)
615 # pylint: disable=protected-access
616 helper.add_terms_to_constraint(
617 c.index, flat_expr._variable_indices, flat_expr._coefficients
618 )
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)
621 # pylint: enable=protected-access
622 if name is not None:
623 helper.set_constraint_name(c.index, name)
624 return c
625
627 self,
628 helper: mbh.ModelBuilderHelper,
629 var: Variable,
630 value: bool,
631 name: Optional[str],
632 ) -> "EnforcedLinearConstraint":
633 """Adds an enforced linear constraint to the model."""
634 c = EnforcedLinearConstraint(helper)
635 c.indicator_variable = var
636 c.indicator_value = value
637 flat_expr = _as_flat_linear_expression(self.__expr)
638 # pylint: disable=protected-access
639 helper.add_terms_to_enforced_constraint(
640 c.index, flat_expr._variable_indices, flat_expr._coefficients
641 )
642 helper.set_enforced_constraint_lower_bound(
643 c.index, self.__lb - flat_expr._offset
644 )
645 helper.set_enforced_constraint_upper_bound(
646 c.index, self.__ub - flat_expr._offset
647 )
648 # pylint: enable=protected-access
649 if name is not None:
650 helper.set_enforced_constraint_name(c.index, name)
651 return c
652
653
655 """Stores a linear equation.
656
657 Example:
658 x = model.new_num_var(0, 10, 'x')
659 y = model.new_num_var(0, 10, 'y')
660
661 linear_constraint = model.add(x + 2 * y == 5)
662 """
663
665 self,
666 helper: mbh.ModelBuilderHelper,
667 *,
668 index: Optional[IntegerT] = None,
669 is_under_specified: bool = False,
670 ) -> None:
671 """LinearConstraint constructor.
672
673 Args:
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
677 model.add(bool).
678 """
679 if index is None:
680 self.__index = helper.add_linear_constraint()
681 else:
682 self.__index = index
683 self.__helper: mbh.ModelBuilderHelper = helper
684 self.__is_under_specified = is_under_specified
685
686 @property
687 def index(self) -> IntegerT:
688 """Returns the index of the constraint in the helper."""
689 return self.__index
690
691 @property
692 def helper(self) -> mbh.ModelBuilderHelper:
693 """Returns the ModelBuilderHelper instance."""
694 return self.__helper
695
696 @property
697 def lower_bound(self) -> np.double:
698 return self.__helper.constraint_lower_bound(self.__index)
699
700 @lower_bound.setter
701 def lower_bound(self, bound: NumberT) -> None:
703 self.__helper.set_constraint_lower_bound(self.__index, bound)
704
705 @property
706 def upper_bound(self) -> np.double:
707 return self.__helper.constraint_upper_bound(self.__index)
708
709 @upper_bound.setter
710 def upper_bound(self, bound: NumberT) -> None:
712 self.__helper.set_constraint_upper_bound(self.__index, bound)
713
714 @property
715 def name(self) -> str:
716 constraint_name = self.__helper.constraint_name(self.__index)
717 if constraint_name:
718 return constraint_name
719 return f"linear_constraint#{self.__index}"
720
721 @name.setter
722 def name(self, name: str) -> None:
723 return self.__helper.set_constraint_name(self.__index, name)
724
725 @property
726 def is_under_specified(self) -> bool:
727 """Returns True if the constraint is under specified.
728
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.
731 """
732 return self.__is_under_specified
733
735 """Raises an exception if the constraint is under specified."""
736 if self.__is_under_specified:
737 raise ValueError(
738 f"Constraint {self.index} is under specified and cannot be modified"
739 )
740
741 def __str__(self):
742 return self.namename
743
744 def __repr__(self):
745 return (
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)})"
750 )
751
752 def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
753 """Sets the coefficient of the variable in the constraint."""
755 self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
756
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)
761
762 def clear_terms(self) -> None:
763 """Clear all terms of the constraint."""
765 self.__helper.clear_constraint_terms(self.__index)
766
767
769 """Stores an enforced linear equation, also name indicator constraint.
770
771 Example:
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')
775
776 enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
777 """
778
780 self,
781 helper: mbh.ModelBuilderHelper,
782 *,
783 index: Optional[IntegerT] = None,
784 is_under_specified: bool = False,
785 ) -> None:
786 """EnforcedLinearConstraint constructor.
787
788 Args:
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
792 model.add(bool).
793 """
794 if index is None:
795 self.__index = helper.add_enforced_linear_constraint()
796 else:
797 if not helper.is_enforced_linear_constraint(index):
798 raise ValueError(
799 f"the given index {index} does not refer to an enforced linear"
800 " constraint"
801 )
802
803 self.__index = index
804 self.__helper: mbh.ModelBuilderHelper = helper
805 self.__is_under_specified = is_under_specified
806
807 @property
808 def index(self) -> IntegerT:
809 """Returns the index of the constraint in the helper."""
810 return self.__index
811
812 @property
813 def helper(self) -> mbh.ModelBuilderHelper:
814 """Returns the ModelBuilderHelper instance."""
815 return self.__helper
816
817 @property
818 def lower_bound(self) -> np.double:
819 return self.__helper.enforced_constraint_lower_bound(self.__index)
820
821 @lower_bound.setter
822 def lower_bound(self, bound: NumberT) -> None:
824 self.__helper.set_enforced_constraint_lower_bound(self.__index, bound)
825
826 @property
827 def upper_bound(self) -> np.double:
828 return self.__helper.enforced_constraint_upper_bound(self.__index)
829
830 @upper_bound.setter
831 def upper_bound(self, bound: NumberT) -> None:
833 self.__helper.set_enforced_constraint_upper_bound(self.__index, bound)
834
835 @property
836 def indicator_variable(self) -> "Variable":
837 enforcement_var_index = (
838 self.__helper.enforced_constraint_indicator_variable_index(self.__index)
839 )
840 return Variable(self.__helper, enforcement_var_index, None, None, None)
841
842 @indicator_variable.setter
843 def indicator_variable(self, var: "Variable") -> None:
844 self.__helper.set_enforced_constraint_indicator_variable_index(
845 self.__index, var.index
846 )
847
848 @property
849 def indicator_value(self) -> bool:
850 return self.__helper.enforced_constraint_indicator_value(self.__index)
851
852 @indicator_value.setter
853 def indicator_value(self, value: bool) -> None:
854 self.__helper.set_enforced_constraint_indicator_value(self.__index, value)
855
856 @property
857 def name(self) -> str:
858 constraint_name = self.__helper.enforced_constraint_name(self.__index)
859 if constraint_name:
860 return constraint_name
861 return f"enforced_linear_constraint#{self.__index}"
862
863 @name.setter
864 def name(self, name: str) -> None:
865 return self.__helper.set_enforced_constraint_name(self.__index, name)
866
867 @property
868 def is_under_specified(self) -> bool:
869 """Returns True if the constraint is under specified.
870
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.
873 """
874 return self.__is_under_specified
875
877 """Raises an exception if the constraint is under specified."""
878 if self.__is_under_specified:
879 raise ValueError(
880 f"Constraint {self.index} is under specified and cannot be modified"
881 )
882
883 def __str__(self):
884 return self.namename
885
886 def __repr__(self):
887 return (
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})"
894 )
895
896 def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
897 """Sets the coefficient of the variable in the constraint."""
899 self.__helper.set_enforced_constraint_coefficient(
900 self.__index, var.index, coeff
901 )
902
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(
907 self.__index, var.index, coeff
908 )
909
910 def clear_terms(self) -> None:
911 """Clear all terms of the constraint."""
913 self.__helper.clear_enforced_constraint_terms(self.__index)
914
915
916class Model:
917 """Methods for building a linear model.
918
919 Methods beginning with:
920
921 * ```new_``` create integer, boolean, or interval variables.
922 * ```add_``` create new constraints and add them to the model.
923 """
924
925 def __init__(self):
926 self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper()
927
928 def clone(self) -> "Model":
929 """Returns a clone of the current model."""
930 clone = Model()
931 clone.helper.overwrite_model(self.helperhelper)
932 return clone
933
934 @typing.overload
935 def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ...
936
937 @typing.overload
938 def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ...
939
941 self, constraints: Optional[_IndexOrSeries] = None
942 ) -> _IndexOrSeries:
943 if constraints is None:
944 return self.get_linear_constraints()
945 return constraints
946
947 @typing.overload
948 def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ...
949
950 @typing.overload
951 def _get_variables(self, variables: pd.Series) -> pd.Series: ...
952
954 self, variables: Optional[_IndexOrSeries] = None
955 ) -> _IndexOrSeries:
956 if variables is None:
957 return self.get_variables()
958 return variables
959
960 def get_linear_constraints(self) -> pd.Index:
961 """Gets all linear constraints in the model."""
962 return pd.Index(
964 name="linear_constraint",
965 )
966
968 self, constraints: Optional[_IndexOrSeries] = None
969 ) -> pd.Series:
970 """Gets the expressions of all linear constraints in the set.
971
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
975 dimensions.
976
977 Args:
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.
981
982 Returns:
983 pd.Series: The expressions of all linear constraints in the set.
984 """
985 return _attribute_series(
986 # pylint: disable=g-long-lambda
987 func=lambda c: _as_flat_linear_expression(
988 # pylint: disable=g-complex-comprehension
989 sum(
990 coeff * Variable(self.__helper, var_id, None, None, None)
991 for var_id, coeff in zip(
992 c.helper.constraint_var_indices(c.index),
993 c.helper.constraint_coefficients(c.index),
994 )
995 )
996 ),
998 )
999
1001 self, constraints: Optional[_IndexOrSeries] = None
1002 ) -> pd.Series:
1003 """Gets the lower bounds of all linear constraints in the set.
1004
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
1008 dimensions.
1009
1010 Args:
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.
1014
1015 Returns:
1016 pd.Series: The lower bounds of all linear constraints in the set.
1017 """
1018 return _attribute_series(
1019 func=lambda c: c.lower_bound, # pylint: disable=protected-access
1021 )
1022
1024 self, constraints: Optional[_IndexOrSeries] = None
1025 ) -> pd.Series:
1026 """Gets the upper bounds of all linear constraints in the set.
1027
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
1031 dimensions.
1032
1033 Args:
1034 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1035 constraints. If unspecified, all linear constraints will be in scope.
1036
1037 Returns:
1038 pd.Series: The upper bounds of all linear constraints in the set.
1039 """
1040 return _attribute_series(
1041 func=lambda c: c.upper_bound, # pylint: disable=protected-access
1043 )
1044
1045 def get_variables(self) -> pd.Index:
1046 """Gets all variables in the model."""
1047 return pd.Index(
1048 [self.var_from_index(i) for i in range(self.num_variablesnum_variables)],
1049 name="variable",
1050 )
1051
1053 self, variables: Optional[_IndexOrSeries] = None
1054 ) -> pd.Series:
1055 """Gets the lower bounds of all variables in the set.
1056
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
1060 dimensions.
1061
1062 Args:
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
1065 be in scope.
1066
1067 Returns:
1068 pd.Series: The lower bounds of all variables in the set.
1069 """
1070 return _attribute_series(
1071 func=lambda v: v.lower_bound, # pylint: disable=protected-access
1072 values=self._get_variables_get_variables_get_variables(variables),
1073 )
1074
1076 self, variables: Optional[_IndexOrSeries] = None
1077 ) -> pd.Series:
1078 """Gets the upper bounds of all variables in the set.
1079
1080 Args:
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
1083 be in scope.
1084
1085 Returns:
1086 pd.Series: The upper bounds of all variables in the set.
1087 """
1088 return _attribute_series(
1089 func=lambda v: v.upper_bound, # pylint: disable=protected-access
1090 values=self._get_variables_get_variables_get_variables(variables),
1091 )
1092
1093 # Integer variable.
1094
1096 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
1097 ) -> Variable:
1098 """Create an integer variable with domain [lb, ub].
1099
1100 Args:
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.
1105
1106 Returns:
1107 a variable whose domain is [lb, ub].
1108 """
1109
1110 return Variable(self.__helper, lb, ub, is_integer, name)
1111
1113 self, lb: NumberT, ub: NumberT, name: Optional[str] = None
1114 ) -> Variable:
1115 """Create an integer variable with domain [lb, ub].
1116
1117 Args:
1118 lb: Lower bound of the variable.
1119 ub: Upper bound of the variable.
1120 name: The name of the variable.
1121
1122 Returns:
1123 a variable whose domain is [lb, ub].
1124 """
1125
1126 return self.new_var(lb, ub, True, name)
1127
1129 self, lb: NumberT, ub: NumberT, name: Optional[str] = None
1130 ) -> Variable:
1131 """Create an integer variable with domain [lb, ub].
1132
1133 Args:
1134 lb: Lower bound of the variable.
1135 ub: Upper bound of the variable.
1136 name: The name of the variable.
1137
1138 Returns:
1139 a variable whose domain is [lb, ub].
1140 """
1141
1142 return self.new_var(lb, ub, False, name)
1143
1144 def new_bool_var(self, name: Optional[str] = None) -> Variable:
1145 """Creates a 0-1 variable with the given name."""
1146 return self.new_var(
1147 0, 1, True, name
1148 ) # pytype: disable=wrong-arg-types # numpy-scalars
1149
1150 def new_constant(self, value: NumberT) -> Variable:
1151 """Declares a constant variable."""
1152 return self.new_var(value, value, False, None)
1153
1155 self,
1156 name: str,
1157 index: pd.Index,
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,
1161 ) -> pd.Series:
1162 """Creates a series of (scalar-valued) variables with the given name.
1163
1164 Args:
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.
1176
1177 Returns:
1178 pd.Series: The variable set indexed by its corresponding dimensions.
1179
1180 Raises:
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.
1186 """
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))
1191 if (
1192 mbn.is_a_number(lower_bounds)
1193 and mbn.is_a_number(upper_bounds)
1194 and lower_bounds > upper_bounds
1195 ):
1196 raise ValueError(
1197 "lower_bound={} is greater than upper_bound={} for variable set={}".format(
1198 lower_bounds, upper_bounds, name
1199 )
1200 )
1201 if (
1202 isinstance(is_integral, bool)
1203 and is_integral
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)
1209 ):
1210 raise ValueError(
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)
1215 )
1216 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
1217 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
1218 is_integrals = _convert_to_series_and_validate_index(is_integral, index)
1219 return pd.Series(
1220 index=index,
1221 data=[
1222 # pylint: disable=g-complex-comprehension
1223 Variable(
1224 helper=self.__helper,
1225 name=f"{name}[{i}]",
1226 lb=lower_bounds[i],
1227 ub=upper_bounds[i],
1228 is_integral=is_integrals[i],
1229 )
1230 for i in index
1231 ],
1232 )
1233
1235 self,
1236 name: str,
1237 index: pd.Index,
1238 lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1239 upper_bounds: Union[NumberT, pd.Series] = math.inf,
1240 ) -> pd.Series:
1241 """Creates a series of continuous variables with the given name.
1242
1243 Args:
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.
1252
1253 Returns:
1254 pd.Series: The variable set indexed by its corresponding dimensions.
1255
1256 Raises:
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.
1262 """
1263 return self.new_var_series(name, index, lower_bounds, upper_bounds, False)
1264
1266 self,
1267 name: str,
1268 index: pd.Index,
1269 lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1270 upper_bounds: Union[NumberT, pd.Series] = math.inf,
1271 ) -> pd.Series:
1272 """Creates a series of integer variables with the given name.
1273
1274 Args:
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.
1283
1284 Returns:
1285 pd.Series: The variable set indexed by its corresponding dimensions.
1286
1287 Raises:
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.
1293 """
1294 return self.new_var_series(name, index, lower_bounds, upper_bounds, True)
1295
1297 self,
1298 name: str,
1299 index: pd.Index,
1300 ) -> pd.Series:
1301 """Creates a series of Boolean variables with the given name.
1302
1303 Args:
1304 name (str): Required. The name of the variable set.
1305 index (pd.Index): Required. The index to use for the variable set.
1306
1307 Returns:
1308 pd.Series: The variable set indexed by its corresponding dimensions.
1309
1310 Raises:
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.
1316 """
1317 return self.new_var_series(name, index, 0, 1, True)
1318
1319 def var_from_index(self, index: IntegerT) -> Variable:
1320 """Rebuilds a variable object from the model and its index."""
1321 return Variable(self.__helper, index, None, None, None)
1322
1323 # Linear constraints.
1324
1325 def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars
1326 self,
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."""
1333 ct = LinearConstraint(self.__helper)
1334 if 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):
1344 flat_expr = _as_flat_linear_expression(linear_expr)
1345 # pylint: disable=protected-access
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
1350 )
1351 else:
1352 raise TypeError(
1353 f"Not supported: Model.add_linear_constraint({linear_expr})"
1354 f" with type {type(linear_expr)}"
1355 )
1356 return ct
1357
1358 def add(
1359 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None
1360 ) -> Union[LinearConstraint, pd.Series]:
1361 """Adds a `BoundedLinearExpression` to the model.
1362
1363 Args:
1364 ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1365 name: An optional name.
1366
1367 Returns:
1368 An instance of the `Constraint` class.
1369
1370 Note that a special treatment is done when the argument does not contain any
1371 variable, and thus evaluates to True or False.
1372
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
1375 thereafter.
1376
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
1379 thereafter.
1380
1381 you can check the if a constraint is under specified by reading the
1382 `LinearConstraint.is_under_specified` property.
1383 """
1384 if isinstance(ct, _BoundedLinearExpr):
1385 return ct._add_linear_constraint(self.__helper, name)
1386 elif isinstance(ct, bool):
1387 return _add_linear_constraint_to_helper(ct, self.__helper, name)
1388 elif isinstance(ct, pd.Series):
1389 return pd.Series(
1390 index=ct.index,
1391 data=[
1393 expr, self.__helper, f"{name}[{i}]"
1394 )
1395 for (i, expr) in zip(ct.index, ct)
1396 ],
1397 )
1398 else:
1399 raise TypeError("Not supported: Model.add(" + str(ct) + ")")
1400
1401 def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
1402 """Rebuilds a linear constraint object from the model and its index."""
1403 return LinearConstraint(self.__helper, index=index)
1404
1405 # EnforcedLinear constraints.
1406
1407 def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars
1408 self,
1409 linear_expr: LinearExprT,
1410 ivar: "Variable",
1411 ivalue: bool,
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
1420 if name:
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):
1430 flat_expr = _as_flat_linear_expression(linear_expr)
1431 # pylint: disable=protected-access
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
1436 )
1437 else:
1438 raise TypeError(
1439 "Not supported:"
1440 f" Model.add_enforced_linear_constraint({linear_expr}) with"
1441 f" type {type(linear_expr)}"
1442 )
1443 return ct
1444
1446 self,
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.
1453
1454 Args:
1455 ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1456 var: The indicator variable
1457 value: the indicator value
1458 name: An optional name.
1459
1460 Returns:
1461 An instance of the `Constraint` class.
1462
1463 Note that a special treatment is done when the argument does not contain any
1464 variable, and thus evaluates to True or False.
1465
1466 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
1467 sum <= 0
1468
1469 model.add_enforced(False, var, value) will create a constraint inf <=
1470 empty sum <= -inf
1471
1472 you can check the if a constraint is always false (lb=inf, ub=-inf) by
1473 calling EnforcedLinearConstraint.is_always_false()
1474 """
1475 if isinstance(ct, _BoundedLinearExpr):
1476 return ct._add_enforced_linear_constraint(self.__helper, var, value, name)
1477 elif (
1478 isinstance(ct, bool)
1479 and isinstance(var, Variable)
1480 and isinstance(value, bool)
1481 ):
1483 ct, self.__helper, var, value, name
1484 )
1485 elif isinstance(ct, pd.Series):
1486 ivar_series = _convert_to_var_series_and_validate_index(var, ct.index)
1487 ivalue_series = _convert_to_series_and_validate_index(value, ct.index)
1488 return pd.Series(
1489 index=ct.index,
1490 data=[
1492 expr,
1493 self.__helper,
1494 ivar_series[i],
1495 ivalue_series[i],
1496 f"{name}[{i}]",
1497 )
1498 for (i, expr) in zip(ct.index, ct)
1499 ],
1500 )
1501 else:
1502 raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")")
1503
1505 self, index: IntegerT
1506 ) -> EnforcedLinearConstraint:
1507 """Rebuilds an enforced linear constraint object from the model and its index."""
1508 return EnforcedLinearConstraint(self.__helper, index=index)
1509
1510 # Objective.
1511 def minimize(self, linear_expr: LinearExprT) -> None:
1512 """Minimizes the given objective."""
1513 self.__optimize(linear_expr, False)
1514
1515 def maximize(self, linear_expr: LinearExprT) -> None:
1516 """Maximizes the given objective."""
1517 self.__optimize(linear_expr, True)
1518
1519 def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None:
1520 """Defines the objective."""
1521 self.helperhelper.clear_objective()
1522 self.__helper.set_maximize(maximize)
1523 if mbn.is_a_number(linear_expr):
1524 self.helperhelper.set_objective_offset(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):
1528 flat_expr = _as_flat_linear_expression(linear_expr)
1529 # pylint: disable=protected-access
1530 self.helperhelper.set_objective_offset(flat_expr._offset)
1531 self.helperhelper.set_objective_coefficients(
1532 flat_expr._variable_indices, flat_expr._coefficients
1533 )
1534 else:
1535 raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})")
1536
1537 @property
1538 def objective_offset(self) -> np.double:
1539 """Returns the fixed offset of the objective."""
1540 return self.__helper.objective_offset()
1541
1542 @objective_offset.setter
1543 def objective_offset(self, value: NumberT) -> None:
1544 self.__helper.set_objective_offset(value)
1545
1546 def objective_expression(self) -> "_LinearExpression":
1547 """Returns the expression to optimize."""
1549 sum(
1550 variable * self.__helper.var_objective_coefficient(variable.index)
1551 for variable in self.get_variables()
1552 if self.__helper.var_objective_coefficient(variable.index) != 0.0
1553 )
1554 + self.__helper.objective_offset()
1555 )
1556
1557 # Hints.
1558 def clear_hints(self):
1559 """Clears all solution hints."""
1560 self.__helper.clear_hints()
1561
1562 def add_hint(self, var: Variable, value: NumberT) -> None:
1563 """Adds var == value as a hint to the model.
1564
1565 Args:
1566 var: The variable of the hint
1567 value: The value of the hint
1568
1569 Note that variables must not appear more than once in the list of hints.
1570 """
1571 self.__helper.add_hint(var.index, value)
1572
1573 # Input/Output
1574 def export_to_lp_string(self, obfuscate: bool = False) -> str:
1575 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1576 options.obfuscate = obfuscate
1577 return self.__helper.export_to_lp_string(options)
1578
1579 def export_to_mps_string(self, obfuscate: bool = False) -> str:
1580 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1581 options.obfuscate = obfuscate
1582 return self.__helper.export_to_mps_string(options)
1583
1584 def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool:
1585 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1586 options.obfuscate = obfuscate
1587 return self.__helper.write_to_mps_file(filename, options)
1588
1589 def export_to_proto(self) -> linear_solver_pb2.MPModelProto:
1590 """Exports the optimization model to a ProtoBuf format."""
1591 return mbh.to_mpmodel_proto(self.__helper)
1592
1593 def import_from_mps_string(self, mps_string: str) -> bool:
1594 """Reads a model from a MPS string."""
1595 return self.__helper.import_from_mps_string(mps_string)
1596
1597 def import_from_mps_file(self, mps_file: str) -> bool:
1598 """Reads a model from a .mps file."""
1599 return self.__helper.import_from_mps_file(mps_file)
1600
1601 def import_from_lp_string(self, lp_string: str) -> bool:
1602 """Reads a model from a LP string."""
1603 return self.__helper.import_from_lp_string(lp_string)
1604
1605 def import_from_lp_file(self, lp_file: str) -> bool:
1606 """Reads a model from a .lp file."""
1607 return self.__helper.import_from_lp_file(lp_file)
1608
1609 def import_from_proto_file(self, proto_file: str) -> bool:
1610 """Reads a model from a proto file."""
1611 return self.__helper.read_model_from_proto_file(proto_file)
1612
1613 def export_to_proto_file(self, proto_file: str) -> bool:
1614 """Writes a model to a proto file."""
1615 return self.__helper.write_model_to_proto_file(proto_file)
1616
1617 # Model getters and Setters
1618
1619 @property
1620 def num_variables(self) -> int:
1621 """Returns the number of variables in the model."""
1622 return self.__helper.num_variables()
1623
1624 @property
1625 def num_constraints(self) -> int:
1626 """The number of constraints in the model."""
1627 return self.__helper.num_constraints()
1628
1629 @property
1630 def name(self) -> str:
1631 """The name of the model."""
1632 return self.__helper.name()
1633
1634 @name.setter
1635 def name(self, name: str):
1636 self.__helper.set_name(name)
1637
1638 @property
1639 def helper(self) -> mbh.ModelBuilderHelper:
1640 """Returns the model builder helper."""
1641 return self.__helper
1642
1643
1645 """Main solver class.
1646
1647 The purpose of this class is to search for a solution to the model provided
1648 to the solve() method.
1649
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
1652 procedure.
1653 """
1654
1655 def __init__(self, solver_name: str):
1656 self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name)
1657 self.log_callback: Optional[Callable[[str], None]] = None
1658
1659 def solver_is_supported(self) -> bool:
1660 """Checks whether the requested solver backend was found."""
1662
1663 # Solver backend and parameters.
1664 def set_time_limit_in_seconds(self, limit: NumberT) -> None:
1665 """Sets a time limit for the solve() call."""
1667
1668 def set_solver_specific_parameters(self, parameters: str) -> None:
1669 """Sets parameters specific to the solver backend."""
1671
1672 def enable_output(self, enabled: bool) -> None:
1673 """Controls the solver backend logs."""
1674 self.__solve_helper.enable_output(enabled)
1675
1676 def solve(self, model: Model) -> SolveStatus:
1677 """Solves a problem and passes each solution to the callback if not null."""
1678 if self.log_callback is not None:
1679 self.__solve_helper.set_log_callback(self.log_callback)
1680 else:
1681 self.__solve_helper.clear_log_callback()
1682 self.__solve_helper.solve(model.helper)
1683 return SolveStatus(self.__solve_helper.status())
1684
1685 def stop_search(self):
1686 """Stops the current search asynchronously."""
1687 self.__solve_helper.interrupt_solve()
1688
1689 def value(self, expr: LinearExprT) -> np.double:
1690 """Returns the value of a linear expression after solve."""
1691 if not self.__solve_helper.has_solution():
1692 return pd.NA
1693 if mbn.is_a_number(expr):
1694 return expr
1695 elif isinstance(expr, Variable):
1696 return self.__solve_helper.var_value(expr.index)
1697 elif isinstance(expr, LinearExpr):
1698 flat_expr = _as_flat_linear_expression(expr)
1699 return self.__solve_helper.expression_value(
1700 flat_expr._variable_indices,
1701 flat_expr._coefficients,
1702 flat_expr._offset,
1703 )
1704 else:
1705 raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}")
1706
1707 def values(self, variables: _IndexOrSeries) -> pd.Series:
1708 """Returns the values of the input variables.
1709
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
1713 dimensions.
1714
1715 Args:
1716 variables (Union[pd.Index, pd.Series]): The set of variables from which to
1717 get the values.
1718
1719 Returns:
1720 pd.Series: The values of all variables in the set.
1721 """
1722 if not self.__solve_helper.has_solution():
1723 return _attribute_series(func=lambda v: pd.NA, values=variables)
1724 return _attribute_series(
1725 func=lambda v: self.__solve_helper.var_value(v.index),
1726 values=variables,
1727 )
1728
1729 def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series:
1730 """Returns the reduced cost of the input variables.
1731
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
1735 dimensions.
1736
1737 Args:
1738 variables (Union[pd.Index, pd.Series]): The set of variables from which to
1739 get the values.
1740
1741 Returns:
1742 pd.Series: The reduced cost of all variables in the set.
1743 """
1744 if not self.__solve_helper.has_solution():
1745 return _attribute_series(func=lambda v: pd.NA, values=variables)
1746 return _attribute_series(
1747 func=lambda v: self.__solve_helper.reduced_cost(v.index),
1748 values=variables,
1749 )
1750
1751 def reduced_cost(self, var: Variable) -> np.double:
1752 """Returns the reduced cost of a linear expression after solve."""
1753 if not self.__solve_helper.has_solution():
1754 return pd.NA
1755 return self.__solve_helper.reduced_cost(var.index)
1756
1757 def dual_values(self, constraints: _IndexOrSeries) -> pd.Series:
1758 """Returns the dual values of the input constraints.
1759
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
1763 dimensions.
1764
1765 Args:
1766 constraints (Union[pd.Index, pd.Series]): The set of constraints from
1767 which to get the dual values.
1768
1769 Returns:
1770 pd.Series: The dual_values of all constraints in the set.
1771 """
1772 if not self.__solve_helper.has_solution():
1773 return _attribute_series(func=lambda v: pd.NA, values=constraints)
1774 return _attribute_series(
1775 func=lambda v: self.__solve_helper.dual_value(v.index),
1776 values=constraints,
1777 )
1778
1779 def dual_value(self, ct: LinearConstraint) -> np.double:
1780 """Returns the dual value of a linear constraint after solve."""
1781 if not self.__solve_helper.has_solution():
1782 return pd.NA
1783 return self.__solve_helper.dual_value(ct.index)
1784
1785 def activity(self, ct: LinearConstraint) -> np.double:
1786 """Returns the activity of a linear constraint after solve."""
1787 if not self.__solve_helper.has_solution():
1788 return pd.NA
1789 return self.__solve_helper.activity(ct.index)
1790
1791 @property
1792 def objective_value(self) -> np.double:
1793 """Returns the value of the objective after solve."""
1794 if not self.__solve_helper.has_solution():
1795 return pd.NA
1796 return self.__solve_helper.objective_value()
1797
1798 @property
1799 def best_objective_bound(self) -> np.double:
1800 """Returns the best lower (upper) bound found when min(max)imizing."""
1801 if not self.__solve_helper.has_solution():
1802 return pd.NA
1804
1805 @property
1806 def status_string(self) -> str:
1807 """Returns additional information of the last solve.
1808
1809 It can describe why the model is invalid.
1810 """
1811 return self.__solve_helper.status_string()
1812
1813 @property
1814 def wall_time(self) -> np.double:
1815 return self.__solve_helper.wall_time()
1816
1817 @property
1818 def user_time(self) -> np.double:
1819 return self.__solve_helper.user_time()
1820
1821
1822# The maximum number of terms to display in a linear expression's repr.
1823_MAX_LINEAR_EXPRESSION_REPR_TERMS = 5
1824
1825
1826@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1828 """For variables x, an expression: offset + sum_{i in I} coeff_i * x_i."""
1829
1830 __slots__ = ("_variable_indices", "_coefficients", "_offset", "_helper")
1831
1832 _variable_indices: npt.NDArray[np.int32]
1833 _coefficients: npt.NDArray[np.double]
1834 _offset: float
1835 _helper: Optional[mbh.ModelBuilderHelper]
1836
1837 @property
1838 def variable_indices(self) -> npt.NDArray[np.int32]:
1839 return self._variable_indices
1840
1841 @property
1842 def coefficients(self) -> npt.NDArray[np.double]:
1843 return self._coefficients
1844
1845 @property
1846 def constant(self) -> float:
1847 return self._offset
1848
1849 @property
1850 def helper(self) -> Optional[mbh.ModelBuilderHelper]:
1851 return self._helper
1852
1853 def __repr__(self):
1854 return self.__str__()
1855
1856 def __str__(self):
1857 if self._helper is None:
1858 return str(self._offset)
1859
1860 result = []
1862 if len(result) >= _MAX_LINEAR_EXPRESSION_REPR_TERMS:
1863 result.append(" + ...")
1864 break
1865 var_name = Variable(self._helper, index, None, None, None).name
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}")
1870 elif not result:
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}")
1876 elif coeff > 0.0:
1877 result.append(f" + {coeff} * {var_name}")
1878 elif coeff < 0.0:
1879 result.append(f" - {-coeff} * {var_name}")
1880
1881 if not result:
1882 return f"{self.constant}"
1883 if self.constant > 0:
1884 result.append(f" + {self.constant}")
1885 elif self.constant < 0:
1886 result.append(f" - {-self.constant}")
1887 return "".join(result)
1888
1889
1891 to_process: List[Tuple[LinearExprT, float]], offset: float = 0.0
1892) -> _LinearExpression:
1893 """Creates a _LinearExpression as the sum of terms."""
1894 indices = []
1895 coeffs = []
1896 helper = None
1897 while to_process: # Flatten AST of LinearTypes.
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])
1905 if helper is None:
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))
1916 if helper is None:
1917 helper = expr._helper
1918 else:
1919 raise TypeError(
1920 "Unrecognized linear expression: " + str(expr) + f" {type(expr)}"
1921 )
1922
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
1928 )
1929 return _LinearExpression(sorted_indices, sorted_coefficients, offset, helper)
1930 else:
1931 assert not indices
1932 assert not coeffs
1933 return _LinearExpression(
1934 _variable_indices=np.zeros(dtype=np.int32, shape=[0]),
1935 _coefficients=np.zeros(dtype=np.double, shape=[0]),
1936 _offset=offset,
1937 _helper=None,
1938 )
1939
1940
1941def _as_flat_linear_expression(base_expr: LinearExprT) -> _LinearExpression:
1942 """Converts floats, ints and Linear objects to a LinearExpression."""
1943 if isinstance(base_expr, _LinearExpression):
1944 return base_expr
1945 return _sum_as_flat_linear_expression(to_process=[(base_expr, 1.0)], offset=0.0)
1946
1947
1948@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1950 """Represents the (deferred) sum of two expressions."""
1951
1952 __slots__ = ("_left", "_right")
1953
1954 _left: LinearExprT
1955 _right: LinearExprT
1956
1957 def __repr__(self):
1958 return self.__str__()
1959
1960 def __str__(self):
1961 return str(_as_flat_linear_expression(self))
1962
1963
1964@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1966 """Represents the (deferred) product of an expression by a constant."""
1967
1968 __slots__ = ("_expression", "_coefficient")
1969
1970 _expression: LinearExpr
1971 _coefficient: NumberT
1972
1973 def __repr__(self):
1974 return self.__str__()
1975
1976 def __str__(self):
1977 return str(_as_flat_linear_expression(self))
1978
1979
1980def _get_index(obj: _IndexOrSeries) -> pd.Index:
1981 """Returns the indices of `obj` as a `pd.Index`."""
1982 if isinstance(obj, pd.Series):
1983 return obj.index
1984 return obj
1985
1986
1988 *,
1989 func: Callable[[_VariableOrConstraint], NumberT],
1990 values: _IndexOrSeries,
1991) -> pd.Series:
1992 """Returns the attributes of `values`.
1993
1994 Args:
1995 func: The function to call for getting the attribute data.
1996 values: The values that the function will be applied (element-wise) to.
1997
1998 Returns:
1999 pd.Series: The attribute values.
2000 """
2001 return pd.Series(
2002 data=[func(v) for v in values],
2003 index=_get_index(values),
2004 )
2005
2006
2008 value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index
2009) -> pd.Series:
2010 """Returns a pd.Series of the given index with the corresponding values.
2011
2012 Args:
2013 value_or_series: the values to be converted (if applicable).
2014 index: the index of the resulting pd.Series.
2015
2016 Returns:
2017 pd.Series: The set of values with the given index.
2018
2019 Raises:
2020 TypeError: If the type of `value_or_series` is not recognized.
2021 ValueError: If the index does not match.
2022 """
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
2028 else:
2029 raise ValueError("index does not match")
2030 else:
2031 raise TypeError("invalid type={}".format(type(value_or_series)))
2032 return result
2033
2034
2036 var_or_series: Union["Variable", pd.Series], index: pd.Index
2037) -> pd.Series:
2038 """Returns a pd.Series of the given index with the corresponding values.
2039
2040 Args:
2041 var_or_series: the variables to be converted (if applicable).
2042 index: the index of the resulting pd.Series.
2043
2044 Returns:
2045 pd.Series: The set of values with the given index.
2046
2047 Raises:
2048 TypeError: If the type of `value_or_series` is not recognized.
2049 ValueError: If the index does not match.
2050 """
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
2056 else:
2057 raise ValueError("index does not match")
2058 else:
2059 raise TypeError("invalid type={}".format(type(var_or_series)))
2060 return result
2061
2062
2063# Compatibility.
2064ModelBuilder = Model
2065ModelSolver = Solver
"EnforcedLinearConstraint" _add_enforced_linear_constraint(self, mbh.ModelBuilderHelper helper, Variable var, bool value, Optional[str] name)
None __init__(self, LinearExprT expr, NumberT lb, NumberT ub)
"LinearConstraint" _add_linear_constraint(self, mbh.ModelBuilderHelper helper, Optional[str] name)
None __init__(self, mbh.ModelBuilderHelper helper, *, Optional[IntegerT] index=None, bool is_under_specified=False)
None set_coefficient(self, Variable var, NumberT coeff)
None __init__(self, mbh.ModelBuilderHelper helper, *, Optional[IntegerT] index=None, bool is_under_specified=False)
None add_term(self, Variable var, NumberT coeff)
"BoundedLinearExpression" __eq__(self, LinearExprT arg)
Union[NumberT, "_LinearExpression"] weighted_sum(cls, Sequence[LinearExprT] expressions, Sequence[NumberT] coefficients, *, NumberT constant=0.0)
LinearExprT sum(cls, Sequence[LinearExprT] expressions, *, NumberT constant=0.0)
LinearExprT term(cls, LinearExprT expression, NumberT coefficient, *, NumberT constant=0.0)
"BoundedLinearExpression" __ge__(self, LinearExprT arg)
"BoundedLinearExpression" __le__(self, LinearExprT arg)
pd.Series new_num_var_series(self, str name, pd.Index index, Union[NumberT, pd.Series] lower_bounds=-math.inf, Union[NumberT, pd.Series] upper_bounds=math.inf)
pd.Series get_linear_constraint_expressions(self, Optional[_IndexOrSeries] constraints=None)
Variable var_from_index(self, IntegerT index)
EnforcedLinearConstraint enforced_linear_constraint_from_index(self, IntegerT index)
LinearConstraint add_linear_constraint(self, LinearExprT linear_expr, NumberT lb=-math.inf, NumberT ub=math.inf, Optional[str] name=None)
Variable new_int_var(self, NumberT lb, NumberT ub, Optional[str] name=None)
pd.Series get_linear_constraint_lower_bounds(self, Optional[_IndexOrSeries] constraints=None)
_IndexOrSeries _get_variables(self, Optional[_IndexOrSeries] variables=None)
Variable new_num_var(self, NumberT lb, NumberT ub, Optional[str] name=None)
pd.Index _get_variables(self, Optional[pd.Index] variables)
Variable new_var(self, NumberT lb, NumberT ub, bool is_integer, Optional[str] name)
None add_hint(self, Variable var, NumberT value)
_IndexOrSeries _get_linear_constraints(self, Optional[_IndexOrSeries] constraints=None)
pd.Series new_int_var_series(self, str name, pd.Index index, Union[NumberT, pd.Series] lower_bounds=-math.inf, Union[NumberT, pd.Series] upper_bounds=math.inf)
pd.Series _get_linear_constraints(self, pd.Series constraints)
pd.Series new_var_series(self, str name, pd.Index index, Union[NumberT, pd.Series] lower_bounds=-math.inf, Union[NumberT, pd.Series] upper_bounds=math.inf, Union[bool, pd.Series] is_integral=False)
pd.Series get_variable_upper_bounds(self, Optional[_IndexOrSeries] variables=None)
pd.Index _get_linear_constraints(self, Optional[pd.Index] constraints)
str export_to_mps_string(self, bool obfuscate=False)
linear_solver_pb2.MPModelProto export_to_proto(self)
None __optimize(self, LinearExprT linear_expr, bool maximize)
pd.Series new_bool_var_series(self, str name, pd.Index index)
None maximize(self, LinearExprT linear_expr)
Union[EnforcedLinearConstraint, pd.Series] add_enforced(self, Union[ConstraintT, pd.Series] ct, Union[Variable, pd.Series] var, Union[bool, pd.Series] value, Optional[str] name=None)
pd.Series get_linear_constraint_upper_bounds(self, Optional[_IndexOrSeries] constraints=None)
None minimize(self, LinearExprT linear_expr)
bool write_to_mps_file(self, str filename, bool obfuscate=False)
pd.Series _get_variables(self, pd.Series variables)
Union[LinearConstraint, pd.Series] add(self, Union[ConstraintT, pd.Series] ct, Optional[str] name=None)
str export_to_lp_string(self, bool obfuscate=False)
pd.Series get_variable_lower_bounds(self, Optional[_IndexOrSeries] variables=None)
LinearConstraint linear_constraint_from_index(self, IntegerT index)
Variable new_bool_var(self, Optional[str] name=None)
EnforcedLinearConstraint add_enforced_linear_constraint(self, LinearExprT linear_expr, "Variable" ivar, bool ivalue, NumberT lb=-math.inf, NumberT ub=math.inf, Optional[str] name=None)
pd.Series reduced_costs(self, _IndexOrSeries variables)
pd.Series dual_values(self, _IndexOrSeries constraints)
np.double activity(self, LinearConstraint ct)
pd.Series values(self, _IndexOrSeries variables)
np.double dual_value(self, LinearConstraint ct)
None set_solver_specific_parameters(self, str parameters)
"EnforcedLinearConstraint" _add_enforced_linear_constraint(self, mbh.ModelBuilderHelper helper, Variable var, bool value, str name)
"LinearConstraint" _add_linear_constraint(self, mbh.ModelBuilderHelper helper, str name)
None __init__(self, mbh.ModelBuilderHelper helper, NumberT lb, Optional[NumberT] ub, Optional[bool] is_integral, Optional[str] name)
ConstraintT __eq__(self, Optional[LinearExprT] arg)
"LinearConstraint" _add_linear_constraint(self, mbh.ModelBuilderHelper helper, str name)
"EnforcedLinearConstraint" _add_enforced_linear_constraint(self, mbh.ModelBuilderHelper helper, Variable var, bool value, str name)
pd.Series _convert_to_var_series_and_validate_index(Union["Variable", pd.Series] var_or_series, pd.Index index)
pd.Index _get_index(_IndexOrSeries obj)
_LinearExpression _sum_as_flat_linear_expression(List[Tuple[LinearExprT, float]] to_process, float offset=0.0)
pd.Series _convert_to_series_and_validate_index(Union[bool, NumberT, pd.Series] value_or_series, pd.Index index)
_LinearExpression _as_flat_linear_expression(LinearExprT base_expr)
_add_linear_constraint_to_helper(Union[bool, _BoundedLinearExpr] bounded_expr, mbh.ModelBuilderHelper helper, Optional[str] name)
_add_enforced_linear_constraint_to_helper(Union[bool, _BoundedLinearExpr] bounded_expr, mbh.ModelBuilderHelper helper, Variable var, bool value, Optional[str] name)
pd.Series _attribute_series(*, Callable[[_VariableOrConstraint], NumberT] func, _IndexOrSeries values)