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