ortools.linear_solver.python.model_builder
Methods for building and solving model_builder models.
The following two sections describe the main methods for building and solving those models.
Model
: Methods for creating models, including variables and constraints.Solver
: Methods for solving a model and evaluating solutions.
Additional methods for solving Model models:
Constraint
: A few utility methods for modifying constraints created byModel
.LinearExpr
: Methods for creating constraints and the objective from large arrays of coefficients.
Other methods and functions listed are primarily used for developing OR-Tools, rather than for solving specific optimization problems.
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 152 return _sum_as_flat_linear_expression( 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): 186 return _as_flat_linear_expression( 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": 225 return BoundedLinearExpression( 226 self - arg, 0, math.inf 227 ) # pytype: disable=wrong-arg-types # numpy-scalars 228 229 def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression": 230 return BoundedLinearExpression( 231 self - arg, -math.inf, 0 232 ) # pytype: disable=wrong-arg-types # numpy-scalars 233 234 235class Variable(LinearExpr): 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 248 def __init__( 249 self, 250 helper: mbh.ModelBuilderHelper, 251 lb: NumberT, 252 ub: Optional[NumberT], 253 is_integral: Optional[bool], 254 name: Optional[str], 255 ): 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.index == other.index and self.helper == other.helper 296 297 def __str__(self) -> str: 298 return self.name 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) 359 return BoundedLinearExpression( 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 378 def _add_linear_constraint( 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 392 def _add_enforced_linear_constraint( 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 413def _add_linear_constraint_to_helper( 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 Args: 424 bounded_expr: The bounded expression used to create the constraint. 425 helper: The helper to create the constraint. 426 name: The name of the constraint to be created. 427 428 Returns: 429 LinearConstraint: a constraint in the helper corresponding to the input. 430 431 Raises: 432 TypeError: If constraint is an invalid type. 433 """ 434 if isinstance(bounded_expr, bool): 435 c = LinearConstraint(helper) 436 if name is not None: 437 helper.set_constraint_name(c.index, name) 438 if bounded_expr: 439 # constraint that is always feasible: 0.0 <= nothing <= 0.0 440 helper.set_constraint_lower_bound(c.index, 0.0) 441 helper.set_constraint_upper_bound(c.index, 0.0) 442 else: 443 # constraint that is always infeasible: +oo <= nothing <= -oo 444 helper.set_constraint_lower_bound(c.index, 1) 445 helper.set_constraint_upper_bound(c.index, -1) 446 return c 447 if isinstance(bounded_expr, _BoundedLinearExpr): 448 # pylint: disable=protected-access 449 return bounded_expr._add_linear_constraint(helper, name) 450 raise TypeError("invalid type={}".format(type(bounded_expr))) 451 452 453def _add_enforced_linear_constraint_to_helper( 454 bounded_expr: Union[bool, _BoundedLinearExpr], 455 helper: mbh.ModelBuilderHelper, 456 var: Variable, 457 value: bool, 458 name: Optional[str], 459): 460 """Creates a new enforced linear constraint in the helper. 461 462 It handles boolean values (which might arise in the construction of 463 BoundedLinearExpressions). 464 465 Args: 466 bounded_expr: The bounded expression used to create the constraint. 467 helper: The helper to create the constraint. 468 var: the variable used in the indicator 469 value: the value used in the indicator 470 name: The name of the constraint to be created. 471 472 Returns: 473 EnforcedLinearConstraint: a constraint in the helper corresponding to the 474 input. 475 476 Raises: 477 TypeError: If constraint is an invalid type. 478 """ 479 if isinstance(bounded_expr, bool): 480 c = EnforcedLinearConstraint(helper) 481 c.indicator_variable = var 482 c.indicator_value = value 483 if name is not None: 484 helper.set_enforced_constraint_name(c.index, name) 485 if bounded_expr: 486 # constraint that is always feasible: 0.0 <= nothing <= 0.0 487 helper.set_enforced_constraint_lower_bound(c.index, 0.0) 488 helper.set_enforced_constraint_upper_bound(c.index, 0.0) 489 else: 490 # constraint that is always infeasible: +oo <= nothing <= -oo 491 helper.set_enforced_constraint_lower_bound(c.index, 1) 492 helper.set_enforced_constraint_upper_bound(c.index, -1) 493 return c 494 if isinstance(bounded_expr, _BoundedLinearExpr): 495 # pylint: disable=protected-access 496 return bounded_expr._add_enforced_linear_constraint(helper, var, value, name) 497 raise TypeError("invalid type={}".format(type(bounded_expr))) 498 499 500@dataclasses.dataclass(repr=False, eq=False, frozen=True) 501class VarEqVar(_BoundedLinearExpr): 502 """Represents var == var.""" 503 504 __slots__ = ("left", "right") 505 506 left: Variable 507 right: Variable 508 509 def __str__(self): 510 return f"{self.left} == {self.right}" 511 512 def __repr__(self): 513 return self.__str__() 514 515 def __bool__(self) -> bool: 516 return hash(self.left) == hash(self.right) 517 518 def _add_linear_constraint( 519 self, helper: mbh.ModelBuilderHelper, name: str 520 ) -> "LinearConstraint": 521 c = LinearConstraint(helper) 522 helper.set_constraint_lower_bound(c.index, 0.0) 523 helper.set_constraint_upper_bound(c.index, 0.0) 524 # pylint: disable=protected-access 525 helper.add_term_to_constraint(c.index, self.left.index, 1.0) 526 helper.add_term_to_constraint(c.index, self.right.index, -1.0) 527 # pylint: enable=protected-access 528 helper.set_constraint_name(c.index, name) 529 return c 530 531 def _add_enforced_linear_constraint( 532 self, 533 helper: mbh.ModelBuilderHelper, 534 var: Variable, 535 value: bool, 536 name: str, 537 ) -> "EnforcedLinearConstraint": 538 """Adds an enforced linear constraint to the model.""" 539 c = EnforcedLinearConstraint(helper) 540 c.indicator_variable = var 541 c.indicator_value = value 542 helper.set_enforced_constraint_lower_bound(c.index, 0.0) 543 helper.set_enforced_constraint_upper_bound(c.index, 0.0) 544 # pylint: disable=protected-access 545 helper.add_term_to_enforced_constraint(c.index, self.left.index, 1.0) 546 helper.add_term_to_enforced_constraint(c.index, self.right.index, -1.0) 547 # pylint: enable=protected-access 548 helper.set_enforced_constraint_name(c.index, name) 549 return c 550 551 552class BoundedLinearExpression(_BoundedLinearExpr): 553 """Represents a linear constraint: `lb <= linear expression <= ub`. 554 555 The only use of this class is to be added to the Model through 556 `Model.add(bounded expression)`, as in: 557 558 model.Add(x + 2 * y -1 >= z) 559 """ 560 561 def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT): 562 self.__expr: LinearExprT = expr 563 self.__lb: np.double = mbn.assert_is_a_number(lb) 564 self.__ub: np.double = mbn.assert_is_a_number(ub) 565 566 def __str__(self) -> str: 567 if self.__lb > -math.inf and self.__ub < math.inf: 568 if self.__lb == self.__ub: 569 return f"{self.__expr} == {self.__lb}" 570 else: 571 return f"{self.__lb} <= {self.__expr} <= {self.__ub}" 572 elif self.__lb > -math.inf: 573 return f"{self.__expr} >= {self.__lb}" 574 elif self.__ub < math.inf: 575 return f"{self.__expr} <= {self.__ub}" 576 else: 577 return f"{self.__expr} free" 578 579 def __repr__(self): 580 return self.__str__() 581 582 @property 583 def expression(self) -> LinearExprT: 584 return self.__expr 585 586 @property 587 def lower_bound(self) -> np.double: 588 return self.__lb 589 590 @property 591 def upper_bound(self) -> np.double: 592 return self.__ub 593 594 def __bool__(self) -> bool: 595 raise NotImplementedError( 596 f"Cannot use a BoundedLinearExpression {self} as a Boolean value" 597 ) 598 599 def _add_linear_constraint( 600 self, helper: mbh.ModelBuilderHelper, name: Optional[str] 601 ) -> "LinearConstraint": 602 c = LinearConstraint(helper) 603 flat_expr = _as_flat_linear_expression(self.__expr) 604 # pylint: disable=protected-access 605 helper.add_terms_to_constraint( 606 c.index, flat_expr._variable_indices, flat_expr._coefficients 607 ) 608 helper.set_constraint_lower_bound(c.index, self.__lb - flat_expr._offset) 609 helper.set_constraint_upper_bound(c.index, self.__ub - flat_expr._offset) 610 # pylint: enable=protected-access 611 if name is not None: 612 helper.set_constraint_name(c.index, name) 613 return c 614 615 def _add_enforced_linear_constraint( 616 self, 617 helper: mbh.ModelBuilderHelper, 618 var: Variable, 619 value: bool, 620 name: Optional[str], 621 ) -> "EnforcedLinearConstraint": 622 """Adds an enforced linear constraint to the model.""" 623 c = EnforcedLinearConstraint(helper) 624 c.indicator_variable = var 625 c.indicator_value = value 626 flat_expr = _as_flat_linear_expression(self.__expr) 627 # pylint: disable=protected-access 628 helper.add_terms_to_enforced_constraint( 629 c.index, flat_expr._variable_indices, flat_expr._coefficients 630 ) 631 helper.set_enforced_constraint_lower_bound( 632 c.index, self.__lb - flat_expr._offset 633 ) 634 helper.set_enforced_constraint_upper_bound( 635 c.index, self.__ub - flat_expr._offset 636 ) 637 # pylint: enable=protected-access 638 if name is not None: 639 helper.set_enforced_constraint_name(c.index, name) 640 return c 641 642 643class LinearConstraint: 644 """Stores a linear equation. 645 646 Example: 647 x = model.new_num_var(0, 10, 'x') 648 y = model.new_num_var(0, 10, 'y') 649 650 linear_constraint = model.add(x + 2 * y == 5) 651 """ 652 653 def __init__( 654 self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None 655 ): 656 if index is None: 657 self.__index = helper.add_linear_constraint() 658 else: 659 self.__index = index 660 self.__helper: mbh.ModelBuilderHelper = helper 661 662 @property 663 def index(self) -> IntegerT: 664 """Returns the index of the constraint in the helper.""" 665 return self.__index 666 667 @property 668 def helper(self) -> mbh.ModelBuilderHelper: 669 """Returns the ModelBuilderHelper instance.""" 670 return self.__helper 671 672 @property 673 def lower_bound(self) -> np.double: 674 return self.__helper.constraint_lower_bound(self.__index) 675 676 @lower_bound.setter 677 def lower_bound(self, bound: NumberT) -> None: 678 self.__helper.set_constraint_lower_bound(self.__index, bound) 679 680 @property 681 def upper_bound(self) -> np.double: 682 return self.__helper.constraint_upper_bound(self.__index) 683 684 @upper_bound.setter 685 def upper_bound(self, bound: NumberT) -> None: 686 self.__helper.set_constraint_upper_bound(self.__index, bound) 687 688 @property 689 def name(self) -> str: 690 constraint_name = self.__helper.constraint_name(self.__index) 691 if constraint_name: 692 return constraint_name 693 return f"linear_constraint#{self.__index}" 694 695 @name.setter 696 def name(self, name: str) -> None: 697 return self.__helper.set_constraint_name(self.__index, name) 698 699 def is_always_false(self) -> bool: 700 """Returns True if the constraint is always false. 701 702 Usually, it means that it was created by model.add(False) 703 """ 704 return self.lower_bound > self.upper_bound 705 706 def __str__(self): 707 return self.name 708 709 def __repr__(self): 710 return ( 711 f"LinearConstraint({self.name}, lb={self.lower_bound}," 712 f" ub={self.upper_bound}," 713 f" var_indices={self.helper.constraint_var_indices(self.index)}," 714 f" coefficients={self.helper.constraint_coefficients(self.index)})" 715 ) 716 717 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 718 """Sets the coefficient of the variable in the constraint.""" 719 if self.is_always_false(): 720 raise ValueError( 721 f"Constraint {self.index} is always false and cannot be modified" 722 ) 723 self.__helper.set_constraint_coefficient(self.__index, var.index, coeff) 724 725 def add_term(self, var: Variable, coeff: NumberT) -> None: 726 """Adds var * coeff to the constraint.""" 727 if self.is_always_false(): 728 raise ValueError( 729 f"Constraint {self.index} is always false and cannot be modified" 730 ) 731 self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff) 732 733 def clear_terms(self) -> None: 734 """Clear all terms of the constraint.""" 735 self.__helper.clear_constraint_terms(self.__index) 736 737 738class EnforcedLinearConstraint: 739 """Stores an enforced linear equation, also name indicator constraint. 740 741 Example: 742 x = model.new_num_var(0, 10, 'x') 743 y = model.new_num_var(0, 10, 'y') 744 z = model.new_bool_var('z') 745 746 enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False) 747 """ 748 749 def __init__( 750 self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None 751 ): 752 if index is None: 753 self.__index = helper.add_enforced_linear_constraint() 754 else: 755 if not helper.is_enforced_linear_constraint(index): 756 raise ValueError( 757 f"the given index {index} does not refer to an enforced linear" 758 " constraint" 759 ) 760 761 self.__index = index 762 self.__helper: mbh.ModelBuilderHelper = helper 763 764 @property 765 def index(self) -> IntegerT: 766 """Returns the index of the constraint in the helper.""" 767 return self.__index 768 769 @property 770 def helper(self) -> mbh.ModelBuilderHelper: 771 """Returns the ModelBuilderHelper instance.""" 772 return self.__helper 773 774 @property 775 def lower_bound(self) -> np.double: 776 return self.__helper.enforced_constraint_lower_bound(self.__index) 777 778 @lower_bound.setter 779 def lower_bound(self, bound: NumberT) -> None: 780 self.__helper.set_enforced_constraint_lower_bound(self.__index, bound) 781 782 @property 783 def upper_bound(self) -> np.double: 784 return self.__helper.enforced_constraint_upper_bound(self.__index) 785 786 @upper_bound.setter 787 def upper_bound(self, bound: NumberT) -> None: 788 self.__helper.set_enforced_constraint_upper_bound(self.__index, bound) 789 790 @property 791 def indicator_variable(self) -> "Variable": 792 enforcement_var_index = ( 793 self.__helper.enforced_constraint_indicator_variable_index(self.__index) 794 ) 795 return Variable(self.__helper, enforcement_var_index, None, None, None) 796 797 @indicator_variable.setter 798 def indicator_variable(self, var: "Variable") -> None: 799 self.__helper.set_enforced_constraint_indicator_variable_index( 800 self.__index, var.index 801 ) 802 803 @property 804 def indicator_value(self) -> bool: 805 return self.__helper.enforced_constraint_indicator_value(self.__index) 806 807 @indicator_value.setter 808 def indicator_value(self, value: bool) -> None: 809 self.__helper.set_enforced_constraint_indicator_value(self.__index, value) 810 811 @property 812 def name(self) -> str: 813 constraint_name = self.__helper.enforced_constraint_name(self.__index) 814 if constraint_name: 815 return constraint_name 816 return f"enforced_linear_constraint#{self.__index}" 817 818 @name.setter 819 def name(self, name: str) -> None: 820 return self.__helper.set_enforced_constraint_name(self.__index, name) 821 822 def is_always_false(self) -> bool: 823 """Returns True if the constraint is always false. 824 825 Usually, it means that it was created by model.add(False) 826 """ 827 return self.lower_bound > self.upper_bound 828 829 def __str__(self): 830 return self.name 831 832 def __repr__(self): 833 return ( 834 f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound}," 835 f" ub={self.upper_bound}," 836 f" var_indices={self.helper.enforced_constraint_var_indices(self.index)}," 837 f" coefficients={self.helper.enforced_constraint_coefficients(self.index)}," 838 f" indicator_variable={self.indicator_variable}" 839 f" indicator_value={self.indicator_value})" 840 ) 841 842 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 843 """Sets the coefficient of the variable in the constraint.""" 844 if self.is_always_false(): 845 raise ValueError( 846 f"Constraint {self.index} is always false and cannot be modified" 847 ) 848 self.__helper.set_enforced_constraint_coefficient( 849 self.__index, var.index, coeff 850 ) 851 852 def add_term(self, var: Variable, coeff: NumberT) -> None: 853 """Adds var * coeff to the constraint.""" 854 if self.is_always_false(): 855 raise ValueError( 856 f"Constraint {self.index} is always false and cannot be modified" 857 ) 858 self.__helper.safe_add_term_to_enforced_constraint( 859 self.__index, var.index, coeff 860 ) 861 862 def clear_terms(self) -> None: 863 """Clear all terms of the constraint.""" 864 self.__helper.clear_enforced_constraint_terms(self.__index) 865 866 867class Model: 868 """Methods for building a linear model. 869 870 Methods beginning with: 871 872 * ```new_``` create integer, boolean, or interval variables. 873 * ```add_``` create new constraints and add them to the model. 874 """ 875 876 def __init__(self): 877 self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper() 878 879 def clone(self) -> "Model": 880 """Returns a clone of the current model.""" 881 clone = Model() 882 clone.helper.overwrite_model(self.helper) 883 return clone 884 885 @typing.overload 886 def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: 887 ... 888 889 @typing.overload 890 def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: 891 ... 892 893 def _get_linear_constraints( 894 self, constraints: Optional[_IndexOrSeries] = None 895 ) -> _IndexOrSeries: 896 if constraints is None: 897 return self.get_linear_constraints() 898 return constraints 899 900 @typing.overload 901 def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: 902 ... 903 904 @typing.overload 905 def _get_variables(self, variables: pd.Series) -> pd.Series: 906 ... 907 908 def _get_variables( 909 self, variables: Optional[_IndexOrSeries] = None 910 ) -> _IndexOrSeries: 911 if variables is None: 912 return self.get_variables() 913 return variables 914 915 def get_linear_constraints(self) -> pd.Index: 916 """Gets all linear constraints in the model.""" 917 return pd.Index( 918 [self.linear_constraint_from_index(i) for i in range(self.num_constraints)], 919 name="linear_constraint", 920 ) 921 922 def get_linear_constraint_expressions( 923 self, constraints: Optional[_IndexOrSeries] = None 924 ) -> pd.Series: 925 """Gets the expressions of all linear constraints in the set. 926 927 If `constraints` is a `pd.Index`, then the output will be indexed by the 928 constraints. If `constraints` is a `pd.Series` indexed by the underlying 929 dimensions, then the output will be indexed by the same underlying 930 dimensions. 931 932 Args: 933 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 934 constraints from which to get the expressions. If unspecified, all 935 linear constraints will be in scope. 936 937 Returns: 938 pd.Series: The expressions of all linear constraints in the set. 939 """ 940 return _attribute_series( 941 # pylint: disable=g-long-lambda 942 func=lambda c: _as_flat_linear_expression( 943 # pylint: disable=g-complex-comprehension 944 sum( 945 coeff * Variable(self.__helper, var_id, None, None, None) 946 for var_id, coeff in zip( 947 c.helper.constraint_var_indices(c.index), 948 c.helper.constraint_coefficients(c.index), 949 ) 950 ) 951 ), 952 values=self._get_linear_constraints(constraints), 953 ) 954 955 def get_linear_constraint_lower_bounds( 956 self, constraints: Optional[_IndexOrSeries] = None 957 ) -> pd.Series: 958 """Gets the lower bounds of all linear constraints in the set. 959 960 If `constraints` is a `pd.Index`, then the output will be indexed by the 961 constraints. If `constraints` is a `pd.Series` indexed by the underlying 962 dimensions, then the output will be indexed by the same underlying 963 dimensions. 964 965 Args: 966 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 967 constraints from which to get the lower bounds. If unspecified, all 968 linear constraints will be in scope. 969 970 Returns: 971 pd.Series: The lower bounds of all linear constraints in the set. 972 """ 973 return _attribute_series( 974 func=lambda c: c.lower_bound, # pylint: disable=protected-access 975 values=self._get_linear_constraints(constraints), 976 ) 977 978 def get_linear_constraint_upper_bounds( 979 self, constraints: Optional[_IndexOrSeries] = None 980 ) -> pd.Series: 981 """Gets the upper bounds of all linear constraints in the set. 982 983 If `constraints` is a `pd.Index`, then the output will be indexed by the 984 constraints. If `constraints` is a `pd.Series` indexed by the underlying 985 dimensions, then the output will be indexed by the same underlying 986 dimensions. 987 988 Args: 989 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 990 constraints. If unspecified, all linear constraints will be in scope. 991 992 Returns: 993 pd.Series: The upper bounds of all linear constraints in the set. 994 """ 995 return _attribute_series( 996 func=lambda c: c.upper_bound, # pylint: disable=protected-access 997 values=self._get_linear_constraints(constraints), 998 ) 999 1000 def get_variables(self) -> pd.Index: 1001 """Gets all variables in the model.""" 1002 return pd.Index( 1003 [self.var_from_index(i) for i in range(self.num_variables)], 1004 name="variable", 1005 ) 1006 1007 def get_variable_lower_bounds( 1008 self, variables: Optional[_IndexOrSeries] = None 1009 ) -> pd.Series: 1010 """Gets the lower bounds of all variables in the set. 1011 1012 If `variables` is a `pd.Index`, then the output will be indexed by the 1013 variables. If `variables` is a `pd.Series` indexed by the underlying 1014 dimensions, then the output will be indexed by the same underlying 1015 dimensions. 1016 1017 Args: 1018 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 1019 from which to get the lower bounds. If unspecified, all variables will 1020 be in scope. 1021 1022 Returns: 1023 pd.Series: The lower bounds of all variables in the set. 1024 """ 1025 return _attribute_series( 1026 func=lambda v: v.lower_bound, # pylint: disable=protected-access 1027 values=self._get_variables(variables), 1028 ) 1029 1030 def get_variable_upper_bounds( 1031 self, variables: Optional[_IndexOrSeries] = None 1032 ) -> pd.Series: 1033 """Gets the upper bounds of all variables in the set. 1034 1035 Args: 1036 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 1037 from which to get the upper bounds. If unspecified, all variables will 1038 be in scope. 1039 1040 Returns: 1041 pd.Series: The upper bounds of all variables in the set. 1042 """ 1043 return _attribute_series( 1044 func=lambda v: v.upper_bound, # pylint: disable=protected-access 1045 values=self._get_variables(variables), 1046 ) 1047 1048 # Integer variable. 1049 1050 def new_var( 1051 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] 1052 ) -> Variable: 1053 """Create an integer variable with domain [lb, ub]. 1054 1055 Args: 1056 lb: Lower bound of the variable. 1057 ub: Upper bound of the variable. 1058 is_integer: Indicates if the variable must take integral values. 1059 name: The name of the variable. 1060 1061 Returns: 1062 a variable whose domain is [lb, ub]. 1063 """ 1064 1065 return Variable(self.__helper, lb, ub, is_integer, name) 1066 1067 def new_int_var( 1068 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 1069 ) -> Variable: 1070 """Create an integer variable with domain [lb, ub]. 1071 1072 Args: 1073 lb: Lower bound of the variable. 1074 ub: Upper bound of the variable. 1075 name: The name of the variable. 1076 1077 Returns: 1078 a variable whose domain is [lb, ub]. 1079 """ 1080 1081 return self.new_var(lb, ub, True, name) 1082 1083 def new_num_var( 1084 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 1085 ) -> Variable: 1086 """Create an integer variable with domain [lb, ub]. 1087 1088 Args: 1089 lb: Lower bound of the variable. 1090 ub: Upper bound of the variable. 1091 name: The name of the variable. 1092 1093 Returns: 1094 a variable whose domain is [lb, ub]. 1095 """ 1096 1097 return self.new_var(lb, ub, False, name) 1098 1099 def new_bool_var(self, name: Optional[str] = None) -> Variable: 1100 """Creates a 0-1 variable with the given name.""" 1101 return self.new_var( 1102 0, 1, True, name 1103 ) # pytype: disable=wrong-arg-types # numpy-scalars 1104 1105 def new_constant(self, value: NumberT) -> Variable: 1106 """Declares a constant variable.""" 1107 return self.new_var(value, value, False, None) 1108 1109 def new_var_series( 1110 self, 1111 name: str, 1112 index: pd.Index, 1113 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1114 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1115 is_integral: Union[bool, pd.Series] = False, 1116 ) -> pd.Series: 1117 """Creates a series of (scalar-valued) variables with the given name. 1118 1119 Args: 1120 name (str): Required. The name of the variable set. 1121 index (pd.Index): Required. The index to use for the variable set. 1122 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1123 variables in the set. If a `pd.Series` is passed in, it will be based on 1124 the corresponding values of the pd.Series. Defaults to -inf. 1125 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1126 variables in the set. If a `pd.Series` is passed in, it will be based on 1127 the corresponding values of the pd.Series. Defaults to +inf. 1128 is_integral (bool, pd.Series): Optional. Indicates if the variable can 1129 only take integer values. If a `pd.Series` is passed in, it will be 1130 based on the corresponding values of the pd.Series. Defaults to False. 1131 1132 Returns: 1133 pd.Series: The variable set indexed by its corresponding dimensions. 1134 1135 Raises: 1136 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1137 ValueError: if the `name` is not a valid identifier or already exists. 1138 ValueError: if the `lowerbound` is greater than the `upperbound`. 1139 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1140 does not match the input index. 1141 """ 1142 if not isinstance(index, pd.Index): 1143 raise TypeError("Non-index object is used as index") 1144 if not name.isidentifier(): 1145 raise ValueError("name={} is not a valid identifier".format(name)) 1146 if ( 1147 mbn.is_a_number(lower_bounds) 1148 and mbn.is_a_number(upper_bounds) 1149 and lower_bounds > upper_bounds 1150 ): 1151 raise ValueError( 1152 "lower_bound={} is greater than upper_bound={} for variable set={}".format( 1153 lower_bounds, upper_bounds, name 1154 ) 1155 ) 1156 if ( 1157 isinstance(is_integral, bool) 1158 and is_integral 1159 and mbn.is_a_number(lower_bounds) 1160 and mbn.is_a_number(upper_bounds) 1161 and math.isfinite(lower_bounds) 1162 and math.isfinite(upper_bounds) 1163 and math.ceil(lower_bounds) > math.floor(upper_bounds) 1164 ): 1165 raise ValueError( 1166 "ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds)) 1167 + " is greater than floor(" 1168 + "upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds)) 1169 + " for variable set={}".format(name) 1170 ) 1171 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) 1172 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) 1173 is_integrals = _convert_to_series_and_validate_index(is_integral, index) 1174 return pd.Series( 1175 index=index, 1176 data=[ 1177 # pylint: disable=g-complex-comprehension 1178 Variable( 1179 helper=self.__helper, 1180 name=f"{name}[{i}]", 1181 lb=lower_bounds[i], 1182 ub=upper_bounds[i], 1183 is_integral=is_integrals[i], 1184 ) 1185 for i in index 1186 ], 1187 ) 1188 1189 def new_num_var_series( 1190 self, 1191 name: str, 1192 index: pd.Index, 1193 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1194 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1195 ) -> pd.Series: 1196 """Creates a series of continuous variables with the given name. 1197 1198 Args: 1199 name (str): Required. The name of the variable set. 1200 index (pd.Index): Required. The index to use for the variable set. 1201 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1202 variables in the set. If a `pd.Series` is passed in, it will be based on 1203 the corresponding values of the pd.Series. Defaults to -inf. 1204 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1205 variables in the set. If a `pd.Series` is passed in, it will be based on 1206 the corresponding values of the pd.Series. Defaults to +inf. 1207 1208 Returns: 1209 pd.Series: The variable set indexed by its corresponding dimensions. 1210 1211 Raises: 1212 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1213 ValueError: if the `name` is not a valid identifier or already exists. 1214 ValueError: if the `lowerbound` is greater than the `upperbound`. 1215 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1216 does not match the input index. 1217 """ 1218 return self.new_var_series(name, index, lower_bounds, upper_bounds, False) 1219 1220 def new_int_var_series( 1221 self, 1222 name: str, 1223 index: pd.Index, 1224 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1225 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1226 ) -> pd.Series: 1227 """Creates a series of integer variables with the given name. 1228 1229 Args: 1230 name (str): Required. The name of the variable set. 1231 index (pd.Index): Required. The index to use for the variable set. 1232 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1233 variables in the set. If a `pd.Series` is passed in, it will be based on 1234 the corresponding values of the pd.Series. Defaults to -inf. 1235 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1236 variables in the set. If a `pd.Series` is passed in, it will be based on 1237 the corresponding values of the pd.Series. Defaults to +inf. 1238 1239 Returns: 1240 pd.Series: The variable set indexed by its corresponding dimensions. 1241 1242 Raises: 1243 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1244 ValueError: if the `name` is not a valid identifier or already exists. 1245 ValueError: if the `lowerbound` is greater than the `upperbound`. 1246 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1247 does not match the input index. 1248 """ 1249 return self.new_var_series(name, index, lower_bounds, upper_bounds, True) 1250 1251 def new_bool_var_series( 1252 self, 1253 name: str, 1254 index: pd.Index, 1255 ) -> pd.Series: 1256 """Creates a series of Boolean variables with the given name. 1257 1258 Args: 1259 name (str): Required. The name of the variable set. 1260 index (pd.Index): Required. The index to use for the variable set. 1261 1262 Returns: 1263 pd.Series: The variable set indexed by its corresponding dimensions. 1264 1265 Raises: 1266 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1267 ValueError: if the `name` is not a valid identifier or already exists. 1268 ValueError: if the `lowerbound` is greater than the `upperbound`. 1269 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1270 does not match the input index. 1271 """ 1272 return self.new_var_series(name, index, 0, 1, True) 1273 1274 def var_from_index(self, index: IntegerT) -> Variable: 1275 """Rebuilds a variable object from the model and its index.""" 1276 return Variable(self.__helper, index, None, None, None) 1277 1278 # Linear constraints. 1279 1280 def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 1281 self, 1282 linear_expr: LinearExprT, 1283 lb: NumberT = -math.inf, 1284 ub: NumberT = math.inf, 1285 name: Optional[str] = None, 1286 ) -> LinearConstraint: 1287 """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" 1288 ct = LinearConstraint(self.__helper) 1289 if name: 1290 self.__helper.set_constraint_name(ct.index, name) 1291 if mbn.is_a_number(linear_expr): 1292 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 1293 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 1294 elif isinstance(linear_expr, Variable): 1295 self.__helper.set_constraint_lower_bound(ct.index, lb) 1296 self.__helper.set_constraint_upper_bound(ct.index, ub) 1297 self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0) 1298 elif isinstance(linear_expr, LinearExpr): 1299 flat_expr = _as_flat_linear_expression(linear_expr) 1300 # pylint: disable=protected-access 1301 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset) 1302 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset) 1303 self.__helper.add_terms_to_constraint( 1304 ct.index, flat_expr._variable_indices, flat_expr._coefficients 1305 ) 1306 else: 1307 raise TypeError( 1308 f"Not supported: Model.add_linear_constraint({linear_expr})" 1309 f" with type {type(linear_expr)}" 1310 ) 1311 return ct 1312 1313 def add( 1314 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None 1315 ) -> Union[LinearConstraint, pd.Series]: 1316 """Adds a `BoundedLinearExpression` to the model. 1317 1318 Args: 1319 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1320 name: An optional name. 1321 1322 Returns: 1323 An instance of the `Constraint` class. 1324 1325 Note that a special treatment is done when the argument does not contain any 1326 variable, and thus evaluates to True or False. 1327 1328 model.add(True) will create a constraint 0 <= empty sum <= 0 1329 1330 model.add(False) will create a constraint inf <= empty sum <= -inf 1331 1332 you can check the if a constraint is always false (lb=inf, ub=-inf) by 1333 calling LinearConstraint.is_always_false() 1334 """ 1335 if isinstance(ct, _BoundedLinearExpr): 1336 return ct._add_linear_constraint(self.__helper, name) 1337 elif isinstance(ct, bool): 1338 return _add_linear_constraint_to_helper(ct, self.__helper, name) 1339 elif isinstance(ct, pd.Series): 1340 return pd.Series( 1341 index=ct.index, 1342 data=[ 1343 _add_linear_constraint_to_helper( 1344 expr, self.__helper, f"{name}[{i}]" 1345 ) 1346 for (i, expr) in zip(ct.index, ct) 1347 ], 1348 ) 1349 else: 1350 raise TypeError("Not supported: Model.add(" + str(ct) + ")") 1351 1352 def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: 1353 """Rebuilds a linear constraint object from the model and its index.""" 1354 return LinearConstraint(self.__helper, index) 1355 1356 # EnforcedLinear constraints. 1357 1358 def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 1359 self, 1360 linear_expr: LinearExprT, 1361 ivar: "Variable", 1362 ivalue: bool, 1363 lb: NumberT = -math.inf, 1364 ub: NumberT = math.inf, 1365 name: Optional[str] = None, 1366 ) -> EnforcedLinearConstraint: 1367 """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" 1368 ct = EnforcedLinearConstraint(self.__helper) 1369 ct.indicator_variable = ivar 1370 ct.indicator_value = ivalue 1371 if name: 1372 self.__helper.set_constraint_name(ct.index, name) 1373 if mbn.is_a_number(linear_expr): 1374 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 1375 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 1376 elif isinstance(linear_expr, Variable): 1377 self.__helper.set_constraint_lower_bound(ct.index, lb) 1378 self.__helper.set_constraint_upper_bound(ct.index, ub) 1379 self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0) 1380 elif isinstance(linear_expr, LinearExpr): 1381 flat_expr = _as_flat_linear_expression(linear_expr) 1382 # pylint: disable=protected-access 1383 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset) 1384 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset) 1385 self.__helper.add_terms_to_constraint( 1386 ct.index, flat_expr._variable_indices, flat_expr._coefficients 1387 ) 1388 else: 1389 raise TypeError( 1390 "Not supported:" 1391 f" Model.add_enforced_linear_constraint({linear_expr}) with" 1392 f" type {type(linear_expr)}" 1393 ) 1394 return ct 1395 1396 def add_enforced( 1397 self, 1398 ct: Union[ConstraintT, pd.Series], 1399 var: Union[Variable, pd.Series], 1400 value: Union[bool, pd.Series], 1401 name: Optional[str] = None, 1402 ) -> Union[EnforcedLinearConstraint, pd.Series]: 1403 """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. 1404 1405 Args: 1406 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1407 var: The indicator variable 1408 value: the indicator value 1409 name: An optional name. 1410 1411 Returns: 1412 An instance of the `Constraint` class. 1413 1414 Note that a special treatment is done when the argument does not contain any 1415 variable, and thus evaluates to True or False. 1416 1417 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty 1418 sum <= 0 1419 1420 model.add_enforced(False, var, value) will create a constraint inf <= 1421 empty sum <= -inf 1422 1423 you can check the if a constraint is always false (lb=inf, ub=-inf) by 1424 calling EnforcedLinearConstraint.is_always_false() 1425 """ 1426 if isinstance(ct, _BoundedLinearExpr): 1427 return ct._add_enforced_linear_constraint(self.__helper, var, value, name) 1428 elif ( 1429 isinstance(ct, bool) 1430 and isinstance(var, Variable) 1431 and isinstance(value, bool) 1432 ): 1433 return _add_enforced_linear_constraint_to_helper( 1434 ct, self.__helper, var, value, name 1435 ) 1436 elif isinstance(ct, pd.Series): 1437 ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) 1438 ivalue_series = _convert_to_series_and_validate_index(value, ct.index) 1439 return pd.Series( 1440 index=ct.index, 1441 data=[ 1442 _add_enforced_linear_constraint_to_helper( 1443 expr, 1444 self.__helper, 1445 ivar_series[i], 1446 ivalue_series[i], 1447 f"{name}[{i}]", 1448 ) 1449 for (i, expr) in zip(ct.index, ct) 1450 ], 1451 ) 1452 else: 1453 raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")") 1454 1455 def enforced_linear_constraint_from_index( 1456 self, index: IntegerT 1457 ) -> EnforcedLinearConstraint: 1458 """Rebuilds an enforced linear constraint object from the model and its index.""" 1459 return EnforcedLinearConstraint(self.__helper, index) 1460 1461 # Objective. 1462 def minimize(self, linear_expr: LinearExprT) -> None: 1463 """Minimizes the given objective.""" 1464 self.__optimize(linear_expr, False) 1465 1466 def maximize(self, linear_expr: LinearExprT) -> None: 1467 """Maximizes the given objective.""" 1468 self.__optimize(linear_expr, True) 1469 1470 def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None: 1471 """Defines the objective.""" 1472 self.helper.clear_objective() 1473 self.__helper.set_maximize(maximize) 1474 if mbn.is_a_number(linear_expr): 1475 self.helper.set_objective_offset(linear_expr) 1476 elif isinstance(linear_expr, Variable): 1477 self.helper.set_var_objective_coefficient(linear_expr.index, 1.0) 1478 elif isinstance(linear_expr, LinearExpr): 1479 flat_expr = _as_flat_linear_expression(linear_expr) 1480 # pylint: disable=protected-access 1481 self.helper.set_objective_offset(flat_expr._offset) 1482 self.helper.set_objective_coefficients( 1483 flat_expr._variable_indices, flat_expr._coefficients 1484 ) 1485 else: 1486 raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})") 1487 1488 @property 1489 def objective_offset(self) -> np.double: 1490 """Returns the fixed offset of the objective.""" 1491 return self.__helper.objective_offset() 1492 1493 @objective_offset.setter 1494 def objective_offset(self, value: NumberT) -> None: 1495 self.__helper.set_objective_offset(value) 1496 1497 def objective_expression(self) -> "_LinearExpression": 1498 """Returns the expression to optimize.""" 1499 return _as_flat_linear_expression( 1500 sum( 1501 variable * self.__helper.var_objective_coefficient(variable.index) 1502 for variable in self.get_variables() 1503 if self.__helper.var_objective_coefficient(variable.index) != 0.0 1504 ) 1505 + self.__helper.objective_offset() 1506 ) 1507 1508 # Hints. 1509 def clear_hints(self): 1510 """Clears all solution hints.""" 1511 self.__helper.clear_hints() 1512 1513 def add_hint(self, var: Variable, value: NumberT) -> None: 1514 """Adds var == value as a hint to the model. 1515 1516 Args: 1517 var: The variable of the hint 1518 value: The value of the hint 1519 1520 Note that variables must not appear more than once in the list of hints. 1521 """ 1522 self.__helper.add_hint(var.index, value) 1523 1524 # Input/Output 1525 def export_to_lp_string(self, obfuscate: bool = False) -> str: 1526 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1527 options.obfuscate = obfuscate 1528 return self.__helper.export_to_lp_string(options) 1529 1530 def export_to_mps_string(self, obfuscate: bool = False) -> str: 1531 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1532 options.obfuscate = obfuscate 1533 return self.__helper.export_to_mps_string(options) 1534 1535 def export_to_proto(self) -> linear_solver_pb2.MPModelProto: 1536 """Exports the optimization model to a ProtoBuf format.""" 1537 return mbh.to_mpmodel_proto(self.__helper) 1538 1539 def import_from_mps_string(self, mps_string: str) -> bool: 1540 """Reads a model from a MPS string.""" 1541 return self.__helper.import_from_mps_string(mps_string) 1542 1543 def import_from_mps_file(self, mps_file: str) -> bool: 1544 """Reads a model from a .mps file.""" 1545 return self.__helper.import_from_mps_file(mps_file) 1546 1547 def import_from_lp_string(self, lp_string: str) -> bool: 1548 """Reads a model from a LP string.""" 1549 return self.__helper.import_from_lp_string(lp_string) 1550 1551 def import_from_lp_file(self, lp_file: str) -> bool: 1552 """Reads a model from a .lp file.""" 1553 return self.__helper.import_from_lp_file(lp_file) 1554 1555 def import_from_proto_file(self, proto_file: str) -> bool: 1556 """Reads a model from a proto file.""" 1557 return self.__helper.read_model_from_proto_file(proto_file) 1558 1559 def export_to_proto_file(self, proto_file: str) -> bool: 1560 """Writes a model to a proto file.""" 1561 return self.__helper.write_model_to_proto_file(proto_file) 1562 1563 # Model getters and Setters 1564 1565 @property 1566 def num_variables(self) -> int: 1567 """Returns the number of variables in the model.""" 1568 return self.__helper.num_variables() 1569 1570 @property 1571 def num_constraints(self) -> int: 1572 """The number of constraints in the model.""" 1573 return self.__helper.num_constraints() 1574 1575 @property 1576 def name(self) -> str: 1577 """The name of the model.""" 1578 return self.__helper.name() 1579 1580 @name.setter 1581 def name(self, name: str): 1582 self.__helper.set_name(name) 1583 1584 @property 1585 def helper(self) -> mbh.ModelBuilderHelper: 1586 """Returns the model builder helper.""" 1587 return self.__helper 1588 1589 1590class Solver: 1591 """Main solver class. 1592 1593 The purpose of this class is to search for a solution to the model provided 1594 to the solve() method. 1595 1596 Once solve() is called, this class allows inspecting the solution found 1597 with the value() method, as well as general statistics about the solve 1598 procedure. 1599 """ 1600 1601 def __init__(self, solver_name: str): 1602 self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name) 1603 self.log_callback: Optional[Callable[[str], None]] = None 1604 1605 def solver_is_supported(self) -> bool: 1606 """Checks whether the requested solver backend was found.""" 1607 return self.__solve_helper.solver_is_supported() 1608 1609 # Solver backend and parameters. 1610 def set_time_limit_in_seconds(self, limit: NumberT) -> None: 1611 """Sets a time limit for the solve() call.""" 1612 self.__solve_helper.set_time_limit_in_seconds(limit) 1613 1614 def set_solver_specific_parameters(self, parameters: str) -> None: 1615 """Sets parameters specific to the solver backend.""" 1616 self.__solve_helper.set_solver_specific_parameters(parameters) 1617 1618 def enable_output(self, enabled: bool) -> None: 1619 """Controls the solver backend logs.""" 1620 self.__solve_helper.enable_output(enabled) 1621 1622 def solve(self, model: Model) -> SolveStatus: 1623 """Solves a problem and passes each solution to the callback if not null.""" 1624 if self.log_callback is not None: 1625 self.__solve_helper.set_log_callback(self.log_callback) 1626 else: 1627 self.__solve_helper.clear_log_callback() 1628 self.__solve_helper.solve(model.helper) 1629 return SolveStatus(self.__solve_helper.status()) 1630 1631 def stop_search(self): 1632 """Stops the current search asynchronously.""" 1633 self.__solve_helper.interrupt_solve() 1634 1635 def value(self, expr: LinearExprT) -> np.double: 1636 """Returns the value of a linear expression after solve.""" 1637 if not self.__solve_helper.has_solution(): 1638 return pd.NA 1639 if mbn.is_a_number(expr): 1640 return expr 1641 elif isinstance(expr, Variable): 1642 return self.__solve_helper.var_value(expr.index) 1643 elif isinstance(expr, LinearExpr): 1644 flat_expr = _as_flat_linear_expression(expr) 1645 return self.__solve_helper.expression_value( 1646 flat_expr._variable_indices, 1647 flat_expr._coefficients, 1648 flat_expr._offset, 1649 ) 1650 else: 1651 raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}") 1652 1653 def values(self, variables: _IndexOrSeries) -> pd.Series: 1654 """Returns the values of the input variables. 1655 1656 If `variables` is a `pd.Index`, then the output will be indexed by the 1657 variables. If `variables` is a `pd.Series` indexed by the underlying 1658 dimensions, then the output will be indexed by the same underlying 1659 dimensions. 1660 1661 Args: 1662 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1663 get the values. 1664 1665 Returns: 1666 pd.Series: The values of all variables in the set. 1667 """ 1668 if not self.__solve_helper.has_solution(): 1669 return _attribute_series(func=lambda v: pd.NA, values=variables) 1670 return _attribute_series( 1671 func=lambda v: self.__solve_helper.var_value(v.index), 1672 values=variables, 1673 ) 1674 1675 def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: 1676 """Returns the reduced cost of the input variables. 1677 1678 If `variables` is a `pd.Index`, then the output will be indexed by the 1679 variables. If `variables` is a `pd.Series` indexed by the underlying 1680 dimensions, then the output will be indexed by the same underlying 1681 dimensions. 1682 1683 Args: 1684 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1685 get the values. 1686 1687 Returns: 1688 pd.Series: The reduced cost of all variables in the set. 1689 """ 1690 if not self.__solve_helper.has_solution(): 1691 return _attribute_series(func=lambda v: pd.NA, values=variables) 1692 return _attribute_series( 1693 func=lambda v: self.__solve_helper.reduced_cost(v.index), 1694 values=variables, 1695 ) 1696 1697 def reduced_cost(self, var: Variable) -> np.double: 1698 """Returns the reduced cost of a linear expression after solve.""" 1699 if not self.__solve_helper.has_solution(): 1700 return pd.NA 1701 return self.__solve_helper.reduced_cost(var.index) 1702 1703 def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: 1704 """Returns the dual values of the input constraints. 1705 1706 If `constraints` is a `pd.Index`, then the output will be indexed by the 1707 constraints. If `constraints` is a `pd.Series` indexed by the underlying 1708 dimensions, then the output will be indexed by the same underlying 1709 dimensions. 1710 1711 Args: 1712 constraints (Union[pd.Index, pd.Series]): The set of constraints from 1713 which to get the dual values. 1714 1715 Returns: 1716 pd.Series: The dual_values of all constraints in the set. 1717 """ 1718 if not self.__solve_helper.has_solution(): 1719 return _attribute_series(func=lambda v: pd.NA, values=constraints) 1720 return _attribute_series( 1721 func=lambda v: self.__solve_helper.dual_value(v.index), 1722 values=constraints, 1723 ) 1724 1725 def dual_value(self, ct: LinearConstraint) -> np.double: 1726 """Returns the dual value of a linear constraint after solve.""" 1727 if not self.__solve_helper.has_solution(): 1728 return pd.NA 1729 return self.__solve_helper.dual_value(ct.index) 1730 1731 def activity(self, ct: LinearConstraint) -> np.double: 1732 """Returns the activity of a linear constraint after solve.""" 1733 if not self.__solve_helper.has_solution(): 1734 return pd.NA 1735 return self.__solve_helper.activity(ct.index) 1736 1737 @property 1738 def objective_value(self) -> np.double: 1739 """Returns the value of the objective after solve.""" 1740 if not self.__solve_helper.has_solution(): 1741 return pd.NA 1742 return self.__solve_helper.objective_value() 1743 1744 @property 1745 def best_objective_bound(self) -> np.double: 1746 """Returns the best lower (upper) bound found when min(max)imizing.""" 1747 if not self.__solve_helper.has_solution(): 1748 return pd.NA 1749 return self.__solve_helper.best_objective_bound() 1750 1751 @property 1752 def status_string(self) -> str: 1753 """Returns additional information of the last solve. 1754 1755 It can describe why the model is invalid. 1756 """ 1757 return self.__solve_helper.status_string() 1758 1759 @property 1760 def wall_time(self) -> np.double: 1761 return self.__solve_helper.wall_time() 1762 1763 @property 1764 def user_time(self) -> np.double: 1765 return self.__solve_helper.user_time() 1766 1767 1768# The maximum number of terms to display in a linear expression's repr. 1769_MAX_LINEAR_EXPRESSION_REPR_TERMS = 5 1770 1771 1772@dataclasses.dataclass(repr=False, eq=False, frozen=True) 1773class _LinearExpression(LinearExpr): 1774 """For variables x, an expression: offset + sum_{i in I} coeff_i * x_i.""" 1775 1776 __slots__ = ("_variable_indices", "_coefficients", "_offset", "_helper") 1777 1778 _variable_indices: npt.NDArray[np.int32] 1779 _coefficients: npt.NDArray[np.double] 1780 _offset: float 1781 _helper: Optional[mbh.ModelBuilderHelper] 1782 1783 @property 1784 def variable_indices(self) -> npt.NDArray[np.int32]: 1785 return self._variable_indices 1786 1787 @property 1788 def coefficients(self) -> npt.NDArray[np.double]: 1789 return self._coefficients 1790 1791 @property 1792 def constant(self) -> float: 1793 return self._offset 1794 1795 @property 1796 def helper(self) -> Optional[mbh.ModelBuilderHelper]: 1797 return self._helper 1798 1799 def __repr__(self): 1800 return self.__str__() 1801 1802 def __str__(self): 1803 if self._helper is None: 1804 return str(self._offset) 1805 1806 result = [] 1807 for index, coeff in zip(self.variable_indices, self.coefficients): 1808 if len(result) >= _MAX_LINEAR_EXPRESSION_REPR_TERMS: 1809 result.append(" + ...") 1810 break 1811 var_name = Variable(self._helper, index, None, None, None).name 1812 if not result and mbn.is_one(coeff): 1813 result.append(var_name) 1814 elif not result and mbn.is_minus_one(coeff): 1815 result.append(f"-{var_name}") 1816 elif not result: 1817 result.append(f"{coeff} * {var_name}") 1818 elif mbn.is_one(coeff): 1819 result.append(f" + {var_name}") 1820 elif mbn.is_minus_one(coeff): 1821 result.append(f" - {var_name}") 1822 elif coeff > 0.0: 1823 result.append(f" + {coeff} * {var_name}") 1824 elif coeff < 0.0: 1825 result.append(f" - {-coeff} * {var_name}") 1826 1827 if not result: 1828 return f"{self.constant}" 1829 if self.constant > 0: 1830 result.append(f" + {self.constant}") 1831 elif self.constant < 0: 1832 result.append(f" - {-self.constant}") 1833 return "".join(result) 1834 1835 1836def _sum_as_flat_linear_expression( 1837 to_process: List[Tuple[LinearExprT, float]], offset: float = 0.0 1838) -> _LinearExpression: 1839 """Creates a _LinearExpression as the sum of terms.""" 1840 indices = [] 1841 coeffs = [] 1842 helper = None 1843 while to_process: # Flatten AST of LinearTypes. 1844 expr, coeff = to_process.pop() 1845 if isinstance(expr, _Sum): 1846 to_process.append((expr._left, coeff)) 1847 to_process.append((expr._right, coeff)) 1848 elif isinstance(expr, Variable): 1849 indices.append([expr.index]) 1850 coeffs.append([coeff]) 1851 if helper is None: 1852 helper = expr.helper 1853 elif mbn.is_a_number(expr): 1854 offset += coeff * cast(NumberT, expr) 1855 elif isinstance(expr, _Product): 1856 to_process.append((expr._expression, coeff * expr._coefficient)) 1857 elif isinstance(expr, _LinearExpression): 1858 offset += coeff * expr._offset 1859 if expr._helper is not None: 1860 indices.append(expr.variable_indices) 1861 coeffs.append(np.multiply(expr.coefficients, coeff)) 1862 if helper is None: 1863 helper = expr._helper 1864 else: 1865 raise TypeError( 1866 "Unrecognized linear expression: " + str(expr) + f" {type(expr)}" 1867 ) 1868 1869 if helper is not None: 1870 all_indices: npt.NDArray[np.int32] = np.concatenate(indices, axis=0) 1871 all_coeffs: npt.NDArray[np.double] = np.concatenate(coeffs, axis=0) 1872 sorted_indices, sorted_coefficients = helper.sort_and_regroup_terms( 1873 all_indices, all_coeffs 1874 ) 1875 return _LinearExpression(sorted_indices, sorted_coefficients, offset, helper) 1876 else: 1877 assert not indices 1878 assert not coeffs 1879 return _LinearExpression( 1880 _variable_indices=np.zeros(dtype=np.int32, shape=[0]), 1881 _coefficients=np.zeros(dtype=np.double, shape=[0]), 1882 _offset=offset, 1883 _helper=None, 1884 ) 1885 1886 1887def _as_flat_linear_expression(base_expr: LinearExprT) -> _LinearExpression: 1888 """Converts floats, ints and Linear objects to a LinearExpression.""" 1889 if isinstance(base_expr, _LinearExpression): 1890 return base_expr 1891 return _sum_as_flat_linear_expression(to_process=[(base_expr, 1.0)], offset=0.0) 1892 1893 1894@dataclasses.dataclass(repr=False, eq=False, frozen=True) 1895class _Sum(LinearExpr): 1896 """Represents the (deferred) sum of two expressions.""" 1897 1898 __slots__ = ("_left", "_right") 1899 1900 _left: LinearExprT 1901 _right: LinearExprT 1902 1903 def __repr__(self): 1904 return self.__str__() 1905 1906 def __str__(self): 1907 return str(_as_flat_linear_expression(self)) 1908 1909 1910@dataclasses.dataclass(repr=False, eq=False, frozen=True) 1911class _Product(LinearExpr): 1912 """Represents the (deferred) product of an expression by a constant.""" 1913 1914 __slots__ = ("_expression", "_coefficient") 1915 1916 _expression: LinearExpr 1917 _coefficient: NumberT 1918 1919 def __repr__(self): 1920 return self.__str__() 1921 1922 def __str__(self): 1923 return str(_as_flat_linear_expression(self)) 1924 1925 1926def _get_index(obj: _IndexOrSeries) -> pd.Index: 1927 """Returns the indices of `obj` as a `pd.Index`.""" 1928 if isinstance(obj, pd.Series): 1929 return obj.index 1930 return obj 1931 1932 1933def _attribute_series( 1934 *, 1935 func: Callable[[_VariableOrConstraint], NumberT], 1936 values: _IndexOrSeries, 1937) -> pd.Series: 1938 """Returns the attributes of `values`. 1939 1940 Args: 1941 func: The function to call for getting the attribute data. 1942 values: The values that the function will be applied (element-wise) to. 1943 1944 Returns: 1945 pd.Series: The attribute values. 1946 """ 1947 return pd.Series( 1948 data=[func(v) for v in values], 1949 index=_get_index(values), 1950 ) 1951 1952 1953def _convert_to_series_and_validate_index( 1954 value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index 1955) -> pd.Series: 1956 """Returns a pd.Series of the given index with the corresponding values. 1957 1958 Args: 1959 value_or_series: the values to be converted (if applicable). 1960 index: the index of the resulting pd.Series. 1961 1962 Returns: 1963 pd.Series: The set of values with the given index. 1964 1965 Raises: 1966 TypeError: If the type of `value_or_series` is not recognized. 1967 ValueError: If the index does not match. 1968 """ 1969 if mbn.is_a_number(value_or_series) or isinstance(value_or_series, bool): 1970 result = pd.Series(data=value_or_series, index=index) 1971 elif isinstance(value_or_series, pd.Series): 1972 if value_or_series.index.equals(index): 1973 result = value_or_series 1974 else: 1975 raise ValueError("index does not match") 1976 else: 1977 raise TypeError("invalid type={}".format(type(value_or_series))) 1978 return result 1979 1980 1981def _convert_to_var_series_and_validate_index( 1982 var_or_series: Union["Variable", pd.Series], index: pd.Index 1983) -> pd.Series: 1984 """Returns a pd.Series of the given index with the corresponding values. 1985 1986 Args: 1987 var_or_series: the variables to be converted (if applicable). 1988 index: the index of the resulting pd.Series. 1989 1990 Returns: 1991 pd.Series: The set of values with the given index. 1992 1993 Raises: 1994 TypeError: If the type of `value_or_series` is not recognized. 1995 ValueError: If the index does not match. 1996 """ 1997 if isinstance(var_or_series, Variable): 1998 result = pd.Series(data=var_or_series, index=index) 1999 elif isinstance(var_or_series, pd.Series): 2000 if var_or_series.index.equals(index): 2001 result = var_or_series 2002 else: 2003 raise ValueError("index does not match") 2004 else: 2005 raise TypeError("invalid type={}".format(type(var_or_series))) 2006 return result 2007 2008 2009# Compatibility. 2010ModelBuilder = Model 2011ModelSolver = Solver
Members:
OPTIMAL
FEASIBLE
INFEASIBLE
UNBOUNDED
ABNORMAL
NOT_SOLVED
MODEL_IS_VALID
CANCELLED_BY_USER
UNKNOWN_STATUS
MODEL_INVALID
INVALID_SOLVER_PARAMETERS
SOLVER_TYPE_UNAVAILABLE
INCOMPATIBLE_OPTIONS
66class LinearExpr(metaclass=abc.ABCMeta): 67 """Holds an linear expression. 68 69 A linear expression is built from constants and variables. 70 For example, `x + 2.0 * (y - z + 1.0)`. 71 72 Linear expressions are used in Model models in constraints and in the 73 objective: 74 75 * You can define linear constraints as in: 76 77 ``` 78 model.add(x + 2 * y <= 5.0) 79 model.add(sum(array_of_vars) == 5.0) 80 ``` 81 82 * In Model, the objective is a linear expression: 83 84 ``` 85 model.minimize(x + 2.0 * y + z) 86 ``` 87 88 * For large arrays, using the LinearExpr class is faster that using the python 89 `sum()` function. You can create constraints and the objective from lists of 90 linear expressions or coefficients as follows: 91 92 ``` 93 model.minimize(model_builder.LinearExpr.sum(expressions)) 94 model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0) 95 ``` 96 """ 97 98 @classmethod 99 def sum( # pytype: disable=annotation-type-mismatch # numpy-scalars 100 cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0 101 ) -> LinearExprT: 102 """Creates `sum(expressions) + constant`. 103 104 It can perform simple simplifications and returns different objects, 105 including the input. 106 107 Args: 108 expressions: a sequence of linear expressions or constants. 109 constant: a numerical constant. 110 111 Returns: 112 a LinearExpr instance or a numerical constant. 113 """ 114 checked_constant: np.double = mbn.assert_is_a_number(constant) 115 if not expressions: 116 return checked_constant 117 if len(expressions) == 1 and mbn.is_zero(checked_constant): 118 return expressions[0] 119 120 return LinearExpr.weighted_sum( 121 expressions, np.ones(len(expressions)), constant=checked_constant 122 ) 123 124 @classmethod 125 def weighted_sum( # pytype: disable=annotation-type-mismatch # numpy-scalars 126 cls, 127 expressions: Sequence[LinearExprT], 128 coefficients: Sequence[NumberT], 129 *, 130 constant: NumberT = 0.0, 131 ) -> Union[NumberT, "_LinearExpression"]: 132 """Creates `sum(expressions[i] * coefficients[i]) + constant`. 133 134 It can perform simple simplifications and returns different object, 135 including the input. 136 137 Args: 138 expressions: a sequence of linear expressions or constants. 139 coefficients: a sequence of numerical constants. 140 constant: a numerical constant. 141 142 Returns: 143 a _LinearExpression instance or a numerical constant. 144 """ 145 if len(expressions) != len(coefficients): 146 raise ValueError( 147 "LinearExpr.weighted_sum: expressions and coefficients have" 148 " different lengths" 149 ) 150 checked_constant: np.double = mbn.assert_is_a_number(constant) 151 if not expressions: 152 return checked_constant 153 return _sum_as_flat_linear_expression( 154 to_process=list(zip(expressions, coefficients)), offset=checked_constant 155 ) 156 157 @classmethod 158 def term( # pytype: disable=annotation-type-mismatch # numpy-scalars 159 cls, 160 expression: LinearExprT, 161 coefficient: NumberT, 162 *, 163 constant: NumberT = 0.0, 164 ) -> LinearExprT: 165 """Creates `expression * coefficient + constant`. 166 167 It can perform simple simplifications and returns different object, 168 including the input. 169 Args: 170 expression: a linear expression or a constant. 171 coefficient: a numerical constant. 172 constant: a numerical constant. 173 174 Returns: 175 a LinearExpr instance or a numerical constant. 176 """ 177 checked_coefficient: np.double = mbn.assert_is_a_number(coefficient) 178 checked_constant: np.double = mbn.assert_is_a_number(constant) 179 180 if mbn.is_zero(checked_coefficient): 181 return checked_constant 182 if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant): 183 return expression 184 if mbn.is_a_number(expression): 185 return np.double(expression) * checked_coefficient + checked_constant 186 if isinstance(expression, LinearExpr): 187 return _as_flat_linear_expression( 188 expression * checked_coefficient + checked_constant 189 ) 190 raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}") 191 192 def __hash__(self): 193 return object.__hash__(self) 194 195 def __add__(self, arg: LinearExprT) -> "_Sum": 196 return _Sum(self, arg) 197 198 def __radd__(self, arg: LinearExprT) -> "_Sum": 199 return self.__add__(arg) 200 201 def __sub__(self, arg: LinearExprT) -> "_Sum": 202 return _Sum(self, -arg) 203 204 def __rsub__(self, arg: LinearExprT) -> "_Sum": 205 return _Sum(-self, arg) 206 207 def __mul__(self, arg: NumberT) -> "_Product": 208 return _Product(self, arg) 209 210 def __rmul__(self, arg: NumberT) -> "_Product": 211 return self.__mul__(arg) 212 213 def __truediv__(self, coeff: NumberT) -> "_Product": 214 return self.__mul__(1.0 / coeff) 215 216 def __neg__(self) -> "_Product": 217 return _Product(self, -1) 218 219 def __bool__(self): 220 raise NotImplementedError(f"Cannot use a LinearExpr {self} as a Boolean value") 221 222 def __eq__(self, arg: LinearExprT) -> "BoundedLinearExpression": 223 return BoundedLinearExpression(self - arg, 0, 0) 224 225 def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression": 226 return BoundedLinearExpression( 227 self - arg, 0, math.inf 228 ) # pytype: disable=wrong-arg-types # numpy-scalars 229 230 def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression": 231 return BoundedLinearExpression( 232 self - arg, -math.inf, 0 233 ) # pytype: disable=wrong-arg-types # numpy-scalars
Holds an linear expression.
A linear expression is built from constants and variables.
For example, x + 2.0 * (y - z + 1.0)
.
Linear expressions are used in Model models in constraints and in the objective:
- You can define linear constraints as in:
model.add(x + 2 * y <= 5.0)
model.add(sum(array_of_vars) == 5.0)
- In Model, the objective is a linear expression:
model.minimize(x + 2.0 * y + z)
- For large arrays, using the LinearExpr class is faster that using the python
sum()
function. You can create constraints and the objective from lists of linear expressions or coefficients as follows:
model.minimize(model_builder.LinearExpr.sum(expressions))
model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
98 @classmethod 99 def sum( # pytype: disable=annotation-type-mismatch # numpy-scalars 100 cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0 101 ) -> LinearExprT: 102 """Creates `sum(expressions) + constant`. 103 104 It can perform simple simplifications and returns different objects, 105 including the input. 106 107 Args: 108 expressions: a sequence of linear expressions or constants. 109 constant: a numerical constant. 110 111 Returns: 112 a LinearExpr instance or a numerical constant. 113 """ 114 checked_constant: np.double = mbn.assert_is_a_number(constant) 115 if not expressions: 116 return checked_constant 117 if len(expressions) == 1 and mbn.is_zero(checked_constant): 118 return expressions[0] 119 120 return LinearExpr.weighted_sum( 121 expressions, np.ones(len(expressions)), constant=checked_constant 122 )
Creates sum(expressions) + constant
.
It can perform simple simplifications and returns different objects, including the input.
Arguments:
- expressions: a sequence of linear expressions or constants.
- constant: a numerical constant.
Returns:
a LinearExpr instance or a numerical constant.
124 @classmethod 125 def weighted_sum( # pytype: disable=annotation-type-mismatch # numpy-scalars 126 cls, 127 expressions: Sequence[LinearExprT], 128 coefficients: Sequence[NumberT], 129 *, 130 constant: NumberT = 0.0, 131 ) -> Union[NumberT, "_LinearExpression"]: 132 """Creates `sum(expressions[i] * coefficients[i]) + constant`. 133 134 It can perform simple simplifications and returns different object, 135 including the input. 136 137 Args: 138 expressions: a sequence of linear expressions or constants. 139 coefficients: a sequence of numerical constants. 140 constant: a numerical constant. 141 142 Returns: 143 a _LinearExpression instance or a numerical constant. 144 """ 145 if len(expressions) != len(coefficients): 146 raise ValueError( 147 "LinearExpr.weighted_sum: expressions and coefficients have" 148 " different lengths" 149 ) 150 checked_constant: np.double = mbn.assert_is_a_number(constant) 151 if not expressions: 152 return checked_constant 153 return _sum_as_flat_linear_expression( 154 to_process=list(zip(expressions, coefficients)), offset=checked_constant 155 )
Creates sum(expressions[i] * coefficients[i]) + constant
.
It can perform simple simplifications and returns different object, including the input.
Arguments:
- expressions: a sequence of linear expressions or constants.
- coefficients: a sequence of numerical constants.
- constant: a numerical constant.
Returns:
a _LinearExpression instance or a numerical constant.
157 @classmethod 158 def term( # pytype: disable=annotation-type-mismatch # numpy-scalars 159 cls, 160 expression: LinearExprT, 161 coefficient: NumberT, 162 *, 163 constant: NumberT = 0.0, 164 ) -> LinearExprT: 165 """Creates `expression * coefficient + constant`. 166 167 It can perform simple simplifications and returns different object, 168 including the input. 169 Args: 170 expression: a linear expression or a constant. 171 coefficient: a numerical constant. 172 constant: a numerical constant. 173 174 Returns: 175 a LinearExpr instance or a numerical constant. 176 """ 177 checked_coefficient: np.double = mbn.assert_is_a_number(coefficient) 178 checked_constant: np.double = mbn.assert_is_a_number(constant) 179 180 if mbn.is_zero(checked_coefficient): 181 return checked_constant 182 if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant): 183 return expression 184 if mbn.is_a_number(expression): 185 return np.double(expression) * checked_coefficient + checked_constant 186 if isinstance(expression, LinearExpr): 187 return _as_flat_linear_expression( 188 expression * checked_coefficient + checked_constant 189 ) 190 raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}")
Creates expression * coefficient + constant
.
It can perform simple simplifications and returns different object, including the input.
Arguments:
- expression: a linear expression or a constant.
- coefficient: a numerical constant.
- constant: a numerical constant.
Returns:
a LinearExpr instance or a numerical constant.
236class Variable(LinearExpr): 237 """A variable (continuous or integral). 238 239 A Variable is an object that can take on any integer value within defined 240 ranges. Variables appear in constraint like: 241 242 x + y >= 5 243 244 Solving a model is equivalent to finding, for each variable, a single value 245 from the set of initial values (called the initial domain), such that the 246 model is feasible, or optimal if you provided an objective function. 247 """ 248 249 def __init__( 250 self, 251 helper: mbh.ModelBuilderHelper, 252 lb: NumberT, 253 ub: Optional[NumberT], 254 is_integral: Optional[bool], 255 name: Optional[str], 256 ): 257 """See Model.new_var below.""" 258 LinearExpr.__init__(self) 259 self.__helper: mbh.ModelBuilderHelper = helper 260 # Python do not support multiple __init__ methods. 261 # This method is only called from the Model class. 262 # We hack the parameter to support the two cases: 263 # case 1: 264 # helper is a ModelBuilderHelper, lb is a double value, ub is a double 265 # value, is_integral is a Boolean value, and name is a string. 266 # case 2: 267 # helper is a ModelBuilderHelper, lb is an index (int), ub is None, 268 # is_integral is None, and name is None. 269 if mbn.is_integral(lb) and ub is None and is_integral is None: 270 self.__index: np.int32 = np.int32(lb) 271 self.__helper: mbh.ModelBuilderHelper = helper 272 else: 273 index: np.int32 = helper.add_var() 274 self.__index: np.int32 = np.int32(index) 275 self.__helper: mbh.ModelBuilderHelper = helper 276 helper.set_var_lower_bound(index, lb) 277 helper.set_var_upper_bound(index, ub) 278 helper.set_var_integrality(index, is_integral) 279 if name: 280 helper.set_var_name(index, name) 281 282 @property 283 def index(self) -> np.int32: 284 """Returns the index of the variable in the helper.""" 285 return self.__index 286 287 @property 288 def helper(self) -> mbh.ModelBuilderHelper: 289 """Returns the underlying ModelBuilderHelper.""" 290 return self.__helper 291 292 def is_equal_to(self, other: LinearExprT) -> bool: 293 """Returns true if self == other in the python sense.""" 294 if not isinstance(other, Variable): 295 return False 296 return self.index == other.index and self.helper == other.helper 297 298 def __str__(self) -> str: 299 return self.name 300 301 def __repr__(self) -> str: 302 return self.__str__() 303 304 @property 305 def name(self) -> str: 306 """Returns the name of the variable.""" 307 var_name = self.__helper.var_name(self.__index) 308 if var_name: 309 return var_name 310 return f"variable#{self.index}" 311 312 @name.setter 313 def name(self, name: str) -> None: 314 """Sets the name of the variable.""" 315 self.__helper.set_var_name(self.__index, name) 316 317 @property 318 def lower_bound(self) -> np.double: 319 """Returns the lower bound of the variable.""" 320 return self.__helper.var_lower_bound(self.__index) 321 322 @lower_bound.setter 323 def lower_bound(self, bound: NumberT) -> None: 324 """Sets the lower bound of the variable.""" 325 self.__helper.set_var_lower_bound(self.__index, bound) 326 327 @property 328 def upper_bound(self) -> np.double: 329 """Returns the upper bound of the variable.""" 330 return self.__helper.var_upper_bound(self.__index) 331 332 @upper_bound.setter 333 def upper_bound(self, bound: NumberT) -> None: 334 """Sets the upper bound of the variable.""" 335 self.__helper.set_var_upper_bound(self.__index, bound) 336 337 @property 338 def is_integral(self) -> bool: 339 """Returns whether the variable is integral.""" 340 return self.__helper.var_is_integral(self.__index) 341 342 @is_integral.setter 343 def integrality(self, is_integral: bool) -> None: 344 """Sets the integrality of the variable.""" 345 self.__helper.set_var_integrality(self.__index, is_integral) 346 347 @property 348 def objective_coefficient(self) -> NumberT: 349 return self.__helper.var_objective_coefficient(self.__index) 350 351 @objective_coefficient.setter 352 def objective_coefficient(self, coeff: NumberT) -> None: 353 self.__helper.set_var_objective_coefficient(self.__index, coeff) 354 355 def __eq__(self, arg: Optional[LinearExprT]) -> ConstraintT: 356 if arg is None: 357 return False 358 if isinstance(arg, Variable): 359 return VarEqVar(self, arg) 360 return BoundedLinearExpression( 361 self - arg, 0.0, 0.0 362 ) # pytype: disable=wrong-arg-types # numpy-scalars 363 364 def __hash__(self): 365 return hash((self.__helper, self.__index))
A variable (continuous or integral).
A Variable is an object that can take on any integer value within defined ranges. Variables appear in constraint like:
x + y >= 5
Solving a model is equivalent to finding, for each variable, a single value from the set of initial values (called the initial domain), such that the model is feasible, or optimal if you provided an objective function.
249 def __init__( 250 self, 251 helper: mbh.ModelBuilderHelper, 252 lb: NumberT, 253 ub: Optional[NumberT], 254 is_integral: Optional[bool], 255 name: Optional[str], 256 ): 257 """See Model.new_var below.""" 258 LinearExpr.__init__(self) 259 self.__helper: mbh.ModelBuilderHelper = helper 260 # Python do not support multiple __init__ methods. 261 # This method is only called from the Model class. 262 # We hack the parameter to support the two cases: 263 # case 1: 264 # helper is a ModelBuilderHelper, lb is a double value, ub is a double 265 # value, is_integral is a Boolean value, and name is a string. 266 # case 2: 267 # helper is a ModelBuilderHelper, lb is an index (int), ub is None, 268 # is_integral is None, and name is None. 269 if mbn.is_integral(lb) and ub is None and is_integral is None: 270 self.__index: np.int32 = np.int32(lb) 271 self.__helper: mbh.ModelBuilderHelper = helper 272 else: 273 index: np.int32 = helper.add_var() 274 self.__index: np.int32 = np.int32(index) 275 self.__helper: mbh.ModelBuilderHelper = helper 276 helper.set_var_lower_bound(index, lb) 277 helper.set_var_upper_bound(index, ub) 278 helper.set_var_integrality(index, is_integral) 279 if name: 280 helper.set_var_name(index, name)
See Model.new_var below.
282 @property 283 def index(self) -> np.int32: 284 """Returns the index of the variable in the helper.""" 285 return self.__index
Returns the index of the variable in the helper.
287 @property 288 def helper(self) -> mbh.ModelBuilderHelper: 289 """Returns the underlying ModelBuilderHelper.""" 290 return self.__helper
Returns the underlying ModelBuilderHelper.
292 def is_equal_to(self, other: LinearExprT) -> bool: 293 """Returns true if self == other in the python sense.""" 294 if not isinstance(other, Variable): 295 return False 296 return self.index == other.index and self.helper == other.helper
Returns true if self == other in the python sense.
304 @property 305 def name(self) -> str: 306 """Returns the name of the variable.""" 307 var_name = self.__helper.var_name(self.__index) 308 if var_name: 309 return var_name 310 return f"variable#{self.index}"
Returns the name of the variable.
317 @property 318 def lower_bound(self) -> np.double: 319 """Returns the lower bound of the variable.""" 320 return self.__helper.var_lower_bound(self.__index)
Returns the lower bound of the variable.
327 @property 328 def upper_bound(self) -> np.double: 329 """Returns the upper bound of the variable.""" 330 return self.__helper.var_upper_bound(self.__index)
Returns the upper bound of the variable.
337 @property 338 def is_integral(self) -> bool: 339 """Returns whether the variable is integral.""" 340 return self.__helper.var_is_integral(self.__index)
Returns whether the variable is integral.
337 @property 338 def is_integral(self) -> bool: 339 """Returns whether the variable is integral.""" 340 return self.__helper.var_is_integral(self.__index)
Returns whether the variable is integral.
Inherited Members
501@dataclasses.dataclass(repr=False, eq=False, frozen=True) 502class VarEqVar(_BoundedLinearExpr): 503 """Represents var == var.""" 504 505 __slots__ = ("left", "right") 506 507 left: Variable 508 right: Variable 509 510 def __str__(self): 511 return f"{self.left} == {self.right}" 512 513 def __repr__(self): 514 return self.__str__() 515 516 def __bool__(self) -> bool: 517 return hash(self.left) == hash(self.right) 518 519 def _add_linear_constraint( 520 self, helper: mbh.ModelBuilderHelper, name: str 521 ) -> "LinearConstraint": 522 c = LinearConstraint(helper) 523 helper.set_constraint_lower_bound(c.index, 0.0) 524 helper.set_constraint_upper_bound(c.index, 0.0) 525 # pylint: disable=protected-access 526 helper.add_term_to_constraint(c.index, self.left.index, 1.0) 527 helper.add_term_to_constraint(c.index, self.right.index, -1.0) 528 # pylint: enable=protected-access 529 helper.set_constraint_name(c.index, name) 530 return c 531 532 def _add_enforced_linear_constraint( 533 self, 534 helper: mbh.ModelBuilderHelper, 535 var: Variable, 536 value: bool, 537 name: str, 538 ) -> "EnforcedLinearConstraint": 539 """Adds an enforced linear constraint to the model.""" 540 c = EnforcedLinearConstraint(helper) 541 c.indicator_variable = var 542 c.indicator_value = value 543 helper.set_enforced_constraint_lower_bound(c.index, 0.0) 544 helper.set_enforced_constraint_upper_bound(c.index, 0.0) 545 # pylint: disable=protected-access 546 helper.add_term_to_enforced_constraint(c.index, self.left.index, 1.0) 547 helper.add_term_to_enforced_constraint(c.index, self.right.index, -1.0) 548 # pylint: enable=protected-access 549 helper.set_enforced_constraint_name(c.index, name) 550 return c
Represents var == var.
553class BoundedLinearExpression(_BoundedLinearExpr): 554 """Represents a linear constraint: `lb <= linear expression <= ub`. 555 556 The only use of this class is to be added to the Model through 557 `Model.add(bounded expression)`, as in: 558 559 model.Add(x + 2 * y -1 >= z) 560 """ 561 562 def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT): 563 self.__expr: LinearExprT = expr 564 self.__lb: np.double = mbn.assert_is_a_number(lb) 565 self.__ub: np.double = mbn.assert_is_a_number(ub) 566 567 def __str__(self) -> str: 568 if self.__lb > -math.inf and self.__ub < math.inf: 569 if self.__lb == self.__ub: 570 return f"{self.__expr} == {self.__lb}" 571 else: 572 return f"{self.__lb} <= {self.__expr} <= {self.__ub}" 573 elif self.__lb > -math.inf: 574 return f"{self.__expr} >= {self.__lb}" 575 elif self.__ub < math.inf: 576 return f"{self.__expr} <= {self.__ub}" 577 else: 578 return f"{self.__expr} free" 579 580 def __repr__(self): 581 return self.__str__() 582 583 @property 584 def expression(self) -> LinearExprT: 585 return self.__expr 586 587 @property 588 def lower_bound(self) -> np.double: 589 return self.__lb 590 591 @property 592 def upper_bound(self) -> np.double: 593 return self.__ub 594 595 def __bool__(self) -> bool: 596 raise NotImplementedError( 597 f"Cannot use a BoundedLinearExpression {self} as a Boolean value" 598 ) 599 600 def _add_linear_constraint( 601 self, helper: mbh.ModelBuilderHelper, name: Optional[str] 602 ) -> "LinearConstraint": 603 c = LinearConstraint(helper) 604 flat_expr = _as_flat_linear_expression(self.__expr) 605 # pylint: disable=protected-access 606 helper.add_terms_to_constraint( 607 c.index, flat_expr._variable_indices, flat_expr._coefficients 608 ) 609 helper.set_constraint_lower_bound(c.index, self.__lb - flat_expr._offset) 610 helper.set_constraint_upper_bound(c.index, self.__ub - flat_expr._offset) 611 # pylint: enable=protected-access 612 if name is not None: 613 helper.set_constraint_name(c.index, name) 614 return c 615 616 def _add_enforced_linear_constraint( 617 self, 618 helper: mbh.ModelBuilderHelper, 619 var: Variable, 620 value: bool, 621 name: Optional[str], 622 ) -> "EnforcedLinearConstraint": 623 """Adds an enforced linear constraint to the model.""" 624 c = EnforcedLinearConstraint(helper) 625 c.indicator_variable = var 626 c.indicator_value = value 627 flat_expr = _as_flat_linear_expression(self.__expr) 628 # pylint: disable=protected-access 629 helper.add_terms_to_enforced_constraint( 630 c.index, flat_expr._variable_indices, flat_expr._coefficients 631 ) 632 helper.set_enforced_constraint_lower_bound( 633 c.index, self.__lb - flat_expr._offset 634 ) 635 helper.set_enforced_constraint_upper_bound( 636 c.index, self.__ub - flat_expr._offset 637 ) 638 # pylint: enable=protected-access 639 if name is not None: 640 helper.set_enforced_constraint_name(c.index, name) 641 return c
Represents a linear constraint: lb <= linear expression <= ub
.
The only use of this class is to be added to the Model through
Model.add(bounded expression)
, as in:
model.Add(x + 2 * y -1 >= z)
644class LinearConstraint: 645 """Stores a linear equation. 646 647 Example: 648 x = model.new_num_var(0, 10, 'x') 649 y = model.new_num_var(0, 10, 'y') 650 651 linear_constraint = model.add(x + 2 * y == 5) 652 """ 653 654 def __init__( 655 self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None 656 ): 657 if index is None: 658 self.__index = helper.add_linear_constraint() 659 else: 660 self.__index = index 661 self.__helper: mbh.ModelBuilderHelper = helper 662 663 @property 664 def index(self) -> IntegerT: 665 """Returns the index of the constraint in the helper.""" 666 return self.__index 667 668 @property 669 def helper(self) -> mbh.ModelBuilderHelper: 670 """Returns the ModelBuilderHelper instance.""" 671 return self.__helper 672 673 @property 674 def lower_bound(self) -> np.double: 675 return self.__helper.constraint_lower_bound(self.__index) 676 677 @lower_bound.setter 678 def lower_bound(self, bound: NumberT) -> None: 679 self.__helper.set_constraint_lower_bound(self.__index, bound) 680 681 @property 682 def upper_bound(self) -> np.double: 683 return self.__helper.constraint_upper_bound(self.__index) 684 685 @upper_bound.setter 686 def upper_bound(self, bound: NumberT) -> None: 687 self.__helper.set_constraint_upper_bound(self.__index, bound) 688 689 @property 690 def name(self) -> str: 691 constraint_name = self.__helper.constraint_name(self.__index) 692 if constraint_name: 693 return constraint_name 694 return f"linear_constraint#{self.__index}" 695 696 @name.setter 697 def name(self, name: str) -> None: 698 return self.__helper.set_constraint_name(self.__index, name) 699 700 def is_always_false(self) -> bool: 701 """Returns True if the constraint is always false. 702 703 Usually, it means that it was created by model.add(False) 704 """ 705 return self.lower_bound > self.upper_bound 706 707 def __str__(self): 708 return self.name 709 710 def __repr__(self): 711 return ( 712 f"LinearConstraint({self.name}, lb={self.lower_bound}," 713 f" ub={self.upper_bound}," 714 f" var_indices={self.helper.constraint_var_indices(self.index)}," 715 f" coefficients={self.helper.constraint_coefficients(self.index)})" 716 ) 717 718 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 719 """Sets the coefficient of the variable in the constraint.""" 720 if self.is_always_false(): 721 raise ValueError( 722 f"Constraint {self.index} is always false and cannot be modified" 723 ) 724 self.__helper.set_constraint_coefficient(self.__index, var.index, coeff) 725 726 def add_term(self, var: Variable, coeff: NumberT) -> None: 727 """Adds var * coeff to the constraint.""" 728 if self.is_always_false(): 729 raise ValueError( 730 f"Constraint {self.index} is always false and cannot be modified" 731 ) 732 self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff) 733 734 def clear_terms(self) -> None: 735 """Clear all terms of the constraint.""" 736 self.__helper.clear_constraint_terms(self.__index)
Stores a linear equation.
Example:
x = model.new_num_var(0, 10, 'x') y = model.new_num_var(0, 10, 'y')
linear_constraint = model.add(x + 2 * y == 5)
663 @property 664 def index(self) -> IntegerT: 665 """Returns the index of the constraint in the helper.""" 666 return self.__index
Returns the index of the constraint in the helper.
668 @property 669 def helper(self) -> mbh.ModelBuilderHelper: 670 """Returns the ModelBuilderHelper instance.""" 671 return self.__helper
Returns the ModelBuilderHelper instance.
700 def is_always_false(self) -> bool: 701 """Returns True if the constraint is always false. 702 703 Usually, it means that it was created by model.add(False) 704 """ 705 return self.lower_bound > self.upper_bound
Returns True if the constraint is always false.
Usually, it means that it was created by model.add(False)
718 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 719 """Sets the coefficient of the variable in the constraint.""" 720 if self.is_always_false(): 721 raise ValueError( 722 f"Constraint {self.index} is always false and cannot be modified" 723 ) 724 self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
Sets the coefficient of the variable in the constraint.
726 def add_term(self, var: Variable, coeff: NumberT) -> None: 727 """Adds var * coeff to the constraint.""" 728 if self.is_always_false(): 729 raise ValueError( 730 f"Constraint {self.index} is always false and cannot be modified" 731 ) 732 self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)
Adds var * coeff to the constraint.
739class EnforcedLinearConstraint: 740 """Stores an enforced linear equation, also name indicator constraint. 741 742 Example: 743 x = model.new_num_var(0, 10, 'x') 744 y = model.new_num_var(0, 10, 'y') 745 z = model.new_bool_var('z') 746 747 enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False) 748 """ 749 750 def __init__( 751 self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None 752 ): 753 if index is None: 754 self.__index = helper.add_enforced_linear_constraint() 755 else: 756 if not helper.is_enforced_linear_constraint(index): 757 raise ValueError( 758 f"the given index {index} does not refer to an enforced linear" 759 " constraint" 760 ) 761 762 self.__index = index 763 self.__helper: mbh.ModelBuilderHelper = helper 764 765 @property 766 def index(self) -> IntegerT: 767 """Returns the index of the constraint in the helper.""" 768 return self.__index 769 770 @property 771 def helper(self) -> mbh.ModelBuilderHelper: 772 """Returns the ModelBuilderHelper instance.""" 773 return self.__helper 774 775 @property 776 def lower_bound(self) -> np.double: 777 return self.__helper.enforced_constraint_lower_bound(self.__index) 778 779 @lower_bound.setter 780 def lower_bound(self, bound: NumberT) -> None: 781 self.__helper.set_enforced_constraint_lower_bound(self.__index, bound) 782 783 @property 784 def upper_bound(self) -> np.double: 785 return self.__helper.enforced_constraint_upper_bound(self.__index) 786 787 @upper_bound.setter 788 def upper_bound(self, bound: NumberT) -> None: 789 self.__helper.set_enforced_constraint_upper_bound(self.__index, bound) 790 791 @property 792 def indicator_variable(self) -> "Variable": 793 enforcement_var_index = ( 794 self.__helper.enforced_constraint_indicator_variable_index(self.__index) 795 ) 796 return Variable(self.__helper, enforcement_var_index, None, None, None) 797 798 @indicator_variable.setter 799 def indicator_variable(self, var: "Variable") -> None: 800 self.__helper.set_enforced_constraint_indicator_variable_index( 801 self.__index, var.index 802 ) 803 804 @property 805 def indicator_value(self) -> bool: 806 return self.__helper.enforced_constraint_indicator_value(self.__index) 807 808 @indicator_value.setter 809 def indicator_value(self, value: bool) -> None: 810 self.__helper.set_enforced_constraint_indicator_value(self.__index, value) 811 812 @property 813 def name(self) -> str: 814 constraint_name = self.__helper.enforced_constraint_name(self.__index) 815 if constraint_name: 816 return constraint_name 817 return f"enforced_linear_constraint#{self.__index}" 818 819 @name.setter 820 def name(self, name: str) -> None: 821 return self.__helper.set_enforced_constraint_name(self.__index, name) 822 823 def is_always_false(self) -> bool: 824 """Returns True if the constraint is always false. 825 826 Usually, it means that it was created by model.add(False) 827 """ 828 return self.lower_bound > self.upper_bound 829 830 def __str__(self): 831 return self.name 832 833 def __repr__(self): 834 return ( 835 f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound}," 836 f" ub={self.upper_bound}," 837 f" var_indices={self.helper.enforced_constraint_var_indices(self.index)}," 838 f" coefficients={self.helper.enforced_constraint_coefficients(self.index)}," 839 f" indicator_variable={self.indicator_variable}" 840 f" indicator_value={self.indicator_value})" 841 ) 842 843 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 844 """Sets the coefficient of the variable in the constraint.""" 845 if self.is_always_false(): 846 raise ValueError( 847 f"Constraint {self.index} is always false and cannot be modified" 848 ) 849 self.__helper.set_enforced_constraint_coefficient( 850 self.__index, var.index, coeff 851 ) 852 853 def add_term(self, var: Variable, coeff: NumberT) -> None: 854 """Adds var * coeff to the constraint.""" 855 if self.is_always_false(): 856 raise ValueError( 857 f"Constraint {self.index} is always false and cannot be modified" 858 ) 859 self.__helper.safe_add_term_to_enforced_constraint( 860 self.__index, var.index, coeff 861 ) 862 863 def clear_terms(self) -> None: 864 """Clear all terms of the constraint.""" 865 self.__helper.clear_enforced_constraint_terms(self.__index)
Stores an enforced linear equation, also name indicator constraint.
Example:
x = model.new_num_var(0, 10, 'x') y = model.new_num_var(0, 10, 'y') z = model.new_bool_var('z')
enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
750 def __init__( 751 self, helper: mbh.ModelBuilderHelper, index: Optional[IntegerT] = None 752 ): 753 if index is None: 754 self.__index = helper.add_enforced_linear_constraint() 755 else: 756 if not helper.is_enforced_linear_constraint(index): 757 raise ValueError( 758 f"the given index {index} does not refer to an enforced linear" 759 " constraint" 760 ) 761 762 self.__index = index 763 self.__helper: mbh.ModelBuilderHelper = helper
765 @property 766 def index(self) -> IntegerT: 767 """Returns the index of the constraint in the helper.""" 768 return self.__index
Returns the index of the constraint in the helper.
770 @property 771 def helper(self) -> mbh.ModelBuilderHelper: 772 """Returns the ModelBuilderHelper instance.""" 773 return self.__helper
Returns the ModelBuilderHelper instance.
823 def is_always_false(self) -> bool: 824 """Returns True if the constraint is always false. 825 826 Usually, it means that it was created by model.add(False) 827 """ 828 return self.lower_bound > self.upper_bound
Returns True if the constraint is always false.
Usually, it means that it was created by model.add(False)
843 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 844 """Sets the coefficient of the variable in the constraint.""" 845 if self.is_always_false(): 846 raise ValueError( 847 f"Constraint {self.index} is always false and cannot be modified" 848 ) 849 self.__helper.set_enforced_constraint_coefficient( 850 self.__index, var.index, coeff 851 )
Sets the coefficient of the variable in the constraint.
853 def add_term(self, var: Variable, coeff: NumberT) -> None: 854 """Adds var * coeff to the constraint.""" 855 if self.is_always_false(): 856 raise ValueError( 857 f"Constraint {self.index} is always false and cannot be modified" 858 ) 859 self.__helper.safe_add_term_to_enforced_constraint( 860 self.__index, var.index, coeff 861 )
Adds var * coeff to the constraint.
868class Model: 869 """Methods for building a linear model. 870 871 Methods beginning with: 872 873 * ```new_``` create integer, boolean, or interval variables. 874 * ```add_``` create new constraints and add them to the model. 875 """ 876 877 def __init__(self): 878 self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper() 879 880 def clone(self) -> "Model": 881 """Returns a clone of the current model.""" 882 clone = Model() 883 clone.helper.overwrite_model(self.helper) 884 return clone 885 886 @typing.overload 887 def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: 888 ... 889 890 @typing.overload 891 def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: 892 ... 893 894 def _get_linear_constraints( 895 self, constraints: Optional[_IndexOrSeries] = None 896 ) -> _IndexOrSeries: 897 if constraints is None: 898 return self.get_linear_constraints() 899 return constraints 900 901 @typing.overload 902 def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: 903 ... 904 905 @typing.overload 906 def _get_variables(self, variables: pd.Series) -> pd.Series: 907 ... 908 909 def _get_variables( 910 self, variables: Optional[_IndexOrSeries] = None 911 ) -> _IndexOrSeries: 912 if variables is None: 913 return self.get_variables() 914 return variables 915 916 def get_linear_constraints(self) -> pd.Index: 917 """Gets all linear constraints in the model.""" 918 return pd.Index( 919 [self.linear_constraint_from_index(i) for i in range(self.num_constraints)], 920 name="linear_constraint", 921 ) 922 923 def get_linear_constraint_expressions( 924 self, constraints: Optional[_IndexOrSeries] = None 925 ) -> pd.Series: 926 """Gets the expressions of all linear constraints in the set. 927 928 If `constraints` is a `pd.Index`, then the output will be indexed by the 929 constraints. If `constraints` is a `pd.Series` indexed by the underlying 930 dimensions, then the output will be indexed by the same underlying 931 dimensions. 932 933 Args: 934 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 935 constraints from which to get the expressions. If unspecified, all 936 linear constraints will be in scope. 937 938 Returns: 939 pd.Series: The expressions of all linear constraints in the set. 940 """ 941 return _attribute_series( 942 # pylint: disable=g-long-lambda 943 func=lambda c: _as_flat_linear_expression( 944 # pylint: disable=g-complex-comprehension 945 sum( 946 coeff * Variable(self.__helper, var_id, None, None, None) 947 for var_id, coeff in zip( 948 c.helper.constraint_var_indices(c.index), 949 c.helper.constraint_coefficients(c.index), 950 ) 951 ) 952 ), 953 values=self._get_linear_constraints(constraints), 954 ) 955 956 def get_linear_constraint_lower_bounds( 957 self, constraints: Optional[_IndexOrSeries] = None 958 ) -> pd.Series: 959 """Gets the lower bounds of all linear constraints in the set. 960 961 If `constraints` is a `pd.Index`, then the output will be indexed by the 962 constraints. If `constraints` is a `pd.Series` indexed by the underlying 963 dimensions, then the output will be indexed by the same underlying 964 dimensions. 965 966 Args: 967 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 968 constraints from which to get the lower bounds. If unspecified, all 969 linear constraints will be in scope. 970 971 Returns: 972 pd.Series: The lower bounds of all linear constraints in the set. 973 """ 974 return _attribute_series( 975 func=lambda c: c.lower_bound, # pylint: disable=protected-access 976 values=self._get_linear_constraints(constraints), 977 ) 978 979 def get_linear_constraint_upper_bounds( 980 self, constraints: Optional[_IndexOrSeries] = None 981 ) -> pd.Series: 982 """Gets the upper bounds of all linear constraints in the set. 983 984 If `constraints` is a `pd.Index`, then the output will be indexed by the 985 constraints. If `constraints` is a `pd.Series` indexed by the underlying 986 dimensions, then the output will be indexed by the same underlying 987 dimensions. 988 989 Args: 990 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 991 constraints. If unspecified, all linear constraints will be in scope. 992 993 Returns: 994 pd.Series: The upper bounds of all linear constraints in the set. 995 """ 996 return _attribute_series( 997 func=lambda c: c.upper_bound, # pylint: disable=protected-access 998 values=self._get_linear_constraints(constraints), 999 ) 1000 1001 def get_variables(self) -> pd.Index: 1002 """Gets all variables in the model.""" 1003 return pd.Index( 1004 [self.var_from_index(i) for i in range(self.num_variables)], 1005 name="variable", 1006 ) 1007 1008 def get_variable_lower_bounds( 1009 self, variables: Optional[_IndexOrSeries] = None 1010 ) -> pd.Series: 1011 """Gets the lower bounds of all variables in the set. 1012 1013 If `variables` is a `pd.Index`, then the output will be indexed by the 1014 variables. If `variables` is a `pd.Series` indexed by the underlying 1015 dimensions, then the output will be indexed by the same underlying 1016 dimensions. 1017 1018 Args: 1019 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 1020 from which to get the lower bounds. If unspecified, all variables will 1021 be in scope. 1022 1023 Returns: 1024 pd.Series: The lower bounds of all variables in the set. 1025 """ 1026 return _attribute_series( 1027 func=lambda v: v.lower_bound, # pylint: disable=protected-access 1028 values=self._get_variables(variables), 1029 ) 1030 1031 def get_variable_upper_bounds( 1032 self, variables: Optional[_IndexOrSeries] = None 1033 ) -> pd.Series: 1034 """Gets the upper bounds of all variables in the set. 1035 1036 Args: 1037 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 1038 from which to get the upper bounds. If unspecified, all variables will 1039 be in scope. 1040 1041 Returns: 1042 pd.Series: The upper bounds of all variables in the set. 1043 """ 1044 return _attribute_series( 1045 func=lambda v: v.upper_bound, # pylint: disable=protected-access 1046 values=self._get_variables(variables), 1047 ) 1048 1049 # Integer variable. 1050 1051 def new_var( 1052 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] 1053 ) -> Variable: 1054 """Create an integer variable with domain [lb, ub]. 1055 1056 Args: 1057 lb: Lower bound of the variable. 1058 ub: Upper bound of the variable. 1059 is_integer: Indicates if the variable must take integral values. 1060 name: The name of the variable. 1061 1062 Returns: 1063 a variable whose domain is [lb, ub]. 1064 """ 1065 1066 return Variable(self.__helper, lb, ub, is_integer, name) 1067 1068 def new_int_var( 1069 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 1070 ) -> Variable: 1071 """Create an integer variable with domain [lb, ub]. 1072 1073 Args: 1074 lb: Lower bound of the variable. 1075 ub: Upper bound of the variable. 1076 name: The name of the variable. 1077 1078 Returns: 1079 a variable whose domain is [lb, ub]. 1080 """ 1081 1082 return self.new_var(lb, ub, True, name) 1083 1084 def new_num_var( 1085 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 1086 ) -> Variable: 1087 """Create an integer variable with domain [lb, ub]. 1088 1089 Args: 1090 lb: Lower bound of the variable. 1091 ub: Upper bound of the variable. 1092 name: The name of the variable. 1093 1094 Returns: 1095 a variable whose domain is [lb, ub]. 1096 """ 1097 1098 return self.new_var(lb, ub, False, name) 1099 1100 def new_bool_var(self, name: Optional[str] = None) -> Variable: 1101 """Creates a 0-1 variable with the given name.""" 1102 return self.new_var( 1103 0, 1, True, name 1104 ) # pytype: disable=wrong-arg-types # numpy-scalars 1105 1106 def new_constant(self, value: NumberT) -> Variable: 1107 """Declares a constant variable.""" 1108 return self.new_var(value, value, False, None) 1109 1110 def new_var_series( 1111 self, 1112 name: str, 1113 index: pd.Index, 1114 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1115 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1116 is_integral: Union[bool, pd.Series] = False, 1117 ) -> pd.Series: 1118 """Creates a series of (scalar-valued) variables with the given name. 1119 1120 Args: 1121 name (str): Required. The name of the variable set. 1122 index (pd.Index): Required. The index to use for the variable set. 1123 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1124 variables in the set. If a `pd.Series` is passed in, it will be based on 1125 the corresponding values of the pd.Series. Defaults to -inf. 1126 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1127 variables in the set. If a `pd.Series` is passed in, it will be based on 1128 the corresponding values of the pd.Series. Defaults to +inf. 1129 is_integral (bool, pd.Series): Optional. Indicates if the variable can 1130 only take integer values. If a `pd.Series` is passed in, it will be 1131 based on the corresponding values of the pd.Series. Defaults to False. 1132 1133 Returns: 1134 pd.Series: The variable set indexed by its corresponding dimensions. 1135 1136 Raises: 1137 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1138 ValueError: if the `name` is not a valid identifier or already exists. 1139 ValueError: if the `lowerbound` is greater than the `upperbound`. 1140 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1141 does not match the input index. 1142 """ 1143 if not isinstance(index, pd.Index): 1144 raise TypeError("Non-index object is used as index") 1145 if not name.isidentifier(): 1146 raise ValueError("name={} is not a valid identifier".format(name)) 1147 if ( 1148 mbn.is_a_number(lower_bounds) 1149 and mbn.is_a_number(upper_bounds) 1150 and lower_bounds > upper_bounds 1151 ): 1152 raise ValueError( 1153 "lower_bound={} is greater than upper_bound={} for variable set={}".format( 1154 lower_bounds, upper_bounds, name 1155 ) 1156 ) 1157 if ( 1158 isinstance(is_integral, bool) 1159 and is_integral 1160 and mbn.is_a_number(lower_bounds) 1161 and mbn.is_a_number(upper_bounds) 1162 and math.isfinite(lower_bounds) 1163 and math.isfinite(upper_bounds) 1164 and math.ceil(lower_bounds) > math.floor(upper_bounds) 1165 ): 1166 raise ValueError( 1167 "ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds)) 1168 + " is greater than floor(" 1169 + "upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds)) 1170 + " for variable set={}".format(name) 1171 ) 1172 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) 1173 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) 1174 is_integrals = _convert_to_series_and_validate_index(is_integral, index) 1175 return pd.Series( 1176 index=index, 1177 data=[ 1178 # pylint: disable=g-complex-comprehension 1179 Variable( 1180 helper=self.__helper, 1181 name=f"{name}[{i}]", 1182 lb=lower_bounds[i], 1183 ub=upper_bounds[i], 1184 is_integral=is_integrals[i], 1185 ) 1186 for i in index 1187 ], 1188 ) 1189 1190 def new_num_var_series( 1191 self, 1192 name: str, 1193 index: pd.Index, 1194 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1195 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1196 ) -> pd.Series: 1197 """Creates a series of continuous variables with the given name. 1198 1199 Args: 1200 name (str): Required. The name of the variable set. 1201 index (pd.Index): Required. The index to use for the variable set. 1202 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1203 variables in the set. If a `pd.Series` is passed in, it will be based on 1204 the corresponding values of the pd.Series. Defaults to -inf. 1205 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1206 variables in the set. If a `pd.Series` is passed in, it will be based on 1207 the corresponding values of the pd.Series. Defaults to +inf. 1208 1209 Returns: 1210 pd.Series: The variable set indexed by its corresponding dimensions. 1211 1212 Raises: 1213 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1214 ValueError: if the `name` is not a valid identifier or already exists. 1215 ValueError: if the `lowerbound` is greater than the `upperbound`. 1216 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1217 does not match the input index. 1218 """ 1219 return self.new_var_series(name, index, lower_bounds, upper_bounds, False) 1220 1221 def new_int_var_series( 1222 self, 1223 name: str, 1224 index: pd.Index, 1225 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1226 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1227 ) -> pd.Series: 1228 """Creates a series of integer variables with the given name. 1229 1230 Args: 1231 name (str): Required. The name of the variable set. 1232 index (pd.Index): Required. The index to use for the variable set. 1233 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1234 variables in the set. If a `pd.Series` is passed in, it will be based on 1235 the corresponding values of the pd.Series. Defaults to -inf. 1236 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1237 variables in the set. If a `pd.Series` is passed in, it will be based on 1238 the corresponding values of the pd.Series. Defaults to +inf. 1239 1240 Returns: 1241 pd.Series: The variable set indexed by its corresponding dimensions. 1242 1243 Raises: 1244 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1245 ValueError: if the `name` is not a valid identifier or already exists. 1246 ValueError: if the `lowerbound` is greater than the `upperbound`. 1247 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1248 does not match the input index. 1249 """ 1250 return self.new_var_series(name, index, lower_bounds, upper_bounds, True) 1251 1252 def new_bool_var_series( 1253 self, 1254 name: str, 1255 index: pd.Index, 1256 ) -> pd.Series: 1257 """Creates a series of Boolean variables with the given name. 1258 1259 Args: 1260 name (str): Required. The name of the variable set. 1261 index (pd.Index): Required. The index to use for the variable set. 1262 1263 Returns: 1264 pd.Series: The variable set indexed by its corresponding dimensions. 1265 1266 Raises: 1267 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1268 ValueError: if the `name` is not a valid identifier or already exists. 1269 ValueError: if the `lowerbound` is greater than the `upperbound`. 1270 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1271 does not match the input index. 1272 """ 1273 return self.new_var_series(name, index, 0, 1, True) 1274 1275 def var_from_index(self, index: IntegerT) -> Variable: 1276 """Rebuilds a variable object from the model and its index.""" 1277 return Variable(self.__helper, index, None, None, None) 1278 1279 # Linear constraints. 1280 1281 def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 1282 self, 1283 linear_expr: LinearExprT, 1284 lb: NumberT = -math.inf, 1285 ub: NumberT = math.inf, 1286 name: Optional[str] = None, 1287 ) -> LinearConstraint: 1288 """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" 1289 ct = LinearConstraint(self.__helper) 1290 if name: 1291 self.__helper.set_constraint_name(ct.index, name) 1292 if mbn.is_a_number(linear_expr): 1293 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 1294 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 1295 elif isinstance(linear_expr, Variable): 1296 self.__helper.set_constraint_lower_bound(ct.index, lb) 1297 self.__helper.set_constraint_upper_bound(ct.index, ub) 1298 self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0) 1299 elif isinstance(linear_expr, LinearExpr): 1300 flat_expr = _as_flat_linear_expression(linear_expr) 1301 # pylint: disable=protected-access 1302 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset) 1303 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset) 1304 self.__helper.add_terms_to_constraint( 1305 ct.index, flat_expr._variable_indices, flat_expr._coefficients 1306 ) 1307 else: 1308 raise TypeError( 1309 f"Not supported: Model.add_linear_constraint({linear_expr})" 1310 f" with type {type(linear_expr)}" 1311 ) 1312 return ct 1313 1314 def add( 1315 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None 1316 ) -> Union[LinearConstraint, pd.Series]: 1317 """Adds a `BoundedLinearExpression` to the model. 1318 1319 Args: 1320 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1321 name: An optional name. 1322 1323 Returns: 1324 An instance of the `Constraint` class. 1325 1326 Note that a special treatment is done when the argument does not contain any 1327 variable, and thus evaluates to True or False. 1328 1329 model.add(True) will create a constraint 0 <= empty sum <= 0 1330 1331 model.add(False) will create a constraint inf <= empty sum <= -inf 1332 1333 you can check the if a constraint is always false (lb=inf, ub=-inf) by 1334 calling LinearConstraint.is_always_false() 1335 """ 1336 if isinstance(ct, _BoundedLinearExpr): 1337 return ct._add_linear_constraint(self.__helper, name) 1338 elif isinstance(ct, bool): 1339 return _add_linear_constraint_to_helper(ct, self.__helper, name) 1340 elif isinstance(ct, pd.Series): 1341 return pd.Series( 1342 index=ct.index, 1343 data=[ 1344 _add_linear_constraint_to_helper( 1345 expr, self.__helper, f"{name}[{i}]" 1346 ) 1347 for (i, expr) in zip(ct.index, ct) 1348 ], 1349 ) 1350 else: 1351 raise TypeError("Not supported: Model.add(" + str(ct) + ")") 1352 1353 def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: 1354 """Rebuilds a linear constraint object from the model and its index.""" 1355 return LinearConstraint(self.__helper, index) 1356 1357 # EnforcedLinear constraints. 1358 1359 def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 1360 self, 1361 linear_expr: LinearExprT, 1362 ivar: "Variable", 1363 ivalue: bool, 1364 lb: NumberT = -math.inf, 1365 ub: NumberT = math.inf, 1366 name: Optional[str] = None, 1367 ) -> EnforcedLinearConstraint: 1368 """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" 1369 ct = EnforcedLinearConstraint(self.__helper) 1370 ct.indicator_variable = ivar 1371 ct.indicator_value = ivalue 1372 if name: 1373 self.__helper.set_constraint_name(ct.index, name) 1374 if mbn.is_a_number(linear_expr): 1375 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 1376 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 1377 elif isinstance(linear_expr, Variable): 1378 self.__helper.set_constraint_lower_bound(ct.index, lb) 1379 self.__helper.set_constraint_upper_bound(ct.index, ub) 1380 self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0) 1381 elif isinstance(linear_expr, LinearExpr): 1382 flat_expr = _as_flat_linear_expression(linear_expr) 1383 # pylint: disable=protected-access 1384 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset) 1385 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset) 1386 self.__helper.add_terms_to_constraint( 1387 ct.index, flat_expr._variable_indices, flat_expr._coefficients 1388 ) 1389 else: 1390 raise TypeError( 1391 "Not supported:" 1392 f" Model.add_enforced_linear_constraint({linear_expr}) with" 1393 f" type {type(linear_expr)}" 1394 ) 1395 return ct 1396 1397 def add_enforced( 1398 self, 1399 ct: Union[ConstraintT, pd.Series], 1400 var: Union[Variable, pd.Series], 1401 value: Union[bool, pd.Series], 1402 name: Optional[str] = None, 1403 ) -> Union[EnforcedLinearConstraint, pd.Series]: 1404 """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. 1405 1406 Args: 1407 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1408 var: The indicator variable 1409 value: the indicator value 1410 name: An optional name. 1411 1412 Returns: 1413 An instance of the `Constraint` class. 1414 1415 Note that a special treatment is done when the argument does not contain any 1416 variable, and thus evaluates to True or False. 1417 1418 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty 1419 sum <= 0 1420 1421 model.add_enforced(False, var, value) will create a constraint inf <= 1422 empty sum <= -inf 1423 1424 you can check the if a constraint is always false (lb=inf, ub=-inf) by 1425 calling EnforcedLinearConstraint.is_always_false() 1426 """ 1427 if isinstance(ct, _BoundedLinearExpr): 1428 return ct._add_enforced_linear_constraint(self.__helper, var, value, name) 1429 elif ( 1430 isinstance(ct, bool) 1431 and isinstance(var, Variable) 1432 and isinstance(value, bool) 1433 ): 1434 return _add_enforced_linear_constraint_to_helper( 1435 ct, self.__helper, var, value, name 1436 ) 1437 elif isinstance(ct, pd.Series): 1438 ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) 1439 ivalue_series = _convert_to_series_and_validate_index(value, ct.index) 1440 return pd.Series( 1441 index=ct.index, 1442 data=[ 1443 _add_enforced_linear_constraint_to_helper( 1444 expr, 1445 self.__helper, 1446 ivar_series[i], 1447 ivalue_series[i], 1448 f"{name}[{i}]", 1449 ) 1450 for (i, expr) in zip(ct.index, ct) 1451 ], 1452 ) 1453 else: 1454 raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")") 1455 1456 def enforced_linear_constraint_from_index( 1457 self, index: IntegerT 1458 ) -> EnforcedLinearConstraint: 1459 """Rebuilds an enforced linear constraint object from the model and its index.""" 1460 return EnforcedLinearConstraint(self.__helper, index) 1461 1462 # Objective. 1463 def minimize(self, linear_expr: LinearExprT) -> None: 1464 """Minimizes the given objective.""" 1465 self.__optimize(linear_expr, False) 1466 1467 def maximize(self, linear_expr: LinearExprT) -> None: 1468 """Maximizes the given objective.""" 1469 self.__optimize(linear_expr, True) 1470 1471 def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None: 1472 """Defines the objective.""" 1473 self.helper.clear_objective() 1474 self.__helper.set_maximize(maximize) 1475 if mbn.is_a_number(linear_expr): 1476 self.helper.set_objective_offset(linear_expr) 1477 elif isinstance(linear_expr, Variable): 1478 self.helper.set_var_objective_coefficient(linear_expr.index, 1.0) 1479 elif isinstance(linear_expr, LinearExpr): 1480 flat_expr = _as_flat_linear_expression(linear_expr) 1481 # pylint: disable=protected-access 1482 self.helper.set_objective_offset(flat_expr._offset) 1483 self.helper.set_objective_coefficients( 1484 flat_expr._variable_indices, flat_expr._coefficients 1485 ) 1486 else: 1487 raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})") 1488 1489 @property 1490 def objective_offset(self) -> np.double: 1491 """Returns the fixed offset of the objective.""" 1492 return self.__helper.objective_offset() 1493 1494 @objective_offset.setter 1495 def objective_offset(self, value: NumberT) -> None: 1496 self.__helper.set_objective_offset(value) 1497 1498 def objective_expression(self) -> "_LinearExpression": 1499 """Returns the expression to optimize.""" 1500 return _as_flat_linear_expression( 1501 sum( 1502 variable * self.__helper.var_objective_coefficient(variable.index) 1503 for variable in self.get_variables() 1504 if self.__helper.var_objective_coefficient(variable.index) != 0.0 1505 ) 1506 + self.__helper.objective_offset() 1507 ) 1508 1509 # Hints. 1510 def clear_hints(self): 1511 """Clears all solution hints.""" 1512 self.__helper.clear_hints() 1513 1514 def add_hint(self, var: Variable, value: NumberT) -> None: 1515 """Adds var == value as a hint to the model. 1516 1517 Args: 1518 var: The variable of the hint 1519 value: The value of the hint 1520 1521 Note that variables must not appear more than once in the list of hints. 1522 """ 1523 self.__helper.add_hint(var.index, value) 1524 1525 # Input/Output 1526 def export_to_lp_string(self, obfuscate: bool = False) -> str: 1527 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1528 options.obfuscate = obfuscate 1529 return self.__helper.export_to_lp_string(options) 1530 1531 def export_to_mps_string(self, obfuscate: bool = False) -> str: 1532 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1533 options.obfuscate = obfuscate 1534 return self.__helper.export_to_mps_string(options) 1535 1536 def export_to_proto(self) -> linear_solver_pb2.MPModelProto: 1537 """Exports the optimization model to a ProtoBuf format.""" 1538 return mbh.to_mpmodel_proto(self.__helper) 1539 1540 def import_from_mps_string(self, mps_string: str) -> bool: 1541 """Reads a model from a MPS string.""" 1542 return self.__helper.import_from_mps_string(mps_string) 1543 1544 def import_from_mps_file(self, mps_file: str) -> bool: 1545 """Reads a model from a .mps file.""" 1546 return self.__helper.import_from_mps_file(mps_file) 1547 1548 def import_from_lp_string(self, lp_string: str) -> bool: 1549 """Reads a model from a LP string.""" 1550 return self.__helper.import_from_lp_string(lp_string) 1551 1552 def import_from_lp_file(self, lp_file: str) -> bool: 1553 """Reads a model from a .lp file.""" 1554 return self.__helper.import_from_lp_file(lp_file) 1555 1556 def import_from_proto_file(self, proto_file: str) -> bool: 1557 """Reads a model from a proto file.""" 1558 return self.__helper.read_model_from_proto_file(proto_file) 1559 1560 def export_to_proto_file(self, proto_file: str) -> bool: 1561 """Writes a model to a proto file.""" 1562 return self.__helper.write_model_to_proto_file(proto_file) 1563 1564 # Model getters and Setters 1565 1566 @property 1567 def num_variables(self) -> int: 1568 """Returns the number of variables in the model.""" 1569 return self.__helper.num_variables() 1570 1571 @property 1572 def num_constraints(self) -> int: 1573 """The number of constraints in the model.""" 1574 return self.__helper.num_constraints() 1575 1576 @property 1577 def name(self) -> str: 1578 """The name of the model.""" 1579 return self.__helper.name() 1580 1581 @name.setter 1582 def name(self, name: str): 1583 self.__helper.set_name(name) 1584 1585 @property 1586 def helper(self) -> mbh.ModelBuilderHelper: 1587 """Returns the model builder helper.""" 1588 return self.__helper
Methods for building a linear model.
Methods beginning with:
new_
create integer, boolean, or interval variables.add_
create new constraints and add them to the model.
880 def clone(self) -> "Model": 881 """Returns a clone of the current model.""" 882 clone = Model() 883 clone.helper.overwrite_model(self.helper) 884 return clone
Returns a clone of the current model.
916 def get_linear_constraints(self) -> pd.Index: 917 """Gets all linear constraints in the model.""" 918 return pd.Index( 919 [self.linear_constraint_from_index(i) for i in range(self.num_constraints)], 920 name="linear_constraint", 921 )
Gets all linear constraints in the model.
923 def get_linear_constraint_expressions( 924 self, constraints: Optional[_IndexOrSeries] = None 925 ) -> pd.Series: 926 """Gets the expressions of all linear constraints in the set. 927 928 If `constraints` is a `pd.Index`, then the output will be indexed by the 929 constraints. If `constraints` is a `pd.Series` indexed by the underlying 930 dimensions, then the output will be indexed by the same underlying 931 dimensions. 932 933 Args: 934 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 935 constraints from which to get the expressions. If unspecified, all 936 linear constraints will be in scope. 937 938 Returns: 939 pd.Series: The expressions of all linear constraints in the set. 940 """ 941 return _attribute_series( 942 # pylint: disable=g-long-lambda 943 func=lambda c: _as_flat_linear_expression( 944 # pylint: disable=g-complex-comprehension 945 sum( 946 coeff * Variable(self.__helper, var_id, None, None, None) 947 for var_id, coeff in zip( 948 c.helper.constraint_var_indices(c.index), 949 c.helper.constraint_coefficients(c.index), 950 ) 951 ) 952 ), 953 values=self._get_linear_constraints(constraints), 954 )
Gets the expressions of all linear constraints in the set.
If constraints
is a pd.Index
, then the output will be indexed by the
constraints. If constraints
is a pd.Series
indexed by the underlying
dimensions, then the output will be indexed by the same underlying
dimensions.
Arguments:
- constraints (Union[pd.Index, pd.Series]): Optional. The set of linear constraints from which to get the expressions. If unspecified, all linear constraints will be in scope.
Returns:
pd.Series: The expressions of all linear constraints in the set.
956 def get_linear_constraint_lower_bounds( 957 self, constraints: Optional[_IndexOrSeries] = None 958 ) -> pd.Series: 959 """Gets the lower bounds of all linear constraints in the set. 960 961 If `constraints` is a `pd.Index`, then the output will be indexed by the 962 constraints. If `constraints` is a `pd.Series` indexed by the underlying 963 dimensions, then the output will be indexed by the same underlying 964 dimensions. 965 966 Args: 967 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 968 constraints from which to get the lower bounds. If unspecified, all 969 linear constraints will be in scope. 970 971 Returns: 972 pd.Series: The lower bounds of all linear constraints in the set. 973 """ 974 return _attribute_series( 975 func=lambda c: c.lower_bound, # pylint: disable=protected-access 976 values=self._get_linear_constraints(constraints), 977 )
Gets the lower bounds of all linear constraints in the set.
If constraints
is a pd.Index
, then the output will be indexed by the
constraints. If constraints
is a pd.Series
indexed by the underlying
dimensions, then the output will be indexed by the same underlying
dimensions.
Arguments:
- constraints (Union[pd.Index, pd.Series]): Optional. The set of linear constraints from which to get the lower bounds. If unspecified, all linear constraints will be in scope.
Returns:
pd.Series: The lower bounds of all linear constraints in the set.
979 def get_linear_constraint_upper_bounds( 980 self, constraints: Optional[_IndexOrSeries] = None 981 ) -> pd.Series: 982 """Gets the upper bounds of all linear constraints in the set. 983 984 If `constraints` is a `pd.Index`, then the output will be indexed by the 985 constraints. If `constraints` is a `pd.Series` indexed by the underlying 986 dimensions, then the output will be indexed by the same underlying 987 dimensions. 988 989 Args: 990 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 991 constraints. If unspecified, all linear constraints will be in scope. 992 993 Returns: 994 pd.Series: The upper bounds of all linear constraints in the set. 995 """ 996 return _attribute_series( 997 func=lambda c: c.upper_bound, # pylint: disable=protected-access 998 values=self._get_linear_constraints(constraints), 999 )
Gets the upper bounds of all linear constraints in the set.
If constraints
is a pd.Index
, then the output will be indexed by the
constraints. If constraints
is a pd.Series
indexed by the underlying
dimensions, then the output will be indexed by the same underlying
dimensions.
Arguments:
- constraints (Union[pd.Index, pd.Series]): Optional. The set of linear constraints. If unspecified, all linear constraints will be in scope.
Returns:
pd.Series: The upper bounds of all linear constraints in the set.
1001 def get_variables(self) -> pd.Index: 1002 """Gets all variables in the model.""" 1003 return pd.Index( 1004 [self.var_from_index(i) for i in range(self.num_variables)], 1005 name="variable", 1006 )
Gets all variables in the model.
1008 def get_variable_lower_bounds( 1009 self, variables: Optional[_IndexOrSeries] = None 1010 ) -> pd.Series: 1011 """Gets the lower bounds of all variables in the set. 1012 1013 If `variables` is a `pd.Index`, then the output will be indexed by the 1014 variables. If `variables` is a `pd.Series` indexed by the underlying 1015 dimensions, then the output will be indexed by the same underlying 1016 dimensions. 1017 1018 Args: 1019 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 1020 from which to get the lower bounds. If unspecified, all variables will 1021 be in scope. 1022 1023 Returns: 1024 pd.Series: The lower bounds of all variables in the set. 1025 """ 1026 return _attribute_series( 1027 func=lambda v: v.lower_bound, # pylint: disable=protected-access 1028 values=self._get_variables(variables), 1029 )
Gets the lower bounds of all variables in the set.
If variables
is a pd.Index
, then the output will be indexed by the
variables. If variables
is a pd.Series
indexed by the underlying
dimensions, then the output will be indexed by the same underlying
dimensions.
Arguments:
- variables (Union[pd.Index, pd.Series]): Optional. The set of variables from which to get the lower bounds. If unspecified, all variables will be in scope.
Returns:
pd.Series: The lower bounds of all variables in the set.
1031 def get_variable_upper_bounds( 1032 self, variables: Optional[_IndexOrSeries] = None 1033 ) -> pd.Series: 1034 """Gets the upper bounds of all variables in the set. 1035 1036 Args: 1037 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 1038 from which to get the upper bounds. If unspecified, all variables will 1039 be in scope. 1040 1041 Returns: 1042 pd.Series: The upper bounds of all variables in the set. 1043 """ 1044 return _attribute_series( 1045 func=lambda v: v.upper_bound, # pylint: disable=protected-access 1046 values=self._get_variables(variables), 1047 )
Gets the upper bounds of all variables in the set.
Arguments:
- variables (Union[pd.Index, pd.Series]): Optional. The set of variables from which to get the upper bounds. If unspecified, all variables will be in scope.
Returns:
pd.Series: The upper bounds of all variables in the set.
1051 def new_var( 1052 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] 1053 ) -> Variable: 1054 """Create an integer variable with domain [lb, ub]. 1055 1056 Args: 1057 lb: Lower bound of the variable. 1058 ub: Upper bound of the variable. 1059 is_integer: Indicates if the variable must take integral values. 1060 name: The name of the variable. 1061 1062 Returns: 1063 a variable whose domain is [lb, ub]. 1064 """ 1065 1066 return Variable(self.__helper, lb, ub, is_integer, name)
Create an integer variable with domain [lb, ub].
Arguments:
- lb: Lower bound of the variable.
- ub: Upper bound of the variable.
- is_integer: Indicates if the variable must take integral values.
- name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
1068 def new_int_var( 1069 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 1070 ) -> Variable: 1071 """Create an integer variable with domain [lb, ub]. 1072 1073 Args: 1074 lb: Lower bound of the variable. 1075 ub: Upper bound of the variable. 1076 name: The name of the variable. 1077 1078 Returns: 1079 a variable whose domain is [lb, ub]. 1080 """ 1081 1082 return self.new_var(lb, ub, True, name)
Create an integer variable with domain [lb, ub].
Arguments:
- lb: Lower bound of the variable.
- ub: Upper bound of the variable.
- name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
1084 def new_num_var( 1085 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 1086 ) -> Variable: 1087 """Create an integer variable with domain [lb, ub]. 1088 1089 Args: 1090 lb: Lower bound of the variable. 1091 ub: Upper bound of the variable. 1092 name: The name of the variable. 1093 1094 Returns: 1095 a variable whose domain is [lb, ub]. 1096 """ 1097 1098 return self.new_var(lb, ub, False, name)
Create an integer variable with domain [lb, ub].
Arguments:
- lb: Lower bound of the variable.
- ub: Upper bound of the variable.
- name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
1100 def new_bool_var(self, name: Optional[str] = None) -> Variable: 1101 """Creates a 0-1 variable with the given name.""" 1102 return self.new_var( 1103 0, 1, True, name 1104 ) # pytype: disable=wrong-arg-types # numpy-scalars
Creates a 0-1 variable with the given name.
1106 def new_constant(self, value: NumberT) -> Variable: 1107 """Declares a constant variable.""" 1108 return self.new_var(value, value, False, None)
Declares a constant variable.
1110 def new_var_series( 1111 self, 1112 name: str, 1113 index: pd.Index, 1114 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1115 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1116 is_integral: Union[bool, pd.Series] = False, 1117 ) -> pd.Series: 1118 """Creates a series of (scalar-valued) variables with the given name. 1119 1120 Args: 1121 name (str): Required. The name of the variable set. 1122 index (pd.Index): Required. The index to use for the variable set. 1123 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1124 variables in the set. If a `pd.Series` is passed in, it will be based on 1125 the corresponding values of the pd.Series. Defaults to -inf. 1126 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1127 variables in the set. If a `pd.Series` is passed in, it will be based on 1128 the corresponding values of the pd.Series. Defaults to +inf. 1129 is_integral (bool, pd.Series): Optional. Indicates if the variable can 1130 only take integer values. If a `pd.Series` is passed in, it will be 1131 based on the corresponding values of the pd.Series. Defaults to False. 1132 1133 Returns: 1134 pd.Series: The variable set indexed by its corresponding dimensions. 1135 1136 Raises: 1137 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1138 ValueError: if the `name` is not a valid identifier or already exists. 1139 ValueError: if the `lowerbound` is greater than the `upperbound`. 1140 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1141 does not match the input index. 1142 """ 1143 if not isinstance(index, pd.Index): 1144 raise TypeError("Non-index object is used as index") 1145 if not name.isidentifier(): 1146 raise ValueError("name={} is not a valid identifier".format(name)) 1147 if ( 1148 mbn.is_a_number(lower_bounds) 1149 and mbn.is_a_number(upper_bounds) 1150 and lower_bounds > upper_bounds 1151 ): 1152 raise ValueError( 1153 "lower_bound={} is greater than upper_bound={} for variable set={}".format( 1154 lower_bounds, upper_bounds, name 1155 ) 1156 ) 1157 if ( 1158 isinstance(is_integral, bool) 1159 and is_integral 1160 and mbn.is_a_number(lower_bounds) 1161 and mbn.is_a_number(upper_bounds) 1162 and math.isfinite(lower_bounds) 1163 and math.isfinite(upper_bounds) 1164 and math.ceil(lower_bounds) > math.floor(upper_bounds) 1165 ): 1166 raise ValueError( 1167 "ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds)) 1168 + " is greater than floor(" 1169 + "upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds)) 1170 + " for variable set={}".format(name) 1171 ) 1172 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) 1173 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) 1174 is_integrals = _convert_to_series_and_validate_index(is_integral, index) 1175 return pd.Series( 1176 index=index, 1177 data=[ 1178 # pylint: disable=g-complex-comprehension 1179 Variable( 1180 helper=self.__helper, 1181 name=f"{name}[{i}]", 1182 lb=lower_bounds[i], 1183 ub=upper_bounds[i], 1184 is_integral=is_integrals[i], 1185 ) 1186 for i in index 1187 ], 1188 )
Creates a series of (scalar-valued) variables with the given name.
Arguments:
- name (str): Required. The name of the variable set.
- index (pd.Index): Required. The index to use for the variable set.
- lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
variables in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. Defaults to -inf. - upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
variables in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. Defaults to +inf. - is_integral (bool, pd.Series): Optional. Indicates if the variable can
only take integer values. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. Defaults to False.
Returns:
pd.Series: The variable set indexed by its corresponding dimensions.
Raises:
- TypeError: if the
index
is invalid (e.g. aDataFrame
). - ValueError: if the
name
is not a valid identifier or already exists. - ValueError: if the
lowerbound
is greater than theupperbound
. - ValueError: if the index of
lower_bound
,upper_bound
, oris_integer
- does not match the input index.
1190 def new_num_var_series( 1191 self, 1192 name: str, 1193 index: pd.Index, 1194 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1195 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1196 ) -> pd.Series: 1197 """Creates a series of continuous variables with the given name. 1198 1199 Args: 1200 name (str): Required. The name of the variable set. 1201 index (pd.Index): Required. The index to use for the variable set. 1202 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1203 variables in the set. If a `pd.Series` is passed in, it will be based on 1204 the corresponding values of the pd.Series. Defaults to -inf. 1205 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1206 variables in the set. If a `pd.Series` is passed in, it will be based on 1207 the corresponding values of the pd.Series. Defaults to +inf. 1208 1209 Returns: 1210 pd.Series: The variable set indexed by its corresponding dimensions. 1211 1212 Raises: 1213 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1214 ValueError: if the `name` is not a valid identifier or already exists. 1215 ValueError: if the `lowerbound` is greater than the `upperbound`. 1216 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1217 does not match the input index. 1218 """ 1219 return self.new_var_series(name, index, lower_bounds, upper_bounds, False)
Creates a series of continuous variables with the given name.
Arguments:
- name (str): Required. The name of the variable set.
- index (pd.Index): Required. The index to use for the variable set.
- lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
variables in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. Defaults to -inf. - upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
variables in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. Defaults to +inf.
Returns:
pd.Series: The variable set indexed by its corresponding dimensions.
Raises:
- TypeError: if the
index
is invalid (e.g. aDataFrame
). - ValueError: if the
name
is not a valid identifier or already exists. - ValueError: if the
lowerbound
is greater than theupperbound
. - ValueError: if the index of
lower_bound
,upper_bound
, oris_integer
- does not match the input index.
1221 def new_int_var_series( 1222 self, 1223 name: str, 1224 index: pd.Index, 1225 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 1226 upper_bounds: Union[NumberT, pd.Series] = math.inf, 1227 ) -> pd.Series: 1228 """Creates a series of integer variables with the given name. 1229 1230 Args: 1231 name (str): Required. The name of the variable set. 1232 index (pd.Index): Required. The index to use for the variable set. 1233 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 1234 variables in the set. If a `pd.Series` is passed in, it will be based on 1235 the corresponding values of the pd.Series. Defaults to -inf. 1236 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 1237 variables in the set. If a `pd.Series` is passed in, it will be based on 1238 the corresponding values of the pd.Series. Defaults to +inf. 1239 1240 Returns: 1241 pd.Series: The variable set indexed by its corresponding dimensions. 1242 1243 Raises: 1244 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1245 ValueError: if the `name` is not a valid identifier or already exists. 1246 ValueError: if the `lowerbound` is greater than the `upperbound`. 1247 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1248 does not match the input index. 1249 """ 1250 return self.new_var_series(name, index, lower_bounds, upper_bounds, True)
Creates a series of integer variables with the given name.
Arguments:
- name (str): Required. The name of the variable set.
- index (pd.Index): Required. The index to use for the variable set.
- lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
variables in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. Defaults to -inf. - upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
variables in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. Defaults to +inf.
Returns:
pd.Series: The variable set indexed by its corresponding dimensions.
Raises:
- TypeError: if the
index
is invalid (e.g. aDataFrame
). - ValueError: if the
name
is not a valid identifier or already exists. - ValueError: if the
lowerbound
is greater than theupperbound
. - ValueError: if the index of
lower_bound
,upper_bound
, oris_integer
- does not match the input index.
1252 def new_bool_var_series( 1253 self, 1254 name: str, 1255 index: pd.Index, 1256 ) -> pd.Series: 1257 """Creates a series of Boolean variables with the given name. 1258 1259 Args: 1260 name (str): Required. The name of the variable set. 1261 index (pd.Index): Required. The index to use for the variable set. 1262 1263 Returns: 1264 pd.Series: The variable set indexed by its corresponding dimensions. 1265 1266 Raises: 1267 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1268 ValueError: if the `name` is not a valid identifier or already exists. 1269 ValueError: if the `lowerbound` is greater than the `upperbound`. 1270 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 1271 does not match the input index. 1272 """ 1273 return self.new_var_series(name, index, 0, 1, True)
Creates a series of Boolean variables with the given name.
Arguments:
- name (str): Required. The name of the variable set.
- index (pd.Index): Required. The index to use for the variable set.
Returns:
pd.Series: The variable set indexed by its corresponding dimensions.
Raises:
- TypeError: if the
index
is invalid (e.g. aDataFrame
). - ValueError: if the
name
is not a valid identifier or already exists. - ValueError: if the
lowerbound
is greater than theupperbound
. - ValueError: if the index of
lower_bound
,upper_bound
, oris_integer
- does not match the input index.
1275 def var_from_index(self, index: IntegerT) -> Variable: 1276 """Rebuilds a variable object from the model and its index.""" 1277 return Variable(self.__helper, index, None, None, None)
Rebuilds a variable object from the model and its index.
1281 def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 1282 self, 1283 linear_expr: LinearExprT, 1284 lb: NumberT = -math.inf, 1285 ub: NumberT = math.inf, 1286 name: Optional[str] = None, 1287 ) -> LinearConstraint: 1288 """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" 1289 ct = LinearConstraint(self.__helper) 1290 if name: 1291 self.__helper.set_constraint_name(ct.index, name) 1292 if mbn.is_a_number(linear_expr): 1293 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 1294 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 1295 elif isinstance(linear_expr, Variable): 1296 self.__helper.set_constraint_lower_bound(ct.index, lb) 1297 self.__helper.set_constraint_upper_bound(ct.index, ub) 1298 self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0) 1299 elif isinstance(linear_expr, LinearExpr): 1300 flat_expr = _as_flat_linear_expression(linear_expr) 1301 # pylint: disable=protected-access 1302 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset) 1303 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset) 1304 self.__helper.add_terms_to_constraint( 1305 ct.index, flat_expr._variable_indices, flat_expr._coefficients 1306 ) 1307 else: 1308 raise TypeError( 1309 f"Not supported: Model.add_linear_constraint({linear_expr})" 1310 f" with type {type(linear_expr)}" 1311 ) 1312 return ct
Adds the constraint: lb <= linear_expr <= ub
with the given name.
1314 def add( 1315 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None 1316 ) -> Union[LinearConstraint, pd.Series]: 1317 """Adds a `BoundedLinearExpression` to the model. 1318 1319 Args: 1320 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1321 name: An optional name. 1322 1323 Returns: 1324 An instance of the `Constraint` class. 1325 1326 Note that a special treatment is done when the argument does not contain any 1327 variable, and thus evaluates to True or False. 1328 1329 model.add(True) will create a constraint 0 <= empty sum <= 0 1330 1331 model.add(False) will create a constraint inf <= empty sum <= -inf 1332 1333 you can check the if a constraint is always false (lb=inf, ub=-inf) by 1334 calling LinearConstraint.is_always_false() 1335 """ 1336 if isinstance(ct, _BoundedLinearExpr): 1337 return ct._add_linear_constraint(self.__helper, name) 1338 elif isinstance(ct, bool): 1339 return _add_linear_constraint_to_helper(ct, self.__helper, name) 1340 elif isinstance(ct, pd.Series): 1341 return pd.Series( 1342 index=ct.index, 1343 data=[ 1344 _add_linear_constraint_to_helper( 1345 expr, self.__helper, f"{name}[{i}]" 1346 ) 1347 for (i, expr) in zip(ct.index, ct) 1348 ], 1349 ) 1350 else: 1351 raise TypeError("Not supported: Model.add(" + str(ct) + ")")
Adds a BoundedLinearExpression
to the model.
Arguments:
- ct: A
BoundedLinearExpression
. - name: An optional name.
Returns:
An instance of the
Constraint
class.
Note that a special treatment is done when the argument does not contain any variable, and thus evaluates to True or False.
model.add(True) will create a constraint 0 <= empty sum <= 0
model.add(False) will create a constraint inf <= empty sum <= -inf
you can check the if a constraint is always false (lb=inf, ub=-inf) by calling LinearConstraint.is_always_false()
1353 def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: 1354 """Rebuilds a linear constraint object from the model and its index.""" 1355 return LinearConstraint(self.__helper, index)
Rebuilds a linear constraint object from the model and its index.
1359 def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 1360 self, 1361 linear_expr: LinearExprT, 1362 ivar: "Variable", 1363 ivalue: bool, 1364 lb: NumberT = -math.inf, 1365 ub: NumberT = math.inf, 1366 name: Optional[str] = None, 1367 ) -> EnforcedLinearConstraint: 1368 """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" 1369 ct = EnforcedLinearConstraint(self.__helper) 1370 ct.indicator_variable = ivar 1371 ct.indicator_value = ivalue 1372 if name: 1373 self.__helper.set_constraint_name(ct.index, name) 1374 if mbn.is_a_number(linear_expr): 1375 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 1376 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 1377 elif isinstance(linear_expr, Variable): 1378 self.__helper.set_constraint_lower_bound(ct.index, lb) 1379 self.__helper.set_constraint_upper_bound(ct.index, ub) 1380 self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0) 1381 elif isinstance(linear_expr, LinearExpr): 1382 flat_expr = _as_flat_linear_expression(linear_expr) 1383 # pylint: disable=protected-access 1384 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset) 1385 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset) 1386 self.__helper.add_terms_to_constraint( 1387 ct.index, flat_expr._variable_indices, flat_expr._coefficients 1388 ) 1389 else: 1390 raise TypeError( 1391 "Not supported:" 1392 f" Model.add_enforced_linear_constraint({linear_expr}) with" 1393 f" type {type(linear_expr)}" 1394 ) 1395 return ct
Adds the constraint: ivar == ivalue => lb <= linear_expr <= ub
with the given name.
1397 def add_enforced( 1398 self, 1399 ct: Union[ConstraintT, pd.Series], 1400 var: Union[Variable, pd.Series], 1401 value: Union[bool, pd.Series], 1402 name: Optional[str] = None, 1403 ) -> Union[EnforcedLinearConstraint, pd.Series]: 1404 """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. 1405 1406 Args: 1407 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1408 var: The indicator variable 1409 value: the indicator value 1410 name: An optional name. 1411 1412 Returns: 1413 An instance of the `Constraint` class. 1414 1415 Note that a special treatment is done when the argument does not contain any 1416 variable, and thus evaluates to True or False. 1417 1418 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty 1419 sum <= 0 1420 1421 model.add_enforced(False, var, value) will create a constraint inf <= 1422 empty sum <= -inf 1423 1424 you can check the if a constraint is always false (lb=inf, ub=-inf) by 1425 calling EnforcedLinearConstraint.is_always_false() 1426 """ 1427 if isinstance(ct, _BoundedLinearExpr): 1428 return ct._add_enforced_linear_constraint(self.__helper, var, value, name) 1429 elif ( 1430 isinstance(ct, bool) 1431 and isinstance(var, Variable) 1432 and isinstance(value, bool) 1433 ): 1434 return _add_enforced_linear_constraint_to_helper( 1435 ct, self.__helper, var, value, name 1436 ) 1437 elif isinstance(ct, pd.Series): 1438 ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) 1439 ivalue_series = _convert_to_series_and_validate_index(value, ct.index) 1440 return pd.Series( 1441 index=ct.index, 1442 data=[ 1443 _add_enforced_linear_constraint_to_helper( 1444 expr, 1445 self.__helper, 1446 ivar_series[i], 1447 ivalue_series[i], 1448 f"{name}[{i}]", 1449 ) 1450 for (i, expr) in zip(ct.index, ct) 1451 ], 1452 ) 1453 else: 1454 raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")")
Adds a ivar == ivalue => BoundedLinearExpression
to the model.
Arguments:
- ct: A
BoundedLinearExpression
. - var: The indicator variable
- value: the indicator value
- name: An optional name.
Returns:
An instance of the
Constraint
class.
Note that a special treatment is done when the argument does not contain any variable, and thus evaluates to True or False.
model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty sum <= 0
model.add_enforced(False, var, value) will create a constraint inf <= empty sum <= -inf
you can check the if a constraint is always false (lb=inf, ub=-inf) by calling EnforcedLinearConstraint.is_always_false()
1456 def enforced_linear_constraint_from_index( 1457 self, index: IntegerT 1458 ) -> EnforcedLinearConstraint: 1459 """Rebuilds an enforced linear constraint object from the model and its index.""" 1460 return EnforcedLinearConstraint(self.__helper, index)
Rebuilds an enforced linear constraint object from the model and its index.
1463 def minimize(self, linear_expr: LinearExprT) -> None: 1464 """Minimizes the given objective.""" 1465 self.__optimize(linear_expr, False)
Minimizes the given objective.
1467 def maximize(self, linear_expr: LinearExprT) -> None: 1468 """Maximizes the given objective.""" 1469 self.__optimize(linear_expr, True)
Maximizes the given objective.
1489 @property 1490 def objective_offset(self) -> np.double: 1491 """Returns the fixed offset of the objective.""" 1492 return self.__helper.objective_offset()
Returns the fixed offset of the objective.
1498 def objective_expression(self) -> "_LinearExpression": 1499 """Returns the expression to optimize.""" 1500 return _as_flat_linear_expression( 1501 sum( 1502 variable * self.__helper.var_objective_coefficient(variable.index) 1503 for variable in self.get_variables() 1504 if self.__helper.var_objective_coefficient(variable.index) != 0.0 1505 ) 1506 + self.__helper.objective_offset() 1507 )
Returns the expression to optimize.
1514 def add_hint(self, var: Variable, value: NumberT) -> None: 1515 """Adds var == value as a hint to the model. 1516 1517 Args: 1518 var: The variable of the hint 1519 value: The value of the hint 1520 1521 Note that variables must not appear more than once in the list of hints. 1522 """ 1523 self.__helper.add_hint(var.index, value)
Adds var == value as a hint to the model.
Arguments:
- var: The variable of the hint
- value: The value of the hint
Note that variables must not appear more than once in the list of hints.
1536 def export_to_proto(self) -> linear_solver_pb2.MPModelProto: 1537 """Exports the optimization model to a ProtoBuf format.""" 1538 return mbh.to_mpmodel_proto(self.__helper)
Exports the optimization model to a ProtoBuf format.
1540 def import_from_mps_string(self, mps_string: str) -> bool: 1541 """Reads a model from a MPS string.""" 1542 return self.__helper.import_from_mps_string(mps_string)
Reads a model from a MPS string.
1544 def import_from_mps_file(self, mps_file: str) -> bool: 1545 """Reads a model from a .mps file.""" 1546 return self.__helper.import_from_mps_file(mps_file)
Reads a model from a .mps file.
1548 def import_from_lp_string(self, lp_string: str) -> bool: 1549 """Reads a model from a LP string.""" 1550 return self.__helper.import_from_lp_string(lp_string)
Reads a model from a LP string.
1552 def import_from_lp_file(self, lp_file: str) -> bool: 1553 """Reads a model from a .lp file.""" 1554 return self.__helper.import_from_lp_file(lp_file)
Reads a model from a .lp file.
1556 def import_from_proto_file(self, proto_file: str) -> bool: 1557 """Reads a model from a proto file.""" 1558 return self.__helper.read_model_from_proto_file(proto_file)
Reads a model from a proto file.
1560 def export_to_proto_file(self, proto_file: str) -> bool: 1561 """Writes a model to a proto file.""" 1562 return self.__helper.write_model_to_proto_file(proto_file)
Writes a model to a proto file.
1566 @property 1567 def num_variables(self) -> int: 1568 """Returns the number of variables in the model.""" 1569 return self.__helper.num_variables()
Returns the number of variables in the model.
1571 @property 1572 def num_constraints(self) -> int: 1573 """The number of constraints in the model.""" 1574 return self.__helper.num_constraints()
The number of constraints in the model.
1576 @property 1577 def name(self) -> str: 1578 """The name of the model.""" 1579 return self.__helper.name()
The name of the model.
1591class Solver: 1592 """Main solver class. 1593 1594 The purpose of this class is to search for a solution to the model provided 1595 to the solve() method. 1596 1597 Once solve() is called, this class allows inspecting the solution found 1598 with the value() method, as well as general statistics about the solve 1599 procedure. 1600 """ 1601 1602 def __init__(self, solver_name: str): 1603 self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name) 1604 self.log_callback: Optional[Callable[[str], None]] = None 1605 1606 def solver_is_supported(self) -> bool: 1607 """Checks whether the requested solver backend was found.""" 1608 return self.__solve_helper.solver_is_supported() 1609 1610 # Solver backend and parameters. 1611 def set_time_limit_in_seconds(self, limit: NumberT) -> None: 1612 """Sets a time limit for the solve() call.""" 1613 self.__solve_helper.set_time_limit_in_seconds(limit) 1614 1615 def set_solver_specific_parameters(self, parameters: str) -> None: 1616 """Sets parameters specific to the solver backend.""" 1617 self.__solve_helper.set_solver_specific_parameters(parameters) 1618 1619 def enable_output(self, enabled: bool) -> None: 1620 """Controls the solver backend logs.""" 1621 self.__solve_helper.enable_output(enabled) 1622 1623 def solve(self, model: Model) -> SolveStatus: 1624 """Solves a problem and passes each solution to the callback if not null.""" 1625 if self.log_callback is not None: 1626 self.__solve_helper.set_log_callback(self.log_callback) 1627 else: 1628 self.__solve_helper.clear_log_callback() 1629 self.__solve_helper.solve(model.helper) 1630 return SolveStatus(self.__solve_helper.status()) 1631 1632 def stop_search(self): 1633 """Stops the current search asynchronously.""" 1634 self.__solve_helper.interrupt_solve() 1635 1636 def value(self, expr: LinearExprT) -> np.double: 1637 """Returns the value of a linear expression after solve.""" 1638 if not self.__solve_helper.has_solution(): 1639 return pd.NA 1640 if mbn.is_a_number(expr): 1641 return expr 1642 elif isinstance(expr, Variable): 1643 return self.__solve_helper.var_value(expr.index) 1644 elif isinstance(expr, LinearExpr): 1645 flat_expr = _as_flat_linear_expression(expr) 1646 return self.__solve_helper.expression_value( 1647 flat_expr._variable_indices, 1648 flat_expr._coefficients, 1649 flat_expr._offset, 1650 ) 1651 else: 1652 raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}") 1653 1654 def values(self, variables: _IndexOrSeries) -> pd.Series: 1655 """Returns the values of the input variables. 1656 1657 If `variables` is a `pd.Index`, then the output will be indexed by the 1658 variables. If `variables` is a `pd.Series` indexed by the underlying 1659 dimensions, then the output will be indexed by the same underlying 1660 dimensions. 1661 1662 Args: 1663 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1664 get the values. 1665 1666 Returns: 1667 pd.Series: The values of all variables in the set. 1668 """ 1669 if not self.__solve_helper.has_solution(): 1670 return _attribute_series(func=lambda v: pd.NA, values=variables) 1671 return _attribute_series( 1672 func=lambda v: self.__solve_helper.var_value(v.index), 1673 values=variables, 1674 ) 1675 1676 def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: 1677 """Returns the reduced cost of the input variables. 1678 1679 If `variables` is a `pd.Index`, then the output will be indexed by the 1680 variables. If `variables` is a `pd.Series` indexed by the underlying 1681 dimensions, then the output will be indexed by the same underlying 1682 dimensions. 1683 1684 Args: 1685 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1686 get the values. 1687 1688 Returns: 1689 pd.Series: The reduced cost of all variables in the set. 1690 """ 1691 if not self.__solve_helper.has_solution(): 1692 return _attribute_series(func=lambda v: pd.NA, values=variables) 1693 return _attribute_series( 1694 func=lambda v: self.__solve_helper.reduced_cost(v.index), 1695 values=variables, 1696 ) 1697 1698 def reduced_cost(self, var: Variable) -> np.double: 1699 """Returns the reduced cost of a linear expression after solve.""" 1700 if not self.__solve_helper.has_solution(): 1701 return pd.NA 1702 return self.__solve_helper.reduced_cost(var.index) 1703 1704 def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: 1705 """Returns the dual values of the input constraints. 1706 1707 If `constraints` is a `pd.Index`, then the output will be indexed by the 1708 constraints. If `constraints` is a `pd.Series` indexed by the underlying 1709 dimensions, then the output will be indexed by the same underlying 1710 dimensions. 1711 1712 Args: 1713 constraints (Union[pd.Index, pd.Series]): The set of constraints from 1714 which to get the dual values. 1715 1716 Returns: 1717 pd.Series: The dual_values of all constraints in the set. 1718 """ 1719 if not self.__solve_helper.has_solution(): 1720 return _attribute_series(func=lambda v: pd.NA, values=constraints) 1721 return _attribute_series( 1722 func=lambda v: self.__solve_helper.dual_value(v.index), 1723 values=constraints, 1724 ) 1725 1726 def dual_value(self, ct: LinearConstraint) -> np.double: 1727 """Returns the dual value of a linear constraint after solve.""" 1728 if not self.__solve_helper.has_solution(): 1729 return pd.NA 1730 return self.__solve_helper.dual_value(ct.index) 1731 1732 def activity(self, ct: LinearConstraint) -> np.double: 1733 """Returns the activity of a linear constraint after solve.""" 1734 if not self.__solve_helper.has_solution(): 1735 return pd.NA 1736 return self.__solve_helper.activity(ct.index) 1737 1738 @property 1739 def objective_value(self) -> np.double: 1740 """Returns the value of the objective after solve.""" 1741 if not self.__solve_helper.has_solution(): 1742 return pd.NA 1743 return self.__solve_helper.objective_value() 1744 1745 @property 1746 def best_objective_bound(self) -> np.double: 1747 """Returns the best lower (upper) bound found when min(max)imizing.""" 1748 if not self.__solve_helper.has_solution(): 1749 return pd.NA 1750 return self.__solve_helper.best_objective_bound() 1751 1752 @property 1753 def status_string(self) -> str: 1754 """Returns additional information of the last solve. 1755 1756 It can describe why the model is invalid. 1757 """ 1758 return self.__solve_helper.status_string() 1759 1760 @property 1761 def wall_time(self) -> np.double: 1762 return self.__solve_helper.wall_time() 1763 1764 @property 1765 def user_time(self) -> np.double: 1766 return self.__solve_helper.user_time()
Main solver class.
The purpose of this class is to search for a solution to the model provided to the solve() method.
Once solve() is called, this class allows inspecting the solution found with the value() method, as well as general statistics about the solve procedure.
1606 def solver_is_supported(self) -> bool: 1607 """Checks whether the requested solver backend was found.""" 1608 return self.__solve_helper.solver_is_supported()
Checks whether the requested solver backend was found.
1611 def set_time_limit_in_seconds(self, limit: NumberT) -> None: 1612 """Sets a time limit for the solve() call.""" 1613 self.__solve_helper.set_time_limit_in_seconds(limit)
Sets a time limit for the solve() call.
1615 def set_solver_specific_parameters(self, parameters: str) -> None: 1616 """Sets parameters specific to the solver backend.""" 1617 self.__solve_helper.set_solver_specific_parameters(parameters)
Sets parameters specific to the solver backend.
1619 def enable_output(self, enabled: bool) -> None: 1620 """Controls the solver backend logs.""" 1621 self.__solve_helper.enable_output(enabled)
Controls the solver backend logs.
1623 def solve(self, model: Model) -> SolveStatus: 1624 """Solves a problem and passes each solution to the callback if not null.""" 1625 if self.log_callback is not None: 1626 self.__solve_helper.set_log_callback(self.log_callback) 1627 else: 1628 self.__solve_helper.clear_log_callback() 1629 self.__solve_helper.solve(model.helper) 1630 return SolveStatus(self.__solve_helper.status())
Solves a problem and passes each solution to the callback if not null.
1632 def stop_search(self): 1633 """Stops the current search asynchronously.""" 1634 self.__solve_helper.interrupt_solve()
Stops the current search asynchronously.
1636 def value(self, expr: LinearExprT) -> np.double: 1637 """Returns the value of a linear expression after solve.""" 1638 if not self.__solve_helper.has_solution(): 1639 return pd.NA 1640 if mbn.is_a_number(expr): 1641 return expr 1642 elif isinstance(expr, Variable): 1643 return self.__solve_helper.var_value(expr.index) 1644 elif isinstance(expr, LinearExpr): 1645 flat_expr = _as_flat_linear_expression(expr) 1646 return self.__solve_helper.expression_value( 1647 flat_expr._variable_indices, 1648 flat_expr._coefficients, 1649 flat_expr._offset, 1650 ) 1651 else: 1652 raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}")
Returns the value of a linear expression after solve.
1654 def values(self, variables: _IndexOrSeries) -> pd.Series: 1655 """Returns the values of the input variables. 1656 1657 If `variables` is a `pd.Index`, then the output will be indexed by the 1658 variables. If `variables` is a `pd.Series` indexed by the underlying 1659 dimensions, then the output will be indexed by the same underlying 1660 dimensions. 1661 1662 Args: 1663 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1664 get the values. 1665 1666 Returns: 1667 pd.Series: The values of all variables in the set. 1668 """ 1669 if not self.__solve_helper.has_solution(): 1670 return _attribute_series(func=lambda v: pd.NA, values=variables) 1671 return _attribute_series( 1672 func=lambda v: self.__solve_helper.var_value(v.index), 1673 values=variables, 1674 )
Returns the values of the input variables.
If variables
is a pd.Index
, then the output will be indexed by the
variables. If variables
is a pd.Series
indexed by the underlying
dimensions, then the output will be indexed by the same underlying
dimensions.
Arguments:
- variables (Union[pd.Index, pd.Series]): The set of variables from which to get the values.
Returns:
pd.Series: The values of all variables in the set.
1676 def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: 1677 """Returns the reduced cost of the input variables. 1678 1679 If `variables` is a `pd.Index`, then the output will be indexed by the 1680 variables. If `variables` is a `pd.Series` indexed by the underlying 1681 dimensions, then the output will be indexed by the same underlying 1682 dimensions. 1683 1684 Args: 1685 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1686 get the values. 1687 1688 Returns: 1689 pd.Series: The reduced cost of all variables in the set. 1690 """ 1691 if not self.__solve_helper.has_solution(): 1692 return _attribute_series(func=lambda v: pd.NA, values=variables) 1693 return _attribute_series( 1694 func=lambda v: self.__solve_helper.reduced_cost(v.index), 1695 values=variables, 1696 )
Returns the reduced cost of the input variables.
If variables
is a pd.Index
, then the output will be indexed by the
variables. If variables
is a pd.Series
indexed by the underlying
dimensions, then the output will be indexed by the same underlying
dimensions.
Arguments:
- variables (Union[pd.Index, pd.Series]): The set of variables from which to get the values.
Returns:
pd.Series: The reduced cost of all variables in the set.
1698 def reduced_cost(self, var: Variable) -> np.double: 1699 """Returns the reduced cost of a linear expression after solve.""" 1700 if not self.__solve_helper.has_solution(): 1701 return pd.NA 1702 return self.__solve_helper.reduced_cost(var.index)
Returns the reduced cost of a linear expression after solve.
1704 def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: 1705 """Returns the dual values of the input constraints. 1706 1707 If `constraints` is a `pd.Index`, then the output will be indexed by the 1708 constraints. If `constraints` is a `pd.Series` indexed by the underlying 1709 dimensions, then the output will be indexed by the same underlying 1710 dimensions. 1711 1712 Args: 1713 constraints (Union[pd.Index, pd.Series]): The set of constraints from 1714 which to get the dual values. 1715 1716 Returns: 1717 pd.Series: The dual_values of all constraints in the set. 1718 """ 1719 if not self.__solve_helper.has_solution(): 1720 return _attribute_series(func=lambda v: pd.NA, values=constraints) 1721 return _attribute_series( 1722 func=lambda v: self.__solve_helper.dual_value(v.index), 1723 values=constraints, 1724 )
Returns the dual values of the input constraints.
If constraints
is a pd.Index
, then the output will be indexed by the
constraints. If constraints
is a pd.Series
indexed by the underlying
dimensions, then the output will be indexed by the same underlying
dimensions.
Arguments:
- constraints (Union[pd.Index, pd.Series]): The set of constraints from which to get the dual values.
Returns:
pd.Series: The dual_values of all constraints in the set.
1726 def dual_value(self, ct: LinearConstraint) -> np.double: 1727 """Returns the dual value of a linear constraint after solve.""" 1728 if not self.__solve_helper.has_solution(): 1729 return pd.NA 1730 return self.__solve_helper.dual_value(ct.index)
Returns the dual value of a linear constraint after solve.
1732 def activity(self, ct: LinearConstraint) -> np.double: 1733 """Returns the activity of a linear constraint after solve.""" 1734 if not self.__solve_helper.has_solution(): 1735 return pd.NA 1736 return self.__solve_helper.activity(ct.index)
Returns the activity of a linear constraint after solve.
1738 @property 1739 def objective_value(self) -> np.double: 1740 """Returns the value of the objective after solve.""" 1741 if not self.__solve_helper.has_solution(): 1742 return pd.NA 1743 return self.__solve_helper.objective_value()
Returns the value of the objective after solve.
1745 @property 1746 def best_objective_bound(self) -> np.double: 1747 """Returns the best lower (upper) bound found when min(max)imizing.""" 1748 if not self.__solve_helper.has_solution(): 1749 return pd.NA 1750 return self.__solve_helper.best_objective_bound()
Returns the best lower (upper) bound found when min(max)imizing.
1752 @property 1753 def status_string(self) -> str: 1754 """Returns additional information of the last solve. 1755 1756 It can describe why the model is invalid. 1757 """ 1758 return self.__solve_helper.status_string()
Returns additional information of the last solve.
It can describe why the model is invalid.