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-2025 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 math 36import numbers 37import typing 38from typing import Callable, Optional, Union 39 40import numpy as np 41import pandas as pd 42 43from ortools.linear_solver import linear_solver_pb2 44from ortools.linear_solver.python import model_builder_helper as mbh 45from ortools.linear_solver.python import model_builder_numbers as mbn 46 47# Custom types. 48NumberT = Union[int, float, numbers.Real, np.number] 49IntegerT = Union[int, numbers.Integral, np.integer] 50LinearExprT = Union[mbh.LinearExpr, NumberT] 51ConstraintT = Union[mbh.BoundedLinearExpression, bool] 52_IndexOrSeries = Union[pd.Index, pd.Series] 53_VariableOrConstraint = Union["LinearConstraint", mbh.Variable] 54 55# Forward solve statuses. 56AffineExpr = mbh.AffineExpr 57BoundedLinearExpression = mbh.BoundedLinearExpression 58FlatExpr = mbh.FlatExpr 59LinearExpr = mbh.LinearExpr 60SolveStatus = mbh.SolveStatus 61Variable = mbh.Variable 62 63 64def _add_linear_constraint_to_helper( 65 bounded_expr: Union[bool, mbh.BoundedLinearExpression], 66 helper: mbh.ModelBuilderHelper, 67 name: Optional[str], 68): 69 """Creates a new linear constraint in the helper. 70 71 It handles boolean values (which might arise in the construction of 72 BoundedLinearExpressions). 73 74 If bounded_expr is a Boolean value, the created constraint is different. 75 In that case, the constraint will be immutable and marked as under-specified. 76 It will be always feasible or infeasible whether the value is True or False. 77 78 Args: 79 bounded_expr: The bounded expression used to create the constraint. 80 helper: The helper to create the constraint. 81 name: The name of the constraint to be created. 82 83 Returns: 84 LinearConstraint: a constraint in the helper corresponding to the input. 85 86 Raises: 87 TypeError: If constraint is an invalid type. 88 """ 89 if isinstance(bounded_expr, bool): 90 c = LinearConstraint(helper, is_under_specified=True) 91 if name is not None: 92 helper.set_constraint_name(c.index, name) 93 if bounded_expr: 94 # constraint that is always feasible: 0.0 <= nothing <= 0.0 95 helper.set_constraint_lower_bound(c.index, 0.0) 96 helper.set_constraint_upper_bound(c.index, 0.0) 97 else: 98 # constraint that is always infeasible: +oo <= nothing <= -oo 99 helper.set_constraint_lower_bound(c.index, 1) 100 helper.set_constraint_upper_bound(c.index, -1) 101 return c 102 if isinstance(bounded_expr, mbh.BoundedLinearExpression): 103 c = LinearConstraint(helper) 104 # pylint: disable=protected-access 105 helper.add_terms_to_constraint(c.index, bounded_expr.vars, bounded_expr.coeffs) 106 helper.set_constraint_lower_bound(c.index, bounded_expr.lower_bound) 107 helper.set_constraint_upper_bound(c.index, bounded_expr.upper_bound) 108 # pylint: enable=protected-access 109 if name is not None: 110 helper.set_constraint_name(c.index, name) 111 return c 112 raise TypeError(f"invalid type={type(bounded_expr).__name__!r}") 113 114 115def _add_enforced_linear_constraint_to_helper( 116 bounded_expr: Union[bool, mbh.BoundedLinearExpression], 117 helper: mbh.ModelBuilderHelper, 118 var: Variable, 119 value: bool, 120 name: Optional[str], 121): 122 """Creates a new enforced linear constraint in the helper. 123 124 It handles boolean values (which might arise in the construction of 125 BoundedLinearExpressions). 126 127 If bounded_expr is a Boolean value, the linear part of the constraint is 128 different. 129 In that case, the constraint will be immutable and marked as under-specified. 130 Its linear part will be always feasible or infeasible whether the value is 131 True or False. 132 133 Args: 134 bounded_expr: The bounded expression used to create the constraint. 135 helper: The helper to create the constraint. 136 var: the variable used in the indicator 137 value: the value used in the indicator 138 name: The name of the constraint to be created. 139 140 Returns: 141 EnforcedLinearConstraint: a constraint in the helper corresponding to the 142 input. 143 144 Raises: 145 TypeError: If constraint is an invalid type. 146 """ 147 if isinstance(bounded_expr, bool): 148 # TODO(user): create indicator variable assignment instead ? 149 c = EnforcedLinearConstraint(helper, is_under_specified=True) 150 c.indicator_variable = var 151 c.indicator_value = value 152 if name is not None: 153 helper.set_enforced_constraint_name(c.index, name) 154 if bounded_expr: 155 # constraint that is always feasible: 0.0 <= nothing <= 0.0 156 helper.set_enforced_constraint_lower_bound(c.index, 0.0) 157 helper.set_enforced_constraint_upper_bound(c.index, 0.0) 158 else: 159 # constraint that is always infeasible: +oo <= nothing <= -oo 160 helper.set_enforced_constraint_lower_bound(c.index, 1) 161 helper.set_enforced_constraint_upper_bound(c.index, -1) 162 return c 163 if isinstance(bounded_expr, mbh.BoundedLinearExpression): 164 c = EnforcedLinearConstraint(helper) 165 c.indicator_variable = var 166 c.indicator_value = value 167 helper.add_terms_to_enforced_constraint( 168 c.index, bounded_expr.vars, bounded_expr.coeffs 169 ) 170 helper.set_enforced_constraint_lower_bound(c.index, bounded_expr.lower_bound) 171 helper.set_enforced_constraint_upper_bound(c.index, bounded_expr.upper_bound) 172 if name is not None: 173 helper.set_constraint_name(c.index, name) 174 return c 175 176 raise TypeError(f"invalid type={type(bounded_expr).__name__!r}") 177 178 179class LinearConstraint: 180 """Stores a linear equation. 181 182 Example: 183 x = model.new_num_var(0, 10, 'x') 184 y = model.new_num_var(0, 10, 'y') 185 186 linear_constraint = model.add(x + 2 * y == 5) 187 """ 188 189 def __init__( 190 self, 191 helper: mbh.ModelBuilderHelper, 192 *, 193 index: Optional[IntegerT] = None, 194 is_under_specified: bool = False, 195 ) -> None: 196 """LinearConstraint constructor. 197 198 Args: 199 helper: The pybind11 ModelBuilderHelper. 200 index: If specified, recreates a wrapper to an existing linear constraint. 201 is_under_specified: indicates if the constraint was created by 202 model.add(bool). 203 """ 204 if index is None: 205 self.__index = helper.add_linear_constraint() 206 else: 207 self.__index = index 208 self.__helper: mbh.ModelBuilderHelper = helper 209 self.__is_under_specified = is_under_specified 210 211 def __hash__(self): 212 return hash((self.__helper, self.__index)) 213 214 @property 215 def index(self) -> IntegerT: 216 """Returns the index of the constraint in the helper.""" 217 return self.__index 218 219 @property 220 def helper(self) -> mbh.ModelBuilderHelper: 221 """Returns the ModelBuilderHelper instance.""" 222 return self.__helper 223 224 @property 225 def lower_bound(self) -> np.double: 226 return self.__helper.constraint_lower_bound(self.__index) 227 228 @lower_bound.setter 229 def lower_bound(self, bound: NumberT) -> None: 230 self.assert_constraint_is_well_defined() 231 self.__helper.set_constraint_lower_bound(self.__index, bound) 232 233 @property 234 def upper_bound(self) -> np.double: 235 return self.__helper.constraint_upper_bound(self.__index) 236 237 @upper_bound.setter 238 def upper_bound(self, bound: NumberT) -> None: 239 self.assert_constraint_is_well_defined() 240 self.__helper.set_constraint_upper_bound(self.__index, bound) 241 242 @property 243 def name(self) -> str: 244 constraint_name = self.__helper.constraint_name(self.__index) 245 if constraint_name: 246 return constraint_name 247 return f"linear_constraint#{self.__index}" 248 249 @name.setter 250 def name(self, name: str) -> None: 251 return self.__helper.set_constraint_name(self.__index, name) 252 253 @property 254 def is_under_specified(self) -> bool: 255 """Returns True if the constraint is under specified. 256 257 Usually, it means that it was created by model.add(False) or model.add(True) 258 The effect is that modifying the constraint will raise an exception. 259 """ 260 return self.__is_under_specified 261 262 def assert_constraint_is_well_defined(self) -> None: 263 """Raises an exception if the constraint is under specified.""" 264 if self.__is_under_specified: 265 raise ValueError( 266 f"Constraint {self.index} is under specified and cannot be modified" 267 ) 268 269 def __str__(self): 270 return self.name 271 272 def __repr__(self): 273 return ( 274 f"LinearConstraint({self.name}, lb={self.lower_bound}," 275 f" ub={self.upper_bound}," 276 f" var_indices={self.helper.constraint_var_indices(self.index)}," 277 f" coefficients={self.helper.constraint_coefficients(self.index)})" 278 ) 279 280 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 281 """Sets the coefficient of the variable in the constraint.""" 282 self.assert_constraint_is_well_defined() 283 self.__helper.set_constraint_coefficient(self.__index, var.index, coeff) 284 285 def add_term(self, var: Variable, coeff: NumberT) -> None: 286 """Adds var * coeff to the constraint.""" 287 self.assert_constraint_is_well_defined() 288 self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff) 289 290 def clear_terms(self) -> None: 291 """Clear all terms of the constraint.""" 292 self.assert_constraint_is_well_defined() 293 self.__helper.clear_constraint_terms(self.__index) 294 295 296class EnforcedLinearConstraint: 297 """Stores an enforced linear equation, also name indicator constraint. 298 299 Example: 300 x = model.new_num_var(0, 10, 'x') 301 y = model.new_num_var(0, 10, 'y') 302 z = model.new_bool_var('z') 303 304 enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False) 305 """ 306 307 def __init__( 308 self, 309 helper: mbh.ModelBuilderHelper, 310 *, 311 index: Optional[IntegerT] = None, 312 is_under_specified: bool = False, 313 ) -> None: 314 """EnforcedLinearConstraint constructor. 315 316 Args: 317 helper: The pybind11 ModelBuilderHelper. 318 index: If specified, recreates a wrapper to an existing linear constraint. 319 is_under_specified: indicates if the constraint was created by 320 model.add(bool). 321 """ 322 if index is None: 323 self.__index = helper.add_enforced_linear_constraint() 324 else: 325 if not helper.is_enforced_linear_constraint(index): 326 raise ValueError( 327 f"the given index {index} does not refer to an enforced linear" 328 " constraint" 329 ) 330 331 self.__index = index 332 self.__helper: mbh.ModelBuilderHelper = helper 333 self.__is_under_specified = is_under_specified 334 335 @property 336 def index(self) -> IntegerT: 337 """Returns the index of the constraint in the helper.""" 338 return self.__index 339 340 @property 341 def helper(self) -> mbh.ModelBuilderHelper: 342 """Returns the ModelBuilderHelper instance.""" 343 return self.__helper 344 345 @property 346 def lower_bound(self) -> np.double: 347 return self.__helper.enforced_constraint_lower_bound(self.__index) 348 349 @lower_bound.setter 350 def lower_bound(self, bound: NumberT) -> None: 351 self.assert_constraint_is_well_defined() 352 self.__helper.set_enforced_constraint_lower_bound(self.__index, bound) 353 354 @property 355 def upper_bound(self) -> np.double: 356 return self.__helper.enforced_constraint_upper_bound(self.__index) 357 358 @upper_bound.setter 359 def upper_bound(self, bound: NumberT) -> None: 360 self.assert_constraint_is_well_defined() 361 self.__helper.set_enforced_constraint_upper_bound(self.__index, bound) 362 363 @property 364 def indicator_variable(self) -> "Variable": 365 enforcement_var_index = ( 366 self.__helper.enforced_constraint_indicator_variable_index(self.__index) 367 ) 368 return Variable(self.__helper, enforcement_var_index) 369 370 @indicator_variable.setter 371 def indicator_variable(self, var: "Variable") -> None: 372 self.__helper.set_enforced_constraint_indicator_variable_index( 373 self.__index, var.index 374 ) 375 376 @property 377 def indicator_value(self) -> bool: 378 return self.__helper.enforced_constraint_indicator_value(self.__index) 379 380 @indicator_value.setter 381 def indicator_value(self, value: bool) -> None: 382 self.__helper.set_enforced_constraint_indicator_value(self.__index, value) 383 384 @property 385 def name(self) -> str: 386 constraint_name = self.__helper.enforced_constraint_name(self.__index) 387 if constraint_name: 388 return constraint_name 389 return f"enforced_linear_constraint#{self.__index}" 390 391 @name.setter 392 def name(self, name: str) -> None: 393 return self.__helper.set_enforced_constraint_name(self.__index, name) 394 395 @property 396 def is_under_specified(self) -> bool: 397 """Returns True if the constraint is under specified. 398 399 Usually, it means that it was created by model.add(False) or model.add(True) 400 The effect is that modifying the constraint will raise an exception. 401 """ 402 return self.__is_under_specified 403 404 def assert_constraint_is_well_defined(self) -> None: 405 """Raises an exception if the constraint is under specified.""" 406 if self.__is_under_specified: 407 raise ValueError( 408 f"Constraint {self.index} is under specified and cannot be modified" 409 ) 410 411 def __str__(self): 412 return self.name 413 414 def __repr__(self): 415 return ( 416 f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound}," 417 f" ub={self.upper_bound}," 418 f" var_indices={self.helper.enforced_constraint_var_indices(self.index)}," 419 f" coefficients={self.helper.enforced_constraint_coefficients(self.index)}," 420 f" indicator_variable={self.indicator_variable}" 421 f" indicator_value={self.indicator_value})" 422 ) 423 424 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 425 """Sets the coefficient of the variable in the constraint.""" 426 self.assert_constraint_is_well_defined() 427 self.__helper.set_enforced_constraint_coefficient( 428 self.__index, var.index, coeff 429 ) 430 431 def add_term(self, var: Variable, coeff: NumberT) -> None: 432 """Adds var * coeff to the constraint.""" 433 self.assert_constraint_is_well_defined() 434 self.__helper.safe_add_term_to_enforced_constraint( 435 self.__index, var.index, coeff 436 ) 437 438 def clear_terms(self) -> None: 439 """Clear all terms of the constraint.""" 440 self.assert_constraint_is_well_defined() 441 self.__helper.clear_enforced_constraint_terms(self.__index) 442 443 444class Model: 445 """Methods for building a linear model. 446 447 Methods beginning with: 448 449 * ```new_``` create integer, boolean, or interval variables. 450 * ```add_``` create new constraints and add them to the model. 451 """ 452 453 def __init__(self): 454 self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper() 455 456 def clone(self) -> "Model": 457 """Returns a clone of the current model.""" 458 clone = Model() 459 clone.helper.overwrite_model(self.helper) 460 return clone 461 462 @typing.overload 463 def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ... 464 465 @typing.overload 466 def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ... 467 468 def _get_linear_constraints( 469 self, constraints: Optional[_IndexOrSeries] = None 470 ) -> _IndexOrSeries: 471 if constraints is None: 472 return self.get_linear_constraints() 473 return constraints 474 475 @typing.overload 476 def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ... 477 478 @typing.overload 479 def _get_variables(self, variables: pd.Series) -> pd.Series: ... 480 481 def _get_variables( 482 self, variables: Optional[_IndexOrSeries] = None 483 ) -> _IndexOrSeries: 484 if variables is None: 485 return self.get_variables() 486 return variables 487 488 def get_linear_constraints(self) -> pd.Index: 489 """Gets all linear constraints in the model.""" 490 return pd.Index( 491 [self.linear_constraint_from_index(i) for i in range(self.num_constraints)], 492 name="linear_constraint", 493 ) 494 495 def get_linear_constraint_expressions( 496 self, constraints: Optional[_IndexOrSeries] = None 497 ) -> pd.Series: 498 """Gets the expressions of all linear constraints in the set. 499 500 If `constraints` is a `pd.Index`, then the output will be indexed by the 501 constraints. If `constraints` is a `pd.Series` indexed by the underlying 502 dimensions, then the output will be indexed by the same underlying 503 dimensions. 504 505 Args: 506 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 507 constraints from which to get the expressions. If unspecified, all 508 linear constraints will be in scope. 509 510 Returns: 511 pd.Series: The expressions of all linear constraints in the set. 512 """ 513 return _attribute_series( 514 # pylint: disable=g-long-lambda 515 func=lambda c: mbh.FlatExpr( 516 # pylint: disable=g-complex-comprehension 517 [ 518 Variable(self.__helper, var_id) 519 for var_id in c.helper.constraint_var_indices(c.index) 520 ], 521 c.helper.constraint_coefficients(c.index), 522 0.0, 523 ), 524 values=self._get_linear_constraints(constraints), 525 ) 526 527 def get_linear_constraint_lower_bounds( 528 self, constraints: Optional[_IndexOrSeries] = None 529 ) -> pd.Series: 530 """Gets the lower bounds of all linear constraints in the set. 531 532 If `constraints` is a `pd.Index`, then the output will be indexed by the 533 constraints. If `constraints` is a `pd.Series` indexed by the underlying 534 dimensions, then the output will be indexed by the same underlying 535 dimensions. 536 537 Args: 538 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 539 constraints from which to get the lower bounds. If unspecified, all 540 linear constraints will be in scope. 541 542 Returns: 543 pd.Series: The lower bounds of all linear constraints in the set. 544 """ 545 return _attribute_series( 546 func=lambda c: c.lower_bound, # pylint: disable=protected-access 547 values=self._get_linear_constraints(constraints), 548 ) 549 550 def get_linear_constraint_upper_bounds( 551 self, constraints: Optional[_IndexOrSeries] = None 552 ) -> pd.Series: 553 """Gets the upper bounds of all linear constraints in the set. 554 555 If `constraints` is a `pd.Index`, then the output will be indexed by the 556 constraints. If `constraints` is a `pd.Series` indexed by the underlying 557 dimensions, then the output will be indexed by the same underlying 558 dimensions. 559 560 Args: 561 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 562 constraints. If unspecified, all linear constraints will be in scope. 563 564 Returns: 565 pd.Series: The upper bounds of all linear constraints in the set. 566 """ 567 return _attribute_series( 568 func=lambda c: c.upper_bound, # pylint: disable=protected-access 569 values=self._get_linear_constraints(constraints), 570 ) 571 572 def get_variables(self) -> pd.Index: 573 """Gets all variables in the model.""" 574 return pd.Index( 575 [self.var_from_index(i) for i in range(self.num_variables)], 576 name="variable", 577 ) 578 579 def get_variable_lower_bounds( 580 self, variables: Optional[_IndexOrSeries] = None 581 ) -> pd.Series: 582 """Gets the lower bounds of all variables in the set. 583 584 If `variables` is a `pd.Index`, then the output will be indexed by the 585 variables. If `variables` is a `pd.Series` indexed by the underlying 586 dimensions, then the output will be indexed by the same underlying 587 dimensions. 588 589 Args: 590 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 591 from which to get the lower bounds. If unspecified, all variables will 592 be in scope. 593 594 Returns: 595 pd.Series: The lower bounds of all variables in the set. 596 """ 597 return _attribute_series( 598 func=lambda v: v.lower_bound, # pylint: disable=protected-access 599 values=self._get_variables(variables), 600 ) 601 602 def get_variable_upper_bounds( 603 self, variables: Optional[_IndexOrSeries] = None 604 ) -> pd.Series: 605 """Gets the upper bounds of all variables in the set. 606 607 Args: 608 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 609 from which to get the upper bounds. If unspecified, all variables will 610 be in scope. 611 612 Returns: 613 pd.Series: The upper bounds of all variables in the set. 614 """ 615 return _attribute_series( 616 func=lambda v: v.upper_bound, # pylint: disable=protected-access 617 values=self._get_variables(variables), 618 ) 619 620 # Integer variable. 621 622 def new_var( 623 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] 624 ) -> Variable: 625 """Create an integer variable with domain [lb, ub]. 626 627 Args: 628 lb: Lower bound of the variable. 629 ub: Upper bound of the variable. 630 is_integer: Indicates if the variable must take integral values. 631 name: The name of the variable. 632 633 Returns: 634 a variable whose domain is [lb, ub]. 635 """ 636 if name: 637 return Variable(self.__helper, lb, ub, is_integer, name) 638 else: 639 return Variable(self.__helper, lb, ub, is_integer) 640 641 def new_int_var( 642 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 643 ) -> Variable: 644 """Create an integer variable with domain [lb, ub]. 645 646 Args: 647 lb: Lower bound of the variable. 648 ub: Upper bound of the variable. 649 name: The name of the variable. 650 651 Returns: 652 a variable whose domain is [lb, ub]. 653 """ 654 655 return self.new_var(lb, ub, True, name) 656 657 def new_num_var( 658 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 659 ) -> Variable: 660 """Create an integer variable with domain [lb, ub]. 661 662 Args: 663 lb: Lower bound of the variable. 664 ub: Upper bound of the variable. 665 name: The name of the variable. 666 667 Returns: 668 a variable whose domain is [lb, ub]. 669 """ 670 671 return self.new_var(lb, ub, False, name) 672 673 def new_bool_var(self, name: Optional[str] = None) -> Variable: 674 """Creates a 0-1 variable with the given name.""" 675 return self.new_var( 676 0, 1, True, name 677 ) # pytype: disable=wrong-arg-types # numpy-scalars 678 679 def new_constant(self, value: NumberT) -> Variable: 680 """Declares a constant variable.""" 681 return self.new_var(value, value, False, None) 682 683 def new_var_series( 684 self, 685 name: str, 686 index: pd.Index, 687 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 688 upper_bounds: Union[NumberT, pd.Series] = math.inf, 689 is_integral: Union[bool, pd.Series] = False, 690 ) -> pd.Series: 691 """Creates a series of (scalar-valued) variables with the given name. 692 693 Args: 694 name (str): Required. The name of the variable set. 695 index (pd.Index): Required. The index to use for the variable set. 696 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 697 variables in the set. If a `pd.Series` is passed in, it will be based on 698 the corresponding values of the pd.Series. Defaults to -inf. 699 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 700 variables in the set. If a `pd.Series` is passed in, it will be based on 701 the corresponding values of the pd.Series. Defaults to +inf. 702 is_integral (bool, pd.Series): Optional. Indicates if the variable can 703 only take integer values. If a `pd.Series` is passed in, it will be 704 based on the corresponding values of the pd.Series. Defaults to False. 705 706 Returns: 707 pd.Series: The variable set indexed by its corresponding dimensions. 708 709 Raises: 710 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 711 ValueError: if the `name` is not a valid identifier or already exists. 712 ValueError: if the `lowerbound` is greater than the `upperbound`. 713 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 714 does not match the input index. 715 """ 716 if not isinstance(index, pd.Index): 717 raise TypeError("Non-index object is used as index") 718 if not name.isidentifier(): 719 raise ValueError(f"name={name!r} is not a valid identifier") 720 if ( 721 mbn.is_a_number(lower_bounds) 722 and mbn.is_a_number(upper_bounds) 723 and lower_bounds > upper_bounds 724 ): 725 raise ValueError( 726 f"lower_bound={lower_bounds} is greater than" 727 f" upper_bound={upper_bounds} for variable set={name!r}" 728 ) 729 if ( 730 isinstance(is_integral, bool) 731 and is_integral 732 and mbn.is_a_number(lower_bounds) 733 and mbn.is_a_number(upper_bounds) 734 and math.isfinite(lower_bounds) 735 and math.isfinite(upper_bounds) 736 and math.ceil(lower_bounds) > math.floor(upper_bounds) 737 ): 738 raise ValueError( 739 f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}" 740 f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}" 741 f" for variable set={name!r}" 742 ) 743 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) 744 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) 745 is_integrals = _convert_to_series_and_validate_index(is_integral, index) 746 return pd.Series( 747 index=index, 748 data=[ 749 # pylint: disable=g-complex-comprehension 750 Variable( 751 self.__helper, 752 lower_bounds[i], 753 upper_bounds[i], 754 is_integrals[i], 755 f"{name}[{i}]", 756 ) 757 for i in index 758 ], 759 ) 760 761 def new_num_var_series( 762 self, 763 name: str, 764 index: pd.Index, 765 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 766 upper_bounds: Union[NumberT, pd.Series] = math.inf, 767 ) -> pd.Series: 768 """Creates a series of continuous variables with the given name. 769 770 Args: 771 name (str): Required. The name of the variable set. 772 index (pd.Index): Required. The index to use for the variable set. 773 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 774 variables in the set. If a `pd.Series` is passed in, it will be based on 775 the corresponding values of the pd.Series. Defaults to -inf. 776 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 777 variables in the set. If a `pd.Series` is passed in, it will be based on 778 the corresponding values of the pd.Series. Defaults to +inf. 779 780 Returns: 781 pd.Series: The variable set indexed by its corresponding dimensions. 782 783 Raises: 784 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 785 ValueError: if the `name` is not a valid identifier or already exists. 786 ValueError: if the `lowerbound` is greater than the `upperbound`. 787 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 788 does not match the input index. 789 """ 790 return self.new_var_series(name, index, lower_bounds, upper_bounds, False) 791 792 def new_int_var_series( 793 self, 794 name: str, 795 index: pd.Index, 796 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 797 upper_bounds: Union[NumberT, pd.Series] = math.inf, 798 ) -> pd.Series: 799 """Creates a series of integer variables with the given name. 800 801 Args: 802 name (str): Required. The name of the variable set. 803 index (pd.Index): Required. The index to use for the variable set. 804 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 805 variables in the set. If a `pd.Series` is passed in, it will be based on 806 the corresponding values of the pd.Series. Defaults to -inf. 807 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 808 variables in the set. If a `pd.Series` is passed in, it will be based on 809 the corresponding values of the pd.Series. Defaults to +inf. 810 811 Returns: 812 pd.Series: The variable set indexed by its corresponding dimensions. 813 814 Raises: 815 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 816 ValueError: if the `name` is not a valid identifier or already exists. 817 ValueError: if the `lowerbound` is greater than the `upperbound`. 818 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 819 does not match the input index. 820 """ 821 return self.new_var_series(name, index, lower_bounds, upper_bounds, True) 822 823 def new_bool_var_series( 824 self, 825 name: str, 826 index: pd.Index, 827 ) -> pd.Series: 828 """Creates a series of Boolean variables with the given name. 829 830 Args: 831 name (str): Required. The name of the variable set. 832 index (pd.Index): Required. The index to use for the variable set. 833 834 Returns: 835 pd.Series: The variable set indexed by its corresponding dimensions. 836 837 Raises: 838 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 839 ValueError: if the `name` is not a valid identifier or already exists. 840 ValueError: if the `lowerbound` is greater than the `upperbound`. 841 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 842 does not match the input index. 843 """ 844 return self.new_var_series(name, index, 0, 1, True) 845 846 def var_from_index(self, index: IntegerT) -> Variable: 847 """Rebuilds a variable object from the model and its index.""" 848 return Variable(self.__helper, index) 849 850 # Linear constraints. 851 852 def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 853 self, 854 linear_expr: LinearExprT, 855 lb: NumberT = -math.inf, 856 ub: NumberT = math.inf, 857 name: Optional[str] = None, 858 ) -> LinearConstraint: 859 """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" 860 ct = LinearConstraint(self.__helper) 861 if name: 862 self.__helper.set_constraint_name(ct.index, name) 863 if mbn.is_a_number(linear_expr): 864 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 865 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 866 elif isinstance(linear_expr, LinearExpr): 867 flat_expr = mbh.FlatExpr(linear_expr) 868 # pylint: disable=protected-access 869 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) 870 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) 871 self.__helper.add_terms_to_constraint( 872 ct.index, flat_expr.vars, flat_expr.coeffs 873 ) 874 else: 875 raise TypeError( 876 "Not supported:" 877 f" Model.add_linear_constraint({type(linear_expr).__name__!r})" 878 ) 879 return ct 880 881 def add( 882 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None 883 ) -> Union[LinearConstraint, pd.Series]: 884 """Adds a `BoundedLinearExpression` to the model. 885 886 Args: 887 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 888 name: An optional name. 889 890 Returns: 891 An instance of the `Constraint` class. 892 893 Note that a special treatment is done when the argument does not contain any 894 variable, and thus evaluates to True or False. 895 896 `model.add(True)` will create a constraint 0 <= empty sum <= 0. 897 The constraint will be marked as under specified, and cannot be modified 898 thereafter. 899 900 `model.add(False)` will create a constraint inf <= empty sum <= -inf. The 901 constraint will be marked as under specified, and cannot be modified 902 thereafter. 903 904 you can check the if a constraint is under specified by reading the 905 `LinearConstraint.is_under_specified` property. 906 """ 907 if isinstance(ct, mbh.BoundedLinearExpression): 908 return _add_linear_constraint_to_helper(ct, self.__helper, name) 909 elif isinstance(ct, bool): 910 return _add_linear_constraint_to_helper(ct, self.__helper, name) 911 elif isinstance(ct, pd.Series): 912 return pd.Series( 913 index=ct.index, 914 data=[ 915 _add_linear_constraint_to_helper( 916 expr, self.__helper, f"{name}[{i}]" 917 ) 918 for (i, expr) in zip(ct.index, ct) 919 ], 920 ) 921 else: 922 raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})") 923 924 def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: 925 """Rebuilds a linear constraint object from the model and its index.""" 926 return LinearConstraint(self.__helper, index=index) 927 928 # Enforced Linear constraints. 929 930 def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 931 self, 932 linear_expr: LinearExprT, 933 ivar: "Variable", 934 ivalue: bool, 935 lb: NumberT = -math.inf, 936 ub: NumberT = math.inf, 937 name: Optional[str] = None, 938 ) -> EnforcedLinearConstraint: 939 """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" 940 ct = EnforcedLinearConstraint(self.__helper) 941 ct.indicator_variable = ivar 942 ct.indicator_value = ivalue 943 if name: 944 self.__helper.set_constraint_name(ct.index, name) 945 if mbn.is_a_number(linear_expr): 946 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 947 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 948 elif isinstance(linear_expr, LinearExpr): 949 flat_expr = mbh.FlatExpr(linear_expr) 950 # pylint: disable=protected-access 951 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) 952 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) 953 self.__helper.add_terms_to_constraint( 954 ct.index, flat_expr.vars, flat_expr.coeffs 955 ) 956 else: 957 raise TypeError( 958 "Not supported:" 959 f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})" 960 ) 961 return ct 962 963 def add_enforced( 964 self, 965 ct: Union[ConstraintT, pd.Series], 966 var: Union[Variable, pd.Series], 967 value: Union[bool, pd.Series], 968 name: Optional[str] = None, 969 ) -> Union[EnforcedLinearConstraint, pd.Series]: 970 """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. 971 972 Args: 973 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 974 var: The indicator variable 975 value: the indicator value 976 name: An optional name. 977 978 Returns: 979 An instance of the `Constraint` class. 980 981 Note that a special treatment is done when the argument does not contain any 982 variable, and thus evaluates to True or False. 983 984 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty 985 sum <= 0 986 987 model.add_enforced(False, var, value) will create a constraint inf <= 988 empty sum <= -inf 989 990 you can check the if a constraint is always false (lb=inf, ub=-inf) by 991 calling EnforcedLinearConstraint.is_always_false() 992 """ 993 if isinstance(ct, mbh.BoundedLinearExpression): 994 return _add_enforced_linear_constraint_to_helper( 995 ct, self.__helper, var, value, name 996 ) 997 elif ( 998 isinstance(ct, bool) 999 and isinstance(var, Variable) 1000 and isinstance(value, bool) 1001 ): 1002 return _add_enforced_linear_constraint_to_helper( 1003 ct, self.__helper, var, value, name 1004 ) 1005 elif isinstance(ct, pd.Series): 1006 ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) 1007 ivalue_series = _convert_to_series_and_validate_index(value, ct.index) 1008 return pd.Series( 1009 index=ct.index, 1010 data=[ 1011 _add_enforced_linear_constraint_to_helper( 1012 expr, 1013 self.__helper, 1014 ivar_series[i], 1015 ivalue_series[i], 1016 f"{name}[{i}]", 1017 ) 1018 for (i, expr) in zip(ct.index, ct) 1019 ], 1020 ) 1021 else: 1022 raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}") 1023 1024 def enforced_linear_constraint_from_index( 1025 self, index: IntegerT 1026 ) -> EnforcedLinearConstraint: 1027 """Rebuilds an enforced linear constraint object from the model and its index.""" 1028 return EnforcedLinearConstraint(self.__helper, index=index) 1029 1030 # Objective. 1031 def minimize(self, linear_expr: LinearExprT) -> None: 1032 """Minimizes the given objective.""" 1033 self.__optimize(linear_expr, False) 1034 1035 def maximize(self, linear_expr: LinearExprT) -> None: 1036 """Maximizes the given objective.""" 1037 self.__optimize(linear_expr, True) 1038 1039 def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None: 1040 """Defines the objective.""" 1041 self.helper.clear_objective() 1042 self.__helper.set_maximize(maximize) 1043 if mbn.is_a_number(linear_expr): 1044 self.helper.set_objective_offset(linear_expr) 1045 elif isinstance(linear_expr, Variable): 1046 self.helper.set_var_objective_coefficient(linear_expr.index, 1.0) 1047 elif isinstance(linear_expr, LinearExpr): 1048 flat_expr = mbh.FlatExpr(linear_expr) 1049 # pylint: disable=protected-access 1050 self.helper.set_objective_offset(flat_expr.offset) 1051 var_indices = [var.index for var in flat_expr.vars] 1052 self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs) 1053 else: 1054 raise TypeError( 1055 "Not supported:" 1056 f" Model.minimize/maximize({type(linear_expr).__name__!r})" 1057 ) 1058 1059 @property 1060 def objective_offset(self) -> np.double: 1061 """Returns the fixed offset of the objective.""" 1062 return self.__helper.objective_offset() 1063 1064 @objective_offset.setter 1065 def objective_offset(self, value: NumberT) -> None: 1066 self.__helper.set_objective_offset(value) 1067 1068 def objective_expression(self) -> "LinearExpr": 1069 """Returns the expression to optimize.""" 1070 variables: list[Variable] = [] 1071 coefficients: list[numbers.Real] = [] 1072 for variable in self.get_variables(): 1073 coeff = self.__helper.var_objective_coefficient(variable.index) 1074 if coeff != 0.0: 1075 variables.append(variable) 1076 coefficients.append(coeff) 1077 return mbh.FlatExpr(variables, coefficients, self.__helper.objective_offset()) 1078 1079 # Hints. 1080 def clear_hints(self): 1081 """Clears all solution hints.""" 1082 self.__helper.clear_hints() 1083 1084 def add_hint(self, var: Variable, value: NumberT) -> None: 1085 """Adds var == value as a hint to the model. 1086 1087 Args: 1088 var: The variable of the hint 1089 value: The value of the hint 1090 1091 Note that variables must not appear more than once in the list of hints. 1092 """ 1093 self.__helper.add_hint(var.index, value) 1094 1095 # Input/Output 1096 def export_to_lp_string(self, obfuscate: bool = False) -> str: 1097 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1098 options.obfuscate = obfuscate 1099 return self.__helper.export_to_lp_string(options) 1100 1101 def export_to_mps_string(self, obfuscate: bool = False) -> str: 1102 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1103 options.obfuscate = obfuscate 1104 return self.__helper.export_to_mps_string(options) 1105 1106 def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool: 1107 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1108 options.obfuscate = obfuscate 1109 return self.__helper.write_to_mps_file(filename, options) 1110 1111 def export_to_proto(self) -> linear_solver_pb2.MPModelProto: 1112 """Exports the optimization model to a ProtoBuf format.""" 1113 return mbh.to_mpmodel_proto(self.__helper) 1114 1115 def import_from_mps_string(self, mps_string: str) -> bool: 1116 """Reads a model from a MPS string.""" 1117 return self.__helper.import_from_mps_string(mps_string) 1118 1119 def import_from_mps_file(self, mps_file: str) -> bool: 1120 """Reads a model from a .mps file.""" 1121 return self.__helper.import_from_mps_file(mps_file) 1122 1123 def import_from_lp_string(self, lp_string: str) -> bool: 1124 """Reads a model from a LP string. 1125 1126 Note that this code is very limited, and will not support any real lp. 1127 It is only intented to be use to parse test lp problems. 1128 1129 Args: 1130 lp_string: The LP string to import. 1131 1132 Returns: 1133 True if the import was successful. 1134 """ 1135 return self.__helper.import_from_lp_string(lp_string) 1136 1137 def import_from_lp_file(self, lp_file: str) -> bool: 1138 """Reads a model from a .lp file. 1139 1140 Note that this code is very limited, and will not support any real lp. 1141 It is only intented to be use to parse test lp problems. 1142 1143 Args: 1144 lp_file: The LP file to import. 1145 1146 Returns: 1147 True if the import was successful. 1148 """ 1149 return self.__helper.import_from_lp_file(lp_file) 1150 1151 def import_from_proto_file(self, proto_file: str) -> bool: 1152 """Reads a model from a proto file.""" 1153 return self.__helper.read_model_from_proto_file(proto_file) 1154 1155 def export_to_proto_file(self, proto_file: str) -> bool: 1156 """Writes a model to a proto file.""" 1157 return self.__helper.write_model_to_proto_file(proto_file) 1158 1159 # Model getters and Setters 1160 1161 @property 1162 def num_variables(self) -> int: 1163 """Returns the number of variables in the model.""" 1164 return self.__helper.num_variables() 1165 1166 @property 1167 def num_constraints(self) -> int: 1168 """The number of constraints in the model.""" 1169 return self.__helper.num_constraints() 1170 1171 @property 1172 def name(self) -> str: 1173 """The name of the model.""" 1174 return self.__helper.name() 1175 1176 @name.setter 1177 def name(self, name: str): 1178 self.__helper.set_name(name) 1179 1180 @property 1181 def helper(self) -> mbh.ModelBuilderHelper: 1182 """Returns the model builder helper.""" 1183 return self.__helper 1184 1185 1186class Solver: 1187 """Main solver class. 1188 1189 The purpose of this class is to search for a solution to the model provided 1190 to the solve() method. 1191 1192 Once solve() is called, this class allows inspecting the solution found 1193 with the value() method, as well as general statistics about the solve 1194 procedure. 1195 """ 1196 1197 def __init__(self, solver_name: str): 1198 self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name) 1199 self.log_callback: Optional[Callable[[str], None]] = None 1200 1201 def solver_is_supported(self) -> bool: 1202 """Checks whether the requested solver backend was found.""" 1203 return self.__solve_helper.solver_is_supported() 1204 1205 # Solver backend and parameters. 1206 def set_time_limit_in_seconds(self, limit: NumberT) -> None: 1207 """Sets a time limit for the solve() call.""" 1208 self.__solve_helper.set_time_limit_in_seconds(limit) 1209 1210 def set_solver_specific_parameters(self, parameters: str) -> None: 1211 """Sets parameters specific to the solver backend.""" 1212 self.__solve_helper.set_solver_specific_parameters(parameters) 1213 1214 def enable_output(self, enabled: bool) -> None: 1215 """Controls the solver backend logs.""" 1216 self.__solve_helper.enable_output(enabled) 1217 1218 def solve(self, model: Model) -> SolveStatus: 1219 """Solves a problem and passes each solution to the callback if not null.""" 1220 if self.log_callback is not None: 1221 self.__solve_helper.set_log_callback(self.log_callback) 1222 else: 1223 self.__solve_helper.clear_log_callback() 1224 self.__solve_helper.solve(model.helper) 1225 return SolveStatus(self.__solve_helper.status()) 1226 1227 def stop_search(self): 1228 """Stops the current search asynchronously.""" 1229 self.__solve_helper.interrupt_solve() 1230 1231 def value(self, expr: LinearExprT) -> np.double: 1232 """Returns the value of a linear expression after solve.""" 1233 if not self.__solve_helper.has_solution(): 1234 return pd.NA 1235 if mbn.is_a_number(expr): 1236 return expr 1237 elif isinstance(expr, LinearExpr): 1238 return self.__solve_helper.expression_value(expr) 1239 else: 1240 raise TypeError(f"Unknown expression {type(expr).__name__!r}") 1241 1242 def values(self, variables: _IndexOrSeries) -> pd.Series: 1243 """Returns the values of the input variables. 1244 1245 If `variables` is a `pd.Index`, then the output will be indexed by the 1246 variables. If `variables` is a `pd.Series` indexed by the underlying 1247 dimensions, then the output will be indexed by the same underlying 1248 dimensions. 1249 1250 Args: 1251 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1252 get the values. 1253 1254 Returns: 1255 pd.Series: The values of all variables in the set. 1256 """ 1257 if not self.__solve_helper.has_solution(): 1258 return _attribute_series(func=lambda v: pd.NA, values=variables) 1259 return _attribute_series( 1260 func=lambda v: self.__solve_helper.variable_value(v.index), 1261 values=variables, 1262 ) 1263 1264 def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: 1265 """Returns the reduced cost of the input variables. 1266 1267 If `variables` is a `pd.Index`, then the output will be indexed by the 1268 variables. If `variables` is a `pd.Series` indexed by the underlying 1269 dimensions, then the output will be indexed by the same underlying 1270 dimensions. 1271 1272 Args: 1273 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1274 get the values. 1275 1276 Returns: 1277 pd.Series: The reduced cost of all variables in the set. 1278 """ 1279 if not self.__solve_helper.has_solution(): 1280 return _attribute_series(func=lambda v: pd.NA, values=variables) 1281 return _attribute_series( 1282 func=lambda v: self.__solve_helper.reduced_cost(v.index), 1283 values=variables, 1284 ) 1285 1286 def reduced_cost(self, var: Variable) -> np.double: 1287 """Returns the reduced cost of a linear expression after solve.""" 1288 if not self.__solve_helper.has_solution(): 1289 return pd.NA 1290 return self.__solve_helper.reduced_cost(var.index) 1291 1292 def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: 1293 """Returns the dual values of the input constraints. 1294 1295 If `constraints` is a `pd.Index`, then the output will be indexed by the 1296 constraints. If `constraints` is a `pd.Series` indexed by the underlying 1297 dimensions, then the output will be indexed by the same underlying 1298 dimensions. 1299 1300 Args: 1301 constraints (Union[pd.Index, pd.Series]): The set of constraints from 1302 which to get the dual values. 1303 1304 Returns: 1305 pd.Series: The dual_values of all constraints in the set. 1306 """ 1307 if not self.__solve_helper.has_solution(): 1308 return _attribute_series(func=lambda v: pd.NA, values=constraints) 1309 return _attribute_series( 1310 func=lambda v: self.__solve_helper.dual_value(v.index), 1311 values=constraints, 1312 ) 1313 1314 def dual_value(self, ct: LinearConstraint) -> np.double: 1315 """Returns the dual value of a linear constraint after solve.""" 1316 if not self.__solve_helper.has_solution(): 1317 return pd.NA 1318 return self.__solve_helper.dual_value(ct.index) 1319 1320 def activity(self, ct: LinearConstraint) -> np.double: 1321 """Returns the activity of a linear constraint after solve.""" 1322 if not self.__solve_helper.has_solution(): 1323 return pd.NA 1324 return self.__solve_helper.activity(ct.index) 1325 1326 @property 1327 def objective_value(self) -> np.double: 1328 """Returns the value of the objective after solve.""" 1329 if not self.__solve_helper.has_solution(): 1330 return pd.NA 1331 return self.__solve_helper.objective_value() 1332 1333 @property 1334 def best_objective_bound(self) -> np.double: 1335 """Returns the best lower (upper) bound found when min(max)imizing.""" 1336 if not self.__solve_helper.has_solution(): 1337 return pd.NA 1338 return self.__solve_helper.best_objective_bound() 1339 1340 @property 1341 def status_string(self) -> str: 1342 """Returns additional information of the last solve. 1343 1344 It can describe why the model is invalid. 1345 """ 1346 return self.__solve_helper.status_string() 1347 1348 @property 1349 def wall_time(self) -> np.double: 1350 return self.__solve_helper.wall_time() 1351 1352 @property 1353 def user_time(self) -> np.double: 1354 return self.__solve_helper.user_time() 1355 1356 1357def _get_index(obj: _IndexOrSeries) -> pd.Index: 1358 """Returns the indices of `obj` as a `pd.Index`.""" 1359 if isinstance(obj, pd.Series): 1360 return obj.index 1361 return obj 1362 1363 1364def _attribute_series( 1365 *, 1366 func: Callable[[_VariableOrConstraint], NumberT], 1367 values: _IndexOrSeries, 1368) -> pd.Series: 1369 """Returns the attributes of `values`. 1370 1371 Args: 1372 func: The function to call for getting the attribute data. 1373 values: The values that the function will be applied (element-wise) to. 1374 1375 Returns: 1376 pd.Series: The attribute values. 1377 """ 1378 return pd.Series( 1379 data=[func(v) for v in values], 1380 index=_get_index(values), 1381 ) 1382 1383 1384def _convert_to_series_and_validate_index( 1385 value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index 1386) -> pd.Series: 1387 """Returns a pd.Series of the given index with the corresponding values. 1388 1389 Args: 1390 value_or_series: the values to be converted (if applicable). 1391 index: the index of the resulting pd.Series. 1392 1393 Returns: 1394 pd.Series: The set of values with the given index. 1395 1396 Raises: 1397 TypeError: If the type of `value_or_series` is not recognized. 1398 ValueError: If the index does not match. 1399 """ 1400 if mbn.is_a_number(value_or_series) or isinstance(value_or_series, bool): 1401 result = pd.Series(data=value_or_series, index=index) 1402 elif isinstance(value_or_series, pd.Series): 1403 if value_or_series.index.equals(index): 1404 result = value_or_series 1405 else: 1406 raise ValueError("index does not match") 1407 else: 1408 raise TypeError("invalid type={type(value_or_series).__name!r}") 1409 return result 1410 1411 1412def _convert_to_var_series_and_validate_index( 1413 var_or_series: Union["Variable", pd.Series], index: pd.Index 1414) -> pd.Series: 1415 """Returns a pd.Series of the given index with the corresponding values. 1416 1417 Args: 1418 var_or_series: the variables to be converted (if applicable). 1419 index: the index of the resulting pd.Series. 1420 1421 Returns: 1422 pd.Series: The set of values with the given index. 1423 1424 Raises: 1425 TypeError: If the type of `value_or_series` is not recognized. 1426 ValueError: If the index does not match. 1427 """ 1428 if isinstance(var_or_series, Variable): 1429 result = pd.Series(data=var_or_series, index=index) 1430 elif isinstance(var_or_series, pd.Series): 1431 if var_or_series.index.equals(index): 1432 result = var_or_series 1433 else: 1434 raise ValueError("index does not match") 1435 else: 1436 raise TypeError("invalid type={type(value_or_series).__name!r}") 1437 return result 1438 1439 1440# Compatibility. 1441ModelBuilder = Model 1442ModelSolver = Solver
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)
__init__(self: ortools.linear_solver.python.model_builder_helper.AffineExpr, arg0: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg1: float, arg2: float) -> None
Inherited Members
__init__(args, *kwargs) Overloaded function.
__init__(self: ortools.linear_solver.python.model_builder_helper.BoundedLinearExpression, arg0: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg1: float, arg2: float) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.BoundedLinearExpression, arg0: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg1: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg2: float, arg3: float) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.BoundedLinearExpression, arg0: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg1: int, arg2: int) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.BoundedLinearExpression, arg0: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg1: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg2: int, arg3: int) -> None
(arg0: ortools.linear_solver.python.model_builder_helper.BoundedLinearExpression) -> list[float]
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)
__init__(args, *kwargs) Overloaded function.
__init__(self: ortools.linear_solver.python.model_builder_helper.FlatExpr, arg0: ortools.linear_solver.python.model_builder_helper.LinearExpr) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.FlatExpr, arg0: ortools.linear_solver.python.model_builder_helper.LinearExpr, arg1: ortools.linear_solver.python.model_builder_helper.LinearExpr) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.FlatExpr, arg0: list[operations_research::mb::Variable], arg1: list[float], arg2: float) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.FlatExpr, arg0: float) -> None
(arg0: ortools.linear_solver.python.model_builder_helper.FlatExpr) -> list[operations_research::mb::Variable]
variable_indices(self: ortools.linear_solver.python.model_builder_helper.FlatExpr) -> list[int]
Inherited Members
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)
sum(args, *kwargs) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Creates sum(expressions) [+ constant]
.
weighted_sum(expressions: Sequence, coefficients: list[float], *, constant: float = 0.0) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Creates sum(expressions[i] * coefficients[i]) [+ constant]
.
term(args, *kwargs) Overloaded function.
- term(expr: ortools.linear_solver.python.model_builder_helper.LinearExpr, coeff: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Returns expr * coeff.
- term(expr: ortools.linear_solver.python.model_builder_helper.LinearExpr, coeff: float, *, constant: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Returns expr * coeff [+ constant].
- term(value: float, coeff: float, *, constant: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Returns value * coeff [+ constant].
affine(args, *kwargs) Overloaded function.
- affine(expr: ortools.linear_solver.python.model_builder_helper.LinearExpr, coeff: float, constant: float = 0.0) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Returns expr * coeff + constant.
- affine(value: float, coeff: float, constant: float = 0.0) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Returns value * coeff + constant.
constant(value: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr
Returns a constant linear expression.
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
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.
__init__(args, *kwargs) Overloaded function.
__init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: int) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: float, arg2: float, arg3: bool) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: float, arg2: float, arg3: bool, arg4: str) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: int, arg2: int, arg3: bool) -> None
__init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: int, arg2: int, arg3: bool, arg4: str) -> None
(arg0: ortools.linear_solver.python.model_builder_helper.Variable) -> operations_research::mb::ModelBuilderHelper
Inherited Members
180class LinearConstraint: 181 """Stores a linear equation. 182 183 Example: 184 x = model.new_num_var(0, 10, 'x') 185 y = model.new_num_var(0, 10, 'y') 186 187 linear_constraint = model.add(x + 2 * y == 5) 188 """ 189 190 def __init__( 191 self, 192 helper: mbh.ModelBuilderHelper, 193 *, 194 index: Optional[IntegerT] = None, 195 is_under_specified: bool = False, 196 ) -> None: 197 """LinearConstraint constructor. 198 199 Args: 200 helper: The pybind11 ModelBuilderHelper. 201 index: If specified, recreates a wrapper to an existing linear constraint. 202 is_under_specified: indicates if the constraint was created by 203 model.add(bool). 204 """ 205 if index is None: 206 self.__index = helper.add_linear_constraint() 207 else: 208 self.__index = index 209 self.__helper: mbh.ModelBuilderHelper = helper 210 self.__is_under_specified = is_under_specified 211 212 def __hash__(self): 213 return hash((self.__helper, self.__index)) 214 215 @property 216 def index(self) -> IntegerT: 217 """Returns the index of the constraint in the helper.""" 218 return self.__index 219 220 @property 221 def helper(self) -> mbh.ModelBuilderHelper: 222 """Returns the ModelBuilderHelper instance.""" 223 return self.__helper 224 225 @property 226 def lower_bound(self) -> np.double: 227 return self.__helper.constraint_lower_bound(self.__index) 228 229 @lower_bound.setter 230 def lower_bound(self, bound: NumberT) -> None: 231 self.assert_constraint_is_well_defined() 232 self.__helper.set_constraint_lower_bound(self.__index, bound) 233 234 @property 235 def upper_bound(self) -> np.double: 236 return self.__helper.constraint_upper_bound(self.__index) 237 238 @upper_bound.setter 239 def upper_bound(self, bound: NumberT) -> None: 240 self.assert_constraint_is_well_defined() 241 self.__helper.set_constraint_upper_bound(self.__index, bound) 242 243 @property 244 def name(self) -> str: 245 constraint_name = self.__helper.constraint_name(self.__index) 246 if constraint_name: 247 return constraint_name 248 return f"linear_constraint#{self.__index}" 249 250 @name.setter 251 def name(self, name: str) -> None: 252 return self.__helper.set_constraint_name(self.__index, name) 253 254 @property 255 def is_under_specified(self) -> bool: 256 """Returns True if the constraint is under specified. 257 258 Usually, it means that it was created by model.add(False) or model.add(True) 259 The effect is that modifying the constraint will raise an exception. 260 """ 261 return self.__is_under_specified 262 263 def assert_constraint_is_well_defined(self) -> None: 264 """Raises an exception if the constraint is under specified.""" 265 if self.__is_under_specified: 266 raise ValueError( 267 f"Constraint {self.index} is under specified and cannot be modified" 268 ) 269 270 def __str__(self): 271 return self.name 272 273 def __repr__(self): 274 return ( 275 f"LinearConstraint({self.name}, lb={self.lower_bound}," 276 f" ub={self.upper_bound}," 277 f" var_indices={self.helper.constraint_var_indices(self.index)}," 278 f" coefficients={self.helper.constraint_coefficients(self.index)})" 279 ) 280 281 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 282 """Sets the coefficient of the variable in the constraint.""" 283 self.assert_constraint_is_well_defined() 284 self.__helper.set_constraint_coefficient(self.__index, var.index, coeff) 285 286 def add_term(self, var: Variable, coeff: NumberT) -> None: 287 """Adds var * coeff to the constraint.""" 288 self.assert_constraint_is_well_defined() 289 self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff) 290 291 def clear_terms(self) -> None: 292 """Clear all terms of the constraint.""" 293 self.assert_constraint_is_well_defined() 294 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)
190 def __init__( 191 self, 192 helper: mbh.ModelBuilderHelper, 193 *, 194 index: Optional[IntegerT] = None, 195 is_under_specified: bool = False, 196 ) -> None: 197 """LinearConstraint constructor. 198 199 Args: 200 helper: The pybind11 ModelBuilderHelper. 201 index: If specified, recreates a wrapper to an existing linear constraint. 202 is_under_specified: indicates if the constraint was created by 203 model.add(bool). 204 """ 205 if index is None: 206 self.__index = helper.add_linear_constraint() 207 else: 208 self.__index = index 209 self.__helper: mbh.ModelBuilderHelper = helper 210 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).
215 @property 216 def index(self) -> IntegerT: 217 """Returns the index of the constraint in the helper.""" 218 return self.__index
Returns the index of the constraint in the helper.
220 @property 221 def helper(self) -> mbh.ModelBuilderHelper: 222 """Returns the ModelBuilderHelper instance.""" 223 return self.__helper
Returns the ModelBuilderHelper instance.
254 @property 255 def is_under_specified(self) -> bool: 256 """Returns True if the constraint is under specified. 257 258 Usually, it means that it was created by model.add(False) or model.add(True) 259 The effect is that modifying the constraint will raise an exception. 260 """ 261 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.
263 def assert_constraint_is_well_defined(self) -> None: 264 """Raises an exception if the constraint is under specified.""" 265 if self.__is_under_specified: 266 raise ValueError( 267 f"Constraint {self.index} is under specified and cannot be modified" 268 )
Raises an exception if the constraint is under specified.
281 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 282 """Sets the coefficient of the variable in the constraint.""" 283 self.assert_constraint_is_well_defined() 284 self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
Sets the coefficient of the variable in the constraint.
297class EnforcedLinearConstraint: 298 """Stores an enforced linear equation, also name indicator constraint. 299 300 Example: 301 x = model.new_num_var(0, 10, 'x') 302 y = model.new_num_var(0, 10, 'y') 303 z = model.new_bool_var('z') 304 305 enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False) 306 """ 307 308 def __init__( 309 self, 310 helper: mbh.ModelBuilderHelper, 311 *, 312 index: Optional[IntegerT] = None, 313 is_under_specified: bool = False, 314 ) -> None: 315 """EnforcedLinearConstraint constructor. 316 317 Args: 318 helper: The pybind11 ModelBuilderHelper. 319 index: If specified, recreates a wrapper to an existing linear constraint. 320 is_under_specified: indicates if the constraint was created by 321 model.add(bool). 322 """ 323 if index is None: 324 self.__index = helper.add_enforced_linear_constraint() 325 else: 326 if not helper.is_enforced_linear_constraint(index): 327 raise ValueError( 328 f"the given index {index} does not refer to an enforced linear" 329 " constraint" 330 ) 331 332 self.__index = index 333 self.__helper: mbh.ModelBuilderHelper = helper 334 self.__is_under_specified = is_under_specified 335 336 @property 337 def index(self) -> IntegerT: 338 """Returns the index of the constraint in the helper.""" 339 return self.__index 340 341 @property 342 def helper(self) -> mbh.ModelBuilderHelper: 343 """Returns the ModelBuilderHelper instance.""" 344 return self.__helper 345 346 @property 347 def lower_bound(self) -> np.double: 348 return self.__helper.enforced_constraint_lower_bound(self.__index) 349 350 @lower_bound.setter 351 def lower_bound(self, bound: NumberT) -> None: 352 self.assert_constraint_is_well_defined() 353 self.__helper.set_enforced_constraint_lower_bound(self.__index, bound) 354 355 @property 356 def upper_bound(self) -> np.double: 357 return self.__helper.enforced_constraint_upper_bound(self.__index) 358 359 @upper_bound.setter 360 def upper_bound(self, bound: NumberT) -> None: 361 self.assert_constraint_is_well_defined() 362 self.__helper.set_enforced_constraint_upper_bound(self.__index, bound) 363 364 @property 365 def indicator_variable(self) -> "Variable": 366 enforcement_var_index = ( 367 self.__helper.enforced_constraint_indicator_variable_index(self.__index) 368 ) 369 return Variable(self.__helper, enforcement_var_index) 370 371 @indicator_variable.setter 372 def indicator_variable(self, var: "Variable") -> None: 373 self.__helper.set_enforced_constraint_indicator_variable_index( 374 self.__index, var.index 375 ) 376 377 @property 378 def indicator_value(self) -> bool: 379 return self.__helper.enforced_constraint_indicator_value(self.__index) 380 381 @indicator_value.setter 382 def indicator_value(self, value: bool) -> None: 383 self.__helper.set_enforced_constraint_indicator_value(self.__index, value) 384 385 @property 386 def name(self) -> str: 387 constraint_name = self.__helper.enforced_constraint_name(self.__index) 388 if constraint_name: 389 return constraint_name 390 return f"enforced_linear_constraint#{self.__index}" 391 392 @name.setter 393 def name(self, name: str) -> None: 394 return self.__helper.set_enforced_constraint_name(self.__index, name) 395 396 @property 397 def is_under_specified(self) -> bool: 398 """Returns True if the constraint is under specified. 399 400 Usually, it means that it was created by model.add(False) or model.add(True) 401 The effect is that modifying the constraint will raise an exception. 402 """ 403 return self.__is_under_specified 404 405 def assert_constraint_is_well_defined(self) -> None: 406 """Raises an exception if the constraint is under specified.""" 407 if self.__is_under_specified: 408 raise ValueError( 409 f"Constraint {self.index} is under specified and cannot be modified" 410 ) 411 412 def __str__(self): 413 return self.name 414 415 def __repr__(self): 416 return ( 417 f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound}," 418 f" ub={self.upper_bound}," 419 f" var_indices={self.helper.enforced_constraint_var_indices(self.index)}," 420 f" coefficients={self.helper.enforced_constraint_coefficients(self.index)}," 421 f" indicator_variable={self.indicator_variable}" 422 f" indicator_value={self.indicator_value})" 423 ) 424 425 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 426 """Sets the coefficient of the variable in the constraint.""" 427 self.assert_constraint_is_well_defined() 428 self.__helper.set_enforced_constraint_coefficient( 429 self.__index, var.index, coeff 430 ) 431 432 def add_term(self, var: Variable, coeff: NumberT) -> None: 433 """Adds var * coeff to the constraint.""" 434 self.assert_constraint_is_well_defined() 435 self.__helper.safe_add_term_to_enforced_constraint( 436 self.__index, var.index, coeff 437 ) 438 439 def clear_terms(self) -> None: 440 """Clear all terms of the constraint.""" 441 self.assert_constraint_is_well_defined() 442 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)
308 def __init__( 309 self, 310 helper: mbh.ModelBuilderHelper, 311 *, 312 index: Optional[IntegerT] = None, 313 is_under_specified: bool = False, 314 ) -> None: 315 """EnforcedLinearConstraint constructor. 316 317 Args: 318 helper: The pybind11 ModelBuilderHelper. 319 index: If specified, recreates a wrapper to an existing linear constraint. 320 is_under_specified: indicates if the constraint was created by 321 model.add(bool). 322 """ 323 if index is None: 324 self.__index = helper.add_enforced_linear_constraint() 325 else: 326 if not helper.is_enforced_linear_constraint(index): 327 raise ValueError( 328 f"the given index {index} does not refer to an enforced linear" 329 " constraint" 330 ) 331 332 self.__index = index 333 self.__helper: mbh.ModelBuilderHelper = helper 334 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).
336 @property 337 def index(self) -> IntegerT: 338 """Returns the index of the constraint in the helper.""" 339 return self.__index
Returns the index of the constraint in the helper.
341 @property 342 def helper(self) -> mbh.ModelBuilderHelper: 343 """Returns the ModelBuilderHelper instance.""" 344 return self.__helper
Returns the ModelBuilderHelper instance.
396 @property 397 def is_under_specified(self) -> bool: 398 """Returns True if the constraint is under specified. 399 400 Usually, it means that it was created by model.add(False) or model.add(True) 401 The effect is that modifying the constraint will raise an exception. 402 """ 403 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.
405 def assert_constraint_is_well_defined(self) -> None: 406 """Raises an exception if the constraint is under specified.""" 407 if self.__is_under_specified: 408 raise ValueError( 409 f"Constraint {self.index} is under specified and cannot be modified" 410 )
Raises an exception if the constraint is under specified.
425 def set_coefficient(self, var: Variable, coeff: NumberT) -> None: 426 """Sets the coefficient of the variable in the constraint.""" 427 self.assert_constraint_is_well_defined() 428 self.__helper.set_enforced_constraint_coefficient( 429 self.__index, var.index, coeff 430 )
Sets the coefficient of the variable in the constraint.
445class Model: 446 """Methods for building a linear model. 447 448 Methods beginning with: 449 450 * ```new_``` create integer, boolean, or interval variables. 451 * ```add_``` create new constraints and add them to the model. 452 """ 453 454 def __init__(self): 455 self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper() 456 457 def clone(self) -> "Model": 458 """Returns a clone of the current model.""" 459 clone = Model() 460 clone.helper.overwrite_model(self.helper) 461 return clone 462 463 @typing.overload 464 def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ... 465 466 @typing.overload 467 def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ... 468 469 def _get_linear_constraints( 470 self, constraints: Optional[_IndexOrSeries] = None 471 ) -> _IndexOrSeries: 472 if constraints is None: 473 return self.get_linear_constraints() 474 return constraints 475 476 @typing.overload 477 def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ... 478 479 @typing.overload 480 def _get_variables(self, variables: pd.Series) -> pd.Series: ... 481 482 def _get_variables( 483 self, variables: Optional[_IndexOrSeries] = None 484 ) -> _IndexOrSeries: 485 if variables is None: 486 return self.get_variables() 487 return variables 488 489 def get_linear_constraints(self) -> pd.Index: 490 """Gets all linear constraints in the model.""" 491 return pd.Index( 492 [self.linear_constraint_from_index(i) for i in range(self.num_constraints)], 493 name="linear_constraint", 494 ) 495 496 def get_linear_constraint_expressions( 497 self, constraints: Optional[_IndexOrSeries] = None 498 ) -> pd.Series: 499 """Gets the expressions of all linear constraints in the set. 500 501 If `constraints` is a `pd.Index`, then the output will be indexed by the 502 constraints. If `constraints` is a `pd.Series` indexed by the underlying 503 dimensions, then the output will be indexed by the same underlying 504 dimensions. 505 506 Args: 507 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 508 constraints from which to get the expressions. If unspecified, all 509 linear constraints will be in scope. 510 511 Returns: 512 pd.Series: The expressions of all linear constraints in the set. 513 """ 514 return _attribute_series( 515 # pylint: disable=g-long-lambda 516 func=lambda c: mbh.FlatExpr( 517 # pylint: disable=g-complex-comprehension 518 [ 519 Variable(self.__helper, var_id) 520 for var_id in c.helper.constraint_var_indices(c.index) 521 ], 522 c.helper.constraint_coefficients(c.index), 523 0.0, 524 ), 525 values=self._get_linear_constraints(constraints), 526 ) 527 528 def get_linear_constraint_lower_bounds( 529 self, constraints: Optional[_IndexOrSeries] = None 530 ) -> pd.Series: 531 """Gets the lower bounds of all linear constraints in the set. 532 533 If `constraints` is a `pd.Index`, then the output will be indexed by the 534 constraints. If `constraints` is a `pd.Series` indexed by the underlying 535 dimensions, then the output will be indexed by the same underlying 536 dimensions. 537 538 Args: 539 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 540 constraints from which to get the lower bounds. If unspecified, all 541 linear constraints will be in scope. 542 543 Returns: 544 pd.Series: The lower bounds of all linear constraints in the set. 545 """ 546 return _attribute_series( 547 func=lambda c: c.lower_bound, # pylint: disable=protected-access 548 values=self._get_linear_constraints(constraints), 549 ) 550 551 def get_linear_constraint_upper_bounds( 552 self, constraints: Optional[_IndexOrSeries] = None 553 ) -> pd.Series: 554 """Gets the upper bounds of all linear constraints in the set. 555 556 If `constraints` is a `pd.Index`, then the output will be indexed by the 557 constraints. If `constraints` is a `pd.Series` indexed by the underlying 558 dimensions, then the output will be indexed by the same underlying 559 dimensions. 560 561 Args: 562 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 563 constraints. If unspecified, all linear constraints will be in scope. 564 565 Returns: 566 pd.Series: The upper bounds of all linear constraints in the set. 567 """ 568 return _attribute_series( 569 func=lambda c: c.upper_bound, # pylint: disable=protected-access 570 values=self._get_linear_constraints(constraints), 571 ) 572 573 def get_variables(self) -> pd.Index: 574 """Gets all variables in the model.""" 575 return pd.Index( 576 [self.var_from_index(i) for i in range(self.num_variables)], 577 name="variable", 578 ) 579 580 def get_variable_lower_bounds( 581 self, variables: Optional[_IndexOrSeries] = None 582 ) -> pd.Series: 583 """Gets the lower bounds of all variables in the set. 584 585 If `variables` is a `pd.Index`, then the output will be indexed by the 586 variables. If `variables` is a `pd.Series` indexed by the underlying 587 dimensions, then the output will be indexed by the same underlying 588 dimensions. 589 590 Args: 591 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 592 from which to get the lower bounds. If unspecified, all variables will 593 be in scope. 594 595 Returns: 596 pd.Series: The lower bounds of all variables in the set. 597 """ 598 return _attribute_series( 599 func=lambda v: v.lower_bound, # pylint: disable=protected-access 600 values=self._get_variables(variables), 601 ) 602 603 def get_variable_upper_bounds( 604 self, variables: Optional[_IndexOrSeries] = None 605 ) -> pd.Series: 606 """Gets the upper bounds of all variables in the set. 607 608 Args: 609 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 610 from which to get the upper bounds. If unspecified, all variables will 611 be in scope. 612 613 Returns: 614 pd.Series: The upper bounds of all variables in the set. 615 """ 616 return _attribute_series( 617 func=lambda v: v.upper_bound, # pylint: disable=protected-access 618 values=self._get_variables(variables), 619 ) 620 621 # Integer variable. 622 623 def new_var( 624 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] 625 ) -> Variable: 626 """Create an integer variable with domain [lb, ub]. 627 628 Args: 629 lb: Lower bound of the variable. 630 ub: Upper bound of the variable. 631 is_integer: Indicates if the variable must take integral values. 632 name: The name of the variable. 633 634 Returns: 635 a variable whose domain is [lb, ub]. 636 """ 637 if name: 638 return Variable(self.__helper, lb, ub, is_integer, name) 639 else: 640 return Variable(self.__helper, lb, ub, is_integer) 641 642 def new_int_var( 643 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 644 ) -> Variable: 645 """Create an integer variable with domain [lb, ub]. 646 647 Args: 648 lb: Lower bound of the variable. 649 ub: Upper bound of the variable. 650 name: The name of the variable. 651 652 Returns: 653 a variable whose domain is [lb, ub]. 654 """ 655 656 return self.new_var(lb, ub, True, name) 657 658 def new_num_var( 659 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 660 ) -> Variable: 661 """Create an integer variable with domain [lb, ub]. 662 663 Args: 664 lb: Lower bound of the variable. 665 ub: Upper bound of the variable. 666 name: The name of the variable. 667 668 Returns: 669 a variable whose domain is [lb, ub]. 670 """ 671 672 return self.new_var(lb, ub, False, name) 673 674 def new_bool_var(self, name: Optional[str] = None) -> Variable: 675 """Creates a 0-1 variable with the given name.""" 676 return self.new_var( 677 0, 1, True, name 678 ) # pytype: disable=wrong-arg-types # numpy-scalars 679 680 def new_constant(self, value: NumberT) -> Variable: 681 """Declares a constant variable.""" 682 return self.new_var(value, value, False, None) 683 684 def new_var_series( 685 self, 686 name: str, 687 index: pd.Index, 688 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 689 upper_bounds: Union[NumberT, pd.Series] = math.inf, 690 is_integral: Union[bool, pd.Series] = False, 691 ) -> pd.Series: 692 """Creates a series of (scalar-valued) variables with the given name. 693 694 Args: 695 name (str): Required. The name of the variable set. 696 index (pd.Index): Required. The index to use for the variable set. 697 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 698 variables in the set. If a `pd.Series` is passed in, it will be based on 699 the corresponding values of the pd.Series. Defaults to -inf. 700 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 701 variables in the set. If a `pd.Series` is passed in, it will be based on 702 the corresponding values of the pd.Series. Defaults to +inf. 703 is_integral (bool, pd.Series): Optional. Indicates if the variable can 704 only take integer values. If a `pd.Series` is passed in, it will be 705 based on the corresponding values of the pd.Series. Defaults to False. 706 707 Returns: 708 pd.Series: The variable set indexed by its corresponding dimensions. 709 710 Raises: 711 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 712 ValueError: if the `name` is not a valid identifier or already exists. 713 ValueError: if the `lowerbound` is greater than the `upperbound`. 714 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 715 does not match the input index. 716 """ 717 if not isinstance(index, pd.Index): 718 raise TypeError("Non-index object is used as index") 719 if not name.isidentifier(): 720 raise ValueError(f"name={name!r} is not a valid identifier") 721 if ( 722 mbn.is_a_number(lower_bounds) 723 and mbn.is_a_number(upper_bounds) 724 and lower_bounds > upper_bounds 725 ): 726 raise ValueError( 727 f"lower_bound={lower_bounds} is greater than" 728 f" upper_bound={upper_bounds} for variable set={name!r}" 729 ) 730 if ( 731 isinstance(is_integral, bool) 732 and is_integral 733 and mbn.is_a_number(lower_bounds) 734 and mbn.is_a_number(upper_bounds) 735 and math.isfinite(lower_bounds) 736 and math.isfinite(upper_bounds) 737 and math.ceil(lower_bounds) > math.floor(upper_bounds) 738 ): 739 raise ValueError( 740 f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}" 741 f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}" 742 f" for variable set={name!r}" 743 ) 744 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) 745 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) 746 is_integrals = _convert_to_series_and_validate_index(is_integral, index) 747 return pd.Series( 748 index=index, 749 data=[ 750 # pylint: disable=g-complex-comprehension 751 Variable( 752 self.__helper, 753 lower_bounds[i], 754 upper_bounds[i], 755 is_integrals[i], 756 f"{name}[{i}]", 757 ) 758 for i in index 759 ], 760 ) 761 762 def new_num_var_series( 763 self, 764 name: str, 765 index: pd.Index, 766 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 767 upper_bounds: Union[NumberT, pd.Series] = math.inf, 768 ) -> pd.Series: 769 """Creates a series of continuous variables with the given name. 770 771 Args: 772 name (str): Required. The name of the variable set. 773 index (pd.Index): Required. The index to use for the variable set. 774 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 775 variables in the set. If a `pd.Series` is passed in, it will be based on 776 the corresponding values of the pd.Series. Defaults to -inf. 777 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 778 variables in the set. If a `pd.Series` is passed in, it will be based on 779 the corresponding values of the pd.Series. Defaults to +inf. 780 781 Returns: 782 pd.Series: The variable set indexed by its corresponding dimensions. 783 784 Raises: 785 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 786 ValueError: if the `name` is not a valid identifier or already exists. 787 ValueError: if the `lowerbound` is greater than the `upperbound`. 788 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 789 does not match the input index. 790 """ 791 return self.new_var_series(name, index, lower_bounds, upper_bounds, False) 792 793 def new_int_var_series( 794 self, 795 name: str, 796 index: pd.Index, 797 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 798 upper_bounds: Union[NumberT, pd.Series] = math.inf, 799 ) -> pd.Series: 800 """Creates a series of integer variables with the given name. 801 802 Args: 803 name (str): Required. The name of the variable set. 804 index (pd.Index): Required. The index to use for the variable set. 805 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 806 variables in the set. If a `pd.Series` is passed in, it will be based on 807 the corresponding values of the pd.Series. Defaults to -inf. 808 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 809 variables in the set. If a `pd.Series` is passed in, it will be based on 810 the corresponding values of the pd.Series. Defaults to +inf. 811 812 Returns: 813 pd.Series: The variable set indexed by its corresponding dimensions. 814 815 Raises: 816 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 817 ValueError: if the `name` is not a valid identifier or already exists. 818 ValueError: if the `lowerbound` is greater than the `upperbound`. 819 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 820 does not match the input index. 821 """ 822 return self.new_var_series(name, index, lower_bounds, upper_bounds, True) 823 824 def new_bool_var_series( 825 self, 826 name: str, 827 index: pd.Index, 828 ) -> pd.Series: 829 """Creates a series of Boolean variables with the given name. 830 831 Args: 832 name (str): Required. The name of the variable set. 833 index (pd.Index): Required. The index to use for the variable set. 834 835 Returns: 836 pd.Series: The variable set indexed by its corresponding dimensions. 837 838 Raises: 839 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 840 ValueError: if the `name` is not a valid identifier or already exists. 841 ValueError: if the `lowerbound` is greater than the `upperbound`. 842 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 843 does not match the input index. 844 """ 845 return self.new_var_series(name, index, 0, 1, True) 846 847 def var_from_index(self, index: IntegerT) -> Variable: 848 """Rebuilds a variable object from the model and its index.""" 849 return Variable(self.__helper, index) 850 851 # Linear constraints. 852 853 def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 854 self, 855 linear_expr: LinearExprT, 856 lb: NumberT = -math.inf, 857 ub: NumberT = math.inf, 858 name: Optional[str] = None, 859 ) -> LinearConstraint: 860 """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" 861 ct = LinearConstraint(self.__helper) 862 if name: 863 self.__helper.set_constraint_name(ct.index, name) 864 if mbn.is_a_number(linear_expr): 865 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 866 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 867 elif isinstance(linear_expr, LinearExpr): 868 flat_expr = mbh.FlatExpr(linear_expr) 869 # pylint: disable=protected-access 870 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) 871 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) 872 self.__helper.add_terms_to_constraint( 873 ct.index, flat_expr.vars, flat_expr.coeffs 874 ) 875 else: 876 raise TypeError( 877 "Not supported:" 878 f" Model.add_linear_constraint({type(linear_expr).__name__!r})" 879 ) 880 return ct 881 882 def add( 883 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None 884 ) -> Union[LinearConstraint, pd.Series]: 885 """Adds a `BoundedLinearExpression` to the model. 886 887 Args: 888 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 889 name: An optional name. 890 891 Returns: 892 An instance of the `Constraint` class. 893 894 Note that a special treatment is done when the argument does not contain any 895 variable, and thus evaluates to True or False. 896 897 `model.add(True)` will create a constraint 0 <= empty sum <= 0. 898 The constraint will be marked as under specified, and cannot be modified 899 thereafter. 900 901 `model.add(False)` will create a constraint inf <= empty sum <= -inf. The 902 constraint will be marked as under specified, and cannot be modified 903 thereafter. 904 905 you can check the if a constraint is under specified by reading the 906 `LinearConstraint.is_under_specified` property. 907 """ 908 if isinstance(ct, mbh.BoundedLinearExpression): 909 return _add_linear_constraint_to_helper(ct, self.__helper, name) 910 elif isinstance(ct, bool): 911 return _add_linear_constraint_to_helper(ct, self.__helper, name) 912 elif isinstance(ct, pd.Series): 913 return pd.Series( 914 index=ct.index, 915 data=[ 916 _add_linear_constraint_to_helper( 917 expr, self.__helper, f"{name}[{i}]" 918 ) 919 for (i, expr) in zip(ct.index, ct) 920 ], 921 ) 922 else: 923 raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})") 924 925 def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: 926 """Rebuilds a linear constraint object from the model and its index.""" 927 return LinearConstraint(self.__helper, index=index) 928 929 # Enforced Linear constraints. 930 931 def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 932 self, 933 linear_expr: LinearExprT, 934 ivar: "Variable", 935 ivalue: bool, 936 lb: NumberT = -math.inf, 937 ub: NumberT = math.inf, 938 name: Optional[str] = None, 939 ) -> EnforcedLinearConstraint: 940 """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" 941 ct = EnforcedLinearConstraint(self.__helper) 942 ct.indicator_variable = ivar 943 ct.indicator_value = ivalue 944 if name: 945 self.__helper.set_constraint_name(ct.index, name) 946 if mbn.is_a_number(linear_expr): 947 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 948 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 949 elif isinstance(linear_expr, LinearExpr): 950 flat_expr = mbh.FlatExpr(linear_expr) 951 # pylint: disable=protected-access 952 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) 953 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) 954 self.__helper.add_terms_to_constraint( 955 ct.index, flat_expr.vars, flat_expr.coeffs 956 ) 957 else: 958 raise TypeError( 959 "Not supported:" 960 f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})" 961 ) 962 return ct 963 964 def add_enforced( 965 self, 966 ct: Union[ConstraintT, pd.Series], 967 var: Union[Variable, pd.Series], 968 value: Union[bool, pd.Series], 969 name: Optional[str] = None, 970 ) -> Union[EnforcedLinearConstraint, pd.Series]: 971 """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. 972 973 Args: 974 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 975 var: The indicator variable 976 value: the indicator value 977 name: An optional name. 978 979 Returns: 980 An instance of the `Constraint` class. 981 982 Note that a special treatment is done when the argument does not contain any 983 variable, and thus evaluates to True or False. 984 985 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty 986 sum <= 0 987 988 model.add_enforced(False, var, value) will create a constraint inf <= 989 empty sum <= -inf 990 991 you can check the if a constraint is always false (lb=inf, ub=-inf) by 992 calling EnforcedLinearConstraint.is_always_false() 993 """ 994 if isinstance(ct, mbh.BoundedLinearExpression): 995 return _add_enforced_linear_constraint_to_helper( 996 ct, self.__helper, var, value, name 997 ) 998 elif ( 999 isinstance(ct, bool) 1000 and isinstance(var, Variable) 1001 and isinstance(value, bool) 1002 ): 1003 return _add_enforced_linear_constraint_to_helper( 1004 ct, self.__helper, var, value, name 1005 ) 1006 elif isinstance(ct, pd.Series): 1007 ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) 1008 ivalue_series = _convert_to_series_and_validate_index(value, ct.index) 1009 return pd.Series( 1010 index=ct.index, 1011 data=[ 1012 _add_enforced_linear_constraint_to_helper( 1013 expr, 1014 self.__helper, 1015 ivar_series[i], 1016 ivalue_series[i], 1017 f"{name}[{i}]", 1018 ) 1019 for (i, expr) in zip(ct.index, ct) 1020 ], 1021 ) 1022 else: 1023 raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}") 1024 1025 def enforced_linear_constraint_from_index( 1026 self, index: IntegerT 1027 ) -> EnforcedLinearConstraint: 1028 """Rebuilds an enforced linear constraint object from the model and its index.""" 1029 return EnforcedLinearConstraint(self.__helper, index=index) 1030 1031 # Objective. 1032 def minimize(self, linear_expr: LinearExprT) -> None: 1033 """Minimizes the given objective.""" 1034 self.__optimize(linear_expr, False) 1035 1036 def maximize(self, linear_expr: LinearExprT) -> None: 1037 """Maximizes the given objective.""" 1038 self.__optimize(linear_expr, True) 1039 1040 def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None: 1041 """Defines the objective.""" 1042 self.helper.clear_objective() 1043 self.__helper.set_maximize(maximize) 1044 if mbn.is_a_number(linear_expr): 1045 self.helper.set_objective_offset(linear_expr) 1046 elif isinstance(linear_expr, Variable): 1047 self.helper.set_var_objective_coefficient(linear_expr.index, 1.0) 1048 elif isinstance(linear_expr, LinearExpr): 1049 flat_expr = mbh.FlatExpr(linear_expr) 1050 # pylint: disable=protected-access 1051 self.helper.set_objective_offset(flat_expr.offset) 1052 var_indices = [var.index for var in flat_expr.vars] 1053 self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs) 1054 else: 1055 raise TypeError( 1056 "Not supported:" 1057 f" Model.minimize/maximize({type(linear_expr).__name__!r})" 1058 ) 1059 1060 @property 1061 def objective_offset(self) -> np.double: 1062 """Returns the fixed offset of the objective.""" 1063 return self.__helper.objective_offset() 1064 1065 @objective_offset.setter 1066 def objective_offset(self, value: NumberT) -> None: 1067 self.__helper.set_objective_offset(value) 1068 1069 def objective_expression(self) -> "LinearExpr": 1070 """Returns the expression to optimize.""" 1071 variables: list[Variable] = [] 1072 coefficients: list[numbers.Real] = [] 1073 for variable in self.get_variables(): 1074 coeff = self.__helper.var_objective_coefficient(variable.index) 1075 if coeff != 0.0: 1076 variables.append(variable) 1077 coefficients.append(coeff) 1078 return mbh.FlatExpr(variables, coefficients, self.__helper.objective_offset()) 1079 1080 # Hints. 1081 def clear_hints(self): 1082 """Clears all solution hints.""" 1083 self.__helper.clear_hints() 1084 1085 def add_hint(self, var: Variable, value: NumberT) -> None: 1086 """Adds var == value as a hint to the model. 1087 1088 Args: 1089 var: The variable of the hint 1090 value: The value of the hint 1091 1092 Note that variables must not appear more than once in the list of hints. 1093 """ 1094 self.__helper.add_hint(var.index, value) 1095 1096 # Input/Output 1097 def export_to_lp_string(self, obfuscate: bool = False) -> str: 1098 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1099 options.obfuscate = obfuscate 1100 return self.__helper.export_to_lp_string(options) 1101 1102 def export_to_mps_string(self, obfuscate: bool = False) -> str: 1103 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1104 options.obfuscate = obfuscate 1105 return self.__helper.export_to_mps_string(options) 1106 1107 def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool: 1108 options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() 1109 options.obfuscate = obfuscate 1110 return self.__helper.write_to_mps_file(filename, options) 1111 1112 def export_to_proto(self) -> linear_solver_pb2.MPModelProto: 1113 """Exports the optimization model to a ProtoBuf format.""" 1114 return mbh.to_mpmodel_proto(self.__helper) 1115 1116 def import_from_mps_string(self, mps_string: str) -> bool: 1117 """Reads a model from a MPS string.""" 1118 return self.__helper.import_from_mps_string(mps_string) 1119 1120 def import_from_mps_file(self, mps_file: str) -> bool: 1121 """Reads a model from a .mps file.""" 1122 return self.__helper.import_from_mps_file(mps_file) 1123 1124 def import_from_lp_string(self, lp_string: str) -> bool: 1125 """Reads a model from a LP string. 1126 1127 Note that this code is very limited, and will not support any real lp. 1128 It is only intented to be use to parse test lp problems. 1129 1130 Args: 1131 lp_string: The LP string to import. 1132 1133 Returns: 1134 True if the import was successful. 1135 """ 1136 return self.__helper.import_from_lp_string(lp_string) 1137 1138 def import_from_lp_file(self, lp_file: str) -> bool: 1139 """Reads a model from a .lp file. 1140 1141 Note that this code is very limited, and will not support any real lp. 1142 It is only intented to be use to parse test lp problems. 1143 1144 Args: 1145 lp_file: The LP file to import. 1146 1147 Returns: 1148 True if the import was successful. 1149 """ 1150 return self.__helper.import_from_lp_file(lp_file) 1151 1152 def import_from_proto_file(self, proto_file: str) -> bool: 1153 """Reads a model from a proto file.""" 1154 return self.__helper.read_model_from_proto_file(proto_file) 1155 1156 def export_to_proto_file(self, proto_file: str) -> bool: 1157 """Writes a model to a proto file.""" 1158 return self.__helper.write_model_to_proto_file(proto_file) 1159 1160 # Model getters and Setters 1161 1162 @property 1163 def num_variables(self) -> int: 1164 """Returns the number of variables in the model.""" 1165 return self.__helper.num_variables() 1166 1167 @property 1168 def num_constraints(self) -> int: 1169 """The number of constraints in the model.""" 1170 return self.__helper.num_constraints() 1171 1172 @property 1173 def name(self) -> str: 1174 """The name of the model.""" 1175 return self.__helper.name() 1176 1177 @name.setter 1178 def name(self, name: str): 1179 self.__helper.set_name(name) 1180 1181 @property 1182 def helper(self) -> mbh.ModelBuilderHelper: 1183 """Returns the model builder helper.""" 1184 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.
457 def clone(self) -> "Model": 458 """Returns a clone of the current model.""" 459 clone = Model() 460 clone.helper.overwrite_model(self.helper) 461 return clone
Returns a clone of the current model.
489 def get_linear_constraints(self) -> pd.Index: 490 """Gets all linear constraints in the model.""" 491 return pd.Index( 492 [self.linear_constraint_from_index(i) for i in range(self.num_constraints)], 493 name="linear_constraint", 494 )
Gets all linear constraints in the model.
496 def get_linear_constraint_expressions( 497 self, constraints: Optional[_IndexOrSeries] = None 498 ) -> pd.Series: 499 """Gets the expressions of all linear constraints in the set. 500 501 If `constraints` is a `pd.Index`, then the output will be indexed by the 502 constraints. If `constraints` is a `pd.Series` indexed by the underlying 503 dimensions, then the output will be indexed by the same underlying 504 dimensions. 505 506 Args: 507 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 508 constraints from which to get the expressions. If unspecified, all 509 linear constraints will be in scope. 510 511 Returns: 512 pd.Series: The expressions of all linear constraints in the set. 513 """ 514 return _attribute_series( 515 # pylint: disable=g-long-lambda 516 func=lambda c: mbh.FlatExpr( 517 # pylint: disable=g-complex-comprehension 518 [ 519 Variable(self.__helper, var_id) 520 for var_id in c.helper.constraint_var_indices(c.index) 521 ], 522 c.helper.constraint_coefficients(c.index), 523 0.0, 524 ), 525 values=self._get_linear_constraints(constraints), 526 )
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.
528 def get_linear_constraint_lower_bounds( 529 self, constraints: Optional[_IndexOrSeries] = None 530 ) -> pd.Series: 531 """Gets the lower bounds of all linear constraints in the set. 532 533 If `constraints` is a `pd.Index`, then the output will be indexed by the 534 constraints. If `constraints` is a `pd.Series` indexed by the underlying 535 dimensions, then the output will be indexed by the same underlying 536 dimensions. 537 538 Args: 539 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 540 constraints from which to get the lower bounds. If unspecified, all 541 linear constraints will be in scope. 542 543 Returns: 544 pd.Series: The lower bounds of all linear constraints in the set. 545 """ 546 return _attribute_series( 547 func=lambda c: c.lower_bound, # pylint: disable=protected-access 548 values=self._get_linear_constraints(constraints), 549 )
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.
551 def get_linear_constraint_upper_bounds( 552 self, constraints: Optional[_IndexOrSeries] = None 553 ) -> pd.Series: 554 """Gets the upper bounds of all linear constraints in the set. 555 556 If `constraints` is a `pd.Index`, then the output will be indexed by the 557 constraints. If `constraints` is a `pd.Series` indexed by the underlying 558 dimensions, then the output will be indexed by the same underlying 559 dimensions. 560 561 Args: 562 constraints (Union[pd.Index, pd.Series]): Optional. The set of linear 563 constraints. If unspecified, all linear constraints will be in scope. 564 565 Returns: 566 pd.Series: The upper bounds of all linear constraints in the set. 567 """ 568 return _attribute_series( 569 func=lambda c: c.upper_bound, # pylint: disable=protected-access 570 values=self._get_linear_constraints(constraints), 571 )
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.
573 def get_variables(self) -> pd.Index: 574 """Gets all variables in the model.""" 575 return pd.Index( 576 [self.var_from_index(i) for i in range(self.num_variables)], 577 name="variable", 578 )
Gets all variables in the model.
580 def get_variable_lower_bounds( 581 self, variables: Optional[_IndexOrSeries] = None 582 ) -> pd.Series: 583 """Gets the lower bounds of all variables in the set. 584 585 If `variables` is a `pd.Index`, then the output will be indexed by the 586 variables. If `variables` is a `pd.Series` indexed by the underlying 587 dimensions, then the output will be indexed by the same underlying 588 dimensions. 589 590 Args: 591 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 592 from which to get the lower bounds. If unspecified, all variables will 593 be in scope. 594 595 Returns: 596 pd.Series: The lower bounds of all variables in the set. 597 """ 598 return _attribute_series( 599 func=lambda v: v.lower_bound, # pylint: disable=protected-access 600 values=self._get_variables(variables), 601 )
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.
603 def get_variable_upper_bounds( 604 self, variables: Optional[_IndexOrSeries] = None 605 ) -> pd.Series: 606 """Gets the upper bounds of all variables in the set. 607 608 Args: 609 variables (Union[pd.Index, pd.Series]): Optional. The set of variables 610 from which to get the upper bounds. If unspecified, all variables will 611 be in scope. 612 613 Returns: 614 pd.Series: The upper bounds of all variables in the set. 615 """ 616 return _attribute_series( 617 func=lambda v: v.upper_bound, # pylint: disable=protected-access 618 values=self._get_variables(variables), 619 )
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.
623 def new_var( 624 self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] 625 ) -> Variable: 626 """Create an integer variable with domain [lb, ub]. 627 628 Args: 629 lb: Lower bound of the variable. 630 ub: Upper bound of the variable. 631 is_integer: Indicates if the variable must take integral values. 632 name: The name of the variable. 633 634 Returns: 635 a variable whose domain is [lb, ub]. 636 """ 637 if name: 638 return Variable(self.__helper, lb, ub, is_integer, name) 639 else: 640 return Variable(self.__helper, lb, ub, is_integer)
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].
642 def new_int_var( 643 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 644 ) -> Variable: 645 """Create an integer variable with domain [lb, ub]. 646 647 Args: 648 lb: Lower bound of the variable. 649 ub: Upper bound of the variable. 650 name: The name of the variable. 651 652 Returns: 653 a variable whose domain is [lb, ub]. 654 """ 655 656 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].
658 def new_num_var( 659 self, lb: NumberT, ub: NumberT, name: Optional[str] = None 660 ) -> Variable: 661 """Create an integer variable with domain [lb, ub]. 662 663 Args: 664 lb: Lower bound of the variable. 665 ub: Upper bound of the variable. 666 name: The name of the variable. 667 668 Returns: 669 a variable whose domain is [lb, ub]. 670 """ 671 672 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].
674 def new_bool_var(self, name: Optional[str] = None) -> Variable: 675 """Creates a 0-1 variable with the given name.""" 676 return self.new_var( 677 0, 1, True, name 678 ) # pytype: disable=wrong-arg-types # numpy-scalars
Creates a 0-1 variable with the given name.
680 def new_constant(self, value: NumberT) -> Variable: 681 """Declares a constant variable.""" 682 return self.new_var(value, value, False, None)
Declares a constant variable.
684 def new_var_series( 685 self, 686 name: str, 687 index: pd.Index, 688 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 689 upper_bounds: Union[NumberT, pd.Series] = math.inf, 690 is_integral: Union[bool, pd.Series] = False, 691 ) -> pd.Series: 692 """Creates a series of (scalar-valued) variables with the given name. 693 694 Args: 695 name (str): Required. The name of the variable set. 696 index (pd.Index): Required. The index to use for the variable set. 697 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 698 variables in the set. If a `pd.Series` is passed in, it will be based on 699 the corresponding values of the pd.Series. Defaults to -inf. 700 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 701 variables in the set. If a `pd.Series` is passed in, it will be based on 702 the corresponding values of the pd.Series. Defaults to +inf. 703 is_integral (bool, pd.Series): Optional. Indicates if the variable can 704 only take integer values. If a `pd.Series` is passed in, it will be 705 based on the corresponding values of the pd.Series. Defaults to False. 706 707 Returns: 708 pd.Series: The variable set indexed by its corresponding dimensions. 709 710 Raises: 711 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 712 ValueError: if the `name` is not a valid identifier or already exists. 713 ValueError: if the `lowerbound` is greater than the `upperbound`. 714 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 715 does not match the input index. 716 """ 717 if not isinstance(index, pd.Index): 718 raise TypeError("Non-index object is used as index") 719 if not name.isidentifier(): 720 raise ValueError(f"name={name!r} is not a valid identifier") 721 if ( 722 mbn.is_a_number(lower_bounds) 723 and mbn.is_a_number(upper_bounds) 724 and lower_bounds > upper_bounds 725 ): 726 raise ValueError( 727 f"lower_bound={lower_bounds} is greater than" 728 f" upper_bound={upper_bounds} for variable set={name!r}" 729 ) 730 if ( 731 isinstance(is_integral, bool) 732 and is_integral 733 and mbn.is_a_number(lower_bounds) 734 and mbn.is_a_number(upper_bounds) 735 and math.isfinite(lower_bounds) 736 and math.isfinite(upper_bounds) 737 and math.ceil(lower_bounds) > math.floor(upper_bounds) 738 ): 739 raise ValueError( 740 f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}" 741 f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}" 742 f" for variable set={name!r}" 743 ) 744 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) 745 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) 746 is_integrals = _convert_to_series_and_validate_index(is_integral, index) 747 return pd.Series( 748 index=index, 749 data=[ 750 # pylint: disable=g-complex-comprehension 751 Variable( 752 self.__helper, 753 lower_bounds[i], 754 upper_bounds[i], 755 is_integrals[i], 756 f"{name}[{i}]", 757 ) 758 for i in index 759 ], 760 )
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.
762 def new_num_var_series( 763 self, 764 name: str, 765 index: pd.Index, 766 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 767 upper_bounds: Union[NumberT, pd.Series] = math.inf, 768 ) -> pd.Series: 769 """Creates a series of continuous variables with the given name. 770 771 Args: 772 name (str): Required. The name of the variable set. 773 index (pd.Index): Required. The index to use for the variable set. 774 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 775 variables in the set. If a `pd.Series` is passed in, it will be based on 776 the corresponding values of the pd.Series. Defaults to -inf. 777 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 778 variables in the set. If a `pd.Series` is passed in, it will be based on 779 the corresponding values of the pd.Series. Defaults to +inf. 780 781 Returns: 782 pd.Series: The variable set indexed by its corresponding dimensions. 783 784 Raises: 785 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 786 ValueError: if the `name` is not a valid identifier or already exists. 787 ValueError: if the `lowerbound` is greater than the `upperbound`. 788 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 789 does not match the input index. 790 """ 791 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.
793 def new_int_var_series( 794 self, 795 name: str, 796 index: pd.Index, 797 lower_bounds: Union[NumberT, pd.Series] = -math.inf, 798 upper_bounds: Union[NumberT, pd.Series] = math.inf, 799 ) -> pd.Series: 800 """Creates a series of integer variables with the given name. 801 802 Args: 803 name (str): Required. The name of the variable set. 804 index (pd.Index): Required. The index to use for the variable set. 805 lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for 806 variables in the set. If a `pd.Series` is passed in, it will be based on 807 the corresponding values of the pd.Series. Defaults to -inf. 808 upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for 809 variables in the set. If a `pd.Series` is passed in, it will be based on 810 the corresponding values of the pd.Series. Defaults to +inf. 811 812 Returns: 813 pd.Series: The variable set indexed by its corresponding dimensions. 814 815 Raises: 816 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 817 ValueError: if the `name` is not a valid identifier or already exists. 818 ValueError: if the `lowerbound` is greater than the `upperbound`. 819 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 820 does not match the input index. 821 """ 822 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.
824 def new_bool_var_series( 825 self, 826 name: str, 827 index: pd.Index, 828 ) -> pd.Series: 829 """Creates a series of Boolean variables with the given name. 830 831 Args: 832 name (str): Required. The name of the variable set. 833 index (pd.Index): Required. The index to use for the variable set. 834 835 Returns: 836 pd.Series: The variable set indexed by its corresponding dimensions. 837 838 Raises: 839 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 840 ValueError: if the `name` is not a valid identifier or already exists. 841 ValueError: if the `lowerbound` is greater than the `upperbound`. 842 ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` 843 does not match the input index. 844 """ 845 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.
847 def var_from_index(self, index: IntegerT) -> Variable: 848 """Rebuilds a variable object from the model and its index.""" 849 return Variable(self.__helper, index)
Rebuilds a variable object from the model and its index.
853 def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 854 self, 855 linear_expr: LinearExprT, 856 lb: NumberT = -math.inf, 857 ub: NumberT = math.inf, 858 name: Optional[str] = None, 859 ) -> LinearConstraint: 860 """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" 861 ct = LinearConstraint(self.__helper) 862 if name: 863 self.__helper.set_constraint_name(ct.index, name) 864 if mbn.is_a_number(linear_expr): 865 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 866 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 867 elif isinstance(linear_expr, LinearExpr): 868 flat_expr = mbh.FlatExpr(linear_expr) 869 # pylint: disable=protected-access 870 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) 871 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) 872 self.__helper.add_terms_to_constraint( 873 ct.index, flat_expr.vars, flat_expr.coeffs 874 ) 875 else: 876 raise TypeError( 877 "Not supported:" 878 f" Model.add_linear_constraint({type(linear_expr).__name__!r})" 879 ) 880 return ct
Adds the constraint: lb <= linear_expr <= ub
with the given name.
882 def add( 883 self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None 884 ) -> Union[LinearConstraint, pd.Series]: 885 """Adds a `BoundedLinearExpression` to the model. 886 887 Args: 888 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 889 name: An optional name. 890 891 Returns: 892 An instance of the `Constraint` class. 893 894 Note that a special treatment is done when the argument does not contain any 895 variable, and thus evaluates to True or False. 896 897 `model.add(True)` will create a constraint 0 <= empty sum <= 0. 898 The constraint will be marked as under specified, and cannot be modified 899 thereafter. 900 901 `model.add(False)` will create a constraint inf <= empty sum <= -inf. The 902 constraint will be marked as under specified, and cannot be modified 903 thereafter. 904 905 you can check the if a constraint is under specified by reading the 906 `LinearConstraint.is_under_specified` property. 907 """ 908 if isinstance(ct, mbh.BoundedLinearExpression): 909 return _add_linear_constraint_to_helper(ct, self.__helper, name) 910 elif isinstance(ct, bool): 911 return _add_linear_constraint_to_helper(ct, self.__helper, name) 912 elif isinstance(ct, pd.Series): 913 return pd.Series( 914 index=ct.index, 915 data=[ 916 _add_linear_constraint_to_helper( 917 expr, self.__helper, f"{name}[{i}]" 918 ) 919 for (i, expr) in zip(ct.index, ct) 920 ], 921 ) 922 else: 923 raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})")
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.
925 def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: 926 """Rebuilds a linear constraint object from the model and its index.""" 927 return LinearConstraint(self.__helper, index=index)
Rebuilds a linear constraint object from the model and its index.
931 def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars 932 self, 933 linear_expr: LinearExprT, 934 ivar: "Variable", 935 ivalue: bool, 936 lb: NumberT = -math.inf, 937 ub: NumberT = math.inf, 938 name: Optional[str] = None, 939 ) -> EnforcedLinearConstraint: 940 """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" 941 ct = EnforcedLinearConstraint(self.__helper) 942 ct.indicator_variable = ivar 943 ct.indicator_value = ivalue 944 if name: 945 self.__helper.set_constraint_name(ct.index, name) 946 if mbn.is_a_number(linear_expr): 947 self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) 948 self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) 949 elif isinstance(linear_expr, LinearExpr): 950 flat_expr = mbh.FlatExpr(linear_expr) 951 # pylint: disable=protected-access 952 self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) 953 self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) 954 self.__helper.add_terms_to_constraint( 955 ct.index, flat_expr.vars, flat_expr.coeffs 956 ) 957 else: 958 raise TypeError( 959 "Not supported:" 960 f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})" 961 ) 962 return ct
Adds the constraint: ivar == ivalue => lb <= linear_expr <= ub
with the given name.
964 def add_enforced( 965 self, 966 ct: Union[ConstraintT, pd.Series], 967 var: Union[Variable, pd.Series], 968 value: Union[bool, pd.Series], 969 name: Optional[str] = None, 970 ) -> Union[EnforcedLinearConstraint, pd.Series]: 971 """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. 972 973 Args: 974 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 975 var: The indicator variable 976 value: the indicator value 977 name: An optional name. 978 979 Returns: 980 An instance of the `Constraint` class. 981 982 Note that a special treatment is done when the argument does not contain any 983 variable, and thus evaluates to True or False. 984 985 model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty 986 sum <= 0 987 988 model.add_enforced(False, var, value) will create a constraint inf <= 989 empty sum <= -inf 990 991 you can check the if a constraint is always false (lb=inf, ub=-inf) by 992 calling EnforcedLinearConstraint.is_always_false() 993 """ 994 if isinstance(ct, mbh.BoundedLinearExpression): 995 return _add_enforced_linear_constraint_to_helper( 996 ct, self.__helper, var, value, name 997 ) 998 elif ( 999 isinstance(ct, bool) 1000 and isinstance(var, Variable) 1001 and isinstance(value, bool) 1002 ): 1003 return _add_enforced_linear_constraint_to_helper( 1004 ct, self.__helper, var, value, name 1005 ) 1006 elif isinstance(ct, pd.Series): 1007 ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) 1008 ivalue_series = _convert_to_series_and_validate_index(value, ct.index) 1009 return pd.Series( 1010 index=ct.index, 1011 data=[ 1012 _add_enforced_linear_constraint_to_helper( 1013 expr, 1014 self.__helper, 1015 ivar_series[i], 1016 ivalue_series[i], 1017 f"{name}[{i}]", 1018 ) 1019 for (i, expr) in zip(ct.index, ct) 1020 ], 1021 ) 1022 else: 1023 raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}")
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()
1025 def enforced_linear_constraint_from_index( 1026 self, index: IntegerT 1027 ) -> EnforcedLinearConstraint: 1028 """Rebuilds an enforced linear constraint object from the model and its index.""" 1029 return EnforcedLinearConstraint(self.__helper, index=index)
Rebuilds an enforced linear constraint object from the model and its index.
1032 def minimize(self, linear_expr: LinearExprT) -> None: 1033 """Minimizes the given objective.""" 1034 self.__optimize(linear_expr, False)
Minimizes the given objective.
1036 def maximize(self, linear_expr: LinearExprT) -> None: 1037 """Maximizes the given objective.""" 1038 self.__optimize(linear_expr, True)
Maximizes the given objective.
1060 @property 1061 def objective_offset(self) -> np.double: 1062 """Returns the fixed offset of the objective.""" 1063 return self.__helper.objective_offset()
Returns the fixed offset of the objective.
1069 def objective_expression(self) -> "LinearExpr": 1070 """Returns the expression to optimize.""" 1071 variables: list[Variable] = [] 1072 coefficients: list[numbers.Real] = [] 1073 for variable in self.get_variables(): 1074 coeff = self.__helper.var_objective_coefficient(variable.index) 1075 if coeff != 0.0: 1076 variables.append(variable) 1077 coefficients.append(coeff) 1078 return mbh.FlatExpr(variables, coefficients, self.__helper.objective_offset())
Returns the expression to optimize.
1085 def add_hint(self, var: Variable, value: NumberT) -> None: 1086 """Adds var == value as a hint to the model. 1087 1088 Args: 1089 var: The variable of the hint 1090 value: The value of the hint 1091 1092 Note that variables must not appear more than once in the list of hints. 1093 """ 1094 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.
1112 def export_to_proto(self) -> linear_solver_pb2.MPModelProto: 1113 """Exports the optimization model to a ProtoBuf format.""" 1114 return mbh.to_mpmodel_proto(self.__helper)
Exports the optimization model to a ProtoBuf format.
1116 def import_from_mps_string(self, mps_string: str) -> bool: 1117 """Reads a model from a MPS string.""" 1118 return self.__helper.import_from_mps_string(mps_string)
Reads a model from a MPS string.
1120 def import_from_mps_file(self, mps_file: str) -> bool: 1121 """Reads a model from a .mps file.""" 1122 return self.__helper.import_from_mps_file(mps_file)
Reads a model from a .mps file.
1124 def import_from_lp_string(self, lp_string: str) -> bool: 1125 """Reads a model from a LP string. 1126 1127 Note that this code is very limited, and will not support any real lp. 1128 It is only intented to be use to parse test lp problems. 1129 1130 Args: 1131 lp_string: The LP string to import. 1132 1133 Returns: 1134 True if the import was successful. 1135 """ 1136 return self.__helper.import_from_lp_string(lp_string)
Reads a model from a LP string.
Note that this code is very limited, and will not support any real lp. It is only intented to be use to parse test lp problems.
Arguments:
- lp_string: The LP string to import.
Returns:
True if the import was successful.
1138 def import_from_lp_file(self, lp_file: str) -> bool: 1139 """Reads a model from a .lp file. 1140 1141 Note that this code is very limited, and will not support any real lp. 1142 It is only intented to be use to parse test lp problems. 1143 1144 Args: 1145 lp_file: The LP file to import. 1146 1147 Returns: 1148 True if the import was successful. 1149 """ 1150 return self.__helper.import_from_lp_file(lp_file)
Reads a model from a .lp file.
Note that this code is very limited, and will not support any real lp. It is only intented to be use to parse test lp problems.
Arguments:
- lp_file: The LP file to import.
Returns:
True if the import was successful.
1152 def import_from_proto_file(self, proto_file: str) -> bool: 1153 """Reads a model from a proto file.""" 1154 return self.__helper.read_model_from_proto_file(proto_file)
Reads a model from a proto file.
1156 def export_to_proto_file(self, proto_file: str) -> bool: 1157 """Writes a model to a proto file.""" 1158 return self.__helper.write_model_to_proto_file(proto_file)
Writes a model to a proto file.
1162 @property 1163 def num_variables(self) -> int: 1164 """Returns the number of variables in the model.""" 1165 return self.__helper.num_variables()
Returns the number of variables in the model.
1167 @property 1168 def num_constraints(self) -> int: 1169 """The number of constraints in the model.""" 1170 return self.__helper.num_constraints()
The number of constraints in the model.
1172 @property 1173 def name(self) -> str: 1174 """The name of the model.""" 1175 return self.__helper.name()
The name of the model.
1187class Solver: 1188 """Main solver class. 1189 1190 The purpose of this class is to search for a solution to the model provided 1191 to the solve() method. 1192 1193 Once solve() is called, this class allows inspecting the solution found 1194 with the value() method, as well as general statistics about the solve 1195 procedure. 1196 """ 1197 1198 def __init__(self, solver_name: str): 1199 self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name) 1200 self.log_callback: Optional[Callable[[str], None]] = None 1201 1202 def solver_is_supported(self) -> bool: 1203 """Checks whether the requested solver backend was found.""" 1204 return self.__solve_helper.solver_is_supported() 1205 1206 # Solver backend and parameters. 1207 def set_time_limit_in_seconds(self, limit: NumberT) -> None: 1208 """Sets a time limit for the solve() call.""" 1209 self.__solve_helper.set_time_limit_in_seconds(limit) 1210 1211 def set_solver_specific_parameters(self, parameters: str) -> None: 1212 """Sets parameters specific to the solver backend.""" 1213 self.__solve_helper.set_solver_specific_parameters(parameters) 1214 1215 def enable_output(self, enabled: bool) -> None: 1216 """Controls the solver backend logs.""" 1217 self.__solve_helper.enable_output(enabled) 1218 1219 def solve(self, model: Model) -> SolveStatus: 1220 """Solves a problem and passes each solution to the callback if not null.""" 1221 if self.log_callback is not None: 1222 self.__solve_helper.set_log_callback(self.log_callback) 1223 else: 1224 self.__solve_helper.clear_log_callback() 1225 self.__solve_helper.solve(model.helper) 1226 return SolveStatus(self.__solve_helper.status()) 1227 1228 def stop_search(self): 1229 """Stops the current search asynchronously.""" 1230 self.__solve_helper.interrupt_solve() 1231 1232 def value(self, expr: LinearExprT) -> np.double: 1233 """Returns the value of a linear expression after solve.""" 1234 if not self.__solve_helper.has_solution(): 1235 return pd.NA 1236 if mbn.is_a_number(expr): 1237 return expr 1238 elif isinstance(expr, LinearExpr): 1239 return self.__solve_helper.expression_value(expr) 1240 else: 1241 raise TypeError(f"Unknown expression {type(expr).__name__!r}") 1242 1243 def values(self, variables: _IndexOrSeries) -> pd.Series: 1244 """Returns the values of the input variables. 1245 1246 If `variables` is a `pd.Index`, then the output will be indexed by the 1247 variables. If `variables` is a `pd.Series` indexed by the underlying 1248 dimensions, then the output will be indexed by the same underlying 1249 dimensions. 1250 1251 Args: 1252 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1253 get the values. 1254 1255 Returns: 1256 pd.Series: The values of all variables in the set. 1257 """ 1258 if not self.__solve_helper.has_solution(): 1259 return _attribute_series(func=lambda v: pd.NA, values=variables) 1260 return _attribute_series( 1261 func=lambda v: self.__solve_helper.variable_value(v.index), 1262 values=variables, 1263 ) 1264 1265 def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: 1266 """Returns the reduced cost of the input variables. 1267 1268 If `variables` is a `pd.Index`, then the output will be indexed by the 1269 variables. If `variables` is a `pd.Series` indexed by the underlying 1270 dimensions, then the output will be indexed by the same underlying 1271 dimensions. 1272 1273 Args: 1274 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1275 get the values. 1276 1277 Returns: 1278 pd.Series: The reduced cost of all variables in the set. 1279 """ 1280 if not self.__solve_helper.has_solution(): 1281 return _attribute_series(func=lambda v: pd.NA, values=variables) 1282 return _attribute_series( 1283 func=lambda v: self.__solve_helper.reduced_cost(v.index), 1284 values=variables, 1285 ) 1286 1287 def reduced_cost(self, var: Variable) -> np.double: 1288 """Returns the reduced cost of a linear expression after solve.""" 1289 if not self.__solve_helper.has_solution(): 1290 return pd.NA 1291 return self.__solve_helper.reduced_cost(var.index) 1292 1293 def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: 1294 """Returns the dual values of the input constraints. 1295 1296 If `constraints` is a `pd.Index`, then the output will be indexed by the 1297 constraints. If `constraints` is a `pd.Series` indexed by the underlying 1298 dimensions, then the output will be indexed by the same underlying 1299 dimensions. 1300 1301 Args: 1302 constraints (Union[pd.Index, pd.Series]): The set of constraints from 1303 which to get the dual values. 1304 1305 Returns: 1306 pd.Series: The dual_values of all constraints in the set. 1307 """ 1308 if not self.__solve_helper.has_solution(): 1309 return _attribute_series(func=lambda v: pd.NA, values=constraints) 1310 return _attribute_series( 1311 func=lambda v: self.__solve_helper.dual_value(v.index), 1312 values=constraints, 1313 ) 1314 1315 def dual_value(self, ct: LinearConstraint) -> np.double: 1316 """Returns the dual value of a linear constraint after solve.""" 1317 if not self.__solve_helper.has_solution(): 1318 return pd.NA 1319 return self.__solve_helper.dual_value(ct.index) 1320 1321 def activity(self, ct: LinearConstraint) -> np.double: 1322 """Returns the activity of a linear constraint after solve.""" 1323 if not self.__solve_helper.has_solution(): 1324 return pd.NA 1325 return self.__solve_helper.activity(ct.index) 1326 1327 @property 1328 def objective_value(self) -> np.double: 1329 """Returns the value of the objective after solve.""" 1330 if not self.__solve_helper.has_solution(): 1331 return pd.NA 1332 return self.__solve_helper.objective_value() 1333 1334 @property 1335 def best_objective_bound(self) -> np.double: 1336 """Returns the best lower (upper) bound found when min(max)imizing.""" 1337 if not self.__solve_helper.has_solution(): 1338 return pd.NA 1339 return self.__solve_helper.best_objective_bound() 1340 1341 @property 1342 def status_string(self) -> str: 1343 """Returns additional information of the last solve. 1344 1345 It can describe why the model is invalid. 1346 """ 1347 return self.__solve_helper.status_string() 1348 1349 @property 1350 def wall_time(self) -> np.double: 1351 return self.__solve_helper.wall_time() 1352 1353 @property 1354 def user_time(self) -> np.double: 1355 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.
1202 def solver_is_supported(self) -> bool: 1203 """Checks whether the requested solver backend was found.""" 1204 return self.__solve_helper.solver_is_supported()
Checks whether the requested solver backend was found.
1207 def set_time_limit_in_seconds(self, limit: NumberT) -> None: 1208 """Sets a time limit for the solve() call.""" 1209 self.__solve_helper.set_time_limit_in_seconds(limit)
Sets a time limit for the solve() call.
1211 def set_solver_specific_parameters(self, parameters: str) -> None: 1212 """Sets parameters specific to the solver backend.""" 1213 self.__solve_helper.set_solver_specific_parameters(parameters)
Sets parameters specific to the solver backend.
1215 def enable_output(self, enabled: bool) -> None: 1216 """Controls the solver backend logs.""" 1217 self.__solve_helper.enable_output(enabled)
Controls the solver backend logs.
1219 def solve(self, model: Model) -> SolveStatus: 1220 """Solves a problem and passes each solution to the callback if not null.""" 1221 if self.log_callback is not None: 1222 self.__solve_helper.set_log_callback(self.log_callback) 1223 else: 1224 self.__solve_helper.clear_log_callback() 1225 self.__solve_helper.solve(model.helper) 1226 return SolveStatus(self.__solve_helper.status())
Solves a problem and passes each solution to the callback if not null.
1228 def stop_search(self): 1229 """Stops the current search asynchronously.""" 1230 self.__solve_helper.interrupt_solve()
Stops the current search asynchronously.
1232 def value(self, expr: LinearExprT) -> np.double: 1233 """Returns the value of a linear expression after solve.""" 1234 if not self.__solve_helper.has_solution(): 1235 return pd.NA 1236 if mbn.is_a_number(expr): 1237 return expr 1238 elif isinstance(expr, LinearExpr): 1239 return self.__solve_helper.expression_value(expr) 1240 else: 1241 raise TypeError(f"Unknown expression {type(expr).__name__!r}")
Returns the value of a linear expression after solve.
1243 def values(self, variables: _IndexOrSeries) -> pd.Series: 1244 """Returns the values of the input variables. 1245 1246 If `variables` is a `pd.Index`, then the output will be indexed by the 1247 variables. If `variables` is a `pd.Series` indexed by the underlying 1248 dimensions, then the output will be indexed by the same underlying 1249 dimensions. 1250 1251 Args: 1252 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1253 get the values. 1254 1255 Returns: 1256 pd.Series: The values of all variables in the set. 1257 """ 1258 if not self.__solve_helper.has_solution(): 1259 return _attribute_series(func=lambda v: pd.NA, values=variables) 1260 return _attribute_series( 1261 func=lambda v: self.__solve_helper.variable_value(v.index), 1262 values=variables, 1263 )
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.
1265 def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: 1266 """Returns the reduced cost of the input variables. 1267 1268 If `variables` is a `pd.Index`, then the output will be indexed by the 1269 variables. If `variables` is a `pd.Series` indexed by the underlying 1270 dimensions, then the output will be indexed by the same underlying 1271 dimensions. 1272 1273 Args: 1274 variables (Union[pd.Index, pd.Series]): The set of variables from which to 1275 get the values. 1276 1277 Returns: 1278 pd.Series: The reduced cost of all variables in the set. 1279 """ 1280 if not self.__solve_helper.has_solution(): 1281 return _attribute_series(func=lambda v: pd.NA, values=variables) 1282 return _attribute_series( 1283 func=lambda v: self.__solve_helper.reduced_cost(v.index), 1284 values=variables, 1285 )
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.
1287 def reduced_cost(self, var: Variable) -> np.double: 1288 """Returns the reduced cost of a linear expression after solve.""" 1289 if not self.__solve_helper.has_solution(): 1290 return pd.NA 1291 return self.__solve_helper.reduced_cost(var.index)
Returns the reduced cost of a linear expression after solve.
1293 def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: 1294 """Returns the dual values of the input constraints. 1295 1296 If `constraints` is a `pd.Index`, then the output will be indexed by the 1297 constraints. If `constraints` is a `pd.Series` indexed by the underlying 1298 dimensions, then the output will be indexed by the same underlying 1299 dimensions. 1300 1301 Args: 1302 constraints (Union[pd.Index, pd.Series]): The set of constraints from 1303 which to get the dual values. 1304 1305 Returns: 1306 pd.Series: The dual_values of all constraints in the set. 1307 """ 1308 if not self.__solve_helper.has_solution(): 1309 return _attribute_series(func=lambda v: pd.NA, values=constraints) 1310 return _attribute_series( 1311 func=lambda v: self.__solve_helper.dual_value(v.index), 1312 values=constraints, 1313 )
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.
1315 def dual_value(self, ct: LinearConstraint) -> np.double: 1316 """Returns the dual value of a linear constraint after solve.""" 1317 if not self.__solve_helper.has_solution(): 1318 return pd.NA 1319 return self.__solve_helper.dual_value(ct.index)
Returns the dual value of a linear constraint after solve.
1321 def activity(self, ct: LinearConstraint) -> np.double: 1322 """Returns the activity of a linear constraint after solve.""" 1323 if not self.__solve_helper.has_solution(): 1324 return pd.NA 1325 return self.__solve_helper.activity(ct.index)
Returns the activity of a linear constraint after solve.
1327 @property 1328 def objective_value(self) -> np.double: 1329 """Returns the value of the objective after solve.""" 1330 if not self.__solve_helper.has_solution(): 1331 return pd.NA 1332 return self.__solve_helper.objective_value()
Returns the value of the objective after solve.
1334 @property 1335 def best_objective_bound(self) -> np.double: 1336 """Returns the best lower (upper) bound found when min(max)imizing.""" 1337 if not self.__solve_helper.has_solution(): 1338 return pd.NA 1339 return self.__solve_helper.best_objective_bound()
Returns the best lower (upper) bound found when min(max)imizing.
1341 @property 1342 def status_string(self) -> str: 1343 """Returns additional information of the last solve. 1344 1345 It can describe why the model is invalid. 1346 """ 1347 return self.__solve_helper.status_string()
Returns additional information of the last solve.
It can describe why the model is invalid.