ortools.sat.python.cp_model
Methods for building and solving CP-SAT models.
The following two sections describe the main methods for building and solving CP-SAT models.
- .CpModel">
CpModel
: Methods for creating models, including variables and constraints. - .CpSolver">
CPSolver
: Methods for solving a model and evaluating solutions.
The following methods implement callbacks that the solver calls each time it finds a new solution.
- .CpSolverSolutionCallback">
CpSolverSolutionCallback
: A general method for implementing callbacks. - .ObjectiveSolutionPrinter">
ObjectiveSolutionPrinter
: Print objective values and elapsed time for intermediate solutions. - .VarArraySolutionPrinter">
VarArraySolutionPrinter
: Print intermediate solutions (variable values, time). - [
VarArrayAndObjectiveSolutionPrinter
] (#cp_model.VarArrayAndObjectiveSolutionPrinter): Print both intermediate solutions and objective values.
Additional methods for solving CP-SAT models:
- .Constraint">
Constraint
: A few utility methods for modifying constraints created byCpModel
. - .LinearExpr">
LinearExpr
: Methods for creating constraints and the objective from large arrays of coefficients.
Other methods and functions listed are primarily used for developing OR-Tools, rather than for solving specific optimization problems.
1# Copyright 2010-2024 Google LLC 2# Licensed under the Apache License, Version 2.0 (the "License"); 3# you may not use this file except in compliance with the License. 4# You may obtain a copy of the License at 5# 6# http://www.apache.org/licenses/LICENSE-2.0 7# 8# Unless required by applicable law or agreed to in writing, software 9# distributed under the License is distributed on an "AS IS" BASIS, 10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11# See the License for the specific language governing permissions and 12# limitations under the License. 13 14"""Methods for building and solving CP-SAT models. 15 16The following two sections describe the main 17methods for building and solving CP-SAT models. 18 19* [`CpModel`](#cp_model.CpModel): Methods for creating 20models, including variables and constraints. 21* [`CPSolver`](#cp_model.CpSolver): Methods for solving 22a model and evaluating solutions. 23 24The following methods implement callbacks that the 25solver calls each time it finds a new solution. 26 27* [`CpSolverSolutionCallback`](#cp_model.CpSolverSolutionCallback): 28 A general method for implementing callbacks. 29* [`ObjectiveSolutionPrinter`](#cp_model.ObjectiveSolutionPrinter): 30 Print objective values and elapsed time for intermediate solutions. 31* [`VarArraySolutionPrinter`](#cp_model.VarArraySolutionPrinter): 32 Print intermediate solutions (variable values, time). 33* [`VarArrayAndObjectiveSolutionPrinter`] 34 (#cp_model.VarArrayAndObjectiveSolutionPrinter): 35 Print both intermediate solutions and objective values. 36 37Additional methods for solving CP-SAT models: 38 39* [`Constraint`](#cp_model.Constraint): A few utility methods for modifying 40 constraints created by `CpModel`. 41* [`LinearExpr`](#lineacp_model.LinearExpr): Methods for creating constraints 42 and the objective from large arrays of coefficients. 43 44Other methods and functions listed are primarily used for developing OR-Tools, 45rather than for solving specific optimization problems. 46""" 47 48import collections 49import itertools 50import threading 51import time 52from typing import ( 53 Any, 54 Callable, 55 Dict, 56 Iterable, 57 List, 58 NoReturn, 59 Optional, 60 Sequence, 61 Tuple, 62 Union, 63 cast, 64 overload, 65) 66import warnings 67 68import numpy as np 69import pandas as pd 70 71from ortools.sat import cp_model_pb2 72from ortools.sat import sat_parameters_pb2 73from ortools.sat.python import cp_model_helper as cmh 74from ortools.sat.python import swig_helper 75from ortools.util.python import sorted_interval_list 76 77Domain = sorted_interval_list.Domain 78 79# The classes below allow linear expressions to be expressed naturally with the 80# usual arithmetic operators + - * / and with constant numbers, which makes the 81# python API very intuitive. See../ samples/*.py for examples. 82 83INT_MIN = -(2**63) # hardcoded to be platform independent. 84INT_MAX = 2**63 - 1 85INT32_MIN = -(2**31) 86INT32_MAX = 2**31 - 1 87 88# CpSolver status (exported to avoid importing cp_model_cp2). 89UNKNOWN = cp_model_pb2.UNKNOWN 90MODEL_INVALID = cp_model_pb2.MODEL_INVALID 91FEASIBLE = cp_model_pb2.FEASIBLE 92INFEASIBLE = cp_model_pb2.INFEASIBLE 93OPTIMAL = cp_model_pb2.OPTIMAL 94 95# Variable selection strategy 96CHOOSE_FIRST = cp_model_pb2.DecisionStrategyProto.CHOOSE_FIRST 97CHOOSE_LOWEST_MIN = cp_model_pb2.DecisionStrategyProto.CHOOSE_LOWEST_MIN 98CHOOSE_HIGHEST_MAX = cp_model_pb2.DecisionStrategyProto.CHOOSE_HIGHEST_MAX 99CHOOSE_MIN_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MIN_DOMAIN_SIZE 100CHOOSE_MAX_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MAX_DOMAIN_SIZE 101 102# Domain reduction strategy 103SELECT_MIN_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MIN_VALUE 104SELECT_MAX_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MAX_VALUE 105SELECT_LOWER_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_LOWER_HALF 106SELECT_UPPER_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_UPPER_HALF 107 108# Search branching 109AUTOMATIC_SEARCH = sat_parameters_pb2.SatParameters.AUTOMATIC_SEARCH 110FIXED_SEARCH = sat_parameters_pb2.SatParameters.FIXED_SEARCH 111PORTFOLIO_SEARCH = sat_parameters_pb2.SatParameters.PORTFOLIO_SEARCH 112LP_SEARCH = sat_parameters_pb2.SatParameters.LP_SEARCH 113PSEUDO_COST_SEARCH = sat_parameters_pb2.SatParameters.PSEUDO_COST_SEARCH 114PORTFOLIO_WITH_QUICK_RESTART_SEARCH = ( 115 sat_parameters_pb2.SatParameters.PORTFOLIO_WITH_QUICK_RESTART_SEARCH 116) 117HINT_SEARCH = sat_parameters_pb2.SatParameters.HINT_SEARCH 118PARTIAL_FIXED_SEARCH = sat_parameters_pb2.SatParameters.PARTIAL_FIXED_SEARCH 119RANDOMIZED_SEARCH = sat_parameters_pb2.SatParameters.RANDOMIZED_SEARCH 120 121# Type aliases 122IntegralT = Union[int, np.int8, np.uint8, np.int32, np.uint32, np.int64, np.uint64] 123IntegralTypes = ( 124 int, 125 np.int8, 126 np.uint8, 127 np.int32, 128 np.uint32, 129 np.int64, 130 np.uint64, 131) 132NumberT = Union[ 133 int, 134 float, 135 np.int8, 136 np.uint8, 137 np.int32, 138 np.uint32, 139 np.int64, 140 np.uint64, 141 np.double, 142] 143NumberTypes = ( 144 int, 145 float, 146 np.int8, 147 np.uint8, 148 np.int32, 149 np.uint32, 150 np.int64, 151 np.uint64, 152 np.double, 153) 154 155LiteralT = Union["IntVar", "_NotBooleanVariable", IntegralT, bool] 156BoolVarT = Union["IntVar", "_NotBooleanVariable"] 157VariableT = Union["IntVar", IntegralT] 158 159# We need to add 'IntVar' for pytype. 160LinearExprT = Union["LinearExpr", "IntVar", IntegralT] 161ObjLinearExprT = Union["LinearExpr", NumberT] 162BoundedLinearExprT = Union["BoundedLinearExpression", bool] 163 164ArcT = Tuple[IntegralT, IntegralT, LiteralT] 165_IndexOrSeries = Union[pd.Index, pd.Series] 166 167 168def display_bounds(bounds: Sequence[int]) -> str: 169 """Displays a flattened list of intervals.""" 170 out = "" 171 for i in range(0, len(bounds), 2): 172 if i != 0: 173 out += ", " 174 if bounds[i] == bounds[i + 1]: 175 out += str(bounds[i]) 176 else: 177 out += str(bounds[i]) + ".." + str(bounds[i + 1]) 178 return out 179 180 181def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str: 182 """Returns a short name of an integer variable, or its negation.""" 183 if i < 0: 184 return "not(%s)" % short_name(model, -i - 1) 185 v = model.variables[i] 186 if v.name: 187 return v.name 188 elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: 189 return str(v.domain[0]) 190 else: 191 return "[%s]" % display_bounds(v.domain) 192 193 194def short_expr_name( 195 model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto 196) -> str: 197 """Pretty-print LinearExpressionProto instances.""" 198 if not e.vars: 199 return str(e.offset) 200 if len(e.vars) == 1: 201 var_name = short_name(model, e.vars[0]) 202 coeff = e.coeffs[0] 203 result = "" 204 if coeff == 1: 205 result = var_name 206 elif coeff == -1: 207 result = f"-{var_name}" 208 elif coeff != 0: 209 result = f"{coeff} * {var_name}" 210 if e.offset > 0: 211 result = f"{result} + {e.offset}" 212 elif e.offset < 0: 213 result = f"{result} - {-e.offset}" 214 return result 215 # TODO(user): Support more than affine expressions. 216 return str(e) 217 218 219class LinearExpr: 220 """Holds an integer linear expression. 221 222 A linear expression is built from integer constants and variables. 223 For example, `x + 2 * (y - z + 1)`. 224 225 Linear expressions are used in CP-SAT models in constraints and in the 226 objective: 227 228 * You can define linear constraints as in: 229 230 ``` 231 model.add(x + 2 * y <= 5) 232 model.add(sum(array_of_vars) == 5) 233 ``` 234 235 * In CP-SAT, the objective is a linear expression: 236 237 ``` 238 model.minimize(x + 2 * y + z) 239 ``` 240 241 * For large arrays, using the LinearExpr class is faster that using the python 242 `sum()` function. You can create constraints and the objective from lists of 243 linear expressions or coefficients as follows: 244 245 ``` 246 model.minimize(cp_model.LinearExpr.sum(expressions)) 247 model.add(cp_model.LinearExpr.weighted_sum(expressions, coefficients) >= 0) 248 ``` 249 """ 250 251 @classmethod 252 def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 253 """Creates the expression sum(expressions).""" 254 if len(expressions) == 1: 255 return expressions[0] 256 return _SumArray(expressions) 257 258 @overload 259 @classmethod 260 def weighted_sum( 261 cls, 262 expressions: Sequence[LinearExprT], 263 coefficients: Sequence[IntegralT], 264 ) -> LinearExprT: ... 265 266 @overload 267 @classmethod 268 def weighted_sum( 269 cls, 270 expressions: Sequence[ObjLinearExprT], 271 coefficients: Sequence[NumberT], 272 ) -> ObjLinearExprT: ... 273 274 @classmethod 275 def weighted_sum(cls, expressions, coefficients): 276 """Creates the expression sum(expressions[i] * coefficients[i]).""" 277 if LinearExpr.is_empty_or_all_null(coefficients): 278 return 0 279 elif len(expressions) == 1: 280 return expressions[0] * coefficients[0] 281 else: 282 return _WeightedSum(expressions, coefficients) 283 284 @overload 285 @classmethod 286 def term( 287 cls, 288 expressions: LinearExprT, 289 coefficients: IntegralT, 290 ) -> LinearExprT: ... 291 292 @overload 293 @classmethod 294 def term( 295 cls, 296 expressions: ObjLinearExprT, 297 coefficients: NumberT, 298 ) -> ObjLinearExprT: ... 299 300 @classmethod 301 def term(cls, expression, coefficient): 302 """Creates `expression * coefficient`.""" 303 if cmh.is_zero(coefficient): 304 return 0 305 else: 306 return expression * coefficient 307 308 @classmethod 309 def is_empty_or_all_null(cls, coefficients: Sequence[NumberT]) -> bool: 310 for c in coefficients: 311 if not cmh.is_zero(c): 312 return False 313 return True 314 315 @classmethod 316 def rebuild_from_linear_expression_proto( 317 cls, 318 model: cp_model_pb2.CpModelProto, 319 proto: cp_model_pb2.LinearExpressionProto, 320 ) -> LinearExprT: 321 """Recreate a LinearExpr from a LinearExpressionProto.""" 322 offset = proto.offset 323 num_elements = len(proto.vars) 324 if num_elements == 0: 325 return offset 326 elif num_elements == 1: 327 return ( 328 IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset 329 ) # pytype: disable=bad-return-type 330 else: 331 variables = [] 332 coeffs = [] 333 all_ones = True 334 for index, coeff in zip(proto.vars, proto.coeffs): 335 variables.append(IntVar(model, index, None)) 336 coeffs.append(coeff) 337 if not cmh.is_one(coeff): 338 all_ones = False 339 if all_ones: 340 return _SumArray(variables, offset) 341 else: 342 return _WeightedSum(variables, coeffs, offset) 343 344 def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", int], int]: 345 """Scans the expression, and returns (var_coef_map, constant).""" 346 coeffs: Dict["IntVar", int] = collections.defaultdict(int) 347 constant = 0 348 to_process: List[Tuple[LinearExprT, int]] = [(self, 1)] 349 while to_process: # Flatten to avoid recursion. 350 expr: LinearExprT 351 coeff: int 352 expr, coeff = to_process.pop() 353 if isinstance(expr, IntegralTypes): 354 constant += coeff * int(expr) 355 elif isinstance(expr, _ProductCst): 356 to_process.append((expr.expression(), coeff * expr.coefficient())) 357 elif isinstance(expr, _Sum): 358 to_process.append((expr.left(), coeff)) 359 to_process.append((expr.right(), coeff)) 360 elif isinstance(expr, _SumArray): 361 for e in expr.expressions(): 362 to_process.append((e, coeff)) 363 constant += expr.constant() * coeff 364 elif isinstance(expr, _WeightedSum): 365 for e, c in zip(expr.expressions(), expr.coefficients()): 366 to_process.append((e, coeff * c)) 367 constant += expr.constant() * coeff 368 elif isinstance(expr, IntVar): 369 coeffs[expr] += coeff 370 elif isinstance(expr, _NotBooleanVariable): 371 constant += coeff 372 coeffs[expr.negated()] -= coeff 373 elif isinstance(expr, NumberTypes): 374 raise TypeError( 375 f"Floating point constants are not supported in constraints: {expr}" 376 ) 377 else: 378 raise TypeError("Unrecognized linear expression: " + str(expr)) 379 380 return coeffs, constant 381 382 def get_float_var_value_map( 383 self, 384 ) -> Tuple[Dict["IntVar", float], float, bool]: 385 """Scans the expression. Returns (var_coef_map, constant, is_integer).""" 386 coeffs: Dict["IntVar", Union[int, float]] = {} 387 constant: Union[int, float] = 0 388 to_process: List[Tuple[LinearExprT, Union[int, float]]] = [(self, 1)] 389 while to_process: # Flatten to avoid recursion. 390 expr, coeff = to_process.pop() 391 if isinstance(expr, IntegralTypes): # Keep integrality. 392 constant += coeff * int(expr) 393 elif isinstance(expr, NumberTypes): 394 constant += coeff * float(expr) 395 elif isinstance(expr, _ProductCst): 396 to_process.append((expr.expression(), coeff * expr.coefficient())) 397 elif isinstance(expr, _Sum): 398 to_process.append((expr.left(), coeff)) 399 to_process.append((expr.right(), coeff)) 400 elif isinstance(expr, _SumArray): 401 for e in expr.expressions(): 402 to_process.append((e, coeff)) 403 constant += expr.constant() * coeff 404 elif isinstance(expr, _WeightedSum): 405 for e, c in zip(expr.expressions(), expr.coefficients()): 406 to_process.append((e, coeff * c)) 407 constant += expr.constant() * coeff 408 elif isinstance(expr, IntVar): 409 if expr in coeffs: 410 coeffs[expr] += coeff 411 else: 412 coeffs[expr] = coeff 413 elif isinstance(expr, _NotBooleanVariable): 414 constant += coeff 415 if expr.negated() in coeffs: 416 coeffs[expr.negated()] -= coeff 417 else: 418 coeffs[expr.negated()] = -coeff 419 else: 420 raise TypeError("Unrecognized linear expression: " + str(expr)) 421 is_integer = isinstance(constant, IntegralTypes) 422 if is_integer: 423 for coeff in coeffs.values(): 424 if not isinstance(coeff, IntegralTypes): 425 is_integer = False 426 break 427 return coeffs, constant, is_integer 428 429 def __hash__(self) -> int: 430 return object.__hash__(self) 431 432 def __abs__(self) -> NoReturn: 433 raise NotImplementedError( 434 "calling abs() on a linear expression is not supported, " 435 "please use CpModel.add_abs_equality" 436 ) 437 438 @overload 439 def __add__(self, arg: "LinearExpr") -> "LinearExpr": ... 440 441 @overload 442 def __add__(self, arg: NumberT) -> "LinearExpr": ... 443 444 def __add__(self, arg): 445 if cmh.is_zero(arg): 446 return self 447 return _Sum(self, arg) 448 449 @overload 450 def __radd__(self, arg: "LinearExpr") -> "LinearExpr": ... 451 452 @overload 453 def __radd__(self, arg: NumberT) -> "LinearExpr": ... 454 455 def __radd__(self, arg): 456 return self.__add__(arg) 457 458 @overload 459 def __sub__(self, arg: "LinearExpr") -> "LinearExpr": ... 460 461 @overload 462 def __sub__(self, arg: NumberT) -> "LinearExpr": ... 463 464 def __sub__(self, arg): 465 if cmh.is_zero(arg): 466 return self 467 if isinstance(arg, NumberTypes): 468 arg = cmh.assert_is_a_number(arg) 469 return _Sum(self, -arg) 470 else: 471 return _Sum(self, -arg) 472 473 @overload 474 def __rsub__(self, arg: "LinearExpr") -> "LinearExpr": ... 475 476 @overload 477 def __rsub__(self, arg: NumberT) -> "LinearExpr": ... 478 479 def __rsub__(self, arg): 480 return _Sum(-self, arg) 481 482 @overload 483 def __mul__(self, arg: IntegralT) -> Union["LinearExpr", IntegralT]: ... 484 485 @overload 486 def __mul__(self, arg: NumberT) -> Union["LinearExpr", NumberT]: ... 487 488 def __mul__(self, arg): 489 arg = cmh.assert_is_a_number(arg) 490 if cmh.is_one(arg): 491 return self 492 elif cmh.is_zero(arg): 493 return 0 494 return _ProductCst(self, arg) 495 496 @overload 497 def __rmul__(self, arg: IntegralT) -> Union["LinearExpr", IntegralT]: ... 498 499 @overload 500 def __rmul__(self, arg: NumberT) -> Union["LinearExpr", NumberT]: ... 501 502 def __rmul__(self, arg): 503 return self.__mul__(arg) 504 505 def __div__(self, _) -> NoReturn: 506 raise NotImplementedError( 507 "calling / on a linear expression is not supported, " 508 "please use CpModel.add_division_equality" 509 ) 510 511 def __truediv__(self, _) -> NoReturn: 512 raise NotImplementedError( 513 "calling // on a linear expression is not supported, " 514 "please use CpModel.add_division_equality" 515 ) 516 517 def __mod__(self, _) -> NoReturn: 518 raise NotImplementedError( 519 "calling %% on a linear expression is not supported, " 520 "please use CpModel.add_modulo_equality" 521 ) 522 523 def __pow__(self, _) -> NoReturn: 524 raise NotImplementedError( 525 "calling ** on a linear expression is not supported, " 526 "please use CpModel.add_multiplication_equality" 527 ) 528 529 def __lshift__(self, _) -> NoReturn: 530 raise NotImplementedError( 531 "calling left shift on a linear expression is not supported" 532 ) 533 534 def __rshift__(self, _) -> NoReturn: 535 raise NotImplementedError( 536 "calling right shift on a linear expression is not supported" 537 ) 538 539 def __and__(self, _) -> NoReturn: 540 raise NotImplementedError( 541 "calling and on a linear expression is not supported, " 542 "please use CpModel.add_bool_and" 543 ) 544 545 def __or__(self, _) -> NoReturn: 546 raise NotImplementedError( 547 "calling or on a linear expression is not supported, " 548 "please use CpModel.add_bool_or" 549 ) 550 551 def __xor__(self, _) -> NoReturn: 552 raise NotImplementedError( 553 "calling xor on a linear expression is not supported, " 554 "please use CpModel.add_bool_xor" 555 ) 556 557 def __neg__(self) -> "LinearExpr": 558 return _ProductCst(self, -1) 559 560 def __bool__(self) -> NoReturn: 561 raise NotImplementedError( 562 "Evaluating a LinearExpr instance as a Boolean is not implemented." 563 ) 564 565 def __eq__(self, arg: LinearExprT) -> BoundedLinearExprT: # type: ignore[override] 566 if arg is None: 567 return False 568 if isinstance(arg, IntegralTypes): 569 arg = cmh.assert_is_int64(arg) 570 return BoundedLinearExpression(self, [arg, arg]) 571 elif isinstance(arg, LinearExpr): 572 return BoundedLinearExpression(self - arg, [0, 0]) 573 else: 574 return False 575 576 def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression": 577 if isinstance(arg, IntegralTypes): 578 arg = cmh.assert_is_int64(arg) 579 return BoundedLinearExpression(self, [arg, INT_MAX]) 580 else: 581 return BoundedLinearExpression(self - arg, [0, INT_MAX]) 582 583 def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression": 584 if isinstance(arg, IntegralTypes): 585 arg = cmh.assert_is_int64(arg) 586 return BoundedLinearExpression(self, [INT_MIN, arg]) 587 else: 588 return BoundedLinearExpression(self - arg, [INT_MIN, 0]) 589 590 def __lt__(self, arg: LinearExprT) -> "BoundedLinearExpression": 591 if isinstance(arg, IntegralTypes): 592 arg = cmh.assert_is_int64(arg) 593 if arg == INT_MIN: 594 raise ArithmeticError("< INT_MIN is not supported") 595 return BoundedLinearExpression(self, [INT_MIN, arg - 1]) 596 else: 597 return BoundedLinearExpression(self - arg, [INT_MIN, -1]) 598 599 def __gt__(self, arg: LinearExprT) -> "BoundedLinearExpression": 600 if isinstance(arg, IntegralTypes): 601 arg = cmh.assert_is_int64(arg) 602 if arg == INT_MAX: 603 raise ArithmeticError("> INT_MAX is not supported") 604 return BoundedLinearExpression(self, [arg + 1, INT_MAX]) 605 else: 606 return BoundedLinearExpression(self - arg, [1, INT_MAX]) 607 608 def __ne__(self, arg: LinearExprT) -> BoundedLinearExprT: # type: ignore[override] 609 if arg is None: 610 return True 611 if isinstance(arg, IntegralTypes): 612 arg = cmh.assert_is_int64(arg) 613 if arg == INT_MAX: 614 return BoundedLinearExpression(self, [INT_MIN, INT_MAX - 1]) 615 elif arg == INT_MIN: 616 return BoundedLinearExpression(self, [INT_MIN + 1, INT_MAX]) 617 else: 618 return BoundedLinearExpression( 619 self, [INT_MIN, arg - 1, arg + 1, INT_MAX] 620 ) 621 elif isinstance(arg, LinearExpr): 622 return BoundedLinearExpression(self - arg, [INT_MIN, -1, 1, INT_MAX]) 623 else: 624 return True 625 626 # Compatibility with pre PEP8 627 # pylint: disable=invalid-name 628 @classmethod 629 def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 630 """Creates the expression sum(expressions).""" 631 return cls.sum(expressions) 632 633 @overload 634 @classmethod 635 def WeightedSum( 636 cls, 637 expressions: Sequence[LinearExprT], 638 coefficients: Sequence[IntegralT], 639 ) -> LinearExprT: ... 640 641 @overload 642 @classmethod 643 def WeightedSum( 644 cls, 645 expressions: Sequence[ObjLinearExprT], 646 coefficients: Sequence[NumberT], 647 ) -> ObjLinearExprT: ... 648 649 @classmethod 650 def WeightedSum(cls, expressions, coefficients): 651 """Creates the expression sum(expressions[i] * coefficients[i]).""" 652 return cls.weighted_sum(expressions, coefficients) 653 654 @overload 655 @classmethod 656 def Term( 657 cls, 658 expressions: LinearExprT, 659 coefficients: IntegralT, 660 ) -> LinearExprT: ... 661 662 @overload 663 @classmethod 664 def Term( 665 cls, 666 expressions: ObjLinearExprT, 667 coefficients: NumberT, 668 ) -> ObjLinearExprT: ... 669 670 @classmethod 671 def Term(cls, expression, coefficient): 672 """Creates `expression * coefficient`.""" 673 return cls.term(expression, coefficient) 674 675 # pylint: enable=invalid-name 676 677 678class _Sum(LinearExpr): 679 """Represents the sum of two LinearExprs.""" 680 681 def __init__(self, left, right) -> None: 682 for x in [left, right]: 683 if not isinstance(x, (NumberTypes, LinearExpr)): 684 raise TypeError("not an linear expression: " + str(x)) 685 self.__left = left 686 self.__right = right 687 688 def left(self): 689 return self.__left 690 691 def right(self): 692 return self.__right 693 694 def __str__(self): 695 return f"({self.__left} + {self.__right})" 696 697 def __repr__(self): 698 return f"sum({self.__left!r}, {self.__right!r})" 699 700 701class _ProductCst(LinearExpr): 702 """Represents the product of a LinearExpr by a constant.""" 703 704 def __init__(self, expr, coeff) -> None: 705 coeff = cmh.assert_is_a_number(coeff) 706 if isinstance(expr, _ProductCst): 707 self.__expr = expr.expression() 708 self.__coef = expr.coefficient() * coeff 709 else: 710 self.__expr = expr 711 self.__coef = coeff 712 713 def __str__(self): 714 if self.__coef == -1: 715 return "-" + str(self.__expr) 716 else: 717 return "(" + str(self.__coef) + " * " + str(self.__expr) + ")" 718 719 def __repr__(self): 720 return f"ProductCst({self.__expr!r}, {self.__coef!r})" 721 722 def coefficient(self): 723 return self.__coef 724 725 def expression(self): 726 return self.__expr 727 728 729class _SumArray(LinearExpr): 730 """Represents the sum of a list of LinearExpr and a constant.""" 731 732 def __init__(self, expressions, constant=0) -> None: 733 self.__expressions = [] 734 self.__constant = constant 735 for x in expressions: 736 if isinstance(x, NumberTypes): 737 if cmh.is_zero(x): 738 continue 739 x = cmh.assert_is_a_number(x) 740 self.__constant += x 741 elif isinstance(x, LinearExpr): 742 self.__expressions.append(x) 743 else: 744 raise TypeError("not an linear expression: " + str(x)) 745 746 def __str__(self): 747 constant_terms = (self.__constant,) if self.__constant != 0 else () 748 exprs_str = " + ".join( 749 map(repr, itertools.chain(self.__expressions, constant_terms)) 750 ) 751 if not exprs_str: 752 return "0" 753 return f"({exprs_str})" 754 755 def __repr__(self): 756 exprs_str = ", ".join(map(repr, self.__expressions)) 757 return f"SumArray({exprs_str}, {self.__constant})" 758 759 def expressions(self): 760 return self.__expressions 761 762 def constant(self): 763 return self.__constant 764 765 766class _WeightedSum(LinearExpr): 767 """Represents sum(ai * xi) + b.""" 768 769 def __init__(self, expressions, coefficients, constant=0) -> None: 770 self.__expressions = [] 771 self.__coefficients = [] 772 self.__constant = constant 773 if len(expressions) != len(coefficients): 774 raise TypeError( 775 "In the LinearExpr.weighted_sum method, the expression array and the " 776 " coefficient array must have the same length." 777 ) 778 for e, c in zip(expressions, coefficients): 779 c = cmh.assert_is_a_number(c) 780 if cmh.is_zero(c): 781 continue 782 if isinstance(e, NumberTypes): 783 e = cmh.assert_is_a_number(e) 784 self.__constant += e * c 785 elif isinstance(e, LinearExpr): 786 self.__expressions.append(e) 787 self.__coefficients.append(c) 788 else: 789 raise TypeError("not an linear expression: " + str(e)) 790 791 def __str__(self): 792 output = None 793 for expr, coeff in zip(self.__expressions, self.__coefficients): 794 if not output and cmh.is_one(coeff): 795 output = str(expr) 796 elif not output and cmh.is_minus_one(coeff): 797 output = "-" + str(expr) 798 elif not output: 799 output = f"{coeff} * {expr}" 800 elif cmh.is_one(coeff): 801 output += f" + {expr}" 802 elif cmh.is_minus_one(coeff): 803 output += f" - {expr}" 804 elif coeff > 1: 805 output += f" + {coeff} * {expr}" 806 elif coeff < -1: 807 output += f" - {-coeff} * {expr}" 808 if output is None: 809 output = str(self.__constant) 810 elif self.__constant > 0: 811 output += f" + {self.__constant}" 812 elif self.__constant < 0: 813 output += f" - {-self.__constant}" 814 return output 815 816 def __repr__(self): 817 return ( 818 f"weighted_sum({self.__expressions!r}, {self.__coefficients!r}," 819 f" {self.__constant})" 820 ) 821 822 def expressions(self): 823 return self.__expressions 824 825 def coefficients(self): 826 return self.__coefficients 827 828 def constant(self): 829 return self.__constant 830 831 832class IntVar(LinearExpr): 833 """An integer variable. 834 835 An IntVar is an object that can take on any integer value within defined 836 ranges. Variables appear in constraint like: 837 838 x + y >= 5 839 AllDifferent([x, y, z]) 840 841 Solving a model is equivalent to finding, for each variable, a single value 842 from the set of initial values (called the initial domain), such that the 843 model is feasible, or optimal if you provided an objective function. 844 """ 845 846 def __init__( 847 self, 848 model: cp_model_pb2.CpModelProto, 849 domain: Union[int, sorted_interval_list.Domain], 850 name: Optional[str], 851 ) -> None: 852 """See CpModel.new_int_var below.""" 853 self.__index: int 854 self.__var: cp_model_pb2.IntegerVariableProto 855 self.__negation: Optional[_NotBooleanVariable] = None 856 # Python do not support multiple __init__ methods. 857 # This method is only called from the CpModel class. 858 # We hack the parameter to support the two cases: 859 # case 1: 860 # model is a CpModelProto, domain is a Domain, and name is a string. 861 # case 2: 862 # model is a CpModelProto, domain is an index (int), and name is None. 863 if isinstance(domain, IntegralTypes) and name is None: 864 self.__index = int(domain) 865 self.__var = model.variables[domain] 866 else: 867 self.__index = len(model.variables) 868 self.__var = model.variables.add() 869 self.__var.domain.extend( 870 cast(sorted_interval_list.Domain, domain).flattened_intervals() 871 ) 872 if name is not None: 873 self.__var.name = name 874 875 @property 876 def index(self) -> int: 877 """Returns the index of the variable in the model.""" 878 return self.__index 879 880 @property 881 def proto(self) -> cp_model_pb2.IntegerVariableProto: 882 """Returns the variable protobuf.""" 883 return self.__var 884 885 def is_equal_to(self, other: Any) -> bool: 886 """Returns true if self == other in the python sense.""" 887 if not isinstance(other, IntVar): 888 return False 889 return self.index == other.index 890 891 def __str__(self) -> str: 892 if not self.__var.name: 893 if ( 894 len(self.__var.domain) == 2 895 and self.__var.domain[0] == self.__var.domain[1] 896 ): 897 # Special case for constants. 898 return str(self.__var.domain[0]) 899 else: 900 return "unnamed_var_%i" % self.__index 901 return self.__var.name 902 903 def __repr__(self) -> str: 904 return "%s(%s)" % (self.__var.name, display_bounds(self.__var.domain)) 905 906 @property 907 def name(self) -> str: 908 if not self.__var or not self.__var.name: 909 return "" 910 return self.__var.name 911 912 def negated(self) -> "_NotBooleanVariable": 913 """Returns the negation of a Boolean variable. 914 915 This method implements the logical negation of a Boolean variable. 916 It is only valid if the variable has a Boolean domain (0 or 1). 917 918 Note that this method is nilpotent: `x.negated().negated() == x`. 919 """ 920 921 for bound in self.__var.domain: 922 if bound < 0 or bound > 1: 923 raise TypeError( 924 f"cannot call negated on a non boolean variable: {self}" 925 ) 926 if self.__negation is None: 927 self.__negation = _NotBooleanVariable(self) 928 return self.__negation 929 930 def __invert__(self) -> "_NotBooleanVariable": 931 """Returns the logical negation of a Boolean variable.""" 932 return self.negated() 933 934 # Pre PEP8 compatibility. 935 # pylint: disable=invalid-name 936 Not = negated 937 938 def Name(self) -> str: 939 return self.name 940 941 def Proto(self) -> cp_model_pb2.IntegerVariableProto: 942 return self.proto 943 944 def Index(self) -> int: 945 return self.index 946 947 # pylint: enable=invalid-name 948 949 950class _NotBooleanVariable(LinearExpr): 951 """Negation of a boolean variable.""" 952 953 def __init__(self, boolvar: IntVar) -> None: 954 self.__boolvar: IntVar = boolvar 955 956 @property 957 def index(self) -> int: 958 return -self.__boolvar.index - 1 959 960 def negated(self) -> IntVar: 961 return self.__boolvar 962 963 def __invert__(self) -> IntVar: 964 """Returns the logical negation of a Boolean literal.""" 965 return self.negated() 966 967 def __str__(self) -> str: 968 return self.name 969 970 @property 971 def name(self) -> str: 972 return "not(%s)" % str(self.__boolvar) 973 974 def __bool__(self) -> NoReturn: 975 raise NotImplementedError( 976 "Evaluating a literal as a Boolean value is not implemented." 977 ) 978 979 # Pre PEP8 compatibility. 980 # pylint: disable=invalid-name 981 def Not(self) -> "IntVar": 982 return self.negated() 983 984 def Index(self) -> int: 985 return self.index 986 987 # pylint: enable=invalid-name 988 989 990class BoundedLinearExpression: 991 """Represents a linear constraint: `lb <= linear expression <= ub`. 992 993 The only use of this class is to be added to the CpModel through 994 `CpModel.add(expression)`, as in: 995 996 model.add(x + 2 * y -1 >= z) 997 """ 998 999 def __init__(self, expr: LinearExprT, bounds: Sequence[int]) -> None: 1000 self.__expr: LinearExprT = expr 1001 self.__bounds: Sequence[int] = bounds 1002 1003 def __str__(self): 1004 if len(self.__bounds) == 2: 1005 lb, ub = self.__bounds 1006 if lb > INT_MIN and ub < INT_MAX: 1007 if lb == ub: 1008 return str(self.__expr) + " == " + str(lb) 1009 else: 1010 return str(lb) + " <= " + str(self.__expr) + " <= " + str(ub) 1011 elif lb > INT_MIN: 1012 return str(self.__expr) + " >= " + str(lb) 1013 elif ub < INT_MAX: 1014 return str(self.__expr) + " <= " + str(ub) 1015 else: 1016 return "True (unbounded expr " + str(self.__expr) + ")" 1017 elif ( 1018 len(self.__bounds) == 4 1019 and self.__bounds[0] == INT_MIN 1020 and self.__bounds[1] + 2 == self.__bounds[2] 1021 and self.__bounds[3] == INT_MAX 1022 ): 1023 return str(self.__expr) + " != " + str(self.__bounds[1] + 1) 1024 else: 1025 return str(self.__expr) + " in [" + display_bounds(self.__bounds) + "]" 1026 1027 def expression(self) -> LinearExprT: 1028 return self.__expr 1029 1030 def bounds(self) -> Sequence[int]: 1031 return self.__bounds 1032 1033 def __bool__(self) -> bool: 1034 expr = self.__expr 1035 if isinstance(expr, LinearExpr): 1036 coeffs_map, constant = expr.get_integer_var_value_map() 1037 all_coeffs = set(coeffs_map.values()) 1038 same_var = set([0]) 1039 eq_bounds = [0, 0] 1040 different_vars = set([-1, 1]) 1041 ne_bounds = [INT_MIN, -1, 1, INT_MAX] 1042 if ( 1043 len(coeffs_map) == 1 1044 and all_coeffs == same_var 1045 and constant == 0 1046 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1047 ): 1048 return self.__bounds == eq_bounds 1049 if ( 1050 len(coeffs_map) == 2 1051 and all_coeffs == different_vars 1052 and constant == 0 1053 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1054 ): 1055 return self.__bounds == ne_bounds 1056 1057 raise NotImplementedError( 1058 f'Evaluating a BoundedLinearExpression "{self}" as a Boolean value' 1059 + " is not supported." 1060 ) 1061 1062 1063class Constraint: 1064 """Base class for constraints. 1065 1066 Constraints are built by the CpModel through the add<XXX> methods. 1067 Once created by the CpModel class, they are automatically added to the model. 1068 The purpose of this class is to allow specification of enforcement literals 1069 for this constraint. 1070 1071 b = model.new_bool_var('b') 1072 x = model.new_int_var(0, 10, 'x') 1073 y = model.new_int_var(0, 10, 'y') 1074 1075 model.add(x + 2 * y == 5).only_enforce_if(b.negated()) 1076 """ 1077 1078 def __init__( 1079 self, 1080 cp_model: "CpModel", 1081 ) -> None: 1082 self.__index: int = len(cp_model.proto.constraints) 1083 self.__cp_model: "CpModel" = cp_model 1084 self.__constraint: cp_model_pb2.ConstraintProto = ( 1085 cp_model.proto.constraints.add() 1086 ) 1087 1088 @overload 1089 def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ... 1090 1091 @overload 1092 def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ... 1093 1094 def only_enforce_if(self, *boolvar) -> "Constraint": 1095 """Adds an enforcement literal to the constraint. 1096 1097 This method adds one or more literals (that is, a boolean variable or its 1098 negation) as enforcement literals. The conjunction of all these literals 1099 determines whether the constraint is active or not. It acts as an 1100 implication, so if the conjunction is true, it implies that the constraint 1101 must be enforced. If it is false, then the constraint is ignored. 1102 1103 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1104 1105 Args: 1106 *boolvar: One or more Boolean literals. 1107 1108 Returns: 1109 self. 1110 """ 1111 for lit in expand_generator_or_tuple(boolvar): 1112 if (cmh.is_boolean(lit) and lit) or ( 1113 isinstance(lit, IntegralTypes) and lit == 1 1114 ): 1115 # Always true. Do nothing. 1116 pass 1117 elif (cmh.is_boolean(lit) and not lit) or ( 1118 isinstance(lit, IntegralTypes) and lit == 0 1119 ): 1120 self.__constraint.enforcement_literal.append( 1121 self.__cp_model.new_constant(0).index 1122 ) 1123 else: 1124 self.__constraint.enforcement_literal.append( 1125 cast(Union[IntVar, _NotBooleanVariable], lit).index 1126 ) 1127 return self 1128 1129 def with_name(self, name: str) -> "Constraint": 1130 """Sets the name of the constraint.""" 1131 if name: 1132 self.__constraint.name = name 1133 else: 1134 self.__constraint.ClearField("name") 1135 return self 1136 1137 @property 1138 def name(self) -> str: 1139 """Returns the name of the constraint.""" 1140 if not self.__constraint or not self.__constraint.name: 1141 return "" 1142 return self.__constraint.name 1143 1144 @property 1145 def index(self) -> int: 1146 """Returns the index of the constraint in the model.""" 1147 return self.__index 1148 1149 @property 1150 def proto(self) -> cp_model_pb2.ConstraintProto: 1151 """Returns the constraint protobuf.""" 1152 return self.__constraint 1153 1154 # Pre PEP8 compatibility. 1155 # pylint: disable=invalid-name 1156 OnlyEnforceIf = only_enforce_if 1157 WithName = with_name 1158 1159 def Name(self) -> str: 1160 return self.name 1161 1162 def Index(self) -> int: 1163 return self.index 1164 1165 def Proto(self) -> cp_model_pb2.ConstraintProto: 1166 return self.proto 1167 1168 # pylint: enable=invalid-name 1169 1170 1171class IntervalVar: 1172 """Represents an Interval variable. 1173 1174 An interval variable is both a constraint and a variable. It is defined by 1175 three integer variables: start, size, and end. 1176 1177 It is a constraint because, internally, it enforces that start + size == end. 1178 1179 It is also a variable as it can appear in specific scheduling constraints: 1180 NoOverlap, NoOverlap2D, Cumulative. 1181 1182 Optionally, an enforcement literal can be added to this constraint, in which 1183 case these scheduling constraints will ignore interval variables with 1184 enforcement literals assigned to false. Conversely, these constraints will 1185 also set these enforcement literals to false if they cannot fit these 1186 intervals into the schedule. 1187 1188 Raises: 1189 ValueError: if start, size, end are not defined, or have the wrong type. 1190 """ 1191 1192 def __init__( 1193 self, 1194 model: cp_model_pb2.CpModelProto, 1195 start: Union[cp_model_pb2.LinearExpressionProto, int], 1196 size: Optional[cp_model_pb2.LinearExpressionProto], 1197 end: Optional[cp_model_pb2.LinearExpressionProto], 1198 is_present_index: Optional[int], 1199 name: Optional[str], 1200 ) -> None: 1201 self.__model: cp_model_pb2.CpModelProto = model 1202 self.__index: int 1203 self.__ct: cp_model_pb2.ConstraintProto 1204 # As with the IntVar::__init__ method, we hack the __init__ method to 1205 # support two use cases: 1206 # case 1: called when creating a new interval variable. 1207 # {start|size|end} are linear expressions, is_present_index is either 1208 # None or the index of a Boolean literal. name is a string 1209 # case 2: called when querying an existing interval variable. 1210 # start_index is an int, all parameters after are None. 1211 if isinstance(start, int): 1212 if size is not None: 1213 raise ValueError("size should be None") 1214 if end is not None: 1215 raise ValueError("end should be None") 1216 if is_present_index is not None: 1217 raise ValueError("is_present_index should be None") 1218 self.__index = cast(int, start) 1219 self.__ct = model.constraints[self.__index] 1220 else: 1221 self.__index = len(model.constraints) 1222 self.__ct = self.__model.constraints.add() 1223 if start is None: 1224 raise TypeError("start is not defined") 1225 self.__ct.interval.start.CopyFrom(start) 1226 if size is None: 1227 raise TypeError("size is not defined") 1228 self.__ct.interval.size.CopyFrom(size) 1229 if end is None: 1230 raise TypeError("end is not defined") 1231 self.__ct.interval.end.CopyFrom(end) 1232 if is_present_index is not None: 1233 self.__ct.enforcement_literal.append(is_present_index) 1234 if name: 1235 self.__ct.name = name 1236 1237 @property 1238 def index(self) -> int: 1239 """Returns the index of the interval constraint in the model.""" 1240 return self.__index 1241 1242 @property 1243 def proto(self) -> cp_model_pb2.IntervalConstraintProto: 1244 """Returns the interval protobuf.""" 1245 return self.__ct.interval 1246 1247 def __str__(self): 1248 return self.__ct.name 1249 1250 def __repr__(self): 1251 interval = self.__ct.interval 1252 if self.__ct.enforcement_literal: 1253 return "%s(start = %s, size = %s, end = %s, is_present = %s)" % ( 1254 self.__ct.name, 1255 short_expr_name(self.__model, interval.start), 1256 short_expr_name(self.__model, interval.size), 1257 short_expr_name(self.__model, interval.end), 1258 short_name(self.__model, self.__ct.enforcement_literal[0]), 1259 ) 1260 else: 1261 return "%s(start = %s, size = %s, end = %s)" % ( 1262 self.__ct.name, 1263 short_expr_name(self.__model, interval.start), 1264 short_expr_name(self.__model, interval.size), 1265 short_expr_name(self.__model, interval.end), 1266 ) 1267 1268 @property 1269 def name(self) -> str: 1270 if not self.__ct or not self.__ct.name: 1271 return "" 1272 return self.__ct.name 1273 1274 def start_expr(self) -> LinearExprT: 1275 return LinearExpr.rebuild_from_linear_expression_proto( 1276 self.__model, self.__ct.interval.start 1277 ) 1278 1279 def size_expr(self) -> LinearExprT: 1280 return LinearExpr.rebuild_from_linear_expression_proto( 1281 self.__model, self.__ct.interval.size 1282 ) 1283 1284 def end_expr(self) -> LinearExprT: 1285 return LinearExpr.rebuild_from_linear_expression_proto( 1286 self.__model, self.__ct.interval.end 1287 ) 1288 1289 # Pre PEP8 compatibility. 1290 # pylint: disable=invalid-name 1291 def Name(self) -> str: 1292 return self.name 1293 1294 def Index(self) -> int: 1295 return self.index 1296 1297 def Proto(self) -> cp_model_pb2.IntervalConstraintProto: 1298 return self.proto 1299 1300 StartExpr = start_expr 1301 SizeExpr = size_expr 1302 EndExpr = end_expr 1303 1304 # pylint: enable=invalid-name 1305 1306 1307def object_is_a_true_literal(literal: LiteralT) -> bool: 1308 """Checks if literal is either True, or a Boolean literals fixed to True.""" 1309 if isinstance(literal, IntVar): 1310 proto = literal.proto 1311 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1312 if isinstance(literal, _NotBooleanVariable): 1313 proto = literal.negated().proto 1314 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1315 if isinstance(literal, IntegralTypes): 1316 return int(literal) == 1 1317 return False 1318 1319 1320def object_is_a_false_literal(literal: LiteralT) -> bool: 1321 """Checks if literal is either False, or a Boolean literals fixed to False.""" 1322 if isinstance(literal, IntVar): 1323 proto = literal.proto 1324 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1325 if isinstance(literal, _NotBooleanVariable): 1326 proto = literal.negated().proto 1327 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1328 if isinstance(literal, IntegralTypes): 1329 return int(literal) == 0 1330 return False 1331 1332 1333class CpModel: 1334 """Methods for building a CP model. 1335 1336 Methods beginning with: 1337 1338 * ```New``` create integer, boolean, or interval variables. 1339 * ```add``` create new constraints and add them to the model. 1340 """ 1341 1342 def __init__(self) -> None: 1343 self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() 1344 self.__constant_map: Dict[IntegralT, int] = {} 1345 1346 # Naming. 1347 @property 1348 def name(self) -> str: 1349 """Returns the name of the model.""" 1350 if not self.__model or not self.__model.name: 1351 return "" 1352 return self.__model.name 1353 1354 @name.setter 1355 def name(self, name: str): 1356 """Sets the name of the model.""" 1357 self.__model.name = name 1358 1359 # Integer variable. 1360 1361 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1362 """Create an integer variable with domain [lb, ub]. 1363 1364 The CP-SAT solver is limited to integer variables. If you have fractional 1365 values, scale them up so that they become integers; if you have strings, 1366 encode them as integers. 1367 1368 Args: 1369 lb: Lower bound for the variable. 1370 ub: Upper bound for the variable. 1371 name: The name of the variable. 1372 1373 Returns: 1374 a variable whose domain is [lb, ub]. 1375 """ 1376 1377 return IntVar(self.__model, sorted_interval_list.Domain(lb, ub), name) 1378 1379 def new_int_var_from_domain( 1380 self, domain: sorted_interval_list.Domain, name: str 1381 ) -> IntVar: 1382 """Create an integer variable from a domain. 1383 1384 A domain is a set of integers specified by a collection of intervals. 1385 For example, `model.new_int_var_from_domain(cp_model. 1386 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1387 1388 Args: 1389 domain: An instance of the Domain class. 1390 name: The name of the variable. 1391 1392 Returns: 1393 a variable whose domain is the given domain. 1394 """ 1395 return IntVar(self.__model, domain, name) 1396 1397 def new_bool_var(self, name: str) -> IntVar: 1398 """Creates a 0-1 variable with the given name.""" 1399 return IntVar(self.__model, sorted_interval_list.Domain(0, 1), name) 1400 1401 def new_constant(self, value: IntegralT) -> IntVar: 1402 """Declares a constant integer.""" 1403 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None) 1404 1405 def new_int_var_series( 1406 self, 1407 name: str, 1408 index: pd.Index, 1409 lower_bounds: Union[IntegralT, pd.Series], 1410 upper_bounds: Union[IntegralT, pd.Series], 1411 ) -> pd.Series: 1412 """Creates a series of (scalar-valued) variables with the given name. 1413 1414 Args: 1415 name (str): Required. The name of the variable set. 1416 index (pd.Index): Required. The index to use for the variable set. 1417 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1418 set. If a `pd.Series` is passed in, it will be based on the 1419 corresponding values of the pd.Series. 1420 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1421 set. If a `pd.Series` is passed in, it will be based on the 1422 corresponding values of the pd.Series. 1423 1424 Returns: 1425 pd.Series: The variable set indexed by its corresponding dimensions. 1426 1427 Raises: 1428 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1429 ValueError: if the `name` is not a valid identifier or already exists. 1430 ValueError: if the `lowerbound` is greater than the `upperbound`. 1431 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1432 the input index. 1433 """ 1434 if not isinstance(index, pd.Index): 1435 raise TypeError("Non-index object is used as index") 1436 if not name.isidentifier(): 1437 raise ValueError("name={} is not a valid identifier".format(name)) 1438 if ( 1439 isinstance(lower_bounds, IntegralTypes) 1440 and isinstance(upper_bounds, IntegralTypes) 1441 and lower_bounds > upper_bounds 1442 ): 1443 raise ValueError( 1444 f"lower_bound={lower_bounds} is greater than" 1445 f" upper_bound={upper_bounds} for variable set={name}" 1446 ) 1447 1448 lower_bounds = _convert_to_integral_series_and_validate_index( 1449 lower_bounds, index 1450 ) 1451 upper_bounds = _convert_to_integral_series_and_validate_index( 1452 upper_bounds, index 1453 ) 1454 return pd.Series( 1455 index=index, 1456 data=[ 1457 # pylint: disable=g-complex-comprehension 1458 IntVar( 1459 model=self.__model, 1460 name=f"{name}[{i}]", 1461 domain=sorted_interval_list.Domain( 1462 lower_bounds[i], upper_bounds[i] 1463 ), 1464 ) 1465 for i in index 1466 ], 1467 ) 1468 1469 def new_bool_var_series( 1470 self, 1471 name: str, 1472 index: pd.Index, 1473 ) -> pd.Series: 1474 """Creates a series of (scalar-valued) variables with the given name. 1475 1476 Args: 1477 name (str): Required. The name of the variable set. 1478 index (pd.Index): Required. The index to use for the variable set. 1479 1480 Returns: 1481 pd.Series: The variable set indexed by its corresponding dimensions. 1482 1483 Raises: 1484 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1485 ValueError: if the `name` is not a valid identifier or already exists. 1486 """ 1487 return self.new_int_var_series( 1488 name=name, index=index, lower_bounds=0, upper_bounds=1 1489 ) 1490 1491 # Linear constraints. 1492 1493 def add_linear_constraint( 1494 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1495 ) -> Constraint: 1496 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1497 return self.add_linear_expression_in_domain( 1498 linear_expr, sorted_interval_list.Domain(lb, ub) 1499 ) 1500 1501 def add_linear_expression_in_domain( 1502 self, linear_expr: LinearExprT, domain: sorted_interval_list.Domain 1503 ) -> Constraint: 1504 """Adds the constraint: `linear_expr` in `domain`.""" 1505 if isinstance(linear_expr, LinearExpr): 1506 ct = Constraint(self) 1507 model_ct = self.__model.constraints[ct.index] 1508 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1509 for t in coeffs_map.items(): 1510 if not isinstance(t[0], IntVar): 1511 raise TypeError("Wrong argument" + str(t)) 1512 c = cmh.assert_is_int64(t[1]) 1513 model_ct.linear.vars.append(t[0].index) 1514 model_ct.linear.coeffs.append(c) 1515 model_ct.linear.domain.extend( 1516 [ 1517 cmh.capped_subtraction(x, constant) 1518 for x in domain.flattened_intervals() 1519 ] 1520 ) 1521 return ct 1522 if isinstance(linear_expr, IntegralTypes): 1523 if not domain.contains(int(linear_expr)): 1524 return self.add_bool_or([]) # Evaluate to false. 1525 else: 1526 return self.add_bool_and([]) # Evaluate to true. 1527 raise TypeError( 1528 "not supported: CpModel.add_linear_expression_in_domain(" 1529 + str(linear_expr) 1530 + " " 1531 + str(domain) 1532 + ")" 1533 ) 1534 1535 @overload 1536 def add(self, ct: BoundedLinearExpression) -> Constraint: ... 1537 1538 @overload 1539 def add(self, ct: Union[bool, np.bool_]) -> Constraint: ... 1540 1541 def add(self, ct): 1542 """Adds a `BoundedLinearExpression` to the model. 1543 1544 Args: 1545 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1546 1547 Returns: 1548 An instance of the `Constraint` class. 1549 """ 1550 if isinstance(ct, BoundedLinearExpression): 1551 return self.add_linear_expression_in_domain( 1552 ct.expression(), 1553 sorted_interval_list.Domain.from_flat_intervals(ct.bounds()), 1554 ) 1555 if ct and cmh.is_boolean(ct): 1556 return self.add_bool_or([True]) 1557 if not ct and cmh.is_boolean(ct): 1558 return self.add_bool_or([]) # Evaluate to false. 1559 raise TypeError("not supported: CpModel.add(" + str(ct) + ")") 1560 1561 # General Integer Constraints. 1562 1563 @overload 1564 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ... 1565 1566 @overload 1567 def add_all_different(self, *expressions: LinearExprT) -> Constraint: ... 1568 1569 def add_all_different(self, *expressions): 1570 """Adds AllDifferent(expressions). 1571 1572 This constraint forces all expressions to have different values. 1573 1574 Args: 1575 *expressions: simple expressions of the form a * var + constant. 1576 1577 Returns: 1578 An instance of the `Constraint` class. 1579 """ 1580 ct = Constraint(self) 1581 model_ct = self.__model.constraints[ct.index] 1582 expanded = expand_generator_or_tuple(expressions) 1583 model_ct.all_diff.exprs.extend( 1584 self.parse_linear_expression(x) for x in expanded 1585 ) 1586 return ct 1587 1588 def add_element( 1589 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1590 ) -> Constraint: 1591 """Adds the element constraint: `variables[index] == target`. 1592 1593 Args: 1594 index: The index of the variable that's being constrained. 1595 variables: A list of variables. 1596 target: The value that the variable must be equal to. 1597 1598 Returns: 1599 An instance of the `Constraint` class. 1600 """ 1601 1602 if not variables: 1603 raise ValueError("add_element expects a non-empty variables array") 1604 1605 if isinstance(index, IntegralTypes): 1606 variable: VariableT = list(variables)[int(index)] 1607 return self.add(variable == target) 1608 1609 ct = Constraint(self) 1610 model_ct = self.__model.constraints[ct.index] 1611 model_ct.element.index = self.get_or_make_index(index) 1612 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1613 model_ct.element.target = self.get_or_make_index(target) 1614 return ct 1615 1616 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1617 """Adds Circuit(arcs). 1618 1619 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1620 1621 A circuit is a unique Hamiltonian path in a subgraph of the total 1622 graph. In case a node 'i' is not in the path, then there must be a 1623 loop arc 'i -> i' associated with a true literal. Otherwise 1624 this constraint will fail. 1625 1626 Args: 1627 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1628 literal). The arc is selected in the circuit if the literal is true. 1629 Both source_node and destination_node must be integers between 0 and the 1630 number of nodes - 1. 1631 1632 Returns: 1633 An instance of the `Constraint` class. 1634 1635 Raises: 1636 ValueError: If the list of arcs is empty. 1637 """ 1638 if not arcs: 1639 raise ValueError("add_circuit expects a non-empty array of arcs") 1640 ct = Constraint(self) 1641 model_ct = self.__model.constraints[ct.index] 1642 for arc in arcs: 1643 tail = cmh.assert_is_int32(arc[0]) 1644 head = cmh.assert_is_int32(arc[1]) 1645 lit = self.get_or_make_boolean_index(arc[2]) 1646 model_ct.circuit.tails.append(tail) 1647 model_ct.circuit.heads.append(head) 1648 model_ct.circuit.literals.append(lit) 1649 return ct 1650 1651 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1652 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1653 1654 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1655 literals[i] is true must satisfy this set of properties: 1656 - #incoming arcs == 1 except for node 0. 1657 - #outgoing arcs == 1 except for node 0. 1658 - for node zero, #incoming arcs == #outgoing arcs. 1659 - There are no duplicate arcs. 1660 - Self-arcs are allowed except for node 0. 1661 - There is no cycle in this graph, except through node 0. 1662 1663 Args: 1664 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1665 literal). The arc is selected in the circuit if the literal is true. 1666 Both source_node and destination_node must be integers between 0 and the 1667 number of nodes - 1. 1668 1669 Returns: 1670 An instance of the `Constraint` class. 1671 1672 Raises: 1673 ValueError: If the list of arcs is empty. 1674 """ 1675 if not arcs: 1676 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1677 ct = Constraint(self) 1678 model_ct = self.__model.constraints[ct.index] 1679 for arc in arcs: 1680 tail = cmh.assert_is_int32(arc[0]) 1681 head = cmh.assert_is_int32(arc[1]) 1682 lit = self.get_or_make_boolean_index(arc[2]) 1683 model_ct.routes.tails.append(tail) 1684 model_ct.routes.heads.append(head) 1685 model_ct.routes.literals.append(lit) 1686 return ct 1687 1688 def add_allowed_assignments( 1689 self, 1690 variables: Sequence[VariableT], 1691 tuples_list: Iterable[Sequence[IntegralT]], 1692 ) -> Constraint: 1693 """Adds AllowedAssignments(variables, tuples_list). 1694 1695 An AllowedAssignments constraint is a constraint on an array of variables, 1696 which requires that when all variables are assigned values, the resulting 1697 array equals one of the tuples in `tuple_list`. 1698 1699 Args: 1700 variables: A list of variables. 1701 tuples_list: A list of admissible tuples. Each tuple must have the same 1702 length as the variables, and the ith value of a tuple corresponds to the 1703 ith variable. 1704 1705 Returns: 1706 An instance of the `Constraint` class. 1707 1708 Raises: 1709 TypeError: If a tuple does not have the same size as the list of 1710 variables. 1711 ValueError: If the array of variables is empty. 1712 """ 1713 1714 if not variables: 1715 raise ValueError( 1716 "add_allowed_assignments expects a non-empty variables array" 1717 ) 1718 1719 ct: Constraint = Constraint(self) 1720 model_ct = self.__model.constraints[ct.index] 1721 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1722 arity: int = len(variables) 1723 for t in tuples_list: 1724 if len(t) != arity: 1725 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1726 1727 # duck-typing (no explicit type checks here) 1728 try: 1729 model_ct.table.values.extend(a for b in tuples_list for a in b) 1730 except ValueError as ex: 1731 raise TypeError(f"add_xxx_assignment: Not an integer or does not fit in an int64_t: {ex.args}") from ex 1732 1733 return ct 1734 1735 def add_forbidden_assignments( 1736 self, 1737 variables: Sequence[VariableT], 1738 tuples_list: Iterable[Sequence[IntegralT]], 1739 ) -> Constraint: 1740 """Adds add_forbidden_assignments(variables, [tuples_list]). 1741 1742 A ForbiddenAssignments constraint is a constraint on an array of variables 1743 where the list of impossible combinations is provided in the tuples list. 1744 1745 Args: 1746 variables: A list of variables. 1747 tuples_list: A list of forbidden tuples. Each tuple must have the same 1748 length as the variables, and the *i*th value of a tuple corresponds to 1749 the *i*th variable. 1750 1751 Returns: 1752 An instance of the `Constraint` class. 1753 1754 Raises: 1755 TypeError: If a tuple does not have the same size as the list of 1756 variables. 1757 ValueError: If the array of variables is empty. 1758 """ 1759 1760 if not variables: 1761 raise ValueError( 1762 "add_forbidden_assignments expects a non-empty variables array" 1763 ) 1764 1765 index = len(self.__model.constraints) 1766 ct: Constraint = self.add_allowed_assignments(variables, tuples_list) 1767 self.__model.constraints[index].table.negated = True 1768 return ct 1769 1770 def add_automaton( 1771 self, 1772 transition_variables: Sequence[VariableT], 1773 starting_state: IntegralT, 1774 final_states: Sequence[IntegralT], 1775 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1776 ) -> Constraint: 1777 """Adds an automaton constraint. 1778 1779 An automaton constraint takes a list of variables (of size *n*), an initial 1780 state, a set of final states, and a set of transitions. A transition is a 1781 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1782 and *transition* is the label of an arc from *head* to *tail*, 1783 corresponding to the value of one variable in the list of variables. 1784 1785 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1786 contains the possible states of the automaton. The first state contains the 1787 initial state. The last phase contains the final states. 1788 1789 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1790 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1791 an arc from the state *tail* of phase *i* and the state *head* of phase 1792 *i* + 1. This arc is labeled by the value *transition* of the variables 1793 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1794 is assigned the value *transition*. 1795 1796 A feasible solution of this constraint is an assignment of variables such 1797 that, starting from the initial state in phase 0, there is a path labeled by 1798 the values of the variables that ends in one of the final states in the 1799 final phase. 1800 1801 Args: 1802 transition_variables: A non-empty list of variables whose values 1803 correspond to the labels of the arcs traversed by the automaton. 1804 starting_state: The initial state of the automaton. 1805 final_states: A non-empty list of admissible final states. 1806 transition_triples: A list of transitions for the automaton, in the 1807 following format (current_state, variable_value, next_state). 1808 1809 Returns: 1810 An instance of the `Constraint` class. 1811 1812 Raises: 1813 ValueError: if `transition_variables`, `final_states`, or 1814 `transition_triples` are empty. 1815 """ 1816 1817 if not transition_variables: 1818 raise ValueError( 1819 "add_automaton expects a non-empty transition_variables array" 1820 ) 1821 if not final_states: 1822 raise ValueError("add_automaton expects some final states") 1823 1824 if not transition_triples: 1825 raise ValueError("add_automaton expects some transition triples") 1826 1827 ct = Constraint(self) 1828 model_ct = self.__model.constraints[ct.index] 1829 model_ct.automaton.vars.extend( 1830 [self.get_or_make_index(x) for x in transition_variables] 1831 ) 1832 starting_state = cmh.assert_is_int64(starting_state) 1833 model_ct.automaton.starting_state = starting_state 1834 for v in final_states: 1835 v = cmh.assert_is_int64(v) 1836 model_ct.automaton.final_states.append(v) 1837 for t in transition_triples: 1838 if len(t) != 3: 1839 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1840 tail = cmh.assert_is_int64(t[0]) 1841 label = cmh.assert_is_int64(t[1]) 1842 head = cmh.assert_is_int64(t[2]) 1843 model_ct.automaton.transition_tail.append(tail) 1844 model_ct.automaton.transition_label.append(label) 1845 model_ct.automaton.transition_head.append(head) 1846 return ct 1847 1848 def add_inverse( 1849 self, 1850 variables: Sequence[VariableT], 1851 inverse_variables: Sequence[VariableT], 1852 ) -> Constraint: 1853 """Adds Inverse(variables, inverse_variables). 1854 1855 An inverse constraint enforces that if `variables[i]` is assigned a value 1856 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1857 1858 Args: 1859 variables: An array of integer variables. 1860 inverse_variables: An array of integer variables. 1861 1862 Returns: 1863 An instance of the `Constraint` class. 1864 1865 Raises: 1866 TypeError: if variables and inverse_variables have different lengths, or 1867 if they are empty. 1868 """ 1869 1870 if not variables or not inverse_variables: 1871 raise TypeError("The Inverse constraint does not accept empty arrays") 1872 if len(variables) != len(inverse_variables): 1873 raise TypeError( 1874 "In the inverse constraint, the two array variables and" 1875 " inverse_variables must have the same length." 1876 ) 1877 ct = Constraint(self) 1878 model_ct = self.__model.constraints[ct.index] 1879 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1880 model_ct.inverse.f_inverse.extend( 1881 [self.get_or_make_index(x) for x in inverse_variables] 1882 ) 1883 return ct 1884 1885 def add_reservoir_constraint( 1886 self, 1887 times: Iterable[LinearExprT], 1888 level_changes: Iterable[LinearExprT], 1889 min_level: int, 1890 max_level: int, 1891 ) -> Constraint: 1892 """Adds Reservoir(times, level_changes, min_level, max_level). 1893 1894 Maintains a reservoir level within bounds. The water level starts at 0, and 1895 at any time, it must be between min_level and max_level. 1896 1897 If the affine expression `times[i]` is assigned a value t, then the current 1898 level changes by `level_changes[i]`, which is constant, at time t. 1899 1900 Note that min level must be <= 0, and the max level must be >= 0. Please 1901 use fixed level_changes to simulate initial state. 1902 1903 Therefore, at any time: 1904 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1905 1906 Args: 1907 times: A list of 1-var affine expressions (a * x + b) which specify the 1908 time of the filling or emptying the reservoir. 1909 level_changes: A list of integer values that specifies the amount of the 1910 emptying or filling. Currently, variable demands are not supported. 1911 min_level: At any time, the level of the reservoir must be greater or 1912 equal than the min level. 1913 max_level: At any time, the level of the reservoir must be less or equal 1914 than the max level. 1915 1916 Returns: 1917 An instance of the `Constraint` class. 1918 1919 Raises: 1920 ValueError: if max_level < min_level. 1921 1922 ValueError: if max_level < 0. 1923 1924 ValueError: if min_level > 0 1925 """ 1926 1927 if max_level < min_level: 1928 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1929 1930 if max_level < 0: 1931 raise ValueError("Reservoir constraint must have a max_level >= 0") 1932 1933 if min_level > 0: 1934 raise ValueError("Reservoir constraint must have a min_level <= 0") 1935 1936 ct = Constraint(self) 1937 model_ct = self.__model.constraints[ct.index] 1938 model_ct.reservoir.time_exprs.extend( 1939 [self.parse_linear_expression(x) for x in times] 1940 ) 1941 model_ct.reservoir.level_changes.extend( 1942 [self.parse_linear_expression(x) for x in level_changes] 1943 ) 1944 model_ct.reservoir.min_level = min_level 1945 model_ct.reservoir.max_level = max_level 1946 return ct 1947 1948 def add_reservoir_constraint_with_active( 1949 self, 1950 times: Iterable[LinearExprT], 1951 level_changes: Iterable[LinearExprT], 1952 actives: Iterable[LiteralT], 1953 min_level: int, 1954 max_level: int, 1955 ) -> Constraint: 1956 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1957 1958 Maintains a reservoir level within bounds. The water level starts at 0, and 1959 at any time, it must be between min_level and max_level. 1960 1961 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1962 `True`, then the current level changes by `level_changes[i]`, which is 1963 constant, 1964 at time t. 1965 1966 Note that min level must be <= 0, and the max level must be >= 0. Please 1967 use fixed level_changes to simulate initial state. 1968 1969 Therefore, at any time: 1970 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1971 max_level] 1972 1973 1974 The array of boolean variables 'actives', if defined, indicates which 1975 actions are actually performed. 1976 1977 Args: 1978 times: A list of 1-var affine expressions (a * x + b) which specify the 1979 time of the filling or emptying the reservoir. 1980 level_changes: A list of integer values that specifies the amount of the 1981 emptying or filling. Currently, variable demands are not supported. 1982 actives: a list of boolean variables. They indicates if the 1983 emptying/refilling events actually take place. 1984 min_level: At any time, the level of the reservoir must be greater or 1985 equal than the min level. 1986 max_level: At any time, the level of the reservoir must be less or equal 1987 than the max level. 1988 1989 Returns: 1990 An instance of the `Constraint` class. 1991 1992 Raises: 1993 ValueError: if max_level < min_level. 1994 1995 ValueError: if max_level < 0. 1996 1997 ValueError: if min_level > 0 1998 """ 1999 2000 if max_level < min_level: 2001 raise ValueError("Reservoir constraint must have a max_level >= min_level") 2002 2003 if max_level < 0: 2004 raise ValueError("Reservoir constraint must have a max_level >= 0") 2005 2006 if min_level > 0: 2007 raise ValueError("Reservoir constraint must have a min_level <= 0") 2008 2009 ct = Constraint(self) 2010 model_ct = self.__model.constraints[ct.index] 2011 model_ct.reservoir.time_exprs.extend( 2012 [self.parse_linear_expression(x) for x in times] 2013 ) 2014 model_ct.reservoir.level_changes.extend( 2015 [self.parse_linear_expression(x) for x in level_changes] 2016 ) 2017 model_ct.reservoir.active_literals.extend( 2018 [self.get_or_make_boolean_index(x) for x in actives] 2019 ) 2020 model_ct.reservoir.min_level = min_level 2021 model_ct.reservoir.max_level = max_level 2022 return ct 2023 2024 def add_map_domain( 2025 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 2026 ): 2027 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 2028 2029 for i, bool_var in enumerate(bool_var_array): 2030 b_index = bool_var.index 2031 var_index = var.index 2032 model_ct = self.__model.constraints.add() 2033 model_ct.linear.vars.append(var_index) 2034 model_ct.linear.coeffs.append(1) 2035 offset_as_int = int(offset) 2036 model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) 2037 model_ct.enforcement_literal.append(b_index) 2038 2039 model_ct = self.__model.constraints.add() 2040 model_ct.linear.vars.append(var_index) 2041 model_ct.linear.coeffs.append(1) 2042 model_ct.enforcement_literal.append(-b_index - 1) 2043 if offset + i - 1 >= INT_MIN: 2044 model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) 2045 if offset + i + 1 <= INT_MAX: 2046 model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX]) 2047 2048 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 2049 """Adds `a => b` (`a` implies `b`).""" 2050 ct = Constraint(self) 2051 model_ct = self.__model.constraints[ct.index] 2052 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2053 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2054 return ct 2055 2056 @overload 2057 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ... 2058 2059 @overload 2060 def add_bool_or(self, *literals: LiteralT) -> Constraint: ... 2061 2062 def add_bool_or(self, *literals): 2063 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2064 ct = Constraint(self) 2065 model_ct = self.__model.constraints[ct.index] 2066 model_ct.bool_or.literals.extend( 2067 [ 2068 self.get_or_make_boolean_index(x) 2069 for x in expand_generator_or_tuple(literals) 2070 ] 2071 ) 2072 return ct 2073 2074 @overload 2075 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 2076 2077 @overload 2078 def add_at_least_one(self, *literals: LiteralT) -> Constraint: ... 2079 2080 def add_at_least_one(self, *literals): 2081 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2082 return self.add_bool_or(*literals) 2083 2084 @overload 2085 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 2086 2087 @overload 2088 def add_at_most_one(self, *literals: LiteralT) -> Constraint: ... 2089 2090 def add_at_most_one(self, *literals): 2091 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2092 ct = Constraint(self) 2093 model_ct = self.__model.constraints[ct.index] 2094 model_ct.at_most_one.literals.extend( 2095 [ 2096 self.get_or_make_boolean_index(x) 2097 for x in expand_generator_or_tuple(literals) 2098 ] 2099 ) 2100 return ct 2101 2102 @overload 2103 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 2104 2105 @overload 2106 def add_exactly_one(self, *literals: LiteralT) -> Constraint: ... 2107 2108 def add_exactly_one(self, *literals): 2109 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2110 ct = Constraint(self) 2111 model_ct = self.__model.constraints[ct.index] 2112 model_ct.exactly_one.literals.extend( 2113 [ 2114 self.get_or_make_boolean_index(x) 2115 for x in expand_generator_or_tuple(literals) 2116 ] 2117 ) 2118 return ct 2119 2120 @overload 2121 def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ... 2122 2123 @overload 2124 def add_bool_and(self, *literals: LiteralT) -> Constraint: ... 2125 2126 def add_bool_and(self, *literals): 2127 """Adds `And(literals) == true`.""" 2128 ct = Constraint(self) 2129 model_ct = self.__model.constraints[ct.index] 2130 model_ct.bool_and.literals.extend( 2131 [ 2132 self.get_or_make_boolean_index(x) 2133 for x in expand_generator_or_tuple(literals) 2134 ] 2135 ) 2136 return ct 2137 2138 @overload 2139 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ... 2140 2141 @overload 2142 def add_bool_xor(self, *literals: LiteralT) -> Constraint: ... 2143 2144 def add_bool_xor(self, *literals): 2145 """Adds `XOr(literals) == true`. 2146 2147 In contrast to add_bool_or and add_bool_and, it does not support 2148 .only_enforce_if(). 2149 2150 Args: 2151 *literals: the list of literals in the constraint. 2152 2153 Returns: 2154 An `Constraint` object. 2155 """ 2156 ct = Constraint(self) 2157 model_ct = self.__model.constraints[ct.index] 2158 model_ct.bool_xor.literals.extend( 2159 [ 2160 self.get_or_make_boolean_index(x) 2161 for x in expand_generator_or_tuple(literals) 2162 ] 2163 ) 2164 return ct 2165 2166 def add_min_equality( 2167 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2168 ) -> Constraint: 2169 """Adds `target == Min(exprs)`.""" 2170 ct = Constraint(self) 2171 model_ct = self.__model.constraints[ct.index] 2172 model_ct.lin_max.exprs.extend( 2173 [self.parse_linear_expression(x, True) for x in exprs] 2174 ) 2175 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2176 return ct 2177 2178 def add_max_equality( 2179 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2180 ) -> Constraint: 2181 """Adds `target == Max(exprs)`.""" 2182 ct = Constraint(self) 2183 model_ct = self.__model.constraints[ct.index] 2184 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2185 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2186 return ct 2187 2188 def add_division_equality( 2189 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2190 ) -> Constraint: 2191 """Adds `target == num // denom` (integer division rounded towards 0).""" 2192 ct = Constraint(self) 2193 model_ct = self.__model.constraints[ct.index] 2194 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2195 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2196 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2197 return ct 2198 2199 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2200 """Adds `target == Abs(expr)`.""" 2201 ct = Constraint(self) 2202 model_ct = self.__model.constraints[ct.index] 2203 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2204 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2205 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2206 return ct 2207 2208 def add_modulo_equality( 2209 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2210 ) -> Constraint: 2211 """Adds `target = expr % mod`. 2212 2213 It uses the C convention, that is the result is the remainder of the 2214 integral division rounded towards 0. 2215 2216 For example: 2217 * 10 % 3 = 1 2218 * -10 % 3 = -1 2219 * 10 % -3 = 1 2220 * -10 % -3 = -1 2221 2222 Args: 2223 target: the target expression. 2224 expr: the expression to compute the modulo of. 2225 mod: the modulus expression. 2226 2227 Returns: 2228 A `Constraint` object. 2229 """ 2230 ct = Constraint(self) 2231 model_ct = self.__model.constraints[ct.index] 2232 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2233 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2234 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2235 return ct 2236 2237 def add_multiplication_equality( 2238 self, 2239 target: LinearExprT, 2240 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2241 ) -> Constraint: 2242 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2243 ct = Constraint(self) 2244 model_ct = self.__model.constraints[ct.index] 2245 model_ct.int_prod.exprs.extend( 2246 [ 2247 self.parse_linear_expression(expr) 2248 for expr in expand_generator_or_tuple(expressions) 2249 ] 2250 ) 2251 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2252 return ct 2253 2254 # Scheduling support 2255 2256 def new_interval_var( 2257 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2258 ) -> IntervalVar: 2259 """Creates an interval variable from start, size, and end. 2260 2261 An interval variable is a constraint, that is itself used in other 2262 constraints like NoOverlap. 2263 2264 Internally, it ensures that `start + size == end`. 2265 2266 Args: 2267 start: The start of the interval. It must be of the form a * var + b. 2268 size: The size of the interval. It must be of the form a * var + b. 2269 end: The end of the interval. It must be of the form a * var + b. 2270 name: The name of the interval variable. 2271 2272 Returns: 2273 An `IntervalVar` object. 2274 """ 2275 2276 start_expr = self.parse_linear_expression(start) 2277 size_expr = self.parse_linear_expression(size) 2278 end_expr = self.parse_linear_expression(end) 2279 if len(start_expr.vars) > 1: 2280 raise TypeError( 2281 "cp_model.new_interval_var: start must be 1-var affine or constant." 2282 ) 2283 if len(size_expr.vars) > 1: 2284 raise TypeError( 2285 "cp_model.new_interval_var: size must be 1-var affine or constant." 2286 ) 2287 if len(end_expr.vars) > 1: 2288 raise TypeError( 2289 "cp_model.new_interval_var: end must be 1-var affine or constant." 2290 ) 2291 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2292 2293 def new_interval_var_series( 2294 self, 2295 name: str, 2296 index: pd.Index, 2297 starts: Union[LinearExprT, pd.Series], 2298 sizes: Union[LinearExprT, pd.Series], 2299 ends: Union[LinearExprT, pd.Series], 2300 ) -> pd.Series: 2301 """Creates a series of interval variables with the given name. 2302 2303 Args: 2304 name (str): Required. The name of the variable set. 2305 index (pd.Index): Required. The index to use for the variable set. 2306 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2307 set. If a `pd.Series` is passed in, it will be based on the 2308 corresponding values of the pd.Series. 2309 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2310 set. If a `pd.Series` is passed in, it will be based on the 2311 corresponding values of the pd.Series. 2312 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2313 set. If a `pd.Series` is passed in, it will be based on the 2314 corresponding values of the pd.Series. 2315 2316 Returns: 2317 pd.Series: The interval variable set indexed by its corresponding 2318 dimensions. 2319 2320 Raises: 2321 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2322 ValueError: if the `name` is not a valid identifier or already exists. 2323 ValueError: if the all the indexes do not match. 2324 """ 2325 if not isinstance(index, pd.Index): 2326 raise TypeError("Non-index object is used as index") 2327 if not name.isidentifier(): 2328 raise ValueError("name={} is not a valid identifier".format(name)) 2329 2330 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2331 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2332 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2333 interval_array = [] 2334 for i in index: 2335 interval_array.append( 2336 self.new_interval_var( 2337 start=starts[i], 2338 size=sizes[i], 2339 end=ends[i], 2340 name=f"{name}[{i}]", 2341 ) 2342 ) 2343 return pd.Series(index=index, data=interval_array) 2344 2345 def new_fixed_size_interval_var( 2346 self, start: LinearExprT, size: IntegralT, name: str 2347 ) -> IntervalVar: 2348 """Creates an interval variable from start, and a fixed size. 2349 2350 An interval variable is a constraint, that is itself used in other 2351 constraints like NoOverlap. 2352 2353 Args: 2354 start: The start of the interval. It must be of the form a * var + b. 2355 size: The size of the interval. It must be an integer value. 2356 name: The name of the interval variable. 2357 2358 Returns: 2359 An `IntervalVar` object. 2360 """ 2361 size = cmh.assert_is_int64(size) 2362 start_expr = self.parse_linear_expression(start) 2363 size_expr = self.parse_linear_expression(size) 2364 end_expr = self.parse_linear_expression(start + size) 2365 if len(start_expr.vars) > 1: 2366 raise TypeError( 2367 "cp_model.new_interval_var: start must be affine or constant." 2368 ) 2369 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2370 2371 def new_fixed_size_interval_var_series( 2372 self, 2373 name: str, 2374 index: pd.Index, 2375 starts: Union[LinearExprT, pd.Series], 2376 sizes: Union[IntegralT, pd.Series], 2377 ) -> pd.Series: 2378 """Creates a series of interval variables with the given name. 2379 2380 Args: 2381 name (str): Required. The name of the variable set. 2382 index (pd.Index): Required. The index to use for the variable set. 2383 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2384 set. If a `pd.Series` is passed in, it will be based on the 2385 corresponding values of the pd.Series. 2386 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2387 the set. If a `pd.Series` is passed in, it will be based on the 2388 corresponding values of the pd.Series. 2389 2390 Returns: 2391 pd.Series: The interval variable set indexed by its corresponding 2392 dimensions. 2393 2394 Raises: 2395 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2396 ValueError: if the `name` is not a valid identifier or already exists. 2397 ValueError: if the all the indexes do not match. 2398 """ 2399 if not isinstance(index, pd.Index): 2400 raise TypeError("Non-index object is used as index") 2401 if not name.isidentifier(): 2402 raise ValueError("name={} is not a valid identifier".format(name)) 2403 2404 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2405 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2406 interval_array = [] 2407 for i in index: 2408 interval_array.append( 2409 self.new_fixed_size_interval_var( 2410 start=starts[i], 2411 size=sizes[i], 2412 name=f"{name}[{i}]", 2413 ) 2414 ) 2415 return pd.Series(index=index, data=interval_array) 2416 2417 def new_optional_interval_var( 2418 self, 2419 start: LinearExprT, 2420 size: LinearExprT, 2421 end: LinearExprT, 2422 is_present: LiteralT, 2423 name: str, 2424 ) -> IntervalVar: 2425 """Creates an optional interval var from start, size, end, and is_present. 2426 2427 An optional interval variable is a constraint, that is itself used in other 2428 constraints like NoOverlap. This constraint is protected by a presence 2429 literal that indicates if it is active or not. 2430 2431 Internally, it ensures that `is_present` implies `start + size == 2432 end`. 2433 2434 Args: 2435 start: The start of the interval. It must be of the form a * var + b. 2436 size: The size of the interval. It must be of the form a * var + b. 2437 end: The end of the interval. It must be of the form a * var + b. 2438 is_present: A literal that indicates if the interval is active or not. A 2439 inactive interval is simply ignored by all constraints. 2440 name: The name of the interval variable. 2441 2442 Returns: 2443 An `IntervalVar` object. 2444 """ 2445 2446 # Creates the IntervalConstraintProto object. 2447 is_present_index = self.get_or_make_boolean_index(is_present) 2448 start_expr = self.parse_linear_expression(start) 2449 size_expr = self.parse_linear_expression(size) 2450 end_expr = self.parse_linear_expression(end) 2451 if len(start_expr.vars) > 1: 2452 raise TypeError( 2453 "cp_model.new_interval_var: start must be affine or constant." 2454 ) 2455 if len(size_expr.vars) > 1: 2456 raise TypeError( 2457 "cp_model.new_interval_var: size must be affine or constant." 2458 ) 2459 if len(end_expr.vars) > 1: 2460 raise TypeError( 2461 "cp_model.new_interval_var: end must be affine or constant." 2462 ) 2463 return IntervalVar( 2464 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2465 ) 2466 2467 def new_optional_interval_var_series( 2468 self, 2469 name: str, 2470 index: pd.Index, 2471 starts: Union[LinearExprT, pd.Series], 2472 sizes: Union[LinearExprT, pd.Series], 2473 ends: Union[LinearExprT, pd.Series], 2474 are_present: Union[LiteralT, pd.Series], 2475 ) -> pd.Series: 2476 """Creates a series of interval variables with the given name. 2477 2478 Args: 2479 name (str): Required. The name of the variable set. 2480 index (pd.Index): Required. The index to use for the variable set. 2481 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2482 set. If a `pd.Series` is passed in, it will be based on the 2483 corresponding values of the pd.Series. 2484 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2485 set. If a `pd.Series` is passed in, it will be based on the 2486 corresponding values of the pd.Series. 2487 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2488 set. If a `pd.Series` is passed in, it will be based on the 2489 corresponding values of the pd.Series. 2490 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2491 interval in the set. If a `pd.Series` is passed in, it will be based on 2492 the corresponding values of the pd.Series. 2493 2494 Returns: 2495 pd.Series: The interval variable set indexed by its corresponding 2496 dimensions. 2497 2498 Raises: 2499 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2500 ValueError: if the `name` is not a valid identifier or already exists. 2501 ValueError: if the all the indexes do not match. 2502 """ 2503 if not isinstance(index, pd.Index): 2504 raise TypeError("Non-index object is used as index") 2505 if not name.isidentifier(): 2506 raise ValueError("name={} is not a valid identifier".format(name)) 2507 2508 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2509 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2510 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2511 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2512 2513 interval_array = [] 2514 for i in index: 2515 interval_array.append( 2516 self.new_optional_interval_var( 2517 start=starts[i], 2518 size=sizes[i], 2519 end=ends[i], 2520 is_present=are_present[i], 2521 name=f"{name}[{i}]", 2522 ) 2523 ) 2524 return pd.Series(index=index, data=interval_array) 2525 2526 def new_optional_fixed_size_interval_var( 2527 self, 2528 start: LinearExprT, 2529 size: IntegralT, 2530 is_present: LiteralT, 2531 name: str, 2532 ) -> IntervalVar: 2533 """Creates an interval variable from start, and a fixed size. 2534 2535 An interval variable is a constraint, that is itself used in other 2536 constraints like NoOverlap. 2537 2538 Args: 2539 start: The start of the interval. It must be of the form a * var + b. 2540 size: The size of the interval. It must be an integer value. 2541 is_present: A literal that indicates if the interval is active or not. A 2542 inactive interval is simply ignored by all constraints. 2543 name: The name of the interval variable. 2544 2545 Returns: 2546 An `IntervalVar` object. 2547 """ 2548 size = cmh.assert_is_int64(size) 2549 start_expr = self.parse_linear_expression(start) 2550 size_expr = self.parse_linear_expression(size) 2551 end_expr = self.parse_linear_expression(start + size) 2552 if len(start_expr.vars) > 1: 2553 raise TypeError( 2554 "cp_model.new_interval_var: start must be affine or constant." 2555 ) 2556 is_present_index = self.get_or_make_boolean_index(is_present) 2557 return IntervalVar( 2558 self.__model, 2559 start_expr, 2560 size_expr, 2561 end_expr, 2562 is_present_index, 2563 name, 2564 ) 2565 2566 def new_optional_fixed_size_interval_var_series( 2567 self, 2568 name: str, 2569 index: pd.Index, 2570 starts: Union[LinearExprT, pd.Series], 2571 sizes: Union[IntegralT, pd.Series], 2572 are_present: Union[LiteralT, pd.Series], 2573 ) -> pd.Series: 2574 """Creates a series of interval variables with the given name. 2575 2576 Args: 2577 name (str): Required. The name of the variable set. 2578 index (pd.Index): Required. The index to use for the variable set. 2579 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2580 set. If a `pd.Series` is passed in, it will be based on the 2581 corresponding values of the pd.Series. 2582 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2583 the set. If a `pd.Series` is passed in, it will be based on the 2584 corresponding values of the pd.Series. 2585 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2586 interval in the set. If a `pd.Series` is passed in, it will be based on 2587 the corresponding values of the pd.Series. 2588 2589 Returns: 2590 pd.Series: The interval variable set indexed by its corresponding 2591 dimensions. 2592 2593 Raises: 2594 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2595 ValueError: if the `name` is not a valid identifier or already exists. 2596 ValueError: if the all the indexes do not match. 2597 """ 2598 if not isinstance(index, pd.Index): 2599 raise TypeError("Non-index object is used as index") 2600 if not name.isidentifier(): 2601 raise ValueError("name={} is not a valid identifier".format(name)) 2602 2603 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2604 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2605 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2606 interval_array = [] 2607 for i in index: 2608 interval_array.append( 2609 self.new_optional_fixed_size_interval_var( 2610 start=starts[i], 2611 size=sizes[i], 2612 is_present=are_present[i], 2613 name=f"{name}[{i}]", 2614 ) 2615 ) 2616 return pd.Series(index=index, data=interval_array) 2617 2618 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2619 """Adds NoOverlap(interval_vars). 2620 2621 A NoOverlap constraint ensures that all present intervals do not overlap 2622 in time. 2623 2624 Args: 2625 interval_vars: The list of interval variables to constrain. 2626 2627 Returns: 2628 An instance of the `Constraint` class. 2629 """ 2630 ct = Constraint(self) 2631 model_ct = self.__model.constraints[ct.index] 2632 model_ct.no_overlap.intervals.extend( 2633 [self.get_interval_index(x) for x in interval_vars] 2634 ) 2635 return ct 2636 2637 def add_no_overlap_2d( 2638 self, 2639 x_intervals: Iterable[IntervalVar], 2640 y_intervals: Iterable[IntervalVar], 2641 ) -> Constraint: 2642 """Adds NoOverlap2D(x_intervals, y_intervals). 2643 2644 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2645 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2646 by two intervals which represent its projection onto the X and Y axis. 2647 2648 Furthermore, one box is optional if at least one of the x or y interval is 2649 optional. 2650 2651 Args: 2652 x_intervals: The X coordinates of the rectangles. 2653 y_intervals: The Y coordinates of the rectangles. 2654 2655 Returns: 2656 An instance of the `Constraint` class. 2657 """ 2658 ct = Constraint(self) 2659 model_ct = self.__model.constraints[ct.index] 2660 model_ct.no_overlap_2d.x_intervals.extend( 2661 [self.get_interval_index(x) for x in x_intervals] 2662 ) 2663 model_ct.no_overlap_2d.y_intervals.extend( 2664 [self.get_interval_index(x) for x in y_intervals] 2665 ) 2666 return ct 2667 2668 def add_cumulative( 2669 self, 2670 intervals: Iterable[IntervalVar], 2671 demands: Iterable[LinearExprT], 2672 capacity: LinearExprT, 2673 ) -> Constraint: 2674 """Adds Cumulative(intervals, demands, capacity). 2675 2676 This constraint enforces that: 2677 2678 for all t: 2679 sum(demands[i] 2680 if (start(intervals[i]) <= t < end(intervals[i])) and 2681 (intervals[i] is present)) <= capacity 2682 2683 Args: 2684 intervals: The list of intervals. 2685 demands: The list of demands for each interval. Each demand must be >= 0. 2686 Each demand can be a 1-var affine expression (a * x + b). 2687 capacity: The maximum capacity of the cumulative constraint. It can be a 2688 1-var affine expression (a * x + b). 2689 2690 Returns: 2691 An instance of the `Constraint` class. 2692 """ 2693 cumulative = Constraint(self) 2694 model_ct = self.__model.constraints[cumulative.index] 2695 model_ct.cumulative.intervals.extend( 2696 [self.get_interval_index(x) for x in intervals] 2697 ) 2698 for d in demands: 2699 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2700 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2701 return cumulative 2702 2703 # Support for model cloning. 2704 def clone(self) -> "CpModel": 2705 """Reset the model, and creates a new one from a CpModelProto instance.""" 2706 clone = CpModel() 2707 clone.proto.CopyFrom(self.proto) 2708 clone.rebuild_constant_map() 2709 return clone 2710 2711 def rebuild_constant_map(self): 2712 """Internal method used during model cloning.""" 2713 for i, var in enumerate(self.__model.variables): 2714 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2715 self.__constant_map[var.domain[0]] = i 2716 2717 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2718 """Returns an already created Boolean variable from its index.""" 2719 if index < 0 or index >= len(self.__model.variables): 2720 raise ValueError( 2721 f"get_bool_var_from_proto_index: out of bound index {index}" 2722 ) 2723 var = self.__model.variables[index] 2724 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2725 raise ValueError( 2726 f"get_bool_var_from_proto_index: index {index} does not reference" 2727 + " a Boolean variable" 2728 ) 2729 2730 return IntVar(self.__model, index, None) 2731 2732 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2733 """Returns an already created integer variable from its index.""" 2734 if index < 0 or index >= len(self.__model.variables): 2735 raise ValueError( 2736 f"get_int_var_from_proto_index: out of bound index {index}" 2737 ) 2738 return IntVar(self.__model, index, None) 2739 2740 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2741 """Returns an already created interval variable from its index.""" 2742 if index < 0 or index >= len(self.__model.constraints): 2743 raise ValueError( 2744 f"get_interval_var_from_proto_index: out of bound index {index}" 2745 ) 2746 ct = self.__model.constraints[index] 2747 if not ct.HasField("interval"): 2748 raise ValueError( 2749 f"get_interval_var_from_proto_index: index {index} does not" 2750 " reference an" + " interval variable" 2751 ) 2752 2753 return IntervalVar(self.__model, index, None, None, None, None) 2754 2755 # Helpers. 2756 2757 def __str__(self) -> str: 2758 return str(self.__model) 2759 2760 @property 2761 def proto(self) -> cp_model_pb2.CpModelProto: 2762 """Returns the underlying CpModelProto.""" 2763 return self.__model 2764 2765 def negated(self, index: int) -> int: 2766 return -index - 1 2767 2768 def get_or_make_index(self, arg: VariableT) -> int: 2769 """Returns the index of a variable, its negation, or a number.""" 2770 if isinstance(arg, IntVar): 2771 return arg.index 2772 if ( 2773 isinstance(arg, _ProductCst) 2774 and isinstance(arg.expression(), IntVar) 2775 and arg.coefficient() == -1 2776 ): 2777 return -arg.expression().index - 1 2778 if isinstance(arg, IntegralTypes): 2779 arg = cmh.assert_is_int64(arg) 2780 return self.get_or_make_index_from_constant(arg) 2781 raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")") 2782 2783 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2784 """Returns an index from a boolean expression.""" 2785 if isinstance(arg, IntVar): 2786 self.assert_is_boolean_variable(arg) 2787 return arg.index 2788 if isinstance(arg, _NotBooleanVariable): 2789 self.assert_is_boolean_variable(arg.negated()) 2790 return arg.index 2791 if isinstance(arg, IntegralTypes): 2792 if arg == ~False: # -1 2793 return self.get_or_make_index_from_constant(1) 2794 if arg == ~True: # -2 2795 return self.get_or_make_index_from_constant(0) 2796 arg = cmh.assert_is_zero_or_one(arg) 2797 return self.get_or_make_index_from_constant(arg) 2798 if cmh.is_boolean(arg): 2799 return self.get_or_make_index_from_constant(int(arg)) 2800 raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})") 2801 2802 def get_interval_index(self, arg: IntervalVar) -> int: 2803 if not isinstance(arg, IntervalVar): 2804 raise TypeError("NotSupported: model.get_interval_index(%s)" % arg) 2805 return arg.index 2806 2807 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2808 if value in self.__constant_map: 2809 return self.__constant_map[value] 2810 index = len(self.__model.variables) 2811 self.__model.variables.add(domain=[value, value]) 2812 self.__constant_map[value] = index 2813 return index 2814 2815 def var_index_to_var_proto( 2816 self, var_index: int 2817 ) -> cp_model_pb2.IntegerVariableProto: 2818 if var_index >= 0: 2819 return self.__model.variables[var_index] 2820 else: 2821 return self.__model.variables[-var_index - 1] 2822 2823 def parse_linear_expression( 2824 self, linear_expr: LinearExprT, negate: bool = False 2825 ) -> cp_model_pb2.LinearExpressionProto: 2826 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2827 result: cp_model_pb2.LinearExpressionProto = ( 2828 cp_model_pb2.LinearExpressionProto() 2829 ) 2830 mult = -1 if negate else 1 2831 if isinstance(linear_expr, IntegralTypes): 2832 result.offset = int(linear_expr) * mult 2833 return result 2834 2835 if isinstance(linear_expr, IntVar): 2836 result.vars.append(self.get_or_make_index(linear_expr)) 2837 result.coeffs.append(mult) 2838 return result 2839 2840 coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map() 2841 result.offset = constant * mult 2842 for t in coeffs_map.items(): 2843 if not isinstance(t[0], IntVar): 2844 raise TypeError("Wrong argument" + str(t)) 2845 c = cmh.assert_is_int64(t[1]) 2846 result.vars.append(t[0].index) 2847 result.coeffs.append(c * mult) 2848 return result 2849 2850 def _set_objective(self, obj: ObjLinearExprT, minimize: bool): 2851 """Sets the objective of the model.""" 2852 self.clear_objective() 2853 if isinstance(obj, IntVar): 2854 self.__model.objective.vars.append(obj.index) 2855 self.__model.objective.offset = 0 2856 if minimize: 2857 self.__model.objective.coeffs.append(1) 2858 self.__model.objective.scaling_factor = 1 2859 else: 2860 self.__model.objective.coeffs.append(-1) 2861 self.__model.objective.scaling_factor = -1 2862 elif isinstance(obj, LinearExpr): 2863 coeffs_map, constant, is_integer = obj.get_float_var_value_map() 2864 if is_integer: 2865 if minimize: 2866 self.__model.objective.scaling_factor = 1 2867 self.__model.objective.offset = constant 2868 else: 2869 self.__model.objective.scaling_factor = -1 2870 self.__model.objective.offset = -constant 2871 for v, c in coeffs_map.items(): 2872 c_as_int = int(c) 2873 self.__model.objective.vars.append(v.index) 2874 if minimize: 2875 self.__model.objective.coeffs.append(c_as_int) 2876 else: 2877 self.__model.objective.coeffs.append(-c_as_int) 2878 else: 2879 self.__model.floating_point_objective.maximize = not minimize 2880 self.__model.floating_point_objective.offset = constant 2881 for v, c in coeffs_map.items(): 2882 self.__model.floating_point_objective.coeffs.append(c) 2883 self.__model.floating_point_objective.vars.append(v.index) 2884 elif isinstance(obj, IntegralTypes): 2885 self.__model.objective.offset = int(obj) 2886 self.__model.objective.scaling_factor = 1 2887 else: 2888 raise TypeError("TypeError: " + str(obj) + " is not a valid objective") 2889 2890 def minimize(self, obj: ObjLinearExprT): 2891 """Sets the objective of the model to minimize(obj).""" 2892 self._set_objective(obj, minimize=True) 2893 2894 def maximize(self, obj: ObjLinearExprT): 2895 """Sets the objective of the model to maximize(obj).""" 2896 self._set_objective(obj, minimize=False) 2897 2898 def has_objective(self) -> bool: 2899 return self.__model.HasField("objective") or self.__model.HasField( 2900 "floating_point_objective" 2901 ) 2902 2903 def clear_objective(self): 2904 self.__model.ClearField("objective") 2905 self.__model.ClearField("floating_point_objective") 2906 2907 def add_decision_strategy( 2908 self, 2909 variables: Sequence[IntVar], 2910 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2911 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2912 ) -> None: 2913 """Adds a search strategy to the model. 2914 2915 Args: 2916 variables: a list of variables this strategy will assign. 2917 var_strategy: heuristic to choose the next variable to assign. 2918 domain_strategy: heuristic to reduce the domain of the selected variable. 2919 Currently, this is advanced code: the union of all strategies added to 2920 the model must be complete, i.e. instantiates all variables. Otherwise, 2921 solve() will fail. 2922 """ 2923 2924 strategy: cp_model_pb2.DecisionStrategyProto = ( 2925 self.__model.search_strategy.add() 2926 ) 2927 for v in variables: 2928 expr = strategy.exprs.add() 2929 if v.index >= 0: 2930 expr.vars.append(v.index) 2931 expr.coeffs.append(1) 2932 else: 2933 expr.vars.append(self.negated(v.index)) 2934 expr.coeffs.append(-1) 2935 expr.offset = 1 2936 2937 strategy.variable_selection_strategy = var_strategy 2938 strategy.domain_reduction_strategy = domain_strategy 2939 2940 def model_stats(self) -> str: 2941 """Returns a string containing some model statistics.""" 2942 return swig_helper.CpSatHelper.model_stats(self.__model) 2943 2944 def validate(self) -> str: 2945 """Returns a string indicating that the model is invalid.""" 2946 return swig_helper.CpSatHelper.validate_model(self.__model) 2947 2948 def export_to_file(self, file: str) -> bool: 2949 """Write the model as a protocol buffer to 'file'. 2950 2951 Args: 2952 file: file to write the model to. If the filename ends with 'txt', the 2953 model will be written as a text file, otherwise, the binary format will 2954 be used. 2955 2956 Returns: 2957 True if the model was correctly written. 2958 """ 2959 return swig_helper.CpSatHelper.write_model_to_file(self.__model, file) 2960 2961 def add_hint(self, var: IntVar, value: int) -> None: 2962 """Adds 'var == value' as a hint to the solver.""" 2963 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2964 self.__model.solution_hint.values.append(value) 2965 2966 def clear_hints(self): 2967 """Removes any solution hint from the model.""" 2968 self.__model.ClearField("solution_hint") 2969 2970 def add_assumption(self, lit: LiteralT) -> None: 2971 """Adds the literal to the model as assumptions.""" 2972 self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) 2973 2974 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2975 """Adds the literals to the model as assumptions.""" 2976 for lit in literals: 2977 self.add_assumption(lit) 2978 2979 def clear_assumptions(self) -> None: 2980 """Removes all assumptions from the model.""" 2981 self.__model.ClearField("assumptions") 2982 2983 # Helpers. 2984 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2985 if isinstance(x, IntVar): 2986 var = self.__model.variables[x.index] 2987 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2988 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2989 elif not isinstance(x, _NotBooleanVariable): 2990 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2991 2992 # Compatibility with pre PEP8 2993 # pylint: disable=invalid-name 2994 2995 def Name(self) -> str: 2996 return self.name 2997 2998 def SetName(self, name: str) -> None: 2999 self.name = name 3000 3001 def Proto(self) -> cp_model_pb2.CpModelProto: 3002 return self.proto 3003 3004 NewIntVar = new_int_var 3005 NewIntVarFromDomain = new_int_var_from_domain 3006 NewBoolVar = new_bool_var 3007 NewConstant = new_constant 3008 NewIntVarSeries = new_int_var_series 3009 NewBoolVarSeries = new_bool_var_series 3010 AddLinearConstraint = add_linear_constraint 3011 AddLinearExpressionInDomain = add_linear_expression_in_domain 3012 Add = add 3013 AddAllDifferent = add_all_different 3014 AddElement = add_element 3015 AddCircuit = add_circuit 3016 AddMultipleCircuit = add_multiple_circuit 3017 AddAllowedAssignments = add_allowed_assignments 3018 AddForbiddenAssignments = add_forbidden_assignments 3019 AddAutomaton = add_automaton 3020 AddInverse = add_inverse 3021 AddReservoirConstraint = add_reservoir_constraint 3022 AddReservoirConstraintWithActive = add_reservoir_constraint_with_active 3023 AddImplication = add_implication 3024 AddBoolOr = add_bool_or 3025 AddAtLeastOne = add_at_least_one 3026 AddAtMostOne = add_at_most_one 3027 AddExactlyOne = add_exactly_one 3028 AddBoolAnd = add_bool_and 3029 AddBoolXOr = add_bool_xor 3030 AddMinEquality = add_min_equality 3031 AddMaxEquality = add_max_equality 3032 AddDivisionEquality = add_division_equality 3033 AddAbsEquality = add_abs_equality 3034 AddModuloEquality = add_modulo_equality 3035 AddMultiplicationEquality = add_multiplication_equality 3036 NewIntervalVar = new_interval_var 3037 NewIntervalVarSeries = new_interval_var_series 3038 NewFixedSizeIntervalVar = new_fixed_size_interval_var 3039 NewOptionalIntervalVar = new_optional_interval_var 3040 NewOptionalIntervalVarSeries = new_optional_interval_var_series 3041 NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var 3042 NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series 3043 AddNoOverlap = add_no_overlap 3044 AddNoOverlap2D = add_no_overlap_2d 3045 AddCumulative = add_cumulative 3046 Clone = clone 3047 GetBoolVarFromProtoIndex = get_bool_var_from_proto_index 3048 GetIntVarFromProtoIndex = get_int_var_from_proto_index 3049 GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index 3050 Minimize = minimize 3051 Maximize = maximize 3052 HasObjective = has_objective 3053 ClearObjective = clear_objective 3054 AddDecisionStrategy = add_decision_strategy 3055 ModelStats = model_stats 3056 Validate = validate 3057 ExportToFile = export_to_file 3058 AddHint = add_hint 3059 ClearHints = clear_hints 3060 AddAssumption = add_assumption 3061 AddAssumptions = add_assumptions 3062 ClearAssumptions = clear_assumptions 3063 3064 # pylint: enable=invalid-name 3065 3066 3067@overload 3068def expand_generator_or_tuple( 3069 args: Union[Tuple[LiteralT, ...], Iterable[LiteralT]] 3070) -> Union[Iterable[LiteralT], LiteralT]: ... 3071 3072 3073@overload 3074def expand_generator_or_tuple( 3075 args: Union[Tuple[LinearExprT, ...], Iterable[LinearExprT]] 3076) -> Union[Iterable[LinearExprT], LinearExprT]: ... 3077 3078 3079def expand_generator_or_tuple(args): 3080 if hasattr(args, "__len__"): # Tuple 3081 if len(args) != 1: 3082 return args 3083 if isinstance(args[0], (NumberTypes, LinearExpr)): 3084 return args 3085 # Generator 3086 return args[0] 3087 3088 3089def evaluate_linear_expr( 3090 expression: LinearExprT, solution: cp_model_pb2.CpSolverResponse 3091) -> int: 3092 """Evaluate a linear expression against a solution.""" 3093 if isinstance(expression, IntegralTypes): 3094 return int(expression) 3095 if not isinstance(expression, LinearExpr): 3096 raise TypeError("Cannot interpret %s as a linear expression." % expression) 3097 3098 value = 0 3099 to_process = [(expression, 1)] 3100 while to_process: 3101 expr, coeff = to_process.pop() 3102 if isinstance(expr, IntegralTypes): 3103 value += int(expr) * coeff 3104 elif isinstance(expr, _ProductCst): 3105 to_process.append((expr.expression(), coeff * expr.coefficient())) 3106 elif isinstance(expr, _Sum): 3107 to_process.append((expr.left(), coeff)) 3108 to_process.append((expr.right(), coeff)) 3109 elif isinstance(expr, _SumArray): 3110 for e in expr.expressions(): 3111 to_process.append((e, coeff)) 3112 value += expr.constant() * coeff 3113 elif isinstance(expr, _WeightedSum): 3114 for e, c in zip(expr.expressions(), expr.coefficients()): 3115 to_process.append((e, coeff * c)) 3116 value += expr.constant() * coeff 3117 elif isinstance(expr, IntVar): 3118 value += coeff * solution.solution[expr.index] 3119 elif isinstance(expr, _NotBooleanVariable): 3120 value += coeff * (1 - solution.solution[expr.negated().index]) 3121 else: 3122 raise TypeError(f"Cannot interpret {expr} as a linear expression.") 3123 3124 return value 3125 3126 3127def evaluate_boolean_expression( 3128 literal: LiteralT, solution: cp_model_pb2.CpSolverResponse 3129) -> bool: 3130 """Evaluate a boolean expression against a solution.""" 3131 if isinstance(literal, IntegralTypes): 3132 return bool(literal) 3133 elif isinstance(literal, IntVar) or isinstance(literal, _NotBooleanVariable): 3134 index: int = cast(Union[IntVar, _NotBooleanVariable], literal).index 3135 if index >= 0: 3136 return bool(solution.solution[index]) 3137 else: 3138 return not solution.solution[-index - 1] 3139 else: 3140 raise TypeError(f"Cannot interpret {literal} as a boolean expression.") 3141 3142 3143class CpSolver: 3144 """Main solver class. 3145 3146 The purpose of this class is to search for a solution to the model provided 3147 to the solve() method. 3148 3149 Once solve() is called, this class allows inspecting the solution found 3150 with the value() and boolean_value() methods, as well as general statistics 3151 about the solve procedure. 3152 """ 3153 3154 def __init__(self) -> None: 3155 self.__solution: Optional[cp_model_pb2.CpSolverResponse] = None 3156 self.parameters: sat_parameters_pb2.SatParameters = ( 3157 sat_parameters_pb2.SatParameters() 3158 ) 3159 self.log_callback: Optional[Callable[[str], None]] = None 3160 self.best_bound_callback: Optional[Callable[[float], None]] = None 3161 self.__solve_wrapper: Optional[swig_helper.SolveWrapper] = None 3162 self.__lock: threading.Lock = threading.Lock() 3163 3164 def solve( 3165 self, 3166 model: CpModel, 3167 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3168 ) -> cp_model_pb2.CpSolverStatus: 3169 """Solves a problem and passes each solution to the callback if not null.""" 3170 with self.__lock: 3171 self.__solve_wrapper = swig_helper.SolveWrapper() 3172 3173 self.__solve_wrapper.set_parameters(self.parameters) 3174 if solution_callback is not None: 3175 self.__solve_wrapper.add_solution_callback(solution_callback) 3176 3177 if self.log_callback is not None: 3178 self.__solve_wrapper.add_log_callback(self.log_callback) 3179 3180 if self.best_bound_callback is not None: 3181 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) 3182 3183 solution: cp_model_pb2.CpSolverResponse = self.__solve_wrapper.solve( 3184 model.proto 3185 ) 3186 self.__solution = solution 3187 3188 if solution_callback is not None: 3189 self.__solve_wrapper.clear_solution_callback(solution_callback) 3190 3191 with self.__lock: 3192 self.__solve_wrapper = None 3193 3194 return solution.status 3195 3196 def stop_search(self) -> None: 3197 """Stops the current search asynchronously.""" 3198 with self.__lock: 3199 if self.__solve_wrapper: 3200 self.__solve_wrapper.stop_search() 3201 3202 def value(self, expression: LinearExprT) -> int: 3203 """Returns the value of a linear expression after solve.""" 3204 return evaluate_linear_expr(expression, self._solution) 3205 3206 def values(self, variables: _IndexOrSeries) -> pd.Series: 3207 """Returns the values of the input variables. 3208 3209 If `variables` is a `pd.Index`, then the output will be indexed by the 3210 variables. If `variables` is a `pd.Series` indexed by the underlying 3211 dimensions, then the output will be indexed by the same underlying 3212 dimensions. 3213 3214 Args: 3215 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3216 get the values. 3217 3218 Returns: 3219 pd.Series: The values of all variables in the set. 3220 """ 3221 solution = self._solution 3222 return _attribute_series( 3223 func=lambda v: solution.solution[v.index], 3224 values=variables, 3225 ) 3226 3227 def boolean_value(self, literal: LiteralT) -> bool: 3228 """Returns the boolean value of a literal after solve.""" 3229 return evaluate_boolean_expression(literal, self._solution) 3230 3231 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 3232 """Returns the values of the input variables. 3233 3234 If `variables` is a `pd.Index`, then the output will be indexed by the 3235 variables. If `variables` is a `pd.Series` indexed by the underlying 3236 dimensions, then the output will be indexed by the same underlying 3237 dimensions. 3238 3239 Args: 3240 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3241 get the values. 3242 3243 Returns: 3244 pd.Series: The values of all variables in the set. 3245 """ 3246 solution = self._solution 3247 return _attribute_series( 3248 func=lambda literal: evaluate_boolean_expression(literal, solution), 3249 values=variables, 3250 ) 3251 3252 @property 3253 def objective_value(self) -> float: 3254 """Returns the value of the objective after solve.""" 3255 return self._solution.objective_value 3256 3257 @property 3258 def best_objective_bound(self) -> float: 3259 """Returns the best lower (upper) bound found when min(max)imizing.""" 3260 return self._solution.best_objective_bound 3261 3262 @property 3263 def num_booleans(self) -> int: 3264 """Returns the number of boolean variables managed by the SAT solver.""" 3265 return self._solution.num_booleans 3266 3267 @property 3268 def num_conflicts(self) -> int: 3269 """Returns the number of conflicts since the creation of the solver.""" 3270 return self._solution.num_conflicts 3271 3272 @property 3273 def num_branches(self) -> int: 3274 """Returns the number of search branches explored by the solver.""" 3275 return self._solution.num_branches 3276 3277 @property 3278 def wall_time(self) -> float: 3279 """Returns the wall time in seconds since the creation of the solver.""" 3280 return self._solution.wall_time 3281 3282 @property 3283 def user_time(self) -> float: 3284 """Returns the user time in seconds since the creation of the solver.""" 3285 return self._solution.user_time 3286 3287 @property 3288 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3289 """Returns the response object.""" 3290 return self._solution 3291 3292 def response_stats(self) -> str: 3293 """Returns some statistics on the solution found as a string.""" 3294 return swig_helper.CpSatHelper.solver_response_stats(self._solution) 3295 3296 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 3297 """Returns the indices of the infeasible assumptions.""" 3298 return self._solution.sufficient_assumptions_for_infeasibility 3299 3300 def status_name(self, status: Optional[Any] = None) -> str: 3301 """Returns the name of the status returned by solve().""" 3302 if status is None: 3303 status = self._solution.status 3304 return cp_model_pb2.CpSolverStatus.Name(status) 3305 3306 def solution_info(self) -> str: 3307 """Returns some information on the solve process. 3308 3309 Returns some information on how the solution was found, or the reason 3310 why the model or the parameters are invalid. 3311 3312 Raises: 3313 RuntimeError: if solve() has not been called. 3314 """ 3315 return self._solution.solution_info 3316 3317 @property 3318 def _solution(self) -> cp_model_pb2.CpSolverResponse: 3319 """Checks solve() has been called, and returns the solution.""" 3320 if self.__solution is None: 3321 raise RuntimeError("solve() has not been called.") 3322 return self.__solution 3323 3324 # Compatibility with pre PEP8 3325 # pylint: disable=invalid-name 3326 3327 def BestObjectiveBound(self) -> float: 3328 return self.best_objective_bound 3329 3330 def BooleanValue(self, literal: LiteralT) -> bool: 3331 return self.boolean_value(literal) 3332 3333 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: 3334 return self.boolean_values(variables) 3335 3336 def NumBooleans(self) -> int: 3337 return self.num_booleans 3338 3339 def NumConflicts(self) -> int: 3340 return self.num_conflicts 3341 3342 def NumBranches(self) -> int: 3343 return self.num_branches 3344 3345 def ObjectiveValue(self) -> float: 3346 return self.objective_value 3347 3348 def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: 3349 return self.response_proto 3350 3351 def ResponseStats(self) -> str: 3352 return self.response_stats() 3353 3354 def Solve( 3355 self, 3356 model: CpModel, 3357 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3358 ) -> cp_model_pb2.CpSolverStatus: 3359 return self.solve(model, solution_callback) 3360 3361 def SolutionInfo(self) -> str: 3362 return self.solution_info() 3363 3364 def StatusName(self, status: Optional[Any] = None) -> str: 3365 return self.status_name(status) 3366 3367 def StopSearch(self) -> None: 3368 self.stop_search() 3369 3370 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: 3371 return self.sufficient_assumptions_for_infeasibility() 3372 3373 def UserTime(self) -> float: 3374 return self.user_time 3375 3376 def Value(self, expression: LinearExprT) -> int: 3377 return self.value(expression) 3378 3379 def Values(self, variables: _IndexOrSeries) -> pd.Series: 3380 return self.values(variables) 3381 3382 def WallTime(self) -> float: 3383 return self.wall_time 3384 3385 def SolveWithSolutionCallback( 3386 self, model: CpModel, callback: "CpSolverSolutionCallback" 3387 ) -> cp_model_pb2.CpSolverStatus: 3388 """DEPRECATED Use solve() with the callback argument.""" 3389 warnings.warn( 3390 "solve_with_solution_callback is deprecated; use solve() with" 3391 + "the callback argument.", 3392 DeprecationWarning, 3393 ) 3394 return self.solve(model, callback) 3395 3396 def SearchForAllSolutions( 3397 self, model: CpModel, callback: "CpSolverSolutionCallback" 3398 ) -> cp_model_pb2.CpSolverStatus: 3399 """DEPRECATED Use solve() with the right parameter. 3400 3401 Search for all solutions of a satisfiability problem. 3402 3403 This method searches for all feasible solutions of a given model. 3404 Then it feeds the solution to the callback. 3405 3406 Note that the model cannot contain an objective. 3407 3408 Args: 3409 model: The model to solve. 3410 callback: The callback that will be called at each solution. 3411 3412 Returns: 3413 The status of the solve: 3414 3415 * *FEASIBLE* if some solutions have been found 3416 * *INFEASIBLE* if the solver has proved there are no solution 3417 * *OPTIMAL* if all solutions have been found 3418 """ 3419 warnings.warn( 3420 "search_for_all_solutions is deprecated; use solve() with" 3421 + "enumerate_all_solutions = True.", 3422 DeprecationWarning, 3423 ) 3424 if model.has_objective(): 3425 raise TypeError( 3426 "Search for all solutions is only defined on satisfiability problems" 3427 ) 3428 # Store old parameter. 3429 enumerate_all = self.parameters.enumerate_all_solutions 3430 self.parameters.enumerate_all_solutions = True 3431 3432 status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) 3433 3434 # Restore parameter. 3435 self.parameters.enumerate_all_solutions = enumerate_all 3436 return status 3437 3438 3439# pylint: enable=invalid-name 3440 3441 3442class CpSolverSolutionCallback(swig_helper.SolutionCallback): 3443 """Solution callback. 3444 3445 This class implements a callback that will be called at each new solution 3446 found during search. 3447 3448 The method on_solution_callback() will be called by the solver, and must be 3449 implemented. The current solution can be queried using the boolean_value() 3450 and value() methods. 3451 3452 These methods returns the same information as their counterpart in the 3453 `CpSolver` class. 3454 """ 3455 3456 def __init__(self) -> None: 3457 swig_helper.SolutionCallback.__init__(self) 3458 3459 def OnSolutionCallback(self) -> None: 3460 """Proxy for the same method in snake case.""" 3461 self.on_solution_callback() 3462 3463 def boolean_value(self, lit: LiteralT) -> bool: 3464 """Returns the boolean value of a boolean literal. 3465 3466 Args: 3467 lit: A boolean variable or its negation. 3468 3469 Returns: 3470 The Boolean value of the literal in the solution. 3471 3472 Raises: 3473 RuntimeError: if `lit` is not a boolean variable or its negation. 3474 """ 3475 if not self.has_response(): 3476 raise RuntimeError("solve() has not been called.") 3477 if isinstance(lit, IntegralTypes): 3478 return bool(lit) 3479 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3480 return self.SolutionBooleanValue( 3481 cast(Union[IntVar, _NotBooleanVariable], lit).index 3482 ) 3483 if cmh.is_boolean(lit): 3484 return bool(lit) 3485 raise TypeError(f"Cannot interpret {lit} as a boolean expression.") 3486 3487 def value(self, expression: LinearExprT) -> int: 3488 """Evaluates an linear expression in the current solution. 3489 3490 Args: 3491 expression: a linear expression of the model. 3492 3493 Returns: 3494 An integer value equal to the evaluation of the linear expression 3495 against the current solution. 3496 3497 Raises: 3498 RuntimeError: if 'expression' is not a LinearExpr. 3499 """ 3500 if not self.has_response(): 3501 raise RuntimeError("solve() has not been called.") 3502 3503 value = 0 3504 to_process = [(expression, 1)] 3505 while to_process: 3506 expr, coeff = to_process.pop() 3507 if isinstance(expr, IntegralTypes): 3508 value += int(expr) * coeff 3509 elif isinstance(expr, _ProductCst): 3510 to_process.append((expr.expression(), coeff * expr.coefficient())) 3511 elif isinstance(expr, _Sum): 3512 to_process.append((expr.left(), coeff)) 3513 to_process.append((expr.right(), coeff)) 3514 elif isinstance(expr, _SumArray): 3515 for e in expr.expressions(): 3516 to_process.append((e, coeff)) 3517 value += expr.constant() * coeff 3518 elif isinstance(expr, _WeightedSum): 3519 for e, c in zip(expr.expressions(), expr.coefficients()): 3520 to_process.append((e, coeff * c)) 3521 value += expr.constant() * coeff 3522 elif isinstance(expr, IntVar): 3523 value += coeff * self.SolutionIntegerValue(expr.index) 3524 elif isinstance(expr, _NotBooleanVariable): 3525 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3526 else: 3527 raise TypeError( 3528 f"cannot interpret {expression} as a linear expression." 3529 ) 3530 3531 return value 3532 3533 def has_response(self) -> bool: 3534 return self.HasResponse() 3535 3536 def stop_search(self) -> None: 3537 """Stops the current search asynchronously.""" 3538 if not self.has_response(): 3539 raise RuntimeError("solve() has not been called.") 3540 self.StopSearch() 3541 3542 @property 3543 def objective_value(self) -> float: 3544 """Returns the value of the objective after solve.""" 3545 if not self.has_response(): 3546 raise RuntimeError("solve() has not been called.") 3547 return self.ObjectiveValue() 3548 3549 @property 3550 def best_objective_bound(self) -> float: 3551 """Returns the best lower (upper) bound found when min(max)imizing.""" 3552 if not self.has_response(): 3553 raise RuntimeError("solve() has not been called.") 3554 return self.BestObjectiveBound() 3555 3556 @property 3557 def num_booleans(self) -> int: 3558 """Returns the number of boolean variables managed by the SAT solver.""" 3559 if not self.has_response(): 3560 raise RuntimeError("solve() has not been called.") 3561 return self.NumBooleans() 3562 3563 @property 3564 def num_conflicts(self) -> int: 3565 """Returns the number of conflicts since the creation of the solver.""" 3566 if not self.has_response(): 3567 raise RuntimeError("solve() has not been called.") 3568 return self.NumConflicts() 3569 3570 @property 3571 def num_branches(self) -> int: 3572 """Returns the number of search branches explored by the solver.""" 3573 if not self.has_response(): 3574 raise RuntimeError("solve() has not been called.") 3575 return self.NumBranches() 3576 3577 @property 3578 def num_integer_propagations(self) -> int: 3579 """Returns the number of integer propagations done by the solver.""" 3580 if not self.has_response(): 3581 raise RuntimeError("solve() has not been called.") 3582 return self.NumIntegerPropagations() 3583 3584 @property 3585 def num_boolean_propagations(self) -> int: 3586 """Returns the number of Boolean propagations done by the solver.""" 3587 if not self.has_response(): 3588 raise RuntimeError("solve() has not been called.") 3589 return self.NumBooleanPropagations() 3590 3591 @property 3592 def deterministic_time(self) -> float: 3593 """Returns the determistic time in seconds since the creation of the solver.""" 3594 if not self.has_response(): 3595 raise RuntimeError("solve() has not been called.") 3596 return self.DeterministicTime() 3597 3598 @property 3599 def wall_time(self) -> float: 3600 """Returns the wall time in seconds since the creation of the solver.""" 3601 if not self.has_response(): 3602 raise RuntimeError("solve() has not been called.") 3603 return self.WallTime() 3604 3605 @property 3606 def user_time(self) -> float: 3607 """Returns the user time in seconds since the creation of the solver.""" 3608 if not self.has_response(): 3609 raise RuntimeError("solve() has not been called.") 3610 return self.UserTime() 3611 3612 @property 3613 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3614 """Returns the response object.""" 3615 if not self.has_response(): 3616 raise RuntimeError("solve() has not been called.") 3617 return self.Response() 3618 3619 # Compatibility with pre PEP8 3620 # pylint: disable=invalid-name 3621 Value = value 3622 BooleanValue = boolean_value 3623 # pylint: enable=invalid-name 3624 3625 3626class ObjectiveSolutionPrinter(CpSolverSolutionCallback): 3627 """Display the objective value and time of intermediate solutions.""" 3628 3629 def __init__(self) -> None: 3630 CpSolverSolutionCallback.__init__(self) 3631 self.__solution_count = 0 3632 self.__start_time = time.time() 3633 3634 def on_solution_callback(self) -> None: 3635 """Called on each new solution.""" 3636 current_time = time.time() 3637 obj = self.objective_value 3638 print( 3639 "Solution %i, time = %0.2f s, objective = %i" 3640 % (self.__solution_count, current_time - self.__start_time, obj) 3641 ) 3642 self.__solution_count += 1 3643 3644 def solution_count(self) -> int: 3645 """Returns the number of solutions found.""" 3646 return self.__solution_count 3647 3648 3649class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): 3650 """Print intermediate solutions (objective, variable values, time).""" 3651 3652 def __init__(self, variables: Sequence[IntVar]) -> None: 3653 CpSolverSolutionCallback.__init__(self) 3654 self.__variables: Sequence[IntVar] = variables 3655 self.__solution_count: int = 0 3656 self.__start_time: float = time.time() 3657 3658 def on_solution_callback(self) -> None: 3659 """Called on each new solution.""" 3660 current_time = time.time() 3661 obj = self.objective_value 3662 print( 3663 "Solution %i, time = %0.2f s, objective = %i" 3664 % (self.__solution_count, current_time - self.__start_time, obj) 3665 ) 3666 for v in self.__variables: 3667 print(" %s = %i" % (v, self.value(v)), end=" ") 3668 print() 3669 self.__solution_count += 1 3670 3671 @property 3672 def solution_count(self) -> int: 3673 """Returns the number of solutions found.""" 3674 return self.__solution_count 3675 3676 3677class VarArraySolutionPrinter(CpSolverSolutionCallback): 3678 """Print intermediate solutions (variable values, time).""" 3679 3680 def __init__(self, variables: Sequence[IntVar]) -> None: 3681 CpSolverSolutionCallback.__init__(self) 3682 self.__variables: Sequence[IntVar] = variables 3683 self.__solution_count: int = 0 3684 self.__start_time: float = time.time() 3685 3686 def on_solution_callback(self) -> None: 3687 """Called on each new solution.""" 3688 current_time = time.time() 3689 print( 3690 "Solution %i, time = %0.2f s" 3691 % (self.__solution_count, current_time - self.__start_time) 3692 ) 3693 for v in self.__variables: 3694 print(" %s = %i" % (v, self.value(v)), end=" ") 3695 print() 3696 self.__solution_count += 1 3697 3698 @property 3699 def solution_count(self) -> int: 3700 """Returns the number of solutions found.""" 3701 return self.__solution_count 3702 3703 3704def _get_index(obj: _IndexOrSeries) -> pd.Index: 3705 """Returns the indices of `obj` as a `pd.Index`.""" 3706 if isinstance(obj, pd.Series): 3707 return obj.index 3708 return obj 3709 3710 3711def _attribute_series( 3712 *, 3713 func: Callable[[IntVar], IntegralT], 3714 values: _IndexOrSeries, 3715) -> pd.Series: 3716 """Returns the attributes of `values`. 3717 3718 Args: 3719 func: The function to call for getting the attribute data. 3720 values: The values that the function will be applied (element-wise) to. 3721 3722 Returns: 3723 pd.Series: The attribute values. 3724 """ 3725 return pd.Series( 3726 data=[func(v) for v in values], 3727 index=_get_index(values), 3728 ) 3729 3730 3731def _convert_to_integral_series_and_validate_index( 3732 value_or_series: Union[IntegralT, pd.Series], index: pd.Index 3733) -> pd.Series: 3734 """Returns a pd.Series of the given index with the corresponding values. 3735 3736 Args: 3737 value_or_series: the values to be converted (if applicable). 3738 index: the index of the resulting pd.Series. 3739 3740 Returns: 3741 pd.Series: The set of values with the given index. 3742 3743 Raises: 3744 TypeError: If the type of `value_or_series` is not recognized. 3745 ValueError: If the index does not match. 3746 """ 3747 if isinstance(value_or_series, IntegralTypes): 3748 result = pd.Series(data=value_or_series, index=index) 3749 elif isinstance(value_or_series, pd.Series): 3750 if value_or_series.index.equals(index): 3751 result = value_or_series 3752 else: 3753 raise ValueError("index does not match") 3754 else: 3755 raise TypeError("invalid type={}".format(type(value_or_series))) 3756 return result 3757 3758 3759def _convert_to_linear_expr_series_and_validate_index( 3760 value_or_series: Union[LinearExprT, pd.Series], index: pd.Index 3761) -> pd.Series: 3762 """Returns a pd.Series of the given index with the corresponding values. 3763 3764 Args: 3765 value_or_series: the values to be converted (if applicable). 3766 index: the index of the resulting pd.Series. 3767 3768 Returns: 3769 pd.Series: The set of values with the given index. 3770 3771 Raises: 3772 TypeError: If the type of `value_or_series` is not recognized. 3773 ValueError: If the index does not match. 3774 """ 3775 if isinstance(value_or_series, IntegralTypes): 3776 result = pd.Series(data=value_or_series, index=index) 3777 elif isinstance(value_or_series, pd.Series): 3778 if value_or_series.index.equals(index): 3779 result = value_or_series 3780 else: 3781 raise ValueError("index does not match") 3782 else: 3783 raise TypeError("invalid type={}".format(type(value_or_series))) 3784 return result 3785 3786 3787def _convert_to_literal_series_and_validate_index( 3788 value_or_series: Union[LiteralT, pd.Series], index: pd.Index 3789) -> pd.Series: 3790 """Returns a pd.Series of the given index with the corresponding values. 3791 3792 Args: 3793 value_or_series: the values to be converted (if applicable). 3794 index: the index of the resulting pd.Series. 3795 3796 Returns: 3797 pd.Series: The set of values with the given index. 3798 3799 Raises: 3800 TypeError: If the type of `value_or_series` is not recognized. 3801 ValueError: If the index does not match. 3802 """ 3803 if isinstance(value_or_series, IntegralTypes): 3804 result = pd.Series(data=value_or_series, index=index) 3805 elif isinstance(value_or_series, pd.Series): 3806 if value_or_series.index.equals(index): 3807 result = value_or_series 3808 else: 3809 raise ValueError("index does not match") 3810 else: 3811 raise TypeError("invalid type={}".format(type(value_or_series))) 3812 return result
We call domain any subset of Int64 = [kint64min, kint64max].
This class can be used to represent such set efficiently as a sorted and non-adjacent list of intervals. This is efficient as long as the size of such list stays reasonable.
In the comments below, the domain of *this will always be written 'D'. Note that all the functions are safe with respect to integer overflow.
__init__(self: ortools.util.python.sorted_interval_list.Domain, arg0: int, arg1: int) -> None
By default, Domain will be empty.
all_values() -> ortools.util.python.sorted_interval_list.Domain
Returns the full domain Int64.
from_values(values: list[int]) -> ortools.util.python.sorted_interval_list.Domain
Creates a domain from the union of an unsorted list of integer values. Input values may be repeated, with no consequence on the output
from_intervals(intervals: list[list[int]]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a list of intervals (long[][] in Java and .NET, [[0, 2], [5, 5], [8, 10]] in python).
from_flat_intervals(flat_intervals: list[int]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a flattened list of intervals (long[] in Java and .NET, [0, 2, 5, 5, 8, 10] in python).
addition_with(self: ortools.util.python.sorted_interval_list.Domain, domain: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns {x ∈ Int64, ∃ a ∈ D, ∃ b ∈ domain, x = a + b}.
complement(self: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns the set Int64 ∖ D.
contains(self: ortools.util.python.sorted_interval_list.Domain, value: int) -> bool
Returns true iff value is in Domain.
flattened_intervals(self: ortools.util.python.sorted_interval_list.Domain) -> list[int]
This method returns the flattened list of interval bounds of the domain.
Thus the domain {0, 1, 2, 5, 8, 9, 10} will return [0, 2, 5, 5, 8, 10]
(as a C++ std::vector
intersection_with(self: ortools.util.python.sorted_interval_list.Domain, domain: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns the intersection of D and domain.
is_empty(self: ortools.util.python.sorted_interval_list.Domain) -> bool
Returns true if this is the empty set.
size(self: ortools.util.python.sorted_interval_list.Domain) -> int
Returns the number of elements in the domain. It is capped at kint64max
max(self: ortools.util.python.sorted_interval_list.Domain) -> int
Returns the max value of the domain. The domain must not be empty.
min(self: ortools.util.python.sorted_interval_list.Domain) -> int
Returns the min value of the domain. The domain must not be empty.
negation(self: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns {x ∈ Int64, ∃ e ∈ D, x = -e}.
Note in particular that if the negation of Int64 is not Int64 but Int64 \ {kint64min} !!
union_with(self: ortools.util.python.sorted_interval_list.Domain, domain: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns the union of D and domain.
AllValues() -> ortools.util.python.sorted_interval_list.Domain
Returns the full domain Int64.
FromValues(values: list[int]) -> ortools.util.python.sorted_interval_list.Domain
Creates a domain from the union of an unsorted list of integer values. Input values may be repeated, with no consequence on the output
FromIntervals(intervals: list[list[int]]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a list of intervals (long[][] in Java and .NET, [[0, 2], [5, 5], [8, 10]] in python).
FromFlatIntervals(flat_intervals: list[int]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a flattened list of intervals (long[] in Java and .NET, [0, 2, 5, 5, 8, 10] in python).
FlattenedIntervals(self: ortools.util.python.sorted_interval_list.Domain) -> list[int]
This method returns the flattened list of interval bounds of the domain.
Thus the domain {0, 1, 2, 5, 8, 9, 10} will return [0, 2, 5, 5, 8, 10]
(as a C++ std::vector
169def display_bounds(bounds: Sequence[int]) -> str: 170 """Displays a flattened list of intervals.""" 171 out = "" 172 for i in range(0, len(bounds), 2): 173 if i != 0: 174 out += ", " 175 if bounds[i] == bounds[i + 1]: 176 out += str(bounds[i]) 177 else: 178 out += str(bounds[i]) + ".." + str(bounds[i + 1]) 179 return out
Displays a flattened list of intervals.
182def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str: 183 """Returns a short name of an integer variable, or its negation.""" 184 if i < 0: 185 return "not(%s)" % short_name(model, -i - 1) 186 v = model.variables[i] 187 if v.name: 188 return v.name 189 elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: 190 return str(v.domain[0]) 191 else: 192 return "[%s]" % display_bounds(v.domain)
Returns a short name of an integer variable, or its negation.
195def short_expr_name( 196 model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto 197) -> str: 198 """Pretty-print LinearExpressionProto instances.""" 199 if not e.vars: 200 return str(e.offset) 201 if len(e.vars) == 1: 202 var_name = short_name(model, e.vars[0]) 203 coeff = e.coeffs[0] 204 result = "" 205 if coeff == 1: 206 result = var_name 207 elif coeff == -1: 208 result = f"-{var_name}" 209 elif coeff != 0: 210 result = f"{coeff} * {var_name}" 211 if e.offset > 0: 212 result = f"{result} + {e.offset}" 213 elif e.offset < 0: 214 result = f"{result} - {-e.offset}" 215 return result 216 # TODO(user): Support more than affine expressions. 217 return str(e)
Pretty-print LinearExpressionProto instances.
220class LinearExpr: 221 """Holds an integer linear expression. 222 223 A linear expression is built from integer constants and variables. 224 For example, `x + 2 * (y - z + 1)`. 225 226 Linear expressions are used in CP-SAT models in constraints and in the 227 objective: 228 229 * You can define linear constraints as in: 230 231 ``` 232 model.add(x + 2 * y <= 5) 233 model.add(sum(array_of_vars) == 5) 234 ``` 235 236 * In CP-SAT, the objective is a linear expression: 237 238 ``` 239 model.minimize(x + 2 * y + z) 240 ``` 241 242 * For large arrays, using the LinearExpr class is faster that using the python 243 `sum()` function. You can create constraints and the objective from lists of 244 linear expressions or coefficients as follows: 245 246 ``` 247 model.minimize(cp_model.LinearExpr.sum(expressions)) 248 model.add(cp_model.LinearExpr.weighted_sum(expressions, coefficients) >= 0) 249 ``` 250 """ 251 252 @classmethod 253 def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 254 """Creates the expression sum(expressions).""" 255 if len(expressions) == 1: 256 return expressions[0] 257 return _SumArray(expressions) 258 259 @overload 260 @classmethod 261 def weighted_sum( 262 cls, 263 expressions: Sequence[LinearExprT], 264 coefficients: Sequence[IntegralT], 265 ) -> LinearExprT: ... 266 267 @overload 268 @classmethod 269 def weighted_sum( 270 cls, 271 expressions: Sequence[ObjLinearExprT], 272 coefficients: Sequence[NumberT], 273 ) -> ObjLinearExprT: ... 274 275 @classmethod 276 def weighted_sum(cls, expressions, coefficients): 277 """Creates the expression sum(expressions[i] * coefficients[i]).""" 278 if LinearExpr.is_empty_or_all_null(coefficients): 279 return 0 280 elif len(expressions) == 1: 281 return expressions[0] * coefficients[0] 282 else: 283 return _WeightedSum(expressions, coefficients) 284 285 @overload 286 @classmethod 287 def term( 288 cls, 289 expressions: LinearExprT, 290 coefficients: IntegralT, 291 ) -> LinearExprT: ... 292 293 @overload 294 @classmethod 295 def term( 296 cls, 297 expressions: ObjLinearExprT, 298 coefficients: NumberT, 299 ) -> ObjLinearExprT: ... 300 301 @classmethod 302 def term(cls, expression, coefficient): 303 """Creates `expression * coefficient`.""" 304 if cmh.is_zero(coefficient): 305 return 0 306 else: 307 return expression * coefficient 308 309 @classmethod 310 def is_empty_or_all_null(cls, coefficients: Sequence[NumberT]) -> bool: 311 for c in coefficients: 312 if not cmh.is_zero(c): 313 return False 314 return True 315 316 @classmethod 317 def rebuild_from_linear_expression_proto( 318 cls, 319 model: cp_model_pb2.CpModelProto, 320 proto: cp_model_pb2.LinearExpressionProto, 321 ) -> LinearExprT: 322 """Recreate a LinearExpr from a LinearExpressionProto.""" 323 offset = proto.offset 324 num_elements = len(proto.vars) 325 if num_elements == 0: 326 return offset 327 elif num_elements == 1: 328 return ( 329 IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset 330 ) # pytype: disable=bad-return-type 331 else: 332 variables = [] 333 coeffs = [] 334 all_ones = True 335 for index, coeff in zip(proto.vars, proto.coeffs): 336 variables.append(IntVar(model, index, None)) 337 coeffs.append(coeff) 338 if not cmh.is_one(coeff): 339 all_ones = False 340 if all_ones: 341 return _SumArray(variables, offset) 342 else: 343 return _WeightedSum(variables, coeffs, offset) 344 345 def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", int], int]: 346 """Scans the expression, and returns (var_coef_map, constant).""" 347 coeffs: Dict["IntVar", int] = collections.defaultdict(int) 348 constant = 0 349 to_process: List[Tuple[LinearExprT, int]] = [(self, 1)] 350 while to_process: # Flatten to avoid recursion. 351 expr: LinearExprT 352 coeff: int 353 expr, coeff = to_process.pop() 354 if isinstance(expr, IntegralTypes): 355 constant += coeff * int(expr) 356 elif isinstance(expr, _ProductCst): 357 to_process.append((expr.expression(), coeff * expr.coefficient())) 358 elif isinstance(expr, _Sum): 359 to_process.append((expr.left(), coeff)) 360 to_process.append((expr.right(), coeff)) 361 elif isinstance(expr, _SumArray): 362 for e in expr.expressions(): 363 to_process.append((e, coeff)) 364 constant += expr.constant() * coeff 365 elif isinstance(expr, _WeightedSum): 366 for e, c in zip(expr.expressions(), expr.coefficients()): 367 to_process.append((e, coeff * c)) 368 constant += expr.constant() * coeff 369 elif isinstance(expr, IntVar): 370 coeffs[expr] += coeff 371 elif isinstance(expr, _NotBooleanVariable): 372 constant += coeff 373 coeffs[expr.negated()] -= coeff 374 elif isinstance(expr, NumberTypes): 375 raise TypeError( 376 f"Floating point constants are not supported in constraints: {expr}" 377 ) 378 else: 379 raise TypeError("Unrecognized linear expression: " + str(expr)) 380 381 return coeffs, constant 382 383 def get_float_var_value_map( 384 self, 385 ) -> Tuple[Dict["IntVar", float], float, bool]: 386 """Scans the expression. Returns (var_coef_map, constant, is_integer).""" 387 coeffs: Dict["IntVar", Union[int, float]] = {} 388 constant: Union[int, float] = 0 389 to_process: List[Tuple[LinearExprT, Union[int, float]]] = [(self, 1)] 390 while to_process: # Flatten to avoid recursion. 391 expr, coeff = to_process.pop() 392 if isinstance(expr, IntegralTypes): # Keep integrality. 393 constant += coeff * int(expr) 394 elif isinstance(expr, NumberTypes): 395 constant += coeff * float(expr) 396 elif isinstance(expr, _ProductCst): 397 to_process.append((expr.expression(), coeff * expr.coefficient())) 398 elif isinstance(expr, _Sum): 399 to_process.append((expr.left(), coeff)) 400 to_process.append((expr.right(), coeff)) 401 elif isinstance(expr, _SumArray): 402 for e in expr.expressions(): 403 to_process.append((e, coeff)) 404 constant += expr.constant() * coeff 405 elif isinstance(expr, _WeightedSum): 406 for e, c in zip(expr.expressions(), expr.coefficients()): 407 to_process.append((e, coeff * c)) 408 constant += expr.constant() * coeff 409 elif isinstance(expr, IntVar): 410 if expr in coeffs: 411 coeffs[expr] += coeff 412 else: 413 coeffs[expr] = coeff 414 elif isinstance(expr, _NotBooleanVariable): 415 constant += coeff 416 if expr.negated() in coeffs: 417 coeffs[expr.negated()] -= coeff 418 else: 419 coeffs[expr.negated()] = -coeff 420 else: 421 raise TypeError("Unrecognized linear expression: " + str(expr)) 422 is_integer = isinstance(constant, IntegralTypes) 423 if is_integer: 424 for coeff in coeffs.values(): 425 if not isinstance(coeff, IntegralTypes): 426 is_integer = False 427 break 428 return coeffs, constant, is_integer 429 430 def __hash__(self) -> int: 431 return object.__hash__(self) 432 433 def __abs__(self) -> NoReturn: 434 raise NotImplementedError( 435 "calling abs() on a linear expression is not supported, " 436 "please use CpModel.add_abs_equality" 437 ) 438 439 @overload 440 def __add__(self, arg: "LinearExpr") -> "LinearExpr": ... 441 442 @overload 443 def __add__(self, arg: NumberT) -> "LinearExpr": ... 444 445 def __add__(self, arg): 446 if cmh.is_zero(arg): 447 return self 448 return _Sum(self, arg) 449 450 @overload 451 def __radd__(self, arg: "LinearExpr") -> "LinearExpr": ... 452 453 @overload 454 def __radd__(self, arg: NumberT) -> "LinearExpr": ... 455 456 def __radd__(self, arg): 457 return self.__add__(arg) 458 459 @overload 460 def __sub__(self, arg: "LinearExpr") -> "LinearExpr": ... 461 462 @overload 463 def __sub__(self, arg: NumberT) -> "LinearExpr": ... 464 465 def __sub__(self, arg): 466 if cmh.is_zero(arg): 467 return self 468 if isinstance(arg, NumberTypes): 469 arg = cmh.assert_is_a_number(arg) 470 return _Sum(self, -arg) 471 else: 472 return _Sum(self, -arg) 473 474 @overload 475 def __rsub__(self, arg: "LinearExpr") -> "LinearExpr": ... 476 477 @overload 478 def __rsub__(self, arg: NumberT) -> "LinearExpr": ... 479 480 def __rsub__(self, arg): 481 return _Sum(-self, arg) 482 483 @overload 484 def __mul__(self, arg: IntegralT) -> Union["LinearExpr", IntegralT]: ... 485 486 @overload 487 def __mul__(self, arg: NumberT) -> Union["LinearExpr", NumberT]: ... 488 489 def __mul__(self, arg): 490 arg = cmh.assert_is_a_number(arg) 491 if cmh.is_one(arg): 492 return self 493 elif cmh.is_zero(arg): 494 return 0 495 return _ProductCst(self, arg) 496 497 @overload 498 def __rmul__(self, arg: IntegralT) -> Union["LinearExpr", IntegralT]: ... 499 500 @overload 501 def __rmul__(self, arg: NumberT) -> Union["LinearExpr", NumberT]: ... 502 503 def __rmul__(self, arg): 504 return self.__mul__(arg) 505 506 def __div__(self, _) -> NoReturn: 507 raise NotImplementedError( 508 "calling / on a linear expression is not supported, " 509 "please use CpModel.add_division_equality" 510 ) 511 512 def __truediv__(self, _) -> NoReturn: 513 raise NotImplementedError( 514 "calling // on a linear expression is not supported, " 515 "please use CpModel.add_division_equality" 516 ) 517 518 def __mod__(self, _) -> NoReturn: 519 raise NotImplementedError( 520 "calling %% on a linear expression is not supported, " 521 "please use CpModel.add_modulo_equality" 522 ) 523 524 def __pow__(self, _) -> NoReturn: 525 raise NotImplementedError( 526 "calling ** on a linear expression is not supported, " 527 "please use CpModel.add_multiplication_equality" 528 ) 529 530 def __lshift__(self, _) -> NoReturn: 531 raise NotImplementedError( 532 "calling left shift on a linear expression is not supported" 533 ) 534 535 def __rshift__(self, _) -> NoReturn: 536 raise NotImplementedError( 537 "calling right shift on a linear expression is not supported" 538 ) 539 540 def __and__(self, _) -> NoReturn: 541 raise NotImplementedError( 542 "calling and on a linear expression is not supported, " 543 "please use CpModel.add_bool_and" 544 ) 545 546 def __or__(self, _) -> NoReturn: 547 raise NotImplementedError( 548 "calling or on a linear expression is not supported, " 549 "please use CpModel.add_bool_or" 550 ) 551 552 def __xor__(self, _) -> NoReturn: 553 raise NotImplementedError( 554 "calling xor on a linear expression is not supported, " 555 "please use CpModel.add_bool_xor" 556 ) 557 558 def __neg__(self) -> "LinearExpr": 559 return _ProductCst(self, -1) 560 561 def __bool__(self) -> NoReturn: 562 raise NotImplementedError( 563 "Evaluating a LinearExpr instance as a Boolean is not implemented." 564 ) 565 566 def __eq__(self, arg: LinearExprT) -> BoundedLinearExprT: # type: ignore[override] 567 if arg is None: 568 return False 569 if isinstance(arg, IntegralTypes): 570 arg = cmh.assert_is_int64(arg) 571 return BoundedLinearExpression(self, [arg, arg]) 572 elif isinstance(arg, LinearExpr): 573 return BoundedLinearExpression(self - arg, [0, 0]) 574 else: 575 return False 576 577 def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression": 578 if isinstance(arg, IntegralTypes): 579 arg = cmh.assert_is_int64(arg) 580 return BoundedLinearExpression(self, [arg, INT_MAX]) 581 else: 582 return BoundedLinearExpression(self - arg, [0, INT_MAX]) 583 584 def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression": 585 if isinstance(arg, IntegralTypes): 586 arg = cmh.assert_is_int64(arg) 587 return BoundedLinearExpression(self, [INT_MIN, arg]) 588 else: 589 return BoundedLinearExpression(self - arg, [INT_MIN, 0]) 590 591 def __lt__(self, arg: LinearExprT) -> "BoundedLinearExpression": 592 if isinstance(arg, IntegralTypes): 593 arg = cmh.assert_is_int64(arg) 594 if arg == INT_MIN: 595 raise ArithmeticError("< INT_MIN is not supported") 596 return BoundedLinearExpression(self, [INT_MIN, arg - 1]) 597 else: 598 return BoundedLinearExpression(self - arg, [INT_MIN, -1]) 599 600 def __gt__(self, arg: LinearExprT) -> "BoundedLinearExpression": 601 if isinstance(arg, IntegralTypes): 602 arg = cmh.assert_is_int64(arg) 603 if arg == INT_MAX: 604 raise ArithmeticError("> INT_MAX is not supported") 605 return BoundedLinearExpression(self, [arg + 1, INT_MAX]) 606 else: 607 return BoundedLinearExpression(self - arg, [1, INT_MAX]) 608 609 def __ne__(self, arg: LinearExprT) -> BoundedLinearExprT: # type: ignore[override] 610 if arg is None: 611 return True 612 if isinstance(arg, IntegralTypes): 613 arg = cmh.assert_is_int64(arg) 614 if arg == INT_MAX: 615 return BoundedLinearExpression(self, [INT_MIN, INT_MAX - 1]) 616 elif arg == INT_MIN: 617 return BoundedLinearExpression(self, [INT_MIN + 1, INT_MAX]) 618 else: 619 return BoundedLinearExpression( 620 self, [INT_MIN, arg - 1, arg + 1, INT_MAX] 621 ) 622 elif isinstance(arg, LinearExpr): 623 return BoundedLinearExpression(self - arg, [INT_MIN, -1, 1, INT_MAX]) 624 else: 625 return True 626 627 # Compatibility with pre PEP8 628 # pylint: disable=invalid-name 629 @classmethod 630 def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 631 """Creates the expression sum(expressions).""" 632 return cls.sum(expressions) 633 634 @overload 635 @classmethod 636 def WeightedSum( 637 cls, 638 expressions: Sequence[LinearExprT], 639 coefficients: Sequence[IntegralT], 640 ) -> LinearExprT: ... 641 642 @overload 643 @classmethod 644 def WeightedSum( 645 cls, 646 expressions: Sequence[ObjLinearExprT], 647 coefficients: Sequence[NumberT], 648 ) -> ObjLinearExprT: ... 649 650 @classmethod 651 def WeightedSum(cls, expressions, coefficients): 652 """Creates the expression sum(expressions[i] * coefficients[i]).""" 653 return cls.weighted_sum(expressions, coefficients) 654 655 @overload 656 @classmethod 657 def Term( 658 cls, 659 expressions: LinearExprT, 660 coefficients: IntegralT, 661 ) -> LinearExprT: ... 662 663 @overload 664 @classmethod 665 def Term( 666 cls, 667 expressions: ObjLinearExprT, 668 coefficients: NumberT, 669 ) -> ObjLinearExprT: ... 670 671 @classmethod 672 def Term(cls, expression, coefficient): 673 """Creates `expression * coefficient`.""" 674 return cls.term(expression, coefficient) 675 676 # pylint: enable=invalid-name
Holds an integer linear expression.
A linear expression is built from integer constants and variables.
For example, x + 2 * (y - z + 1)
.
Linear expressions are used in CP-SAT models in constraints and in the objective:
- You can define linear constraints as in:
model.add(x + 2 * y <= 5)
model.add(sum(array_of_vars) == 5)
- In CP-SAT, the objective is a linear expression:
model.minimize(x + 2 * 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(cp_model.LinearExpr.sum(expressions))
model.add(cp_model.LinearExpr.weighted_sum(expressions, coefficients) >= 0)
252 @classmethod 253 def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 254 """Creates the expression sum(expressions).""" 255 if len(expressions) == 1: 256 return expressions[0] 257 return _SumArray(expressions)
Creates the expression sum(expressions).
275 @classmethod 276 def weighted_sum(cls, expressions, coefficients): 277 """Creates the expression sum(expressions[i] * coefficients[i]).""" 278 if LinearExpr.is_empty_or_all_null(coefficients): 279 return 0 280 elif len(expressions) == 1: 281 return expressions[0] * coefficients[0] 282 else: 283 return _WeightedSum(expressions, coefficients)
Creates the expression sum(expressions[i] * coefficients[i]).
301 @classmethod 302 def term(cls, expression, coefficient): 303 """Creates `expression * coefficient`.""" 304 if cmh.is_zero(coefficient): 305 return 0 306 else: 307 return expression * coefficient
Creates expression * coefficient
.
316 @classmethod 317 def rebuild_from_linear_expression_proto( 318 cls, 319 model: cp_model_pb2.CpModelProto, 320 proto: cp_model_pb2.LinearExpressionProto, 321 ) -> LinearExprT: 322 """Recreate a LinearExpr from a LinearExpressionProto.""" 323 offset = proto.offset 324 num_elements = len(proto.vars) 325 if num_elements == 0: 326 return offset 327 elif num_elements == 1: 328 return ( 329 IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset 330 ) # pytype: disable=bad-return-type 331 else: 332 variables = [] 333 coeffs = [] 334 all_ones = True 335 for index, coeff in zip(proto.vars, proto.coeffs): 336 variables.append(IntVar(model, index, None)) 337 coeffs.append(coeff) 338 if not cmh.is_one(coeff): 339 all_ones = False 340 if all_ones: 341 return _SumArray(variables, offset) 342 else: 343 return _WeightedSum(variables, coeffs, offset)
Recreate a LinearExpr from a LinearExpressionProto.
345 def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", int], int]: 346 """Scans the expression, and returns (var_coef_map, constant).""" 347 coeffs: Dict["IntVar", int] = collections.defaultdict(int) 348 constant = 0 349 to_process: List[Tuple[LinearExprT, int]] = [(self, 1)] 350 while to_process: # Flatten to avoid recursion. 351 expr: LinearExprT 352 coeff: int 353 expr, coeff = to_process.pop() 354 if isinstance(expr, IntegralTypes): 355 constant += coeff * int(expr) 356 elif isinstance(expr, _ProductCst): 357 to_process.append((expr.expression(), coeff * expr.coefficient())) 358 elif isinstance(expr, _Sum): 359 to_process.append((expr.left(), coeff)) 360 to_process.append((expr.right(), coeff)) 361 elif isinstance(expr, _SumArray): 362 for e in expr.expressions(): 363 to_process.append((e, coeff)) 364 constant += expr.constant() * coeff 365 elif isinstance(expr, _WeightedSum): 366 for e, c in zip(expr.expressions(), expr.coefficients()): 367 to_process.append((e, coeff * c)) 368 constant += expr.constant() * coeff 369 elif isinstance(expr, IntVar): 370 coeffs[expr] += coeff 371 elif isinstance(expr, _NotBooleanVariable): 372 constant += coeff 373 coeffs[expr.negated()] -= coeff 374 elif isinstance(expr, NumberTypes): 375 raise TypeError( 376 f"Floating point constants are not supported in constraints: {expr}" 377 ) 378 else: 379 raise TypeError("Unrecognized linear expression: " + str(expr)) 380 381 return coeffs, constant
Scans the expression, and returns (var_coef_map, constant).
383 def get_float_var_value_map( 384 self, 385 ) -> Tuple[Dict["IntVar", float], float, bool]: 386 """Scans the expression. Returns (var_coef_map, constant, is_integer).""" 387 coeffs: Dict["IntVar", Union[int, float]] = {} 388 constant: Union[int, float] = 0 389 to_process: List[Tuple[LinearExprT, Union[int, float]]] = [(self, 1)] 390 while to_process: # Flatten to avoid recursion. 391 expr, coeff = to_process.pop() 392 if isinstance(expr, IntegralTypes): # Keep integrality. 393 constant += coeff * int(expr) 394 elif isinstance(expr, NumberTypes): 395 constant += coeff * float(expr) 396 elif isinstance(expr, _ProductCst): 397 to_process.append((expr.expression(), coeff * expr.coefficient())) 398 elif isinstance(expr, _Sum): 399 to_process.append((expr.left(), coeff)) 400 to_process.append((expr.right(), coeff)) 401 elif isinstance(expr, _SumArray): 402 for e in expr.expressions(): 403 to_process.append((e, coeff)) 404 constant += expr.constant() * coeff 405 elif isinstance(expr, _WeightedSum): 406 for e, c in zip(expr.expressions(), expr.coefficients()): 407 to_process.append((e, coeff * c)) 408 constant += expr.constant() * coeff 409 elif isinstance(expr, IntVar): 410 if expr in coeffs: 411 coeffs[expr] += coeff 412 else: 413 coeffs[expr] = coeff 414 elif isinstance(expr, _NotBooleanVariable): 415 constant += coeff 416 if expr.negated() in coeffs: 417 coeffs[expr.negated()] -= coeff 418 else: 419 coeffs[expr.negated()] = -coeff 420 else: 421 raise TypeError("Unrecognized linear expression: " + str(expr)) 422 is_integer = isinstance(constant, IntegralTypes) 423 if is_integer: 424 for coeff in coeffs.values(): 425 if not isinstance(coeff, IntegralTypes): 426 is_integer = False 427 break 428 return coeffs, constant, is_integer
Scans the expression. Returns (var_coef_map, constant, is_integer).
629 @classmethod 630 def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 631 """Creates the expression sum(expressions).""" 632 return cls.sum(expressions)
Creates the expression sum(expressions).
650 @classmethod 651 def WeightedSum(cls, expressions, coefficients): 652 """Creates the expression sum(expressions[i] * coefficients[i]).""" 653 return cls.weighted_sum(expressions, coefficients)
Creates the expression sum(expressions[i] * coefficients[i]).
833class IntVar(LinearExpr): 834 """An integer variable. 835 836 An IntVar is an object that can take on any integer value within defined 837 ranges. Variables appear in constraint like: 838 839 x + y >= 5 840 AllDifferent([x, y, z]) 841 842 Solving a model is equivalent to finding, for each variable, a single value 843 from the set of initial values (called the initial domain), such that the 844 model is feasible, or optimal if you provided an objective function. 845 """ 846 847 def __init__( 848 self, 849 model: cp_model_pb2.CpModelProto, 850 domain: Union[int, sorted_interval_list.Domain], 851 name: Optional[str], 852 ) -> None: 853 """See CpModel.new_int_var below.""" 854 self.__index: int 855 self.__var: cp_model_pb2.IntegerVariableProto 856 self.__negation: Optional[_NotBooleanVariable] = None 857 # Python do not support multiple __init__ methods. 858 # This method is only called from the CpModel class. 859 # We hack the parameter to support the two cases: 860 # case 1: 861 # model is a CpModelProto, domain is a Domain, and name is a string. 862 # case 2: 863 # model is a CpModelProto, domain is an index (int), and name is None. 864 if isinstance(domain, IntegralTypes) and name is None: 865 self.__index = int(domain) 866 self.__var = model.variables[domain] 867 else: 868 self.__index = len(model.variables) 869 self.__var = model.variables.add() 870 self.__var.domain.extend( 871 cast(sorted_interval_list.Domain, domain).flattened_intervals() 872 ) 873 if name is not None: 874 self.__var.name = name 875 876 @property 877 def index(self) -> int: 878 """Returns the index of the variable in the model.""" 879 return self.__index 880 881 @property 882 def proto(self) -> cp_model_pb2.IntegerVariableProto: 883 """Returns the variable protobuf.""" 884 return self.__var 885 886 def is_equal_to(self, other: Any) -> bool: 887 """Returns true if self == other in the python sense.""" 888 if not isinstance(other, IntVar): 889 return False 890 return self.index == other.index 891 892 def __str__(self) -> str: 893 if not self.__var.name: 894 if ( 895 len(self.__var.domain) == 2 896 and self.__var.domain[0] == self.__var.domain[1] 897 ): 898 # Special case for constants. 899 return str(self.__var.domain[0]) 900 else: 901 return "unnamed_var_%i" % self.__index 902 return self.__var.name 903 904 def __repr__(self) -> str: 905 return "%s(%s)" % (self.__var.name, display_bounds(self.__var.domain)) 906 907 @property 908 def name(self) -> str: 909 if not self.__var or not self.__var.name: 910 return "" 911 return self.__var.name 912 913 def negated(self) -> "_NotBooleanVariable": 914 """Returns the negation of a Boolean variable. 915 916 This method implements the logical negation of a Boolean variable. 917 It is only valid if the variable has a Boolean domain (0 or 1). 918 919 Note that this method is nilpotent: `x.negated().negated() == x`. 920 """ 921 922 for bound in self.__var.domain: 923 if bound < 0 or bound > 1: 924 raise TypeError( 925 f"cannot call negated on a non boolean variable: {self}" 926 ) 927 if self.__negation is None: 928 self.__negation = _NotBooleanVariable(self) 929 return self.__negation 930 931 def __invert__(self) -> "_NotBooleanVariable": 932 """Returns the logical negation of a Boolean variable.""" 933 return self.negated() 934 935 # Pre PEP8 compatibility. 936 # pylint: disable=invalid-name 937 Not = negated 938 939 def Name(self) -> str: 940 return self.name 941 942 def Proto(self) -> cp_model_pb2.IntegerVariableProto: 943 return self.proto 944 945 def Index(self) -> int: 946 return self.index 947 948 # pylint: enable=invalid-name
An integer variable.
An IntVar is an object that can take on any integer value within defined ranges. Variables appear in constraint like:
x + y >= 5
AllDifferent([x, y, z])
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.
847 def __init__( 848 self, 849 model: cp_model_pb2.CpModelProto, 850 domain: Union[int, sorted_interval_list.Domain], 851 name: Optional[str], 852 ) -> None: 853 """See CpModel.new_int_var below.""" 854 self.__index: int 855 self.__var: cp_model_pb2.IntegerVariableProto 856 self.__negation: Optional[_NotBooleanVariable] = None 857 # Python do not support multiple __init__ methods. 858 # This method is only called from the CpModel class. 859 # We hack the parameter to support the two cases: 860 # case 1: 861 # model is a CpModelProto, domain is a Domain, and name is a string. 862 # case 2: 863 # model is a CpModelProto, domain is an index (int), and name is None. 864 if isinstance(domain, IntegralTypes) and name is None: 865 self.__index = int(domain) 866 self.__var = model.variables[domain] 867 else: 868 self.__index = len(model.variables) 869 self.__var = model.variables.add() 870 self.__var.domain.extend( 871 cast(sorted_interval_list.Domain, domain).flattened_intervals() 872 ) 873 if name is not None: 874 self.__var.name = name
See CpModel.new_int_var below.
876 @property 877 def index(self) -> int: 878 """Returns the index of the variable in the model.""" 879 return self.__index
Returns the index of the variable in the model.
881 @property 882 def proto(self) -> cp_model_pb2.IntegerVariableProto: 883 """Returns the variable protobuf.""" 884 return self.__var
Returns the variable protobuf.
886 def is_equal_to(self, other: Any) -> bool: 887 """Returns true if self == other in the python sense.""" 888 if not isinstance(other, IntVar): 889 return False 890 return self.index == other.index
Returns true if self == other in the python sense.
913 def negated(self) -> "_NotBooleanVariable": 914 """Returns the negation of a Boolean variable. 915 916 This method implements the logical negation of a Boolean variable. 917 It is only valid if the variable has a Boolean domain (0 or 1). 918 919 Note that this method is nilpotent: `x.negated().negated() == x`. 920 """ 921 922 for bound in self.__var.domain: 923 if bound < 0 or bound > 1: 924 raise TypeError( 925 f"cannot call negated on a non boolean variable: {self}" 926 ) 927 if self.__negation is None: 928 self.__negation = _NotBooleanVariable(self) 929 return self.__negation
Returns the negation of a Boolean variable.
This method implements the logical negation of a Boolean variable. It is only valid if the variable has a Boolean domain (0 or 1).
Note that this method is nilpotent: x.negated().negated() == x
.
913 def negated(self) -> "_NotBooleanVariable": 914 """Returns the negation of a Boolean variable. 915 916 This method implements the logical negation of a Boolean variable. 917 It is only valid if the variable has a Boolean domain (0 or 1). 918 919 Note that this method is nilpotent: `x.negated().negated() == x`. 920 """ 921 922 for bound in self.__var.domain: 923 if bound < 0 or bound > 1: 924 raise TypeError( 925 f"cannot call negated on a non boolean variable: {self}" 926 ) 927 if self.__negation is None: 928 self.__negation = _NotBooleanVariable(self) 929 return self.__negation
Returns the negation of a Boolean variable.
This method implements the logical negation of a Boolean variable. It is only valid if the variable has a Boolean domain (0 or 1).
Note that this method is nilpotent: x.negated().negated() == x
.
991class BoundedLinearExpression: 992 """Represents a linear constraint: `lb <= linear expression <= ub`. 993 994 The only use of this class is to be added to the CpModel through 995 `CpModel.add(expression)`, as in: 996 997 model.add(x + 2 * y -1 >= z) 998 """ 999 1000 def __init__(self, expr: LinearExprT, bounds: Sequence[int]) -> None: 1001 self.__expr: LinearExprT = expr 1002 self.__bounds: Sequence[int] = bounds 1003 1004 def __str__(self): 1005 if len(self.__bounds) == 2: 1006 lb, ub = self.__bounds 1007 if lb > INT_MIN and ub < INT_MAX: 1008 if lb == ub: 1009 return str(self.__expr) + " == " + str(lb) 1010 else: 1011 return str(lb) + " <= " + str(self.__expr) + " <= " + str(ub) 1012 elif lb > INT_MIN: 1013 return str(self.__expr) + " >= " + str(lb) 1014 elif ub < INT_MAX: 1015 return str(self.__expr) + " <= " + str(ub) 1016 else: 1017 return "True (unbounded expr " + str(self.__expr) + ")" 1018 elif ( 1019 len(self.__bounds) == 4 1020 and self.__bounds[0] == INT_MIN 1021 and self.__bounds[1] + 2 == self.__bounds[2] 1022 and self.__bounds[3] == INT_MAX 1023 ): 1024 return str(self.__expr) + " != " + str(self.__bounds[1] + 1) 1025 else: 1026 return str(self.__expr) + " in [" + display_bounds(self.__bounds) + "]" 1027 1028 def expression(self) -> LinearExprT: 1029 return self.__expr 1030 1031 def bounds(self) -> Sequence[int]: 1032 return self.__bounds 1033 1034 def __bool__(self) -> bool: 1035 expr = self.__expr 1036 if isinstance(expr, LinearExpr): 1037 coeffs_map, constant = expr.get_integer_var_value_map() 1038 all_coeffs = set(coeffs_map.values()) 1039 same_var = set([0]) 1040 eq_bounds = [0, 0] 1041 different_vars = set([-1, 1]) 1042 ne_bounds = [INT_MIN, -1, 1, INT_MAX] 1043 if ( 1044 len(coeffs_map) == 1 1045 and all_coeffs == same_var 1046 and constant == 0 1047 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1048 ): 1049 return self.__bounds == eq_bounds 1050 if ( 1051 len(coeffs_map) == 2 1052 and all_coeffs == different_vars 1053 and constant == 0 1054 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1055 ): 1056 return self.__bounds == ne_bounds 1057 1058 raise NotImplementedError( 1059 f'Evaluating a BoundedLinearExpression "{self}" as a Boolean value' 1060 + " is not supported." 1061 )
Represents a linear constraint: lb <= linear expression <= ub
.
The only use of this class is to be added to the CpModel through
CpModel.add(expression)
, as in:
model.add(x + 2 * y -1 >= z)
1064class Constraint: 1065 """Base class for constraints. 1066 1067 Constraints are built by the CpModel through the add<XXX> methods. 1068 Once created by the CpModel class, they are automatically added to the model. 1069 The purpose of this class is to allow specification of enforcement literals 1070 for this constraint. 1071 1072 b = model.new_bool_var('b') 1073 x = model.new_int_var(0, 10, 'x') 1074 y = model.new_int_var(0, 10, 'y') 1075 1076 model.add(x + 2 * y == 5).only_enforce_if(b.negated()) 1077 """ 1078 1079 def __init__( 1080 self, 1081 cp_model: "CpModel", 1082 ) -> None: 1083 self.__index: int = len(cp_model.proto.constraints) 1084 self.__cp_model: "CpModel" = cp_model 1085 self.__constraint: cp_model_pb2.ConstraintProto = ( 1086 cp_model.proto.constraints.add() 1087 ) 1088 1089 @overload 1090 def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ... 1091 1092 @overload 1093 def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ... 1094 1095 def only_enforce_if(self, *boolvar) -> "Constraint": 1096 """Adds an enforcement literal to the constraint. 1097 1098 This method adds one or more literals (that is, a boolean variable or its 1099 negation) as enforcement literals. The conjunction of all these literals 1100 determines whether the constraint is active or not. It acts as an 1101 implication, so if the conjunction is true, it implies that the constraint 1102 must be enforced. If it is false, then the constraint is ignored. 1103 1104 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1105 1106 Args: 1107 *boolvar: One or more Boolean literals. 1108 1109 Returns: 1110 self. 1111 """ 1112 for lit in expand_generator_or_tuple(boolvar): 1113 if (cmh.is_boolean(lit) and lit) or ( 1114 isinstance(lit, IntegralTypes) and lit == 1 1115 ): 1116 # Always true. Do nothing. 1117 pass 1118 elif (cmh.is_boolean(lit) and not lit) or ( 1119 isinstance(lit, IntegralTypes) and lit == 0 1120 ): 1121 self.__constraint.enforcement_literal.append( 1122 self.__cp_model.new_constant(0).index 1123 ) 1124 else: 1125 self.__constraint.enforcement_literal.append( 1126 cast(Union[IntVar, _NotBooleanVariable], lit).index 1127 ) 1128 return self 1129 1130 def with_name(self, name: str) -> "Constraint": 1131 """Sets the name of the constraint.""" 1132 if name: 1133 self.__constraint.name = name 1134 else: 1135 self.__constraint.ClearField("name") 1136 return self 1137 1138 @property 1139 def name(self) -> str: 1140 """Returns the name of the constraint.""" 1141 if not self.__constraint or not self.__constraint.name: 1142 return "" 1143 return self.__constraint.name 1144 1145 @property 1146 def index(self) -> int: 1147 """Returns the index of the constraint in the model.""" 1148 return self.__index 1149 1150 @property 1151 def proto(self) -> cp_model_pb2.ConstraintProto: 1152 """Returns the constraint protobuf.""" 1153 return self.__constraint 1154 1155 # Pre PEP8 compatibility. 1156 # pylint: disable=invalid-name 1157 OnlyEnforceIf = only_enforce_if 1158 WithName = with_name 1159 1160 def Name(self) -> str: 1161 return self.name 1162 1163 def Index(self) -> int: 1164 return self.index 1165 1166 def Proto(self) -> cp_model_pb2.ConstraintProto: 1167 return self.proto 1168 1169 # pylint: enable=invalid-name
Base class for constraints.
Constraints are built by the CpModel through the add
b = model.new_bool_var('b')
x = model.new_int_var(0, 10, 'x')
y = model.new_int_var(0, 10, 'y')
model.add(x + 2 * y == 5).only_enforce_if(b.negated())
1095 def only_enforce_if(self, *boolvar) -> "Constraint": 1096 """Adds an enforcement literal to the constraint. 1097 1098 This method adds one or more literals (that is, a boolean variable or its 1099 negation) as enforcement literals. The conjunction of all these literals 1100 determines whether the constraint is active or not. It acts as an 1101 implication, so if the conjunction is true, it implies that the constraint 1102 must be enforced. If it is false, then the constraint is ignored. 1103 1104 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1105 1106 Args: 1107 *boolvar: One or more Boolean literals. 1108 1109 Returns: 1110 self. 1111 """ 1112 for lit in expand_generator_or_tuple(boolvar): 1113 if (cmh.is_boolean(lit) and lit) or ( 1114 isinstance(lit, IntegralTypes) and lit == 1 1115 ): 1116 # Always true. Do nothing. 1117 pass 1118 elif (cmh.is_boolean(lit) and not lit) or ( 1119 isinstance(lit, IntegralTypes) and lit == 0 1120 ): 1121 self.__constraint.enforcement_literal.append( 1122 self.__cp_model.new_constant(0).index 1123 ) 1124 else: 1125 self.__constraint.enforcement_literal.append( 1126 cast(Union[IntVar, _NotBooleanVariable], lit).index 1127 ) 1128 return self
Adds an enforcement literal to the constraint.
This method adds one or more literals (that is, a boolean variable or its negation) as enforcement literals. The conjunction of all these literals determines whether the constraint is active or not. It acts as an implication, so if the conjunction is true, it implies that the constraint must be enforced. If it is false, then the constraint is ignored.
BoolOr, BoolAnd, and linear constraints all support enforcement literals.
Arguments:
- *boolvar: One or more Boolean literals.
Returns:
self.
1130 def with_name(self, name: str) -> "Constraint": 1131 """Sets the name of the constraint.""" 1132 if name: 1133 self.__constraint.name = name 1134 else: 1135 self.__constraint.ClearField("name") 1136 return self
Sets the name of the constraint.
1138 @property 1139 def name(self) -> str: 1140 """Returns the name of the constraint.""" 1141 if not self.__constraint or not self.__constraint.name: 1142 return "" 1143 return self.__constraint.name
Returns the name of the constraint.
1145 @property 1146 def index(self) -> int: 1147 """Returns the index of the constraint in the model.""" 1148 return self.__index
Returns the index of the constraint in the model.
1150 @property 1151 def proto(self) -> cp_model_pb2.ConstraintProto: 1152 """Returns the constraint protobuf.""" 1153 return self.__constraint
Returns the constraint protobuf.
1095 def only_enforce_if(self, *boolvar) -> "Constraint": 1096 """Adds an enforcement literal to the constraint. 1097 1098 This method adds one or more literals (that is, a boolean variable or its 1099 negation) as enforcement literals. The conjunction of all these literals 1100 determines whether the constraint is active or not. It acts as an 1101 implication, so if the conjunction is true, it implies that the constraint 1102 must be enforced. If it is false, then the constraint is ignored. 1103 1104 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1105 1106 Args: 1107 *boolvar: One or more Boolean literals. 1108 1109 Returns: 1110 self. 1111 """ 1112 for lit in expand_generator_or_tuple(boolvar): 1113 if (cmh.is_boolean(lit) and lit) or ( 1114 isinstance(lit, IntegralTypes) and lit == 1 1115 ): 1116 # Always true. Do nothing. 1117 pass 1118 elif (cmh.is_boolean(lit) and not lit) or ( 1119 isinstance(lit, IntegralTypes) and lit == 0 1120 ): 1121 self.__constraint.enforcement_literal.append( 1122 self.__cp_model.new_constant(0).index 1123 ) 1124 else: 1125 self.__constraint.enforcement_literal.append( 1126 cast(Union[IntVar, _NotBooleanVariable], lit).index 1127 ) 1128 return self
Adds an enforcement literal to the constraint.
This method adds one or more literals (that is, a boolean variable or its negation) as enforcement literals. The conjunction of all these literals determines whether the constraint is active or not. It acts as an implication, so if the conjunction is true, it implies that the constraint must be enforced. If it is false, then the constraint is ignored.
BoolOr, BoolAnd, and linear constraints all support enforcement literals.
Arguments:
- *boolvar: One or more Boolean literals.
Returns:
self.
1130 def with_name(self, name: str) -> "Constraint": 1131 """Sets the name of the constraint.""" 1132 if name: 1133 self.__constraint.name = name 1134 else: 1135 self.__constraint.ClearField("name") 1136 return self
Sets the name of the constraint.
1172class IntervalVar: 1173 """Represents an Interval variable. 1174 1175 An interval variable is both a constraint and a variable. It is defined by 1176 three integer variables: start, size, and end. 1177 1178 It is a constraint because, internally, it enforces that start + size == end. 1179 1180 It is also a variable as it can appear in specific scheduling constraints: 1181 NoOverlap, NoOverlap2D, Cumulative. 1182 1183 Optionally, an enforcement literal can be added to this constraint, in which 1184 case these scheduling constraints will ignore interval variables with 1185 enforcement literals assigned to false. Conversely, these constraints will 1186 also set these enforcement literals to false if they cannot fit these 1187 intervals into the schedule. 1188 1189 Raises: 1190 ValueError: if start, size, end are not defined, or have the wrong type. 1191 """ 1192 1193 def __init__( 1194 self, 1195 model: cp_model_pb2.CpModelProto, 1196 start: Union[cp_model_pb2.LinearExpressionProto, int], 1197 size: Optional[cp_model_pb2.LinearExpressionProto], 1198 end: Optional[cp_model_pb2.LinearExpressionProto], 1199 is_present_index: Optional[int], 1200 name: Optional[str], 1201 ) -> None: 1202 self.__model: cp_model_pb2.CpModelProto = model 1203 self.__index: int 1204 self.__ct: cp_model_pb2.ConstraintProto 1205 # As with the IntVar::__init__ method, we hack the __init__ method to 1206 # support two use cases: 1207 # case 1: called when creating a new interval variable. 1208 # {start|size|end} are linear expressions, is_present_index is either 1209 # None or the index of a Boolean literal. name is a string 1210 # case 2: called when querying an existing interval variable. 1211 # start_index is an int, all parameters after are None. 1212 if isinstance(start, int): 1213 if size is not None: 1214 raise ValueError("size should be None") 1215 if end is not None: 1216 raise ValueError("end should be None") 1217 if is_present_index is not None: 1218 raise ValueError("is_present_index should be None") 1219 self.__index = cast(int, start) 1220 self.__ct = model.constraints[self.__index] 1221 else: 1222 self.__index = len(model.constraints) 1223 self.__ct = self.__model.constraints.add() 1224 if start is None: 1225 raise TypeError("start is not defined") 1226 self.__ct.interval.start.CopyFrom(start) 1227 if size is None: 1228 raise TypeError("size is not defined") 1229 self.__ct.interval.size.CopyFrom(size) 1230 if end is None: 1231 raise TypeError("end is not defined") 1232 self.__ct.interval.end.CopyFrom(end) 1233 if is_present_index is not None: 1234 self.__ct.enforcement_literal.append(is_present_index) 1235 if name: 1236 self.__ct.name = name 1237 1238 @property 1239 def index(self) -> int: 1240 """Returns the index of the interval constraint in the model.""" 1241 return self.__index 1242 1243 @property 1244 def proto(self) -> cp_model_pb2.IntervalConstraintProto: 1245 """Returns the interval protobuf.""" 1246 return self.__ct.interval 1247 1248 def __str__(self): 1249 return self.__ct.name 1250 1251 def __repr__(self): 1252 interval = self.__ct.interval 1253 if self.__ct.enforcement_literal: 1254 return "%s(start = %s, size = %s, end = %s, is_present = %s)" % ( 1255 self.__ct.name, 1256 short_expr_name(self.__model, interval.start), 1257 short_expr_name(self.__model, interval.size), 1258 short_expr_name(self.__model, interval.end), 1259 short_name(self.__model, self.__ct.enforcement_literal[0]), 1260 ) 1261 else: 1262 return "%s(start = %s, size = %s, end = %s)" % ( 1263 self.__ct.name, 1264 short_expr_name(self.__model, interval.start), 1265 short_expr_name(self.__model, interval.size), 1266 short_expr_name(self.__model, interval.end), 1267 ) 1268 1269 @property 1270 def name(self) -> str: 1271 if not self.__ct or not self.__ct.name: 1272 return "" 1273 return self.__ct.name 1274 1275 def start_expr(self) -> LinearExprT: 1276 return LinearExpr.rebuild_from_linear_expression_proto( 1277 self.__model, self.__ct.interval.start 1278 ) 1279 1280 def size_expr(self) -> LinearExprT: 1281 return LinearExpr.rebuild_from_linear_expression_proto( 1282 self.__model, self.__ct.interval.size 1283 ) 1284 1285 def end_expr(self) -> LinearExprT: 1286 return LinearExpr.rebuild_from_linear_expression_proto( 1287 self.__model, self.__ct.interval.end 1288 ) 1289 1290 # Pre PEP8 compatibility. 1291 # pylint: disable=invalid-name 1292 def Name(self) -> str: 1293 return self.name 1294 1295 def Index(self) -> int: 1296 return self.index 1297 1298 def Proto(self) -> cp_model_pb2.IntervalConstraintProto: 1299 return self.proto 1300 1301 StartExpr = start_expr 1302 SizeExpr = size_expr 1303 EndExpr = end_expr 1304 1305 # pylint: enable=invalid-name
Represents an Interval variable.
An interval variable is both a constraint and a variable. It is defined by three integer variables: start, size, and end.
It is a constraint because, internally, it enforces that start + size == end.
It is also a variable as it can appear in specific scheduling constraints: NoOverlap, NoOverlap2D, Cumulative.
Optionally, an enforcement literal can be added to this constraint, in which case these scheduling constraints will ignore interval variables with enforcement literals assigned to false. Conversely, these constraints will also set these enforcement literals to false if they cannot fit these intervals into the schedule.
Raises:
- ValueError: if start, size, end are not defined, or have the wrong type.
1193 def __init__( 1194 self, 1195 model: cp_model_pb2.CpModelProto, 1196 start: Union[cp_model_pb2.LinearExpressionProto, int], 1197 size: Optional[cp_model_pb2.LinearExpressionProto], 1198 end: Optional[cp_model_pb2.LinearExpressionProto], 1199 is_present_index: Optional[int], 1200 name: Optional[str], 1201 ) -> None: 1202 self.__model: cp_model_pb2.CpModelProto = model 1203 self.__index: int 1204 self.__ct: cp_model_pb2.ConstraintProto 1205 # As with the IntVar::__init__ method, we hack the __init__ method to 1206 # support two use cases: 1207 # case 1: called when creating a new interval variable. 1208 # {start|size|end} are linear expressions, is_present_index is either 1209 # None or the index of a Boolean literal. name is a string 1210 # case 2: called when querying an existing interval variable. 1211 # start_index is an int, all parameters after are None. 1212 if isinstance(start, int): 1213 if size is not None: 1214 raise ValueError("size should be None") 1215 if end is not None: 1216 raise ValueError("end should be None") 1217 if is_present_index is not None: 1218 raise ValueError("is_present_index should be None") 1219 self.__index = cast(int, start) 1220 self.__ct = model.constraints[self.__index] 1221 else: 1222 self.__index = len(model.constraints) 1223 self.__ct = self.__model.constraints.add() 1224 if start is None: 1225 raise TypeError("start is not defined") 1226 self.__ct.interval.start.CopyFrom(start) 1227 if size is None: 1228 raise TypeError("size is not defined") 1229 self.__ct.interval.size.CopyFrom(size) 1230 if end is None: 1231 raise TypeError("end is not defined") 1232 self.__ct.interval.end.CopyFrom(end) 1233 if is_present_index is not None: 1234 self.__ct.enforcement_literal.append(is_present_index) 1235 if name: 1236 self.__ct.name = name
1238 @property 1239 def index(self) -> int: 1240 """Returns the index of the interval constraint in the model.""" 1241 return self.__index
Returns the index of the interval constraint in the model.
1243 @property 1244 def proto(self) -> cp_model_pb2.IntervalConstraintProto: 1245 """Returns the interval protobuf.""" 1246 return self.__ct.interval
Returns the interval protobuf.
1308def object_is_a_true_literal(literal: LiteralT) -> bool: 1309 """Checks if literal is either True, or a Boolean literals fixed to True.""" 1310 if isinstance(literal, IntVar): 1311 proto = literal.proto 1312 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1313 if isinstance(literal, _NotBooleanVariable): 1314 proto = literal.negated().proto 1315 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1316 if isinstance(literal, IntegralTypes): 1317 return int(literal) == 1 1318 return False
Checks if literal is either True, or a Boolean literals fixed to True.
1321def object_is_a_false_literal(literal: LiteralT) -> bool: 1322 """Checks if literal is either False, or a Boolean literals fixed to False.""" 1323 if isinstance(literal, IntVar): 1324 proto = literal.proto 1325 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1326 if isinstance(literal, _NotBooleanVariable): 1327 proto = literal.negated().proto 1328 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1329 if isinstance(literal, IntegralTypes): 1330 return int(literal) == 0 1331 return False
Checks if literal is either False, or a Boolean literals fixed to False.
1334class CpModel: 1335 """Methods for building a CP model. 1336 1337 Methods beginning with: 1338 1339 * ```New``` create integer, boolean, or interval variables. 1340 * ```add``` create new constraints and add them to the model. 1341 """ 1342 1343 def __init__(self) -> None: 1344 self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() 1345 self.__constant_map: Dict[IntegralT, int] = {} 1346 1347 # Naming. 1348 @property 1349 def name(self) -> str: 1350 """Returns the name of the model.""" 1351 if not self.__model or not self.__model.name: 1352 return "" 1353 return self.__model.name 1354 1355 @name.setter 1356 def name(self, name: str): 1357 """Sets the name of the model.""" 1358 self.__model.name = name 1359 1360 # Integer variable. 1361 1362 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1363 """Create an integer variable with domain [lb, ub]. 1364 1365 The CP-SAT solver is limited to integer variables. If you have fractional 1366 values, scale them up so that they become integers; if you have strings, 1367 encode them as integers. 1368 1369 Args: 1370 lb: Lower bound for the variable. 1371 ub: Upper bound for the variable. 1372 name: The name of the variable. 1373 1374 Returns: 1375 a variable whose domain is [lb, ub]. 1376 """ 1377 1378 return IntVar(self.__model, sorted_interval_list.Domain(lb, ub), name) 1379 1380 def new_int_var_from_domain( 1381 self, domain: sorted_interval_list.Domain, name: str 1382 ) -> IntVar: 1383 """Create an integer variable from a domain. 1384 1385 A domain is a set of integers specified by a collection of intervals. 1386 For example, `model.new_int_var_from_domain(cp_model. 1387 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1388 1389 Args: 1390 domain: An instance of the Domain class. 1391 name: The name of the variable. 1392 1393 Returns: 1394 a variable whose domain is the given domain. 1395 """ 1396 return IntVar(self.__model, domain, name) 1397 1398 def new_bool_var(self, name: str) -> IntVar: 1399 """Creates a 0-1 variable with the given name.""" 1400 return IntVar(self.__model, sorted_interval_list.Domain(0, 1), name) 1401 1402 def new_constant(self, value: IntegralT) -> IntVar: 1403 """Declares a constant integer.""" 1404 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None) 1405 1406 def new_int_var_series( 1407 self, 1408 name: str, 1409 index: pd.Index, 1410 lower_bounds: Union[IntegralT, pd.Series], 1411 upper_bounds: Union[IntegralT, pd.Series], 1412 ) -> pd.Series: 1413 """Creates a series of (scalar-valued) variables with the given name. 1414 1415 Args: 1416 name (str): Required. The name of the variable set. 1417 index (pd.Index): Required. The index to use for the variable set. 1418 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1419 set. If a `pd.Series` is passed in, it will be based on the 1420 corresponding values of the pd.Series. 1421 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1422 set. If a `pd.Series` is passed in, it will be based on the 1423 corresponding values of the pd.Series. 1424 1425 Returns: 1426 pd.Series: The variable set indexed by its corresponding dimensions. 1427 1428 Raises: 1429 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1430 ValueError: if the `name` is not a valid identifier or already exists. 1431 ValueError: if the `lowerbound` is greater than the `upperbound`. 1432 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1433 the input index. 1434 """ 1435 if not isinstance(index, pd.Index): 1436 raise TypeError("Non-index object is used as index") 1437 if not name.isidentifier(): 1438 raise ValueError("name={} is not a valid identifier".format(name)) 1439 if ( 1440 isinstance(lower_bounds, IntegralTypes) 1441 and isinstance(upper_bounds, IntegralTypes) 1442 and lower_bounds > upper_bounds 1443 ): 1444 raise ValueError( 1445 f"lower_bound={lower_bounds} is greater than" 1446 f" upper_bound={upper_bounds} for variable set={name}" 1447 ) 1448 1449 lower_bounds = _convert_to_integral_series_and_validate_index( 1450 lower_bounds, index 1451 ) 1452 upper_bounds = _convert_to_integral_series_and_validate_index( 1453 upper_bounds, index 1454 ) 1455 return pd.Series( 1456 index=index, 1457 data=[ 1458 # pylint: disable=g-complex-comprehension 1459 IntVar( 1460 model=self.__model, 1461 name=f"{name}[{i}]", 1462 domain=sorted_interval_list.Domain( 1463 lower_bounds[i], upper_bounds[i] 1464 ), 1465 ) 1466 for i in index 1467 ], 1468 ) 1469 1470 def new_bool_var_series( 1471 self, 1472 name: str, 1473 index: pd.Index, 1474 ) -> pd.Series: 1475 """Creates a series of (scalar-valued) variables with the given name. 1476 1477 Args: 1478 name (str): Required. The name of the variable set. 1479 index (pd.Index): Required. The index to use for the variable set. 1480 1481 Returns: 1482 pd.Series: The variable set indexed by its corresponding dimensions. 1483 1484 Raises: 1485 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1486 ValueError: if the `name` is not a valid identifier or already exists. 1487 """ 1488 return self.new_int_var_series( 1489 name=name, index=index, lower_bounds=0, upper_bounds=1 1490 ) 1491 1492 # Linear constraints. 1493 1494 def add_linear_constraint( 1495 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1496 ) -> Constraint: 1497 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1498 return self.add_linear_expression_in_domain( 1499 linear_expr, sorted_interval_list.Domain(lb, ub) 1500 ) 1501 1502 def add_linear_expression_in_domain( 1503 self, linear_expr: LinearExprT, domain: sorted_interval_list.Domain 1504 ) -> Constraint: 1505 """Adds the constraint: `linear_expr` in `domain`.""" 1506 if isinstance(linear_expr, LinearExpr): 1507 ct = Constraint(self) 1508 model_ct = self.__model.constraints[ct.index] 1509 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1510 for t in coeffs_map.items(): 1511 if not isinstance(t[0], IntVar): 1512 raise TypeError("Wrong argument" + str(t)) 1513 c = cmh.assert_is_int64(t[1]) 1514 model_ct.linear.vars.append(t[0].index) 1515 model_ct.linear.coeffs.append(c) 1516 model_ct.linear.domain.extend( 1517 [ 1518 cmh.capped_subtraction(x, constant) 1519 for x in domain.flattened_intervals() 1520 ] 1521 ) 1522 return ct 1523 if isinstance(linear_expr, IntegralTypes): 1524 if not domain.contains(int(linear_expr)): 1525 return self.add_bool_or([]) # Evaluate to false. 1526 else: 1527 return self.add_bool_and([]) # Evaluate to true. 1528 raise TypeError( 1529 "not supported: CpModel.add_linear_expression_in_domain(" 1530 + str(linear_expr) 1531 + " " 1532 + str(domain) 1533 + ")" 1534 ) 1535 1536 @overload 1537 def add(self, ct: BoundedLinearExpression) -> Constraint: ... 1538 1539 @overload 1540 def add(self, ct: Union[bool, np.bool_]) -> Constraint: ... 1541 1542 def add(self, ct): 1543 """Adds a `BoundedLinearExpression` to the model. 1544 1545 Args: 1546 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1547 1548 Returns: 1549 An instance of the `Constraint` class. 1550 """ 1551 if isinstance(ct, BoundedLinearExpression): 1552 return self.add_linear_expression_in_domain( 1553 ct.expression(), 1554 sorted_interval_list.Domain.from_flat_intervals(ct.bounds()), 1555 ) 1556 if ct and cmh.is_boolean(ct): 1557 return self.add_bool_or([True]) 1558 if not ct and cmh.is_boolean(ct): 1559 return self.add_bool_or([]) # Evaluate to false. 1560 raise TypeError("not supported: CpModel.add(" + str(ct) + ")") 1561 1562 # General Integer Constraints. 1563 1564 @overload 1565 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ... 1566 1567 @overload 1568 def add_all_different(self, *expressions: LinearExprT) -> Constraint: ... 1569 1570 def add_all_different(self, *expressions): 1571 """Adds AllDifferent(expressions). 1572 1573 This constraint forces all expressions to have different values. 1574 1575 Args: 1576 *expressions: simple expressions of the form a * var + constant. 1577 1578 Returns: 1579 An instance of the `Constraint` class. 1580 """ 1581 ct = Constraint(self) 1582 model_ct = self.__model.constraints[ct.index] 1583 expanded = expand_generator_or_tuple(expressions) 1584 model_ct.all_diff.exprs.extend( 1585 self.parse_linear_expression(x) for x in expanded 1586 ) 1587 return ct 1588 1589 def add_element( 1590 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1591 ) -> Constraint: 1592 """Adds the element constraint: `variables[index] == target`. 1593 1594 Args: 1595 index: The index of the variable that's being constrained. 1596 variables: A list of variables. 1597 target: The value that the variable must be equal to. 1598 1599 Returns: 1600 An instance of the `Constraint` class. 1601 """ 1602 1603 if not variables: 1604 raise ValueError("add_element expects a non-empty variables array") 1605 1606 if isinstance(index, IntegralTypes): 1607 variable: VariableT = list(variables)[int(index)] 1608 return self.add(variable == target) 1609 1610 ct = Constraint(self) 1611 model_ct = self.__model.constraints[ct.index] 1612 model_ct.element.index = self.get_or_make_index(index) 1613 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1614 model_ct.element.target = self.get_or_make_index(target) 1615 return ct 1616 1617 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1618 """Adds Circuit(arcs). 1619 1620 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1621 1622 A circuit is a unique Hamiltonian path in a subgraph of the total 1623 graph. In case a node 'i' is not in the path, then there must be a 1624 loop arc 'i -> i' associated with a true literal. Otherwise 1625 this constraint will fail. 1626 1627 Args: 1628 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1629 literal). The arc is selected in the circuit if the literal is true. 1630 Both source_node and destination_node must be integers between 0 and the 1631 number of nodes - 1. 1632 1633 Returns: 1634 An instance of the `Constraint` class. 1635 1636 Raises: 1637 ValueError: If the list of arcs is empty. 1638 """ 1639 if not arcs: 1640 raise ValueError("add_circuit expects a non-empty array of arcs") 1641 ct = Constraint(self) 1642 model_ct = self.__model.constraints[ct.index] 1643 for arc in arcs: 1644 tail = cmh.assert_is_int32(arc[0]) 1645 head = cmh.assert_is_int32(arc[1]) 1646 lit = self.get_or_make_boolean_index(arc[2]) 1647 model_ct.circuit.tails.append(tail) 1648 model_ct.circuit.heads.append(head) 1649 model_ct.circuit.literals.append(lit) 1650 return ct 1651 1652 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1653 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1654 1655 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1656 literals[i] is true must satisfy this set of properties: 1657 - #incoming arcs == 1 except for node 0. 1658 - #outgoing arcs == 1 except for node 0. 1659 - for node zero, #incoming arcs == #outgoing arcs. 1660 - There are no duplicate arcs. 1661 - Self-arcs are allowed except for node 0. 1662 - There is no cycle in this graph, except through node 0. 1663 1664 Args: 1665 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1666 literal). The arc is selected in the circuit if the literal is true. 1667 Both source_node and destination_node must be integers between 0 and the 1668 number of nodes - 1. 1669 1670 Returns: 1671 An instance of the `Constraint` class. 1672 1673 Raises: 1674 ValueError: If the list of arcs is empty. 1675 """ 1676 if not arcs: 1677 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1678 ct = Constraint(self) 1679 model_ct = self.__model.constraints[ct.index] 1680 for arc in arcs: 1681 tail = cmh.assert_is_int32(arc[0]) 1682 head = cmh.assert_is_int32(arc[1]) 1683 lit = self.get_or_make_boolean_index(arc[2]) 1684 model_ct.routes.tails.append(tail) 1685 model_ct.routes.heads.append(head) 1686 model_ct.routes.literals.append(lit) 1687 return ct 1688 1689 def add_allowed_assignments( 1690 self, 1691 variables: Sequence[VariableT], 1692 tuples_list: Iterable[Sequence[IntegralT]], 1693 ) -> Constraint: 1694 """Adds AllowedAssignments(variables, tuples_list). 1695 1696 An AllowedAssignments constraint is a constraint on an array of variables, 1697 which requires that when all variables are assigned values, the resulting 1698 array equals one of the tuples in `tuple_list`. 1699 1700 Args: 1701 variables: A list of variables. 1702 tuples_list: A list of admissible tuples. Each tuple must have the same 1703 length as the variables, and the ith value of a tuple corresponds to the 1704 ith variable. 1705 1706 Returns: 1707 An instance of the `Constraint` class. 1708 1709 Raises: 1710 TypeError: If a tuple does not have the same size as the list of 1711 variables. 1712 ValueError: If the array of variables is empty. 1713 """ 1714 1715 if not variables: 1716 raise ValueError( 1717 "add_allowed_assignments expects a non-empty variables array" 1718 ) 1719 1720 ct: Constraint = Constraint(self) 1721 model_ct = self.__model.constraints[ct.index] 1722 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1723 arity: int = len(variables) 1724 for t in tuples_list: 1725 if len(t) != arity: 1726 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1727 1728 # duck-typing (no explicit type checks here) 1729 try: 1730 model_ct.table.values.extend(a for b in tuples_list for a in b) 1731 except ValueError as ex: 1732 raise TypeError(f"add_xxx_assignment: Not an integer or does not fit in an int64_t: {ex.args}") from ex 1733 1734 return ct 1735 1736 def add_forbidden_assignments( 1737 self, 1738 variables: Sequence[VariableT], 1739 tuples_list: Iterable[Sequence[IntegralT]], 1740 ) -> Constraint: 1741 """Adds add_forbidden_assignments(variables, [tuples_list]). 1742 1743 A ForbiddenAssignments constraint is a constraint on an array of variables 1744 where the list of impossible combinations is provided in the tuples list. 1745 1746 Args: 1747 variables: A list of variables. 1748 tuples_list: A list of forbidden tuples. Each tuple must have the same 1749 length as the variables, and the *i*th value of a tuple corresponds to 1750 the *i*th variable. 1751 1752 Returns: 1753 An instance of the `Constraint` class. 1754 1755 Raises: 1756 TypeError: If a tuple does not have the same size as the list of 1757 variables. 1758 ValueError: If the array of variables is empty. 1759 """ 1760 1761 if not variables: 1762 raise ValueError( 1763 "add_forbidden_assignments expects a non-empty variables array" 1764 ) 1765 1766 index = len(self.__model.constraints) 1767 ct: Constraint = self.add_allowed_assignments(variables, tuples_list) 1768 self.__model.constraints[index].table.negated = True 1769 return ct 1770 1771 def add_automaton( 1772 self, 1773 transition_variables: Sequence[VariableT], 1774 starting_state: IntegralT, 1775 final_states: Sequence[IntegralT], 1776 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1777 ) -> Constraint: 1778 """Adds an automaton constraint. 1779 1780 An automaton constraint takes a list of variables (of size *n*), an initial 1781 state, a set of final states, and a set of transitions. A transition is a 1782 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1783 and *transition* is the label of an arc from *head* to *tail*, 1784 corresponding to the value of one variable in the list of variables. 1785 1786 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1787 contains the possible states of the automaton. The first state contains the 1788 initial state. The last phase contains the final states. 1789 1790 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1791 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1792 an arc from the state *tail* of phase *i* and the state *head* of phase 1793 *i* + 1. This arc is labeled by the value *transition* of the variables 1794 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1795 is assigned the value *transition*. 1796 1797 A feasible solution of this constraint is an assignment of variables such 1798 that, starting from the initial state in phase 0, there is a path labeled by 1799 the values of the variables that ends in one of the final states in the 1800 final phase. 1801 1802 Args: 1803 transition_variables: A non-empty list of variables whose values 1804 correspond to the labels of the arcs traversed by the automaton. 1805 starting_state: The initial state of the automaton. 1806 final_states: A non-empty list of admissible final states. 1807 transition_triples: A list of transitions for the automaton, in the 1808 following format (current_state, variable_value, next_state). 1809 1810 Returns: 1811 An instance of the `Constraint` class. 1812 1813 Raises: 1814 ValueError: if `transition_variables`, `final_states`, or 1815 `transition_triples` are empty. 1816 """ 1817 1818 if not transition_variables: 1819 raise ValueError( 1820 "add_automaton expects a non-empty transition_variables array" 1821 ) 1822 if not final_states: 1823 raise ValueError("add_automaton expects some final states") 1824 1825 if not transition_triples: 1826 raise ValueError("add_automaton expects some transition triples") 1827 1828 ct = Constraint(self) 1829 model_ct = self.__model.constraints[ct.index] 1830 model_ct.automaton.vars.extend( 1831 [self.get_or_make_index(x) for x in transition_variables] 1832 ) 1833 starting_state = cmh.assert_is_int64(starting_state) 1834 model_ct.automaton.starting_state = starting_state 1835 for v in final_states: 1836 v = cmh.assert_is_int64(v) 1837 model_ct.automaton.final_states.append(v) 1838 for t in transition_triples: 1839 if len(t) != 3: 1840 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1841 tail = cmh.assert_is_int64(t[0]) 1842 label = cmh.assert_is_int64(t[1]) 1843 head = cmh.assert_is_int64(t[2]) 1844 model_ct.automaton.transition_tail.append(tail) 1845 model_ct.automaton.transition_label.append(label) 1846 model_ct.automaton.transition_head.append(head) 1847 return ct 1848 1849 def add_inverse( 1850 self, 1851 variables: Sequence[VariableT], 1852 inverse_variables: Sequence[VariableT], 1853 ) -> Constraint: 1854 """Adds Inverse(variables, inverse_variables). 1855 1856 An inverse constraint enforces that if `variables[i]` is assigned a value 1857 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1858 1859 Args: 1860 variables: An array of integer variables. 1861 inverse_variables: An array of integer variables. 1862 1863 Returns: 1864 An instance of the `Constraint` class. 1865 1866 Raises: 1867 TypeError: if variables and inverse_variables have different lengths, or 1868 if they are empty. 1869 """ 1870 1871 if not variables or not inverse_variables: 1872 raise TypeError("The Inverse constraint does not accept empty arrays") 1873 if len(variables) != len(inverse_variables): 1874 raise TypeError( 1875 "In the inverse constraint, the two array variables and" 1876 " inverse_variables must have the same length." 1877 ) 1878 ct = Constraint(self) 1879 model_ct = self.__model.constraints[ct.index] 1880 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1881 model_ct.inverse.f_inverse.extend( 1882 [self.get_or_make_index(x) for x in inverse_variables] 1883 ) 1884 return ct 1885 1886 def add_reservoir_constraint( 1887 self, 1888 times: Iterable[LinearExprT], 1889 level_changes: Iterable[LinearExprT], 1890 min_level: int, 1891 max_level: int, 1892 ) -> Constraint: 1893 """Adds Reservoir(times, level_changes, min_level, max_level). 1894 1895 Maintains a reservoir level within bounds. The water level starts at 0, and 1896 at any time, it must be between min_level and max_level. 1897 1898 If the affine expression `times[i]` is assigned a value t, then the current 1899 level changes by `level_changes[i]`, which is constant, at time t. 1900 1901 Note that min level must be <= 0, and the max level must be >= 0. Please 1902 use fixed level_changes to simulate initial state. 1903 1904 Therefore, at any time: 1905 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1906 1907 Args: 1908 times: A list of 1-var affine expressions (a * x + b) which specify the 1909 time of the filling or emptying the reservoir. 1910 level_changes: A list of integer values that specifies the amount of the 1911 emptying or filling. Currently, variable demands are not supported. 1912 min_level: At any time, the level of the reservoir must be greater or 1913 equal than the min level. 1914 max_level: At any time, the level of the reservoir must be less or equal 1915 than the max level. 1916 1917 Returns: 1918 An instance of the `Constraint` class. 1919 1920 Raises: 1921 ValueError: if max_level < min_level. 1922 1923 ValueError: if max_level < 0. 1924 1925 ValueError: if min_level > 0 1926 """ 1927 1928 if max_level < min_level: 1929 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1930 1931 if max_level < 0: 1932 raise ValueError("Reservoir constraint must have a max_level >= 0") 1933 1934 if min_level > 0: 1935 raise ValueError("Reservoir constraint must have a min_level <= 0") 1936 1937 ct = Constraint(self) 1938 model_ct = self.__model.constraints[ct.index] 1939 model_ct.reservoir.time_exprs.extend( 1940 [self.parse_linear_expression(x) for x in times] 1941 ) 1942 model_ct.reservoir.level_changes.extend( 1943 [self.parse_linear_expression(x) for x in level_changes] 1944 ) 1945 model_ct.reservoir.min_level = min_level 1946 model_ct.reservoir.max_level = max_level 1947 return ct 1948 1949 def add_reservoir_constraint_with_active( 1950 self, 1951 times: Iterable[LinearExprT], 1952 level_changes: Iterable[LinearExprT], 1953 actives: Iterable[LiteralT], 1954 min_level: int, 1955 max_level: int, 1956 ) -> Constraint: 1957 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1958 1959 Maintains a reservoir level within bounds. The water level starts at 0, and 1960 at any time, it must be between min_level and max_level. 1961 1962 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1963 `True`, then the current level changes by `level_changes[i]`, which is 1964 constant, 1965 at time t. 1966 1967 Note that min level must be <= 0, and the max level must be >= 0. Please 1968 use fixed level_changes to simulate initial state. 1969 1970 Therefore, at any time: 1971 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1972 max_level] 1973 1974 1975 The array of boolean variables 'actives', if defined, indicates which 1976 actions are actually performed. 1977 1978 Args: 1979 times: A list of 1-var affine expressions (a * x + b) which specify the 1980 time of the filling or emptying the reservoir. 1981 level_changes: A list of integer values that specifies the amount of the 1982 emptying or filling. Currently, variable demands are not supported. 1983 actives: a list of boolean variables. They indicates if the 1984 emptying/refilling events actually take place. 1985 min_level: At any time, the level of the reservoir must be greater or 1986 equal than the min level. 1987 max_level: At any time, the level of the reservoir must be less or equal 1988 than the max level. 1989 1990 Returns: 1991 An instance of the `Constraint` class. 1992 1993 Raises: 1994 ValueError: if max_level < min_level. 1995 1996 ValueError: if max_level < 0. 1997 1998 ValueError: if min_level > 0 1999 """ 2000 2001 if max_level < min_level: 2002 raise ValueError("Reservoir constraint must have a max_level >= min_level") 2003 2004 if max_level < 0: 2005 raise ValueError("Reservoir constraint must have a max_level >= 0") 2006 2007 if min_level > 0: 2008 raise ValueError("Reservoir constraint must have a min_level <= 0") 2009 2010 ct = Constraint(self) 2011 model_ct = self.__model.constraints[ct.index] 2012 model_ct.reservoir.time_exprs.extend( 2013 [self.parse_linear_expression(x) for x in times] 2014 ) 2015 model_ct.reservoir.level_changes.extend( 2016 [self.parse_linear_expression(x) for x in level_changes] 2017 ) 2018 model_ct.reservoir.active_literals.extend( 2019 [self.get_or_make_boolean_index(x) for x in actives] 2020 ) 2021 model_ct.reservoir.min_level = min_level 2022 model_ct.reservoir.max_level = max_level 2023 return ct 2024 2025 def add_map_domain( 2026 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 2027 ): 2028 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 2029 2030 for i, bool_var in enumerate(bool_var_array): 2031 b_index = bool_var.index 2032 var_index = var.index 2033 model_ct = self.__model.constraints.add() 2034 model_ct.linear.vars.append(var_index) 2035 model_ct.linear.coeffs.append(1) 2036 offset_as_int = int(offset) 2037 model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) 2038 model_ct.enforcement_literal.append(b_index) 2039 2040 model_ct = self.__model.constraints.add() 2041 model_ct.linear.vars.append(var_index) 2042 model_ct.linear.coeffs.append(1) 2043 model_ct.enforcement_literal.append(-b_index - 1) 2044 if offset + i - 1 >= INT_MIN: 2045 model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) 2046 if offset + i + 1 <= INT_MAX: 2047 model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX]) 2048 2049 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 2050 """Adds `a => b` (`a` implies `b`).""" 2051 ct = Constraint(self) 2052 model_ct = self.__model.constraints[ct.index] 2053 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2054 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2055 return ct 2056 2057 @overload 2058 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ... 2059 2060 @overload 2061 def add_bool_or(self, *literals: LiteralT) -> Constraint: ... 2062 2063 def add_bool_or(self, *literals): 2064 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2065 ct = Constraint(self) 2066 model_ct = self.__model.constraints[ct.index] 2067 model_ct.bool_or.literals.extend( 2068 [ 2069 self.get_or_make_boolean_index(x) 2070 for x in expand_generator_or_tuple(literals) 2071 ] 2072 ) 2073 return ct 2074 2075 @overload 2076 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 2077 2078 @overload 2079 def add_at_least_one(self, *literals: LiteralT) -> Constraint: ... 2080 2081 def add_at_least_one(self, *literals): 2082 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2083 return self.add_bool_or(*literals) 2084 2085 @overload 2086 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 2087 2088 @overload 2089 def add_at_most_one(self, *literals: LiteralT) -> Constraint: ... 2090 2091 def add_at_most_one(self, *literals): 2092 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2093 ct = Constraint(self) 2094 model_ct = self.__model.constraints[ct.index] 2095 model_ct.at_most_one.literals.extend( 2096 [ 2097 self.get_or_make_boolean_index(x) 2098 for x in expand_generator_or_tuple(literals) 2099 ] 2100 ) 2101 return ct 2102 2103 @overload 2104 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 2105 2106 @overload 2107 def add_exactly_one(self, *literals: LiteralT) -> Constraint: ... 2108 2109 def add_exactly_one(self, *literals): 2110 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2111 ct = Constraint(self) 2112 model_ct = self.__model.constraints[ct.index] 2113 model_ct.exactly_one.literals.extend( 2114 [ 2115 self.get_or_make_boolean_index(x) 2116 for x in expand_generator_or_tuple(literals) 2117 ] 2118 ) 2119 return ct 2120 2121 @overload 2122 def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ... 2123 2124 @overload 2125 def add_bool_and(self, *literals: LiteralT) -> Constraint: ... 2126 2127 def add_bool_and(self, *literals): 2128 """Adds `And(literals) == true`.""" 2129 ct = Constraint(self) 2130 model_ct = self.__model.constraints[ct.index] 2131 model_ct.bool_and.literals.extend( 2132 [ 2133 self.get_or_make_boolean_index(x) 2134 for x in expand_generator_or_tuple(literals) 2135 ] 2136 ) 2137 return ct 2138 2139 @overload 2140 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ... 2141 2142 @overload 2143 def add_bool_xor(self, *literals: LiteralT) -> Constraint: ... 2144 2145 def add_bool_xor(self, *literals): 2146 """Adds `XOr(literals) == true`. 2147 2148 In contrast to add_bool_or and add_bool_and, it does not support 2149 .only_enforce_if(). 2150 2151 Args: 2152 *literals: the list of literals in the constraint. 2153 2154 Returns: 2155 An `Constraint` object. 2156 """ 2157 ct = Constraint(self) 2158 model_ct = self.__model.constraints[ct.index] 2159 model_ct.bool_xor.literals.extend( 2160 [ 2161 self.get_or_make_boolean_index(x) 2162 for x in expand_generator_or_tuple(literals) 2163 ] 2164 ) 2165 return ct 2166 2167 def add_min_equality( 2168 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2169 ) -> Constraint: 2170 """Adds `target == Min(exprs)`.""" 2171 ct = Constraint(self) 2172 model_ct = self.__model.constraints[ct.index] 2173 model_ct.lin_max.exprs.extend( 2174 [self.parse_linear_expression(x, True) for x in exprs] 2175 ) 2176 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2177 return ct 2178 2179 def add_max_equality( 2180 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2181 ) -> Constraint: 2182 """Adds `target == Max(exprs)`.""" 2183 ct = Constraint(self) 2184 model_ct = self.__model.constraints[ct.index] 2185 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2186 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2187 return ct 2188 2189 def add_division_equality( 2190 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2191 ) -> Constraint: 2192 """Adds `target == num // denom` (integer division rounded towards 0).""" 2193 ct = Constraint(self) 2194 model_ct = self.__model.constraints[ct.index] 2195 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2196 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2197 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2198 return ct 2199 2200 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2201 """Adds `target == Abs(expr)`.""" 2202 ct = Constraint(self) 2203 model_ct = self.__model.constraints[ct.index] 2204 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2205 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2206 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2207 return ct 2208 2209 def add_modulo_equality( 2210 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2211 ) -> Constraint: 2212 """Adds `target = expr % mod`. 2213 2214 It uses the C convention, that is the result is the remainder of the 2215 integral division rounded towards 0. 2216 2217 For example: 2218 * 10 % 3 = 1 2219 * -10 % 3 = -1 2220 * 10 % -3 = 1 2221 * -10 % -3 = -1 2222 2223 Args: 2224 target: the target expression. 2225 expr: the expression to compute the modulo of. 2226 mod: the modulus expression. 2227 2228 Returns: 2229 A `Constraint` object. 2230 """ 2231 ct = Constraint(self) 2232 model_ct = self.__model.constraints[ct.index] 2233 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2234 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2235 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2236 return ct 2237 2238 def add_multiplication_equality( 2239 self, 2240 target: LinearExprT, 2241 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2242 ) -> Constraint: 2243 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2244 ct = Constraint(self) 2245 model_ct = self.__model.constraints[ct.index] 2246 model_ct.int_prod.exprs.extend( 2247 [ 2248 self.parse_linear_expression(expr) 2249 for expr in expand_generator_or_tuple(expressions) 2250 ] 2251 ) 2252 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2253 return ct 2254 2255 # Scheduling support 2256 2257 def new_interval_var( 2258 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2259 ) -> IntervalVar: 2260 """Creates an interval variable from start, size, and end. 2261 2262 An interval variable is a constraint, that is itself used in other 2263 constraints like NoOverlap. 2264 2265 Internally, it ensures that `start + size == end`. 2266 2267 Args: 2268 start: The start of the interval. It must be of the form a * var + b. 2269 size: The size of the interval. It must be of the form a * var + b. 2270 end: The end of the interval. It must be of the form a * var + b. 2271 name: The name of the interval variable. 2272 2273 Returns: 2274 An `IntervalVar` object. 2275 """ 2276 2277 start_expr = self.parse_linear_expression(start) 2278 size_expr = self.parse_linear_expression(size) 2279 end_expr = self.parse_linear_expression(end) 2280 if len(start_expr.vars) > 1: 2281 raise TypeError( 2282 "cp_model.new_interval_var: start must be 1-var affine or constant." 2283 ) 2284 if len(size_expr.vars) > 1: 2285 raise TypeError( 2286 "cp_model.new_interval_var: size must be 1-var affine or constant." 2287 ) 2288 if len(end_expr.vars) > 1: 2289 raise TypeError( 2290 "cp_model.new_interval_var: end must be 1-var affine or constant." 2291 ) 2292 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2293 2294 def new_interval_var_series( 2295 self, 2296 name: str, 2297 index: pd.Index, 2298 starts: Union[LinearExprT, pd.Series], 2299 sizes: Union[LinearExprT, pd.Series], 2300 ends: Union[LinearExprT, pd.Series], 2301 ) -> pd.Series: 2302 """Creates a series of interval variables with the given name. 2303 2304 Args: 2305 name (str): Required. The name of the variable set. 2306 index (pd.Index): Required. The index to use for the variable set. 2307 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2308 set. If a `pd.Series` is passed in, it will be based on the 2309 corresponding values of the pd.Series. 2310 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2311 set. If a `pd.Series` is passed in, it will be based on the 2312 corresponding values of the pd.Series. 2313 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2314 set. If a `pd.Series` is passed in, it will be based on the 2315 corresponding values of the pd.Series. 2316 2317 Returns: 2318 pd.Series: The interval variable set indexed by its corresponding 2319 dimensions. 2320 2321 Raises: 2322 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2323 ValueError: if the `name` is not a valid identifier or already exists. 2324 ValueError: if the all the indexes do not match. 2325 """ 2326 if not isinstance(index, pd.Index): 2327 raise TypeError("Non-index object is used as index") 2328 if not name.isidentifier(): 2329 raise ValueError("name={} is not a valid identifier".format(name)) 2330 2331 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2332 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2333 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2334 interval_array = [] 2335 for i in index: 2336 interval_array.append( 2337 self.new_interval_var( 2338 start=starts[i], 2339 size=sizes[i], 2340 end=ends[i], 2341 name=f"{name}[{i}]", 2342 ) 2343 ) 2344 return pd.Series(index=index, data=interval_array) 2345 2346 def new_fixed_size_interval_var( 2347 self, start: LinearExprT, size: IntegralT, name: str 2348 ) -> IntervalVar: 2349 """Creates an interval variable from start, and a fixed size. 2350 2351 An interval variable is a constraint, that is itself used in other 2352 constraints like NoOverlap. 2353 2354 Args: 2355 start: The start of the interval. It must be of the form a * var + b. 2356 size: The size of the interval. It must be an integer value. 2357 name: The name of the interval variable. 2358 2359 Returns: 2360 An `IntervalVar` object. 2361 """ 2362 size = cmh.assert_is_int64(size) 2363 start_expr = self.parse_linear_expression(start) 2364 size_expr = self.parse_linear_expression(size) 2365 end_expr = self.parse_linear_expression(start + size) 2366 if len(start_expr.vars) > 1: 2367 raise TypeError( 2368 "cp_model.new_interval_var: start must be affine or constant." 2369 ) 2370 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2371 2372 def new_fixed_size_interval_var_series( 2373 self, 2374 name: str, 2375 index: pd.Index, 2376 starts: Union[LinearExprT, pd.Series], 2377 sizes: Union[IntegralT, pd.Series], 2378 ) -> pd.Series: 2379 """Creates a series of interval variables with the given name. 2380 2381 Args: 2382 name (str): Required. The name of the variable set. 2383 index (pd.Index): Required. The index to use for the variable set. 2384 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2385 set. If a `pd.Series` is passed in, it will be based on the 2386 corresponding values of the pd.Series. 2387 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2388 the set. If a `pd.Series` is passed in, it will be based on the 2389 corresponding values of the pd.Series. 2390 2391 Returns: 2392 pd.Series: The interval variable set indexed by its corresponding 2393 dimensions. 2394 2395 Raises: 2396 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2397 ValueError: if the `name` is not a valid identifier or already exists. 2398 ValueError: if the all the indexes do not match. 2399 """ 2400 if not isinstance(index, pd.Index): 2401 raise TypeError("Non-index object is used as index") 2402 if not name.isidentifier(): 2403 raise ValueError("name={} is not a valid identifier".format(name)) 2404 2405 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2406 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2407 interval_array = [] 2408 for i in index: 2409 interval_array.append( 2410 self.new_fixed_size_interval_var( 2411 start=starts[i], 2412 size=sizes[i], 2413 name=f"{name}[{i}]", 2414 ) 2415 ) 2416 return pd.Series(index=index, data=interval_array) 2417 2418 def new_optional_interval_var( 2419 self, 2420 start: LinearExprT, 2421 size: LinearExprT, 2422 end: LinearExprT, 2423 is_present: LiteralT, 2424 name: str, 2425 ) -> IntervalVar: 2426 """Creates an optional interval var from start, size, end, and is_present. 2427 2428 An optional interval variable is a constraint, that is itself used in other 2429 constraints like NoOverlap. This constraint is protected by a presence 2430 literal that indicates if it is active or not. 2431 2432 Internally, it ensures that `is_present` implies `start + size == 2433 end`. 2434 2435 Args: 2436 start: The start of the interval. It must be of the form a * var + b. 2437 size: The size of the interval. It must be of the form a * var + b. 2438 end: The end of the interval. It must be of the form a * var + b. 2439 is_present: A literal that indicates if the interval is active or not. A 2440 inactive interval is simply ignored by all constraints. 2441 name: The name of the interval variable. 2442 2443 Returns: 2444 An `IntervalVar` object. 2445 """ 2446 2447 # Creates the IntervalConstraintProto object. 2448 is_present_index = self.get_or_make_boolean_index(is_present) 2449 start_expr = self.parse_linear_expression(start) 2450 size_expr = self.parse_linear_expression(size) 2451 end_expr = self.parse_linear_expression(end) 2452 if len(start_expr.vars) > 1: 2453 raise TypeError( 2454 "cp_model.new_interval_var: start must be affine or constant." 2455 ) 2456 if len(size_expr.vars) > 1: 2457 raise TypeError( 2458 "cp_model.new_interval_var: size must be affine or constant." 2459 ) 2460 if len(end_expr.vars) > 1: 2461 raise TypeError( 2462 "cp_model.new_interval_var: end must be affine or constant." 2463 ) 2464 return IntervalVar( 2465 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2466 ) 2467 2468 def new_optional_interval_var_series( 2469 self, 2470 name: str, 2471 index: pd.Index, 2472 starts: Union[LinearExprT, pd.Series], 2473 sizes: Union[LinearExprT, pd.Series], 2474 ends: Union[LinearExprT, pd.Series], 2475 are_present: Union[LiteralT, pd.Series], 2476 ) -> pd.Series: 2477 """Creates a series of interval variables with the given name. 2478 2479 Args: 2480 name (str): Required. The name of the variable set. 2481 index (pd.Index): Required. The index to use for the variable set. 2482 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2483 set. If a `pd.Series` is passed in, it will be based on the 2484 corresponding values of the pd.Series. 2485 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2486 set. If a `pd.Series` is passed in, it will be based on the 2487 corresponding values of the pd.Series. 2488 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2489 set. If a `pd.Series` is passed in, it will be based on the 2490 corresponding values of the pd.Series. 2491 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2492 interval in the set. If a `pd.Series` is passed in, it will be based on 2493 the corresponding values of the pd.Series. 2494 2495 Returns: 2496 pd.Series: The interval variable set indexed by its corresponding 2497 dimensions. 2498 2499 Raises: 2500 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2501 ValueError: if the `name` is not a valid identifier or already exists. 2502 ValueError: if the all the indexes do not match. 2503 """ 2504 if not isinstance(index, pd.Index): 2505 raise TypeError("Non-index object is used as index") 2506 if not name.isidentifier(): 2507 raise ValueError("name={} is not a valid identifier".format(name)) 2508 2509 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2510 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2511 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2512 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2513 2514 interval_array = [] 2515 for i in index: 2516 interval_array.append( 2517 self.new_optional_interval_var( 2518 start=starts[i], 2519 size=sizes[i], 2520 end=ends[i], 2521 is_present=are_present[i], 2522 name=f"{name}[{i}]", 2523 ) 2524 ) 2525 return pd.Series(index=index, data=interval_array) 2526 2527 def new_optional_fixed_size_interval_var( 2528 self, 2529 start: LinearExprT, 2530 size: IntegralT, 2531 is_present: LiteralT, 2532 name: str, 2533 ) -> IntervalVar: 2534 """Creates an interval variable from start, and a fixed size. 2535 2536 An interval variable is a constraint, that is itself used in other 2537 constraints like NoOverlap. 2538 2539 Args: 2540 start: The start of the interval. It must be of the form a * var + b. 2541 size: The size of the interval. It must be an integer value. 2542 is_present: A literal that indicates if the interval is active or not. A 2543 inactive interval is simply ignored by all constraints. 2544 name: The name of the interval variable. 2545 2546 Returns: 2547 An `IntervalVar` object. 2548 """ 2549 size = cmh.assert_is_int64(size) 2550 start_expr = self.parse_linear_expression(start) 2551 size_expr = self.parse_linear_expression(size) 2552 end_expr = self.parse_linear_expression(start + size) 2553 if len(start_expr.vars) > 1: 2554 raise TypeError( 2555 "cp_model.new_interval_var: start must be affine or constant." 2556 ) 2557 is_present_index = self.get_or_make_boolean_index(is_present) 2558 return IntervalVar( 2559 self.__model, 2560 start_expr, 2561 size_expr, 2562 end_expr, 2563 is_present_index, 2564 name, 2565 ) 2566 2567 def new_optional_fixed_size_interval_var_series( 2568 self, 2569 name: str, 2570 index: pd.Index, 2571 starts: Union[LinearExprT, pd.Series], 2572 sizes: Union[IntegralT, pd.Series], 2573 are_present: Union[LiteralT, pd.Series], 2574 ) -> pd.Series: 2575 """Creates a series of interval variables with the given name. 2576 2577 Args: 2578 name (str): Required. The name of the variable set. 2579 index (pd.Index): Required. The index to use for the variable set. 2580 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2581 set. If a `pd.Series` is passed in, it will be based on the 2582 corresponding values of the pd.Series. 2583 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2584 the set. If a `pd.Series` is passed in, it will be based on the 2585 corresponding values of the pd.Series. 2586 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2587 interval in the set. If a `pd.Series` is passed in, it will be based on 2588 the corresponding values of the pd.Series. 2589 2590 Returns: 2591 pd.Series: The interval variable set indexed by its corresponding 2592 dimensions. 2593 2594 Raises: 2595 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2596 ValueError: if the `name` is not a valid identifier or already exists. 2597 ValueError: if the all the indexes do not match. 2598 """ 2599 if not isinstance(index, pd.Index): 2600 raise TypeError("Non-index object is used as index") 2601 if not name.isidentifier(): 2602 raise ValueError("name={} is not a valid identifier".format(name)) 2603 2604 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2605 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2606 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2607 interval_array = [] 2608 for i in index: 2609 interval_array.append( 2610 self.new_optional_fixed_size_interval_var( 2611 start=starts[i], 2612 size=sizes[i], 2613 is_present=are_present[i], 2614 name=f"{name}[{i}]", 2615 ) 2616 ) 2617 return pd.Series(index=index, data=interval_array) 2618 2619 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2620 """Adds NoOverlap(interval_vars). 2621 2622 A NoOverlap constraint ensures that all present intervals do not overlap 2623 in time. 2624 2625 Args: 2626 interval_vars: The list of interval variables to constrain. 2627 2628 Returns: 2629 An instance of the `Constraint` class. 2630 """ 2631 ct = Constraint(self) 2632 model_ct = self.__model.constraints[ct.index] 2633 model_ct.no_overlap.intervals.extend( 2634 [self.get_interval_index(x) for x in interval_vars] 2635 ) 2636 return ct 2637 2638 def add_no_overlap_2d( 2639 self, 2640 x_intervals: Iterable[IntervalVar], 2641 y_intervals: Iterable[IntervalVar], 2642 ) -> Constraint: 2643 """Adds NoOverlap2D(x_intervals, y_intervals). 2644 2645 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2646 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2647 by two intervals which represent its projection onto the X and Y axis. 2648 2649 Furthermore, one box is optional if at least one of the x or y interval is 2650 optional. 2651 2652 Args: 2653 x_intervals: The X coordinates of the rectangles. 2654 y_intervals: The Y coordinates of the rectangles. 2655 2656 Returns: 2657 An instance of the `Constraint` class. 2658 """ 2659 ct = Constraint(self) 2660 model_ct = self.__model.constraints[ct.index] 2661 model_ct.no_overlap_2d.x_intervals.extend( 2662 [self.get_interval_index(x) for x in x_intervals] 2663 ) 2664 model_ct.no_overlap_2d.y_intervals.extend( 2665 [self.get_interval_index(x) for x in y_intervals] 2666 ) 2667 return ct 2668 2669 def add_cumulative( 2670 self, 2671 intervals: Iterable[IntervalVar], 2672 demands: Iterable[LinearExprT], 2673 capacity: LinearExprT, 2674 ) -> Constraint: 2675 """Adds Cumulative(intervals, demands, capacity). 2676 2677 This constraint enforces that: 2678 2679 for all t: 2680 sum(demands[i] 2681 if (start(intervals[i]) <= t < end(intervals[i])) and 2682 (intervals[i] is present)) <= capacity 2683 2684 Args: 2685 intervals: The list of intervals. 2686 demands: The list of demands for each interval. Each demand must be >= 0. 2687 Each demand can be a 1-var affine expression (a * x + b). 2688 capacity: The maximum capacity of the cumulative constraint. It can be a 2689 1-var affine expression (a * x + b). 2690 2691 Returns: 2692 An instance of the `Constraint` class. 2693 """ 2694 cumulative = Constraint(self) 2695 model_ct = self.__model.constraints[cumulative.index] 2696 model_ct.cumulative.intervals.extend( 2697 [self.get_interval_index(x) for x in intervals] 2698 ) 2699 for d in demands: 2700 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2701 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2702 return cumulative 2703 2704 # Support for model cloning. 2705 def clone(self) -> "CpModel": 2706 """Reset the model, and creates a new one from a CpModelProto instance.""" 2707 clone = CpModel() 2708 clone.proto.CopyFrom(self.proto) 2709 clone.rebuild_constant_map() 2710 return clone 2711 2712 def rebuild_constant_map(self): 2713 """Internal method used during model cloning.""" 2714 for i, var in enumerate(self.__model.variables): 2715 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2716 self.__constant_map[var.domain[0]] = i 2717 2718 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2719 """Returns an already created Boolean variable from its index.""" 2720 if index < 0 or index >= len(self.__model.variables): 2721 raise ValueError( 2722 f"get_bool_var_from_proto_index: out of bound index {index}" 2723 ) 2724 var = self.__model.variables[index] 2725 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2726 raise ValueError( 2727 f"get_bool_var_from_proto_index: index {index} does not reference" 2728 + " a Boolean variable" 2729 ) 2730 2731 return IntVar(self.__model, index, None) 2732 2733 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2734 """Returns an already created integer variable from its index.""" 2735 if index < 0 or index >= len(self.__model.variables): 2736 raise ValueError( 2737 f"get_int_var_from_proto_index: out of bound index {index}" 2738 ) 2739 return IntVar(self.__model, index, None) 2740 2741 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2742 """Returns an already created interval variable from its index.""" 2743 if index < 0 or index >= len(self.__model.constraints): 2744 raise ValueError( 2745 f"get_interval_var_from_proto_index: out of bound index {index}" 2746 ) 2747 ct = self.__model.constraints[index] 2748 if not ct.HasField("interval"): 2749 raise ValueError( 2750 f"get_interval_var_from_proto_index: index {index} does not" 2751 " reference an" + " interval variable" 2752 ) 2753 2754 return IntervalVar(self.__model, index, None, None, None, None) 2755 2756 # Helpers. 2757 2758 def __str__(self) -> str: 2759 return str(self.__model) 2760 2761 @property 2762 def proto(self) -> cp_model_pb2.CpModelProto: 2763 """Returns the underlying CpModelProto.""" 2764 return self.__model 2765 2766 def negated(self, index: int) -> int: 2767 return -index - 1 2768 2769 def get_or_make_index(self, arg: VariableT) -> int: 2770 """Returns the index of a variable, its negation, or a number.""" 2771 if isinstance(arg, IntVar): 2772 return arg.index 2773 if ( 2774 isinstance(arg, _ProductCst) 2775 and isinstance(arg.expression(), IntVar) 2776 and arg.coefficient() == -1 2777 ): 2778 return -arg.expression().index - 1 2779 if isinstance(arg, IntegralTypes): 2780 arg = cmh.assert_is_int64(arg) 2781 return self.get_or_make_index_from_constant(arg) 2782 raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")") 2783 2784 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2785 """Returns an index from a boolean expression.""" 2786 if isinstance(arg, IntVar): 2787 self.assert_is_boolean_variable(arg) 2788 return arg.index 2789 if isinstance(arg, _NotBooleanVariable): 2790 self.assert_is_boolean_variable(arg.negated()) 2791 return arg.index 2792 if isinstance(arg, IntegralTypes): 2793 if arg == ~False: # -1 2794 return self.get_or_make_index_from_constant(1) 2795 if arg == ~True: # -2 2796 return self.get_or_make_index_from_constant(0) 2797 arg = cmh.assert_is_zero_or_one(arg) 2798 return self.get_or_make_index_from_constant(arg) 2799 if cmh.is_boolean(arg): 2800 return self.get_or_make_index_from_constant(int(arg)) 2801 raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})") 2802 2803 def get_interval_index(self, arg: IntervalVar) -> int: 2804 if not isinstance(arg, IntervalVar): 2805 raise TypeError("NotSupported: model.get_interval_index(%s)" % arg) 2806 return arg.index 2807 2808 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2809 if value in self.__constant_map: 2810 return self.__constant_map[value] 2811 index = len(self.__model.variables) 2812 self.__model.variables.add(domain=[value, value]) 2813 self.__constant_map[value] = index 2814 return index 2815 2816 def var_index_to_var_proto( 2817 self, var_index: int 2818 ) -> cp_model_pb2.IntegerVariableProto: 2819 if var_index >= 0: 2820 return self.__model.variables[var_index] 2821 else: 2822 return self.__model.variables[-var_index - 1] 2823 2824 def parse_linear_expression( 2825 self, linear_expr: LinearExprT, negate: bool = False 2826 ) -> cp_model_pb2.LinearExpressionProto: 2827 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2828 result: cp_model_pb2.LinearExpressionProto = ( 2829 cp_model_pb2.LinearExpressionProto() 2830 ) 2831 mult = -1 if negate else 1 2832 if isinstance(linear_expr, IntegralTypes): 2833 result.offset = int(linear_expr) * mult 2834 return result 2835 2836 if isinstance(linear_expr, IntVar): 2837 result.vars.append(self.get_or_make_index(linear_expr)) 2838 result.coeffs.append(mult) 2839 return result 2840 2841 coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map() 2842 result.offset = constant * mult 2843 for t in coeffs_map.items(): 2844 if not isinstance(t[0], IntVar): 2845 raise TypeError("Wrong argument" + str(t)) 2846 c = cmh.assert_is_int64(t[1]) 2847 result.vars.append(t[0].index) 2848 result.coeffs.append(c * mult) 2849 return result 2850 2851 def _set_objective(self, obj: ObjLinearExprT, minimize: bool): 2852 """Sets the objective of the model.""" 2853 self.clear_objective() 2854 if isinstance(obj, IntVar): 2855 self.__model.objective.vars.append(obj.index) 2856 self.__model.objective.offset = 0 2857 if minimize: 2858 self.__model.objective.coeffs.append(1) 2859 self.__model.objective.scaling_factor = 1 2860 else: 2861 self.__model.objective.coeffs.append(-1) 2862 self.__model.objective.scaling_factor = -1 2863 elif isinstance(obj, LinearExpr): 2864 coeffs_map, constant, is_integer = obj.get_float_var_value_map() 2865 if is_integer: 2866 if minimize: 2867 self.__model.objective.scaling_factor = 1 2868 self.__model.objective.offset = constant 2869 else: 2870 self.__model.objective.scaling_factor = -1 2871 self.__model.objective.offset = -constant 2872 for v, c in coeffs_map.items(): 2873 c_as_int = int(c) 2874 self.__model.objective.vars.append(v.index) 2875 if minimize: 2876 self.__model.objective.coeffs.append(c_as_int) 2877 else: 2878 self.__model.objective.coeffs.append(-c_as_int) 2879 else: 2880 self.__model.floating_point_objective.maximize = not minimize 2881 self.__model.floating_point_objective.offset = constant 2882 for v, c in coeffs_map.items(): 2883 self.__model.floating_point_objective.coeffs.append(c) 2884 self.__model.floating_point_objective.vars.append(v.index) 2885 elif isinstance(obj, IntegralTypes): 2886 self.__model.objective.offset = int(obj) 2887 self.__model.objective.scaling_factor = 1 2888 else: 2889 raise TypeError("TypeError: " + str(obj) + " is not a valid objective") 2890 2891 def minimize(self, obj: ObjLinearExprT): 2892 """Sets the objective of the model to minimize(obj).""" 2893 self._set_objective(obj, minimize=True) 2894 2895 def maximize(self, obj: ObjLinearExprT): 2896 """Sets the objective of the model to maximize(obj).""" 2897 self._set_objective(obj, minimize=False) 2898 2899 def has_objective(self) -> bool: 2900 return self.__model.HasField("objective") or self.__model.HasField( 2901 "floating_point_objective" 2902 ) 2903 2904 def clear_objective(self): 2905 self.__model.ClearField("objective") 2906 self.__model.ClearField("floating_point_objective") 2907 2908 def add_decision_strategy( 2909 self, 2910 variables: Sequence[IntVar], 2911 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2912 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2913 ) -> None: 2914 """Adds a search strategy to the model. 2915 2916 Args: 2917 variables: a list of variables this strategy will assign. 2918 var_strategy: heuristic to choose the next variable to assign. 2919 domain_strategy: heuristic to reduce the domain of the selected variable. 2920 Currently, this is advanced code: the union of all strategies added to 2921 the model must be complete, i.e. instantiates all variables. Otherwise, 2922 solve() will fail. 2923 """ 2924 2925 strategy: cp_model_pb2.DecisionStrategyProto = ( 2926 self.__model.search_strategy.add() 2927 ) 2928 for v in variables: 2929 expr = strategy.exprs.add() 2930 if v.index >= 0: 2931 expr.vars.append(v.index) 2932 expr.coeffs.append(1) 2933 else: 2934 expr.vars.append(self.negated(v.index)) 2935 expr.coeffs.append(-1) 2936 expr.offset = 1 2937 2938 strategy.variable_selection_strategy = var_strategy 2939 strategy.domain_reduction_strategy = domain_strategy 2940 2941 def model_stats(self) -> str: 2942 """Returns a string containing some model statistics.""" 2943 return swig_helper.CpSatHelper.model_stats(self.__model) 2944 2945 def validate(self) -> str: 2946 """Returns a string indicating that the model is invalid.""" 2947 return swig_helper.CpSatHelper.validate_model(self.__model) 2948 2949 def export_to_file(self, file: str) -> bool: 2950 """Write the model as a protocol buffer to 'file'. 2951 2952 Args: 2953 file: file to write the model to. If the filename ends with 'txt', the 2954 model will be written as a text file, otherwise, the binary format will 2955 be used. 2956 2957 Returns: 2958 True if the model was correctly written. 2959 """ 2960 return swig_helper.CpSatHelper.write_model_to_file(self.__model, file) 2961 2962 def add_hint(self, var: IntVar, value: int) -> None: 2963 """Adds 'var == value' as a hint to the solver.""" 2964 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2965 self.__model.solution_hint.values.append(value) 2966 2967 def clear_hints(self): 2968 """Removes any solution hint from the model.""" 2969 self.__model.ClearField("solution_hint") 2970 2971 def add_assumption(self, lit: LiteralT) -> None: 2972 """Adds the literal to the model as assumptions.""" 2973 self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) 2974 2975 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2976 """Adds the literals to the model as assumptions.""" 2977 for lit in literals: 2978 self.add_assumption(lit) 2979 2980 def clear_assumptions(self) -> None: 2981 """Removes all assumptions from the model.""" 2982 self.__model.ClearField("assumptions") 2983 2984 # Helpers. 2985 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2986 if isinstance(x, IntVar): 2987 var = self.__model.variables[x.index] 2988 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2989 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2990 elif not isinstance(x, _NotBooleanVariable): 2991 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2992 2993 # Compatibility with pre PEP8 2994 # pylint: disable=invalid-name 2995 2996 def Name(self) -> str: 2997 return self.name 2998 2999 def SetName(self, name: str) -> None: 3000 self.name = name 3001 3002 def Proto(self) -> cp_model_pb2.CpModelProto: 3003 return self.proto 3004 3005 NewIntVar = new_int_var 3006 NewIntVarFromDomain = new_int_var_from_domain 3007 NewBoolVar = new_bool_var 3008 NewConstant = new_constant 3009 NewIntVarSeries = new_int_var_series 3010 NewBoolVarSeries = new_bool_var_series 3011 AddLinearConstraint = add_linear_constraint 3012 AddLinearExpressionInDomain = add_linear_expression_in_domain 3013 Add = add 3014 AddAllDifferent = add_all_different 3015 AddElement = add_element 3016 AddCircuit = add_circuit 3017 AddMultipleCircuit = add_multiple_circuit 3018 AddAllowedAssignments = add_allowed_assignments 3019 AddForbiddenAssignments = add_forbidden_assignments 3020 AddAutomaton = add_automaton 3021 AddInverse = add_inverse 3022 AddReservoirConstraint = add_reservoir_constraint 3023 AddReservoirConstraintWithActive = add_reservoir_constraint_with_active 3024 AddImplication = add_implication 3025 AddBoolOr = add_bool_or 3026 AddAtLeastOne = add_at_least_one 3027 AddAtMostOne = add_at_most_one 3028 AddExactlyOne = add_exactly_one 3029 AddBoolAnd = add_bool_and 3030 AddBoolXOr = add_bool_xor 3031 AddMinEquality = add_min_equality 3032 AddMaxEquality = add_max_equality 3033 AddDivisionEquality = add_division_equality 3034 AddAbsEquality = add_abs_equality 3035 AddModuloEquality = add_modulo_equality 3036 AddMultiplicationEquality = add_multiplication_equality 3037 NewIntervalVar = new_interval_var 3038 NewIntervalVarSeries = new_interval_var_series 3039 NewFixedSizeIntervalVar = new_fixed_size_interval_var 3040 NewOptionalIntervalVar = new_optional_interval_var 3041 NewOptionalIntervalVarSeries = new_optional_interval_var_series 3042 NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var 3043 NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series 3044 AddNoOverlap = add_no_overlap 3045 AddNoOverlap2D = add_no_overlap_2d 3046 AddCumulative = add_cumulative 3047 Clone = clone 3048 GetBoolVarFromProtoIndex = get_bool_var_from_proto_index 3049 GetIntVarFromProtoIndex = get_int_var_from_proto_index 3050 GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index 3051 Minimize = minimize 3052 Maximize = maximize 3053 HasObjective = has_objective 3054 ClearObjective = clear_objective 3055 AddDecisionStrategy = add_decision_strategy 3056 ModelStats = model_stats 3057 Validate = validate 3058 ExportToFile = export_to_file 3059 AddHint = add_hint 3060 ClearHints = clear_hints 3061 AddAssumption = add_assumption 3062 AddAssumptions = add_assumptions 3063 ClearAssumptions = clear_assumptions 3064 3065 # pylint: enable=invalid-name
Methods for building a CP model.
Methods beginning with:
New
create integer, boolean, or interval variables.add
create new constraints and add them to the model.
1348 @property 1349 def name(self) -> str: 1350 """Returns the name of the model.""" 1351 if not self.__model or not self.__model.name: 1352 return "" 1353 return self.__model.name
Returns the name of the model.
1362 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1363 """Create an integer variable with domain [lb, ub]. 1364 1365 The CP-SAT solver is limited to integer variables. If you have fractional 1366 values, scale them up so that they become integers; if you have strings, 1367 encode them as integers. 1368 1369 Args: 1370 lb: Lower bound for the variable. 1371 ub: Upper bound for the variable. 1372 name: The name of the variable. 1373 1374 Returns: 1375 a variable whose domain is [lb, ub]. 1376 """ 1377 1378 return IntVar(self.__model, sorted_interval_list.Domain(lb, ub), name)
Create an integer variable with domain [lb, ub].
The CP-SAT solver is limited to integer variables. If you have fractional values, scale them up so that they become integers; if you have strings, encode them as integers.
Arguments:
- lb: Lower bound for the variable.
- ub: Upper bound for the variable.
- name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
1380 def new_int_var_from_domain( 1381 self, domain: sorted_interval_list.Domain, name: str 1382 ) -> IntVar: 1383 """Create an integer variable from a domain. 1384 1385 A domain is a set of integers specified by a collection of intervals. 1386 For example, `model.new_int_var_from_domain(cp_model. 1387 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1388 1389 Args: 1390 domain: An instance of the Domain class. 1391 name: The name of the variable. 1392 1393 Returns: 1394 a variable whose domain is the given domain. 1395 """ 1396 return IntVar(self.__model, domain, name)
Create an integer variable from a domain.
A domain is a set of integers specified by a collection of intervals.
For example, model.new_int_var_from_domain(cp_model.
Domain.from_intervals([[1, 2], [4, 6]]), 'x')
Arguments:
- domain: An instance of the Domain class.
- name: The name of the variable.
Returns:
a variable whose domain is the given domain.
1398 def new_bool_var(self, name: str) -> IntVar: 1399 """Creates a 0-1 variable with the given name.""" 1400 return IntVar(self.__model, sorted_interval_list.Domain(0, 1), name)
Creates a 0-1 variable with the given name.
1402 def new_constant(self, value: IntegralT) -> IntVar: 1403 """Declares a constant integer.""" 1404 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None)
Declares a constant integer.
1406 def new_int_var_series( 1407 self, 1408 name: str, 1409 index: pd.Index, 1410 lower_bounds: Union[IntegralT, pd.Series], 1411 upper_bounds: Union[IntegralT, pd.Series], 1412 ) -> pd.Series: 1413 """Creates a series of (scalar-valued) variables with the given name. 1414 1415 Args: 1416 name (str): Required. The name of the variable set. 1417 index (pd.Index): Required. The index to use for the variable set. 1418 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1419 set. If a `pd.Series` is passed in, it will be based on the 1420 corresponding values of the pd.Series. 1421 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1422 set. If a `pd.Series` is passed in, it will be based on the 1423 corresponding values of the pd.Series. 1424 1425 Returns: 1426 pd.Series: The variable set indexed by its corresponding dimensions. 1427 1428 Raises: 1429 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1430 ValueError: if the `name` is not a valid identifier or already exists. 1431 ValueError: if the `lowerbound` is greater than the `upperbound`. 1432 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1433 the input index. 1434 """ 1435 if not isinstance(index, pd.Index): 1436 raise TypeError("Non-index object is used as index") 1437 if not name.isidentifier(): 1438 raise ValueError("name={} is not a valid identifier".format(name)) 1439 if ( 1440 isinstance(lower_bounds, IntegralTypes) 1441 and isinstance(upper_bounds, IntegralTypes) 1442 and lower_bounds > upper_bounds 1443 ): 1444 raise ValueError( 1445 f"lower_bound={lower_bounds} is greater than" 1446 f" upper_bound={upper_bounds} for variable set={name}" 1447 ) 1448 1449 lower_bounds = _convert_to_integral_series_and_validate_index( 1450 lower_bounds, index 1451 ) 1452 upper_bounds = _convert_to_integral_series_and_validate_index( 1453 upper_bounds, index 1454 ) 1455 return pd.Series( 1456 index=index, 1457 data=[ 1458 # pylint: disable=g-complex-comprehension 1459 IntVar( 1460 model=self.__model, 1461 name=f"{name}[{i}]", 1462 domain=sorted_interval_list.Domain( 1463 lower_bounds[i], upper_bounds[i] 1464 ), 1465 ) 1466 for i in index 1467 ], 1468 )
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, pd.Series]): 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. - upper_bounds (Union[int, pd.Series]): 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.
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
, orupper_bound
does not match - the input index.
1470 def new_bool_var_series( 1471 self, 1472 name: str, 1473 index: pd.Index, 1474 ) -> pd.Series: 1475 """Creates a series of (scalar-valued) variables with the given name. 1476 1477 Args: 1478 name (str): Required. The name of the variable set. 1479 index (pd.Index): Required. The index to use for the variable set. 1480 1481 Returns: 1482 pd.Series: The variable set indexed by its corresponding dimensions. 1483 1484 Raises: 1485 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1486 ValueError: if the `name` is not a valid identifier or already exists. 1487 """ 1488 return self.new_int_var_series( 1489 name=name, index=index, lower_bounds=0, upper_bounds=1 1490 )
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.
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.
1494 def add_linear_constraint( 1495 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1496 ) -> Constraint: 1497 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1498 return self.add_linear_expression_in_domain( 1499 linear_expr, sorted_interval_list.Domain(lb, ub) 1500 )
Adds the constraint: lb <= linear_expr <= ub
.
1502 def add_linear_expression_in_domain( 1503 self, linear_expr: LinearExprT, domain: sorted_interval_list.Domain 1504 ) -> Constraint: 1505 """Adds the constraint: `linear_expr` in `domain`.""" 1506 if isinstance(linear_expr, LinearExpr): 1507 ct = Constraint(self) 1508 model_ct = self.__model.constraints[ct.index] 1509 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1510 for t in coeffs_map.items(): 1511 if not isinstance(t[0], IntVar): 1512 raise TypeError("Wrong argument" + str(t)) 1513 c = cmh.assert_is_int64(t[1]) 1514 model_ct.linear.vars.append(t[0].index) 1515 model_ct.linear.coeffs.append(c) 1516 model_ct.linear.domain.extend( 1517 [ 1518 cmh.capped_subtraction(x, constant) 1519 for x in domain.flattened_intervals() 1520 ] 1521 ) 1522 return ct 1523 if isinstance(linear_expr, IntegralTypes): 1524 if not domain.contains(int(linear_expr)): 1525 return self.add_bool_or([]) # Evaluate to false. 1526 else: 1527 return self.add_bool_and([]) # Evaluate to true. 1528 raise TypeError( 1529 "not supported: CpModel.add_linear_expression_in_domain(" 1530 + str(linear_expr) 1531 + " " 1532 + str(domain) 1533 + ")" 1534 )
Adds the constraint: linear_expr
in domain
.
1542 def add(self, ct): 1543 """Adds a `BoundedLinearExpression` to the model. 1544 1545 Args: 1546 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1547 1548 Returns: 1549 An instance of the `Constraint` class. 1550 """ 1551 if isinstance(ct, BoundedLinearExpression): 1552 return self.add_linear_expression_in_domain( 1553 ct.expression(), 1554 sorted_interval_list.Domain.from_flat_intervals(ct.bounds()), 1555 ) 1556 if ct and cmh.is_boolean(ct): 1557 return self.add_bool_or([True]) 1558 if not ct and cmh.is_boolean(ct): 1559 return self.add_bool_or([]) # Evaluate to false. 1560 raise TypeError("not supported: CpModel.add(" + str(ct) + ")")
Adds a BoundedLinearExpression
to the model.
Arguments:
- ct: A
BoundedLinearExpression
.
Returns:
An instance of the
Constraint
class.
1570 def add_all_different(self, *expressions): 1571 """Adds AllDifferent(expressions). 1572 1573 This constraint forces all expressions to have different values. 1574 1575 Args: 1576 *expressions: simple expressions of the form a * var + constant. 1577 1578 Returns: 1579 An instance of the `Constraint` class. 1580 """ 1581 ct = Constraint(self) 1582 model_ct = self.__model.constraints[ct.index] 1583 expanded = expand_generator_or_tuple(expressions) 1584 model_ct.all_diff.exprs.extend( 1585 self.parse_linear_expression(x) for x in expanded 1586 ) 1587 return ct
Adds AllDifferent(expressions).
This constraint forces all expressions to have different values.
Arguments:
- *expressions: simple expressions of the form a * var + constant.
Returns:
An instance of the
Constraint
class.
1589 def add_element( 1590 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1591 ) -> Constraint: 1592 """Adds the element constraint: `variables[index] == target`. 1593 1594 Args: 1595 index: The index of the variable that's being constrained. 1596 variables: A list of variables. 1597 target: The value that the variable must be equal to. 1598 1599 Returns: 1600 An instance of the `Constraint` class. 1601 """ 1602 1603 if not variables: 1604 raise ValueError("add_element expects a non-empty variables array") 1605 1606 if isinstance(index, IntegralTypes): 1607 variable: VariableT = list(variables)[int(index)] 1608 return self.add(variable == target) 1609 1610 ct = Constraint(self) 1611 model_ct = self.__model.constraints[ct.index] 1612 model_ct.element.index = self.get_or_make_index(index) 1613 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1614 model_ct.element.target = self.get_or_make_index(target) 1615 return ct
Adds the element constraint: variables[index] == target
.
Arguments:
- index: The index of the variable that's being constrained.
- variables: A list of variables.
- target: The value that the variable must be equal to.
Returns:
An instance of the
Constraint
class.
1617 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1618 """Adds Circuit(arcs). 1619 1620 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1621 1622 A circuit is a unique Hamiltonian path in a subgraph of the total 1623 graph. In case a node 'i' is not in the path, then there must be a 1624 loop arc 'i -> i' associated with a true literal. Otherwise 1625 this constraint will fail. 1626 1627 Args: 1628 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1629 literal). The arc is selected in the circuit if the literal is true. 1630 Both source_node and destination_node must be integers between 0 and the 1631 number of nodes - 1. 1632 1633 Returns: 1634 An instance of the `Constraint` class. 1635 1636 Raises: 1637 ValueError: If the list of arcs is empty. 1638 """ 1639 if not arcs: 1640 raise ValueError("add_circuit expects a non-empty array of arcs") 1641 ct = Constraint(self) 1642 model_ct = self.__model.constraints[ct.index] 1643 for arc in arcs: 1644 tail = cmh.assert_is_int32(arc[0]) 1645 head = cmh.assert_is_int32(arc[1]) 1646 lit = self.get_or_make_boolean_index(arc[2]) 1647 model_ct.circuit.tails.append(tail) 1648 model_ct.circuit.heads.append(head) 1649 model_ct.circuit.literals.append(lit) 1650 return ct
Adds Circuit(arcs).
Adds a circuit constraint from a sparse list of arcs that encode the graph.
A circuit is a unique Hamiltonian path in a subgraph of the total graph. In case a node 'i' is not in the path, then there must be a loop arc 'i -> i' associated with a true literal. Otherwise this constraint will fail.
Arguments:
- arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: If the list of arcs is empty.
1652 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1653 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1654 1655 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1656 literals[i] is true must satisfy this set of properties: 1657 - #incoming arcs == 1 except for node 0. 1658 - #outgoing arcs == 1 except for node 0. 1659 - for node zero, #incoming arcs == #outgoing arcs. 1660 - There are no duplicate arcs. 1661 - Self-arcs are allowed except for node 0. 1662 - There is no cycle in this graph, except through node 0. 1663 1664 Args: 1665 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1666 literal). The arc is selected in the circuit if the literal is true. 1667 Both source_node and destination_node must be integers between 0 and the 1668 number of nodes - 1. 1669 1670 Returns: 1671 An instance of the `Constraint` class. 1672 1673 Raises: 1674 ValueError: If the list of arcs is empty. 1675 """ 1676 if not arcs: 1677 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1678 ct = Constraint(self) 1679 model_ct = self.__model.constraints[ct.index] 1680 for arc in arcs: 1681 tail = cmh.assert_is_int32(arc[0]) 1682 head = cmh.assert_is_int32(arc[1]) 1683 lit = self.get_or_make_boolean_index(arc[2]) 1684 model_ct.routes.tails.append(tail) 1685 model_ct.routes.heads.append(head) 1686 model_ct.routes.literals.append(lit) 1687 return ct
Adds a multiple circuit constraint, aka the 'VRP' constraint.
The direct graph where arc #i (from tails[i] to head[i]) is present iff literals[i] is true must satisfy this set of properties:
- #incoming arcs == 1 except for node 0.
- #outgoing arcs == 1 except for node 0.
- for node zero, #incoming arcs == #outgoing arcs.
- There are no duplicate arcs.
- Self-arcs are allowed except for node 0.
- There is no cycle in this graph, except through node 0.
Arguments:
- arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: If the list of arcs is empty.
1689 def add_allowed_assignments( 1690 self, 1691 variables: Sequence[VariableT], 1692 tuples_list: Iterable[Sequence[IntegralT]], 1693 ) -> Constraint: 1694 """Adds AllowedAssignments(variables, tuples_list). 1695 1696 An AllowedAssignments constraint is a constraint on an array of variables, 1697 which requires that when all variables are assigned values, the resulting 1698 array equals one of the tuples in `tuple_list`. 1699 1700 Args: 1701 variables: A list of variables. 1702 tuples_list: A list of admissible tuples. Each tuple must have the same 1703 length as the variables, and the ith value of a tuple corresponds to the 1704 ith variable. 1705 1706 Returns: 1707 An instance of the `Constraint` class. 1708 1709 Raises: 1710 TypeError: If a tuple does not have the same size as the list of 1711 variables. 1712 ValueError: If the array of variables is empty. 1713 """ 1714 1715 if not variables: 1716 raise ValueError( 1717 "add_allowed_assignments expects a non-empty variables array" 1718 ) 1719 1720 ct: Constraint = Constraint(self) 1721 model_ct = self.__model.constraints[ct.index] 1722 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1723 arity: int = len(variables) 1724 for t in tuples_list: 1725 if len(t) != arity: 1726 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1727 1728 # duck-typing (no explicit type checks here) 1729 try: 1730 model_ct.table.values.extend(a for b in tuples_list for a in b) 1731 except ValueError as ex: 1732 raise TypeError(f"add_xxx_assignment: Not an integer or does not fit in an int64_t: {ex.args}") from ex 1733 1734 return ct
Adds AllowedAssignments(variables, tuples_list).
An AllowedAssignments constraint is a constraint on an array of variables,
which requires that when all variables are assigned values, the resulting
array equals one of the tuples in tuple_list
.
Arguments:
- variables: A list of variables.
- tuples_list: A list of admissible tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of variables.
- ValueError: If the array of variables is empty.
1736 def add_forbidden_assignments( 1737 self, 1738 variables: Sequence[VariableT], 1739 tuples_list: Iterable[Sequence[IntegralT]], 1740 ) -> Constraint: 1741 """Adds add_forbidden_assignments(variables, [tuples_list]). 1742 1743 A ForbiddenAssignments constraint is a constraint on an array of variables 1744 where the list of impossible combinations is provided in the tuples list. 1745 1746 Args: 1747 variables: A list of variables. 1748 tuples_list: A list of forbidden tuples. Each tuple must have the same 1749 length as the variables, and the *i*th value of a tuple corresponds to 1750 the *i*th variable. 1751 1752 Returns: 1753 An instance of the `Constraint` class. 1754 1755 Raises: 1756 TypeError: If a tuple does not have the same size as the list of 1757 variables. 1758 ValueError: If the array of variables is empty. 1759 """ 1760 1761 if not variables: 1762 raise ValueError( 1763 "add_forbidden_assignments expects a non-empty variables array" 1764 ) 1765 1766 index = len(self.__model.constraints) 1767 ct: Constraint = self.add_allowed_assignments(variables, tuples_list) 1768 self.__model.constraints[index].table.negated = True 1769 return ct
Adds add_forbidden_assignments(variables, [tuples_list]).
A ForbiddenAssignments constraint is a constraint on an array of variables where the list of impossible combinations is provided in the tuples list.
Arguments:
- variables: A list of variables.
- tuples_list: A list of forbidden tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of variables.
- ValueError: If the array of variables is empty.
1771 def add_automaton( 1772 self, 1773 transition_variables: Sequence[VariableT], 1774 starting_state: IntegralT, 1775 final_states: Sequence[IntegralT], 1776 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1777 ) -> Constraint: 1778 """Adds an automaton constraint. 1779 1780 An automaton constraint takes a list of variables (of size *n*), an initial 1781 state, a set of final states, and a set of transitions. A transition is a 1782 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1783 and *transition* is the label of an arc from *head* to *tail*, 1784 corresponding to the value of one variable in the list of variables. 1785 1786 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1787 contains the possible states of the automaton. The first state contains the 1788 initial state. The last phase contains the final states. 1789 1790 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1791 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1792 an arc from the state *tail* of phase *i* and the state *head* of phase 1793 *i* + 1. This arc is labeled by the value *transition* of the variables 1794 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1795 is assigned the value *transition*. 1796 1797 A feasible solution of this constraint is an assignment of variables such 1798 that, starting from the initial state in phase 0, there is a path labeled by 1799 the values of the variables that ends in one of the final states in the 1800 final phase. 1801 1802 Args: 1803 transition_variables: A non-empty list of variables whose values 1804 correspond to the labels of the arcs traversed by the automaton. 1805 starting_state: The initial state of the automaton. 1806 final_states: A non-empty list of admissible final states. 1807 transition_triples: A list of transitions for the automaton, in the 1808 following format (current_state, variable_value, next_state). 1809 1810 Returns: 1811 An instance of the `Constraint` class. 1812 1813 Raises: 1814 ValueError: if `transition_variables`, `final_states`, or 1815 `transition_triples` are empty. 1816 """ 1817 1818 if not transition_variables: 1819 raise ValueError( 1820 "add_automaton expects a non-empty transition_variables array" 1821 ) 1822 if not final_states: 1823 raise ValueError("add_automaton expects some final states") 1824 1825 if not transition_triples: 1826 raise ValueError("add_automaton expects some transition triples") 1827 1828 ct = Constraint(self) 1829 model_ct = self.__model.constraints[ct.index] 1830 model_ct.automaton.vars.extend( 1831 [self.get_or_make_index(x) for x in transition_variables] 1832 ) 1833 starting_state = cmh.assert_is_int64(starting_state) 1834 model_ct.automaton.starting_state = starting_state 1835 for v in final_states: 1836 v = cmh.assert_is_int64(v) 1837 model_ct.automaton.final_states.append(v) 1838 for t in transition_triples: 1839 if len(t) != 3: 1840 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1841 tail = cmh.assert_is_int64(t[0]) 1842 label = cmh.assert_is_int64(t[1]) 1843 head = cmh.assert_is_int64(t[2]) 1844 model_ct.automaton.transition_tail.append(tail) 1845 model_ct.automaton.transition_label.append(label) 1846 model_ct.automaton.transition_head.append(head) 1847 return ct
Adds an automaton constraint.
An automaton constraint takes a list of variables (of size n), an initial state, a set of final states, and a set of transitions. A transition is a triplet (tail, transition, head), where tail and head are states, and transition is the label of an arc from head to tail, corresponding to the value of one variable in the list of variables.
This automaton will be unrolled into a flow with n + 1 phases. Each phase contains the possible states of the automaton. The first state contains the initial state. The last phase contains the final states.
Between two consecutive phases i and i + 1, the automaton creates a set
of arcs. For each transition (tail, transition, head), it will add
an arc from the state tail of phase i and the state head of phase
i + 1. This arc is labeled by the value transition of the variables
variables[i]
. That is, this arc can only be selected if variables[i]
is assigned the value transition.
A feasible solution of this constraint is an assignment of variables such that, starting from the initial state in phase 0, there is a path labeled by the values of the variables that ends in one of the final states in the final phase.
Arguments:
- transition_variables: A non-empty list of variables whose values correspond to the labels of the arcs traversed by the automaton.
- starting_state: The initial state of the automaton.
- final_states: A non-empty list of admissible final states.
- transition_triples: A list of transitions for the automaton, in the following format (current_state, variable_value, next_state).
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: if
transition_variables
,final_states
, ortransition_triples
are empty.
1849 def add_inverse( 1850 self, 1851 variables: Sequence[VariableT], 1852 inverse_variables: Sequence[VariableT], 1853 ) -> Constraint: 1854 """Adds Inverse(variables, inverse_variables). 1855 1856 An inverse constraint enforces that if `variables[i]` is assigned a value 1857 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1858 1859 Args: 1860 variables: An array of integer variables. 1861 inverse_variables: An array of integer variables. 1862 1863 Returns: 1864 An instance of the `Constraint` class. 1865 1866 Raises: 1867 TypeError: if variables and inverse_variables have different lengths, or 1868 if they are empty. 1869 """ 1870 1871 if not variables or not inverse_variables: 1872 raise TypeError("The Inverse constraint does not accept empty arrays") 1873 if len(variables) != len(inverse_variables): 1874 raise TypeError( 1875 "In the inverse constraint, the two array variables and" 1876 " inverse_variables must have the same length." 1877 ) 1878 ct = Constraint(self) 1879 model_ct = self.__model.constraints[ct.index] 1880 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1881 model_ct.inverse.f_inverse.extend( 1882 [self.get_or_make_index(x) for x in inverse_variables] 1883 ) 1884 return ct
Adds Inverse(variables, inverse_variables).
An inverse constraint enforces that if variables[i]
is assigned a value
j
, then inverse_variables[j]
is assigned a value i
. And vice versa.
Arguments:
- variables: An array of integer variables.
- inverse_variables: An array of integer variables.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: if variables and inverse_variables have different lengths, or if they are empty.
1886 def add_reservoir_constraint( 1887 self, 1888 times: Iterable[LinearExprT], 1889 level_changes: Iterable[LinearExprT], 1890 min_level: int, 1891 max_level: int, 1892 ) -> Constraint: 1893 """Adds Reservoir(times, level_changes, min_level, max_level). 1894 1895 Maintains a reservoir level within bounds. The water level starts at 0, and 1896 at any time, it must be between min_level and max_level. 1897 1898 If the affine expression `times[i]` is assigned a value t, then the current 1899 level changes by `level_changes[i]`, which is constant, at time t. 1900 1901 Note that min level must be <= 0, and the max level must be >= 0. Please 1902 use fixed level_changes to simulate initial state. 1903 1904 Therefore, at any time: 1905 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1906 1907 Args: 1908 times: A list of 1-var affine expressions (a * x + b) which specify the 1909 time of the filling or emptying the reservoir. 1910 level_changes: A list of integer values that specifies the amount of the 1911 emptying or filling. Currently, variable demands are not supported. 1912 min_level: At any time, the level of the reservoir must be greater or 1913 equal than the min level. 1914 max_level: At any time, the level of the reservoir must be less or equal 1915 than the max level. 1916 1917 Returns: 1918 An instance of the `Constraint` class. 1919 1920 Raises: 1921 ValueError: if max_level < min_level. 1922 1923 ValueError: if max_level < 0. 1924 1925 ValueError: if min_level > 0 1926 """ 1927 1928 if max_level < min_level: 1929 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1930 1931 if max_level < 0: 1932 raise ValueError("Reservoir constraint must have a max_level >= 0") 1933 1934 if min_level > 0: 1935 raise ValueError("Reservoir constraint must have a min_level <= 0") 1936 1937 ct = Constraint(self) 1938 model_ct = self.__model.constraints[ct.index] 1939 model_ct.reservoir.time_exprs.extend( 1940 [self.parse_linear_expression(x) for x in times] 1941 ) 1942 model_ct.reservoir.level_changes.extend( 1943 [self.parse_linear_expression(x) for x in level_changes] 1944 ) 1945 model_ct.reservoir.min_level = min_level 1946 model_ct.reservoir.max_level = max_level 1947 return ct
Adds Reservoir(times, level_changes, min_level, max_level).
Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.
If the affine expression times[i]
is assigned a value t, then the current
level changes by level_changes[i]
, which is constant, at time t.
Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.
Therefore, at any time: sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
Arguments:
- times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
- level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
- min_level: At any time, the level of the reservoir must be greater or equal than the min level.
- max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: if max_level < min_level.
- ValueError: if max_level < 0.
- ValueError: if min_level > 0
1949 def add_reservoir_constraint_with_active( 1950 self, 1951 times: Iterable[LinearExprT], 1952 level_changes: Iterable[LinearExprT], 1953 actives: Iterable[LiteralT], 1954 min_level: int, 1955 max_level: int, 1956 ) -> Constraint: 1957 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1958 1959 Maintains a reservoir level within bounds. The water level starts at 0, and 1960 at any time, it must be between min_level and max_level. 1961 1962 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1963 `True`, then the current level changes by `level_changes[i]`, which is 1964 constant, 1965 at time t. 1966 1967 Note that min level must be <= 0, and the max level must be >= 0. Please 1968 use fixed level_changes to simulate initial state. 1969 1970 Therefore, at any time: 1971 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1972 max_level] 1973 1974 1975 The array of boolean variables 'actives', if defined, indicates which 1976 actions are actually performed. 1977 1978 Args: 1979 times: A list of 1-var affine expressions (a * x + b) which specify the 1980 time of the filling or emptying the reservoir. 1981 level_changes: A list of integer values that specifies the amount of the 1982 emptying or filling. Currently, variable demands are not supported. 1983 actives: a list of boolean variables. They indicates if the 1984 emptying/refilling events actually take place. 1985 min_level: At any time, the level of the reservoir must be greater or 1986 equal than the min level. 1987 max_level: At any time, the level of the reservoir must be less or equal 1988 than the max level. 1989 1990 Returns: 1991 An instance of the `Constraint` class. 1992 1993 Raises: 1994 ValueError: if max_level < min_level. 1995 1996 ValueError: if max_level < 0. 1997 1998 ValueError: if min_level > 0 1999 """ 2000 2001 if max_level < min_level: 2002 raise ValueError("Reservoir constraint must have a max_level >= min_level") 2003 2004 if max_level < 0: 2005 raise ValueError("Reservoir constraint must have a max_level >= 0") 2006 2007 if min_level > 0: 2008 raise ValueError("Reservoir constraint must have a min_level <= 0") 2009 2010 ct = Constraint(self) 2011 model_ct = self.__model.constraints[ct.index] 2012 model_ct.reservoir.time_exprs.extend( 2013 [self.parse_linear_expression(x) for x in times] 2014 ) 2015 model_ct.reservoir.level_changes.extend( 2016 [self.parse_linear_expression(x) for x in level_changes] 2017 ) 2018 model_ct.reservoir.active_literals.extend( 2019 [self.get_or_make_boolean_index(x) for x in actives] 2020 ) 2021 model_ct.reservoir.min_level = min_level 2022 model_ct.reservoir.max_level = max_level 2023 return ct
Adds Reservoir(times, level_changes, actives, min_level, max_level).
Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.
If the variable times[i]
is assigned a value t, and actives[i]
is
True
, then the current level changes by level_changes[i]
, which is
constant,
at time t.
Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.
Therefore, at any time: sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, max_level]
The array of boolean variables 'actives', if defined, indicates which actions are actually performed.
Arguments:
- times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
- level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
- actives: a list of boolean variables. They indicates if the emptying/refilling events actually take place.
- min_level: At any time, the level of the reservoir must be greater or equal than the min level.
- max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: if max_level < min_level.
- ValueError: if max_level < 0.
- ValueError: if min_level > 0
2025 def add_map_domain( 2026 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 2027 ): 2028 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 2029 2030 for i, bool_var in enumerate(bool_var_array): 2031 b_index = bool_var.index 2032 var_index = var.index 2033 model_ct = self.__model.constraints.add() 2034 model_ct.linear.vars.append(var_index) 2035 model_ct.linear.coeffs.append(1) 2036 offset_as_int = int(offset) 2037 model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) 2038 model_ct.enforcement_literal.append(b_index) 2039 2040 model_ct = self.__model.constraints.add() 2041 model_ct.linear.vars.append(var_index) 2042 model_ct.linear.coeffs.append(1) 2043 model_ct.enforcement_literal.append(-b_index - 1) 2044 if offset + i - 1 >= INT_MIN: 2045 model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) 2046 if offset + i + 1 <= INT_MAX: 2047 model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX])
Adds var == i + offset <=> bool_var_array[i] == true for all i
.
2049 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 2050 """Adds `a => b` (`a` implies `b`).""" 2051 ct = Constraint(self) 2052 model_ct = self.__model.constraints[ct.index] 2053 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2054 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2055 return ct
Adds a => b
(a
implies b
).
2063 def add_bool_or(self, *literals): 2064 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2065 ct = Constraint(self) 2066 model_ct = self.__model.constraints[ct.index] 2067 model_ct.bool_or.literals.extend( 2068 [ 2069 self.get_or_make_boolean_index(x) 2070 for x in expand_generator_or_tuple(literals) 2071 ] 2072 ) 2073 return ct
Adds Or(literals) == true
: sum(literals) >= 1.
2081 def add_at_least_one(self, *literals): 2082 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2083 return self.add_bool_or(*literals)
Same as add_bool_or
: sum(literals) >= 1
.
2091 def add_at_most_one(self, *literals): 2092 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2093 ct = Constraint(self) 2094 model_ct = self.__model.constraints[ct.index] 2095 model_ct.at_most_one.literals.extend( 2096 [ 2097 self.get_or_make_boolean_index(x) 2098 for x in expand_generator_or_tuple(literals) 2099 ] 2100 ) 2101 return ct
Adds AtMostOne(literals)
: sum(literals) <= 1
.
2109 def add_exactly_one(self, *literals): 2110 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2111 ct = Constraint(self) 2112 model_ct = self.__model.constraints[ct.index] 2113 model_ct.exactly_one.literals.extend( 2114 [ 2115 self.get_or_make_boolean_index(x) 2116 for x in expand_generator_or_tuple(literals) 2117 ] 2118 ) 2119 return ct
Adds ExactlyOne(literals)
: sum(literals) == 1
.
2127 def add_bool_and(self, *literals): 2128 """Adds `And(literals) == true`.""" 2129 ct = Constraint(self) 2130 model_ct = self.__model.constraints[ct.index] 2131 model_ct.bool_and.literals.extend( 2132 [ 2133 self.get_or_make_boolean_index(x) 2134 for x in expand_generator_or_tuple(literals) 2135 ] 2136 ) 2137 return ct
Adds And(literals) == true
.
2145 def add_bool_xor(self, *literals): 2146 """Adds `XOr(literals) == true`. 2147 2148 In contrast to add_bool_or and add_bool_and, it does not support 2149 .only_enforce_if(). 2150 2151 Args: 2152 *literals: the list of literals in the constraint. 2153 2154 Returns: 2155 An `Constraint` object. 2156 """ 2157 ct = Constraint(self) 2158 model_ct = self.__model.constraints[ct.index] 2159 model_ct.bool_xor.literals.extend( 2160 [ 2161 self.get_or_make_boolean_index(x) 2162 for x in expand_generator_or_tuple(literals) 2163 ] 2164 ) 2165 return ct
Adds XOr(literals) == true
.
In contrast to add_bool_or and add_bool_and, it does not support .only_enforce_if().
Arguments:
- *literals: the list of literals in the constraint.
Returns:
An
Constraint
object.
2167 def add_min_equality( 2168 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2169 ) -> Constraint: 2170 """Adds `target == Min(exprs)`.""" 2171 ct = Constraint(self) 2172 model_ct = self.__model.constraints[ct.index] 2173 model_ct.lin_max.exprs.extend( 2174 [self.parse_linear_expression(x, True) for x in exprs] 2175 ) 2176 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2177 return ct
Adds target == Min(exprs)
.
2179 def add_max_equality( 2180 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2181 ) -> Constraint: 2182 """Adds `target == Max(exprs)`.""" 2183 ct = Constraint(self) 2184 model_ct = self.__model.constraints[ct.index] 2185 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2186 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2187 return ct
Adds target == Max(exprs)
.
2189 def add_division_equality( 2190 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2191 ) -> Constraint: 2192 """Adds `target == num // denom` (integer division rounded towards 0).""" 2193 ct = Constraint(self) 2194 model_ct = self.__model.constraints[ct.index] 2195 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2196 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2197 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2198 return ct
Adds target == num // denom
(integer division rounded towards 0).
2200 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2201 """Adds `target == Abs(expr)`.""" 2202 ct = Constraint(self) 2203 model_ct = self.__model.constraints[ct.index] 2204 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2205 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2206 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2207 return ct
Adds target == Abs(expr)
.
2209 def add_modulo_equality( 2210 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2211 ) -> Constraint: 2212 """Adds `target = expr % mod`. 2213 2214 It uses the C convention, that is the result is the remainder of the 2215 integral division rounded towards 0. 2216 2217 For example: 2218 * 10 % 3 = 1 2219 * -10 % 3 = -1 2220 * 10 % -3 = 1 2221 * -10 % -3 = -1 2222 2223 Args: 2224 target: the target expression. 2225 expr: the expression to compute the modulo of. 2226 mod: the modulus expression. 2227 2228 Returns: 2229 A `Constraint` object. 2230 """ 2231 ct = Constraint(self) 2232 model_ct = self.__model.constraints[ct.index] 2233 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2234 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2235 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2236 return ct
Adds target = expr % mod
.
It uses the C convention, that is the result is the remainder of the integral division rounded towards 0.
For example:
* 10 % 3 = 1
* -10 % 3 = -1
* 10 % -3 = 1
* -10 % -3 = -1
Arguments:
- target: the target expression.
- expr: the expression to compute the modulo of.
- mod: the modulus expression.
Returns:
A
Constraint
object.
2238 def add_multiplication_equality( 2239 self, 2240 target: LinearExprT, 2241 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2242 ) -> Constraint: 2243 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2244 ct = Constraint(self) 2245 model_ct = self.__model.constraints[ct.index] 2246 model_ct.int_prod.exprs.extend( 2247 [ 2248 self.parse_linear_expression(expr) 2249 for expr in expand_generator_or_tuple(expressions) 2250 ] 2251 ) 2252 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2253 return ct
Adds target == expressions[0] * .. * expressions[n]
.
2257 def new_interval_var( 2258 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2259 ) -> IntervalVar: 2260 """Creates an interval variable from start, size, and end. 2261 2262 An interval variable is a constraint, that is itself used in other 2263 constraints like NoOverlap. 2264 2265 Internally, it ensures that `start + size == end`. 2266 2267 Args: 2268 start: The start of the interval. It must be of the form a * var + b. 2269 size: The size of the interval. It must be of the form a * var + b. 2270 end: The end of the interval. It must be of the form a * var + b. 2271 name: The name of the interval variable. 2272 2273 Returns: 2274 An `IntervalVar` object. 2275 """ 2276 2277 start_expr = self.parse_linear_expression(start) 2278 size_expr = self.parse_linear_expression(size) 2279 end_expr = self.parse_linear_expression(end) 2280 if len(start_expr.vars) > 1: 2281 raise TypeError( 2282 "cp_model.new_interval_var: start must be 1-var affine or constant." 2283 ) 2284 if len(size_expr.vars) > 1: 2285 raise TypeError( 2286 "cp_model.new_interval_var: size must be 1-var affine or constant." 2287 ) 2288 if len(end_expr.vars) > 1: 2289 raise TypeError( 2290 "cp_model.new_interval_var: end must be 1-var affine or constant." 2291 ) 2292 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)
Creates an interval variable from start, size, and end.
An interval variable is a constraint, that is itself used in other constraints like NoOverlap.
Internally, it ensures that start + size == end
.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be of the form a * var + b.
- end: The end of the interval. It must be of the form a * var + b.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2294 def new_interval_var_series( 2295 self, 2296 name: str, 2297 index: pd.Index, 2298 starts: Union[LinearExprT, pd.Series], 2299 sizes: Union[LinearExprT, pd.Series], 2300 ends: Union[LinearExprT, pd.Series], 2301 ) -> pd.Series: 2302 """Creates a series of interval variables with the given name. 2303 2304 Args: 2305 name (str): Required. The name of the variable set. 2306 index (pd.Index): Required. The index to use for the variable set. 2307 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2308 set. If a `pd.Series` is passed in, it will be based on the 2309 corresponding values of the pd.Series. 2310 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2311 set. If a `pd.Series` is passed in, it will be based on the 2312 corresponding values of the pd.Series. 2313 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2314 set. If a `pd.Series` is passed in, it will be based on the 2315 corresponding values of the pd.Series. 2316 2317 Returns: 2318 pd.Series: The interval variable set indexed by its corresponding 2319 dimensions. 2320 2321 Raises: 2322 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2323 ValueError: if the `name` is not a valid identifier or already exists. 2324 ValueError: if the all the indexes do not match. 2325 """ 2326 if not isinstance(index, pd.Index): 2327 raise TypeError("Non-index object is used as index") 2328 if not name.isidentifier(): 2329 raise ValueError("name={} is not a valid identifier".format(name)) 2330 2331 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2332 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2333 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2334 interval_array = [] 2335 for i in index: 2336 interval_array.append( 2337 self.new_interval_var( 2338 start=starts[i], 2339 size=sizes[i], 2340 end=ends[i], 2341 name=f"{name}[{i}]", 2342 ) 2343 ) 2344 return pd.Series(index=index, data=interval_array)
Creates a series of interval 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.
- starts (Union[LinearExprT, pd.Series]): The start of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series.
Returns:
pd.Series: The interval 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 all the indexes do not match.
2346 def new_fixed_size_interval_var( 2347 self, start: LinearExprT, size: IntegralT, name: str 2348 ) -> IntervalVar: 2349 """Creates an interval variable from start, and a fixed size. 2350 2351 An interval variable is a constraint, that is itself used in other 2352 constraints like NoOverlap. 2353 2354 Args: 2355 start: The start of the interval. It must be of the form a * var + b. 2356 size: The size of the interval. It must be an integer value. 2357 name: The name of the interval variable. 2358 2359 Returns: 2360 An `IntervalVar` object. 2361 """ 2362 size = cmh.assert_is_int64(size) 2363 start_expr = self.parse_linear_expression(start) 2364 size_expr = self.parse_linear_expression(size) 2365 end_expr = self.parse_linear_expression(start + size) 2366 if len(start_expr.vars) > 1: 2367 raise TypeError( 2368 "cp_model.new_interval_var: start must be affine or constant." 2369 ) 2370 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)
Creates an interval variable from start, and a fixed size.
An interval variable is a constraint, that is itself used in other constraints like NoOverlap.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be an integer value.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2372 def new_fixed_size_interval_var_series( 2373 self, 2374 name: str, 2375 index: pd.Index, 2376 starts: Union[LinearExprT, pd.Series], 2377 sizes: Union[IntegralT, pd.Series], 2378 ) -> pd.Series: 2379 """Creates a series of interval variables with the given name. 2380 2381 Args: 2382 name (str): Required. The name of the variable set. 2383 index (pd.Index): Required. The index to use for the variable set. 2384 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2385 set. If a `pd.Series` is passed in, it will be based on the 2386 corresponding values of the pd.Series. 2387 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2388 the set. If a `pd.Series` is passed in, it will be based on the 2389 corresponding values of the pd.Series. 2390 2391 Returns: 2392 pd.Series: The interval variable set indexed by its corresponding 2393 dimensions. 2394 2395 Raises: 2396 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2397 ValueError: if the `name` is not a valid identifier or already exists. 2398 ValueError: if the all the indexes do not match. 2399 """ 2400 if not isinstance(index, pd.Index): 2401 raise TypeError("Non-index object is used as index") 2402 if not name.isidentifier(): 2403 raise ValueError("name={} is not a valid identifier".format(name)) 2404 2405 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2406 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2407 interval_array = [] 2408 for i in index: 2409 interval_array.append( 2410 self.new_fixed_size_interval_var( 2411 start=starts[i], 2412 size=sizes[i], 2413 name=f"{name}[{i}]", 2414 ) 2415 ) 2416 return pd.Series(index=index, data=interval_array)
Creates a series of interval 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.
- starts (Union[LinearExprT, pd.Series]): The start of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series.
Returns:
pd.Series: The interval 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 all the indexes do not match.
2418 def new_optional_interval_var( 2419 self, 2420 start: LinearExprT, 2421 size: LinearExprT, 2422 end: LinearExprT, 2423 is_present: LiteralT, 2424 name: str, 2425 ) -> IntervalVar: 2426 """Creates an optional interval var from start, size, end, and is_present. 2427 2428 An optional interval variable is a constraint, that is itself used in other 2429 constraints like NoOverlap. This constraint is protected by a presence 2430 literal that indicates if it is active or not. 2431 2432 Internally, it ensures that `is_present` implies `start + size == 2433 end`. 2434 2435 Args: 2436 start: The start of the interval. It must be of the form a * var + b. 2437 size: The size of the interval. It must be of the form a * var + b. 2438 end: The end of the interval. It must be of the form a * var + b. 2439 is_present: A literal that indicates if the interval is active or not. A 2440 inactive interval is simply ignored by all constraints. 2441 name: The name of the interval variable. 2442 2443 Returns: 2444 An `IntervalVar` object. 2445 """ 2446 2447 # Creates the IntervalConstraintProto object. 2448 is_present_index = self.get_or_make_boolean_index(is_present) 2449 start_expr = self.parse_linear_expression(start) 2450 size_expr = self.parse_linear_expression(size) 2451 end_expr = self.parse_linear_expression(end) 2452 if len(start_expr.vars) > 1: 2453 raise TypeError( 2454 "cp_model.new_interval_var: start must be affine or constant." 2455 ) 2456 if len(size_expr.vars) > 1: 2457 raise TypeError( 2458 "cp_model.new_interval_var: size must be affine or constant." 2459 ) 2460 if len(end_expr.vars) > 1: 2461 raise TypeError( 2462 "cp_model.new_interval_var: end must be affine or constant." 2463 ) 2464 return IntervalVar( 2465 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2466 )
Creates an optional interval var from start, size, end, and is_present.
An optional interval variable is a constraint, that is itself used in other constraints like NoOverlap. This constraint is protected by a presence literal that indicates if it is active or not.
Internally, it ensures that is_present
implies start + size ==
end
.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be of the form a * var + b.
- end: The end of the interval. It must be of the form a * var + b.
- is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2468 def new_optional_interval_var_series( 2469 self, 2470 name: str, 2471 index: pd.Index, 2472 starts: Union[LinearExprT, pd.Series], 2473 sizes: Union[LinearExprT, pd.Series], 2474 ends: Union[LinearExprT, pd.Series], 2475 are_present: Union[LiteralT, pd.Series], 2476 ) -> pd.Series: 2477 """Creates a series of interval variables with the given name. 2478 2479 Args: 2480 name (str): Required. The name of the variable set. 2481 index (pd.Index): Required. The index to use for the variable set. 2482 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2483 set. If a `pd.Series` is passed in, it will be based on the 2484 corresponding values of the pd.Series. 2485 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2486 set. If a `pd.Series` is passed in, it will be based on the 2487 corresponding values of the pd.Series. 2488 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2489 set. If a `pd.Series` is passed in, it will be based on the 2490 corresponding values of the pd.Series. 2491 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2492 interval in the set. If a `pd.Series` is passed in, it will be based on 2493 the corresponding values of the pd.Series. 2494 2495 Returns: 2496 pd.Series: The interval variable set indexed by its corresponding 2497 dimensions. 2498 2499 Raises: 2500 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2501 ValueError: if the `name` is not a valid identifier or already exists. 2502 ValueError: if the all the indexes do not match. 2503 """ 2504 if not isinstance(index, pd.Index): 2505 raise TypeError("Non-index object is used as index") 2506 if not name.isidentifier(): 2507 raise ValueError("name={} is not a valid identifier".format(name)) 2508 2509 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2510 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2511 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2512 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2513 2514 interval_array = [] 2515 for i in index: 2516 interval_array.append( 2517 self.new_optional_interval_var( 2518 start=starts[i], 2519 size=sizes[i], 2520 end=ends[i], 2521 is_present=are_present[i], 2522 name=f"{name}[{i}]", 2523 ) 2524 ) 2525 return pd.Series(index=index, data=interval_array)
Creates a series of interval 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.
- starts (Union[LinearExprT, pd.Series]): The start of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - are_present (Union[LiteralT, pd.Series]): The performed literal of each
interval in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series.
Returns:
pd.Series: The interval 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 all the indexes do not match.
2527 def new_optional_fixed_size_interval_var( 2528 self, 2529 start: LinearExprT, 2530 size: IntegralT, 2531 is_present: LiteralT, 2532 name: str, 2533 ) -> IntervalVar: 2534 """Creates an interval variable from start, and a fixed size. 2535 2536 An interval variable is a constraint, that is itself used in other 2537 constraints like NoOverlap. 2538 2539 Args: 2540 start: The start of the interval. It must be of the form a * var + b. 2541 size: The size of the interval. It must be an integer value. 2542 is_present: A literal that indicates if the interval is active or not. A 2543 inactive interval is simply ignored by all constraints. 2544 name: The name of the interval variable. 2545 2546 Returns: 2547 An `IntervalVar` object. 2548 """ 2549 size = cmh.assert_is_int64(size) 2550 start_expr = self.parse_linear_expression(start) 2551 size_expr = self.parse_linear_expression(size) 2552 end_expr = self.parse_linear_expression(start + size) 2553 if len(start_expr.vars) > 1: 2554 raise TypeError( 2555 "cp_model.new_interval_var: start must be affine or constant." 2556 ) 2557 is_present_index = self.get_or_make_boolean_index(is_present) 2558 return IntervalVar( 2559 self.__model, 2560 start_expr, 2561 size_expr, 2562 end_expr, 2563 is_present_index, 2564 name, 2565 )
Creates an interval variable from start, and a fixed size.
An interval variable is a constraint, that is itself used in other constraints like NoOverlap.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be an integer value.
- is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2567 def new_optional_fixed_size_interval_var_series( 2568 self, 2569 name: str, 2570 index: pd.Index, 2571 starts: Union[LinearExprT, pd.Series], 2572 sizes: Union[IntegralT, pd.Series], 2573 are_present: Union[LiteralT, pd.Series], 2574 ) -> pd.Series: 2575 """Creates a series of interval variables with the given name. 2576 2577 Args: 2578 name (str): Required. The name of the variable set. 2579 index (pd.Index): Required. The index to use for the variable set. 2580 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2581 set. If a `pd.Series` is passed in, it will be based on the 2582 corresponding values of the pd.Series. 2583 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2584 the set. If a `pd.Series` is passed in, it will be based on the 2585 corresponding values of the pd.Series. 2586 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2587 interval in the set. If a `pd.Series` is passed in, it will be based on 2588 the corresponding values of the pd.Series. 2589 2590 Returns: 2591 pd.Series: The interval variable set indexed by its corresponding 2592 dimensions. 2593 2594 Raises: 2595 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2596 ValueError: if the `name` is not a valid identifier or already exists. 2597 ValueError: if the all the indexes do not match. 2598 """ 2599 if not isinstance(index, pd.Index): 2600 raise TypeError("Non-index object is used as index") 2601 if not name.isidentifier(): 2602 raise ValueError("name={} is not a valid identifier".format(name)) 2603 2604 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2605 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2606 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2607 interval_array = [] 2608 for i in index: 2609 interval_array.append( 2610 self.new_optional_fixed_size_interval_var( 2611 start=starts[i], 2612 size=sizes[i], 2613 is_present=are_present[i], 2614 name=f"{name}[{i}]", 2615 ) 2616 ) 2617 return pd.Series(index=index, data=interval_array)
Creates a series of interval 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.
- starts (Union[LinearExprT, pd.Series]): The start of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - are_present (Union[LiteralT, pd.Series]): The performed literal of each
interval in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series.
Returns:
pd.Series: The interval 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 all the indexes do not match.
2619 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2620 """Adds NoOverlap(interval_vars). 2621 2622 A NoOverlap constraint ensures that all present intervals do not overlap 2623 in time. 2624 2625 Args: 2626 interval_vars: The list of interval variables to constrain. 2627 2628 Returns: 2629 An instance of the `Constraint` class. 2630 """ 2631 ct = Constraint(self) 2632 model_ct = self.__model.constraints[ct.index] 2633 model_ct.no_overlap.intervals.extend( 2634 [self.get_interval_index(x) for x in interval_vars] 2635 ) 2636 return ct
Adds NoOverlap(interval_vars).
A NoOverlap constraint ensures that all present intervals do not overlap in time.
Arguments:
- interval_vars: The list of interval variables to constrain.
Returns:
An instance of the
Constraint
class.
2638 def add_no_overlap_2d( 2639 self, 2640 x_intervals: Iterable[IntervalVar], 2641 y_intervals: Iterable[IntervalVar], 2642 ) -> Constraint: 2643 """Adds NoOverlap2D(x_intervals, y_intervals). 2644 2645 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2646 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2647 by two intervals which represent its projection onto the X and Y axis. 2648 2649 Furthermore, one box is optional if at least one of the x or y interval is 2650 optional. 2651 2652 Args: 2653 x_intervals: The X coordinates of the rectangles. 2654 y_intervals: The Y coordinates of the rectangles. 2655 2656 Returns: 2657 An instance of the `Constraint` class. 2658 """ 2659 ct = Constraint(self) 2660 model_ct = self.__model.constraints[ct.index] 2661 model_ct.no_overlap_2d.x_intervals.extend( 2662 [self.get_interval_index(x) for x in x_intervals] 2663 ) 2664 model_ct.no_overlap_2d.y_intervals.extend( 2665 [self.get_interval_index(x) for x in y_intervals] 2666 ) 2667 return ct
Adds NoOverlap2D(x_intervals, y_intervals).
A NoOverlap2D constraint ensures that all present rectangles do not overlap on a plane. Each rectangle is aligned with the X and Y axis, and is defined by two intervals which represent its projection onto the X and Y axis.
Furthermore, one box is optional if at least one of the x or y interval is optional.
Arguments:
- x_intervals: The X coordinates of the rectangles.
- y_intervals: The Y coordinates of the rectangles.
Returns:
An instance of the
Constraint
class.
2669 def add_cumulative( 2670 self, 2671 intervals: Iterable[IntervalVar], 2672 demands: Iterable[LinearExprT], 2673 capacity: LinearExprT, 2674 ) -> Constraint: 2675 """Adds Cumulative(intervals, demands, capacity). 2676 2677 This constraint enforces that: 2678 2679 for all t: 2680 sum(demands[i] 2681 if (start(intervals[i]) <= t < end(intervals[i])) and 2682 (intervals[i] is present)) <= capacity 2683 2684 Args: 2685 intervals: The list of intervals. 2686 demands: The list of demands for each interval. Each demand must be >= 0. 2687 Each demand can be a 1-var affine expression (a * x + b). 2688 capacity: The maximum capacity of the cumulative constraint. It can be a 2689 1-var affine expression (a * x + b). 2690 2691 Returns: 2692 An instance of the `Constraint` class. 2693 """ 2694 cumulative = Constraint(self) 2695 model_ct = self.__model.constraints[cumulative.index] 2696 model_ct.cumulative.intervals.extend( 2697 [self.get_interval_index(x) for x in intervals] 2698 ) 2699 for d in demands: 2700 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2701 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2702 return cumulative
Adds Cumulative(intervals, demands, capacity).
This constraint enforces that:
for all t: sum(demands[i] if (start(intervals[i]) <= t < end(intervals[i])) and (intervals[i] is present)) <= capacity
Arguments:
- intervals: The list of intervals.
- demands: The list of demands for each interval. Each demand must be >= 0. Each demand can be a 1-var affine expression (a * x + b).
- capacity: The maximum capacity of the cumulative constraint. It can be a 1-var affine expression (a * x + b).
Returns:
An instance of the
Constraint
class.
2705 def clone(self) -> "CpModel": 2706 """Reset the model, and creates a new one from a CpModelProto instance.""" 2707 clone = CpModel() 2708 clone.proto.CopyFrom(self.proto) 2709 clone.rebuild_constant_map() 2710 return clone
Reset the model, and creates a new one from a CpModelProto instance.
2712 def rebuild_constant_map(self): 2713 """Internal method used during model cloning.""" 2714 for i, var in enumerate(self.__model.variables): 2715 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2716 self.__constant_map[var.domain[0]] = i
Internal method used during model cloning.
2718 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2719 """Returns an already created Boolean variable from its index.""" 2720 if index < 0 or index >= len(self.__model.variables): 2721 raise ValueError( 2722 f"get_bool_var_from_proto_index: out of bound index {index}" 2723 ) 2724 var = self.__model.variables[index] 2725 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2726 raise ValueError( 2727 f"get_bool_var_from_proto_index: index {index} does not reference" 2728 + " a Boolean variable" 2729 ) 2730 2731 return IntVar(self.__model, index, None)
Returns an already created Boolean variable from its index.
2733 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2734 """Returns an already created integer variable from its index.""" 2735 if index < 0 or index >= len(self.__model.variables): 2736 raise ValueError( 2737 f"get_int_var_from_proto_index: out of bound index {index}" 2738 ) 2739 return IntVar(self.__model, index, None)
Returns an already created integer variable from its index.
2741 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2742 """Returns an already created interval variable from its index.""" 2743 if index < 0 or index >= len(self.__model.constraints): 2744 raise ValueError( 2745 f"get_interval_var_from_proto_index: out of bound index {index}" 2746 ) 2747 ct = self.__model.constraints[index] 2748 if not ct.HasField("interval"): 2749 raise ValueError( 2750 f"get_interval_var_from_proto_index: index {index} does not" 2751 " reference an" + " interval variable" 2752 ) 2753 2754 return IntervalVar(self.__model, index, None, None, None, None)
Returns an already created interval variable from its index.
2761 @property 2762 def proto(self) -> cp_model_pb2.CpModelProto: 2763 """Returns the underlying CpModelProto.""" 2764 return self.__model
Returns the underlying CpModelProto.
2769 def get_or_make_index(self, arg: VariableT) -> int: 2770 """Returns the index of a variable, its negation, or a number.""" 2771 if isinstance(arg, IntVar): 2772 return arg.index 2773 if ( 2774 isinstance(arg, _ProductCst) 2775 and isinstance(arg.expression(), IntVar) 2776 and arg.coefficient() == -1 2777 ): 2778 return -arg.expression().index - 1 2779 if isinstance(arg, IntegralTypes): 2780 arg = cmh.assert_is_int64(arg) 2781 return self.get_or_make_index_from_constant(arg) 2782 raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")")
Returns the index of a variable, its negation, or a number.
2784 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2785 """Returns an index from a boolean expression.""" 2786 if isinstance(arg, IntVar): 2787 self.assert_is_boolean_variable(arg) 2788 return arg.index 2789 if isinstance(arg, _NotBooleanVariable): 2790 self.assert_is_boolean_variable(arg.negated()) 2791 return arg.index 2792 if isinstance(arg, IntegralTypes): 2793 if arg == ~False: # -1 2794 return self.get_or_make_index_from_constant(1) 2795 if arg == ~True: # -2 2796 return self.get_or_make_index_from_constant(0) 2797 arg = cmh.assert_is_zero_or_one(arg) 2798 return self.get_or_make_index_from_constant(arg) 2799 if cmh.is_boolean(arg): 2800 return self.get_or_make_index_from_constant(int(arg)) 2801 raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})")
Returns an index from a boolean expression.
2808 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2809 if value in self.__constant_map: 2810 return self.__constant_map[value] 2811 index = len(self.__model.variables) 2812 self.__model.variables.add(domain=[value, value]) 2813 self.__constant_map[value] = index 2814 return index
2824 def parse_linear_expression( 2825 self, linear_expr: LinearExprT, negate: bool = False 2826 ) -> cp_model_pb2.LinearExpressionProto: 2827 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2828 result: cp_model_pb2.LinearExpressionProto = ( 2829 cp_model_pb2.LinearExpressionProto() 2830 ) 2831 mult = -1 if negate else 1 2832 if isinstance(linear_expr, IntegralTypes): 2833 result.offset = int(linear_expr) * mult 2834 return result 2835 2836 if isinstance(linear_expr, IntVar): 2837 result.vars.append(self.get_or_make_index(linear_expr)) 2838 result.coeffs.append(mult) 2839 return result 2840 2841 coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map() 2842 result.offset = constant * mult 2843 for t in coeffs_map.items(): 2844 if not isinstance(t[0], IntVar): 2845 raise TypeError("Wrong argument" + str(t)) 2846 c = cmh.assert_is_int64(t[1]) 2847 result.vars.append(t[0].index) 2848 result.coeffs.append(c * mult) 2849 return result
Returns a LinearExpressionProto built from a LinearExpr instance.
2891 def minimize(self, obj: ObjLinearExprT): 2892 """Sets the objective of the model to minimize(obj).""" 2893 self._set_objective(obj, minimize=True)
Sets the objective of the model to minimize(obj).
2895 def maximize(self, obj: ObjLinearExprT): 2896 """Sets the objective of the model to maximize(obj).""" 2897 self._set_objective(obj, minimize=False)
Sets the objective of the model to maximize(obj).
2908 def add_decision_strategy( 2909 self, 2910 variables: Sequence[IntVar], 2911 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2912 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2913 ) -> None: 2914 """Adds a search strategy to the model. 2915 2916 Args: 2917 variables: a list of variables this strategy will assign. 2918 var_strategy: heuristic to choose the next variable to assign. 2919 domain_strategy: heuristic to reduce the domain of the selected variable. 2920 Currently, this is advanced code: the union of all strategies added to 2921 the model must be complete, i.e. instantiates all variables. Otherwise, 2922 solve() will fail. 2923 """ 2924 2925 strategy: cp_model_pb2.DecisionStrategyProto = ( 2926 self.__model.search_strategy.add() 2927 ) 2928 for v in variables: 2929 expr = strategy.exprs.add() 2930 if v.index >= 0: 2931 expr.vars.append(v.index) 2932 expr.coeffs.append(1) 2933 else: 2934 expr.vars.append(self.negated(v.index)) 2935 expr.coeffs.append(-1) 2936 expr.offset = 1 2937 2938 strategy.variable_selection_strategy = var_strategy 2939 strategy.domain_reduction_strategy = domain_strategy
Adds a search strategy to the model.
Arguments:
- variables: a list of variables this strategy will assign.
- var_strategy: heuristic to choose the next variable to assign.
- domain_strategy: heuristic to reduce the domain of the selected variable. Currently, this is advanced code: the union of all strategies added to the model must be complete, i.e. instantiates all variables. Otherwise, solve() will fail.
2941 def model_stats(self) -> str: 2942 """Returns a string containing some model statistics.""" 2943 return swig_helper.CpSatHelper.model_stats(self.__model)
Returns a string containing some model statistics.
2945 def validate(self) -> str: 2946 """Returns a string indicating that the model is invalid.""" 2947 return swig_helper.CpSatHelper.validate_model(self.__model)
Returns a string indicating that the model is invalid.
2949 def export_to_file(self, file: str) -> bool: 2950 """Write the model as a protocol buffer to 'file'. 2951 2952 Args: 2953 file: file to write the model to. If the filename ends with 'txt', the 2954 model will be written as a text file, otherwise, the binary format will 2955 be used. 2956 2957 Returns: 2958 True if the model was correctly written. 2959 """ 2960 return swig_helper.CpSatHelper.write_model_to_file(self.__model, file)
Write the model as a protocol buffer to 'file'.
Arguments:
- file: file to write the model to. If the filename ends with 'txt', the model will be written as a text file, otherwise, the binary format will be used.
Returns:
True if the model was correctly written.
2962 def add_hint(self, var: IntVar, value: int) -> None: 2963 """Adds 'var == value' as a hint to the solver.""" 2964 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2965 self.__model.solution_hint.values.append(value)
Adds 'var == value' as a hint to the solver.
2967 def clear_hints(self): 2968 """Removes any solution hint from the model.""" 2969 self.__model.ClearField("solution_hint")
Removes any solution hint from the model.
2971 def add_assumption(self, lit: LiteralT) -> None: 2972 """Adds the literal to the model as assumptions.""" 2973 self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
Adds the literal to the model as assumptions.
2975 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2976 """Adds the literals to the model as assumptions.""" 2977 for lit in literals: 2978 self.add_assumption(lit)
Adds the literals to the model as assumptions.
2980 def clear_assumptions(self) -> None: 2981 """Removes all assumptions from the model.""" 2982 self.__model.ClearField("assumptions")
Removes all assumptions from the model.
2985 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2986 if isinstance(x, IntVar): 2987 var = self.__model.variables[x.index] 2988 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2989 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2990 elif not isinstance(x, _NotBooleanVariable): 2991 raise TypeError("TypeError: " + str(x) + " is not a boolean variable")
1362 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1363 """Create an integer variable with domain [lb, ub]. 1364 1365 The CP-SAT solver is limited to integer variables. If you have fractional 1366 values, scale them up so that they become integers; if you have strings, 1367 encode them as integers. 1368 1369 Args: 1370 lb: Lower bound for the variable. 1371 ub: Upper bound for the variable. 1372 name: The name of the variable. 1373 1374 Returns: 1375 a variable whose domain is [lb, ub]. 1376 """ 1377 1378 return IntVar(self.__model, sorted_interval_list.Domain(lb, ub), name)
Create an integer variable with domain [lb, ub].
The CP-SAT solver is limited to integer variables. If you have fractional values, scale them up so that they become integers; if you have strings, encode them as integers.
Arguments:
- lb: Lower bound for the variable.
- ub: Upper bound for the variable.
- name: The name of the variable.
Returns:
a variable whose domain is [lb, ub].
1380 def new_int_var_from_domain( 1381 self, domain: sorted_interval_list.Domain, name: str 1382 ) -> IntVar: 1383 """Create an integer variable from a domain. 1384 1385 A domain is a set of integers specified by a collection of intervals. 1386 For example, `model.new_int_var_from_domain(cp_model. 1387 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1388 1389 Args: 1390 domain: An instance of the Domain class. 1391 name: The name of the variable. 1392 1393 Returns: 1394 a variable whose domain is the given domain. 1395 """ 1396 return IntVar(self.__model, domain, name)
Create an integer variable from a domain.
A domain is a set of integers specified by a collection of intervals.
For example, model.new_int_var_from_domain(cp_model.
Domain.from_intervals([[1, 2], [4, 6]]), 'x')
Arguments:
- domain: An instance of the Domain class.
- name: The name of the variable.
Returns:
a variable whose domain is the given domain.
1398 def new_bool_var(self, name: str) -> IntVar: 1399 """Creates a 0-1 variable with the given name.""" 1400 return IntVar(self.__model, sorted_interval_list.Domain(0, 1), name)
Creates a 0-1 variable with the given name.
1402 def new_constant(self, value: IntegralT) -> IntVar: 1403 """Declares a constant integer.""" 1404 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None)
Declares a constant integer.
1406 def new_int_var_series( 1407 self, 1408 name: str, 1409 index: pd.Index, 1410 lower_bounds: Union[IntegralT, pd.Series], 1411 upper_bounds: Union[IntegralT, pd.Series], 1412 ) -> pd.Series: 1413 """Creates a series of (scalar-valued) variables with the given name. 1414 1415 Args: 1416 name (str): Required. The name of the variable set. 1417 index (pd.Index): Required. The index to use for the variable set. 1418 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1419 set. If a `pd.Series` is passed in, it will be based on the 1420 corresponding values of the pd.Series. 1421 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1422 set. If a `pd.Series` is passed in, it will be based on the 1423 corresponding values of the pd.Series. 1424 1425 Returns: 1426 pd.Series: The variable set indexed by its corresponding dimensions. 1427 1428 Raises: 1429 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1430 ValueError: if the `name` is not a valid identifier or already exists. 1431 ValueError: if the `lowerbound` is greater than the `upperbound`. 1432 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1433 the input index. 1434 """ 1435 if not isinstance(index, pd.Index): 1436 raise TypeError("Non-index object is used as index") 1437 if not name.isidentifier(): 1438 raise ValueError("name={} is not a valid identifier".format(name)) 1439 if ( 1440 isinstance(lower_bounds, IntegralTypes) 1441 and isinstance(upper_bounds, IntegralTypes) 1442 and lower_bounds > upper_bounds 1443 ): 1444 raise ValueError( 1445 f"lower_bound={lower_bounds} is greater than" 1446 f" upper_bound={upper_bounds} for variable set={name}" 1447 ) 1448 1449 lower_bounds = _convert_to_integral_series_and_validate_index( 1450 lower_bounds, index 1451 ) 1452 upper_bounds = _convert_to_integral_series_and_validate_index( 1453 upper_bounds, index 1454 ) 1455 return pd.Series( 1456 index=index, 1457 data=[ 1458 # pylint: disable=g-complex-comprehension 1459 IntVar( 1460 model=self.__model, 1461 name=f"{name}[{i}]", 1462 domain=sorted_interval_list.Domain( 1463 lower_bounds[i], upper_bounds[i] 1464 ), 1465 ) 1466 for i in index 1467 ], 1468 )
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, pd.Series]): 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. - upper_bounds (Union[int, pd.Series]): 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.
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
, orupper_bound
does not match - the input index.
1470 def new_bool_var_series( 1471 self, 1472 name: str, 1473 index: pd.Index, 1474 ) -> pd.Series: 1475 """Creates a series of (scalar-valued) variables with the given name. 1476 1477 Args: 1478 name (str): Required. The name of the variable set. 1479 index (pd.Index): Required. The index to use for the variable set. 1480 1481 Returns: 1482 pd.Series: The variable set indexed by its corresponding dimensions. 1483 1484 Raises: 1485 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1486 ValueError: if the `name` is not a valid identifier or already exists. 1487 """ 1488 return self.new_int_var_series( 1489 name=name, index=index, lower_bounds=0, upper_bounds=1 1490 )
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.
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.
1494 def add_linear_constraint( 1495 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1496 ) -> Constraint: 1497 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1498 return self.add_linear_expression_in_domain( 1499 linear_expr, sorted_interval_list.Domain(lb, ub) 1500 )
Adds the constraint: lb <= linear_expr <= ub
.
1502 def add_linear_expression_in_domain( 1503 self, linear_expr: LinearExprT, domain: sorted_interval_list.Domain 1504 ) -> Constraint: 1505 """Adds the constraint: `linear_expr` in `domain`.""" 1506 if isinstance(linear_expr, LinearExpr): 1507 ct = Constraint(self) 1508 model_ct = self.__model.constraints[ct.index] 1509 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1510 for t in coeffs_map.items(): 1511 if not isinstance(t[0], IntVar): 1512 raise TypeError("Wrong argument" + str(t)) 1513 c = cmh.assert_is_int64(t[1]) 1514 model_ct.linear.vars.append(t[0].index) 1515 model_ct.linear.coeffs.append(c) 1516 model_ct.linear.domain.extend( 1517 [ 1518 cmh.capped_subtraction(x, constant) 1519 for x in domain.flattened_intervals() 1520 ] 1521 ) 1522 return ct 1523 if isinstance(linear_expr, IntegralTypes): 1524 if not domain.contains(int(linear_expr)): 1525 return self.add_bool_or([]) # Evaluate to false. 1526 else: 1527 return self.add_bool_and([]) # Evaluate to true. 1528 raise TypeError( 1529 "not supported: CpModel.add_linear_expression_in_domain(" 1530 + str(linear_expr) 1531 + " " 1532 + str(domain) 1533 + ")" 1534 )
Adds the constraint: linear_expr
in domain
.
1542 def add(self, ct): 1543 """Adds a `BoundedLinearExpression` to the model. 1544 1545 Args: 1546 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1547 1548 Returns: 1549 An instance of the `Constraint` class. 1550 """ 1551 if isinstance(ct, BoundedLinearExpression): 1552 return self.add_linear_expression_in_domain( 1553 ct.expression(), 1554 sorted_interval_list.Domain.from_flat_intervals(ct.bounds()), 1555 ) 1556 if ct and cmh.is_boolean(ct): 1557 return self.add_bool_or([True]) 1558 if not ct and cmh.is_boolean(ct): 1559 return self.add_bool_or([]) # Evaluate to false. 1560 raise TypeError("not supported: CpModel.add(" + str(ct) + ")")
Adds a BoundedLinearExpression
to the model.
Arguments:
- ct: A
BoundedLinearExpression
.
Returns:
An instance of the
Constraint
class.
1570 def add_all_different(self, *expressions): 1571 """Adds AllDifferent(expressions). 1572 1573 This constraint forces all expressions to have different values. 1574 1575 Args: 1576 *expressions: simple expressions of the form a * var + constant. 1577 1578 Returns: 1579 An instance of the `Constraint` class. 1580 """ 1581 ct = Constraint(self) 1582 model_ct = self.__model.constraints[ct.index] 1583 expanded = expand_generator_or_tuple(expressions) 1584 model_ct.all_diff.exprs.extend( 1585 self.parse_linear_expression(x) for x in expanded 1586 ) 1587 return ct
Adds AllDifferent(expressions).
This constraint forces all expressions to have different values.
Arguments:
- *expressions: simple expressions of the form a * var + constant.
Returns:
An instance of the
Constraint
class.
1589 def add_element( 1590 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1591 ) -> Constraint: 1592 """Adds the element constraint: `variables[index] == target`. 1593 1594 Args: 1595 index: The index of the variable that's being constrained. 1596 variables: A list of variables. 1597 target: The value that the variable must be equal to. 1598 1599 Returns: 1600 An instance of the `Constraint` class. 1601 """ 1602 1603 if not variables: 1604 raise ValueError("add_element expects a non-empty variables array") 1605 1606 if isinstance(index, IntegralTypes): 1607 variable: VariableT = list(variables)[int(index)] 1608 return self.add(variable == target) 1609 1610 ct = Constraint(self) 1611 model_ct = self.__model.constraints[ct.index] 1612 model_ct.element.index = self.get_or_make_index(index) 1613 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1614 model_ct.element.target = self.get_or_make_index(target) 1615 return ct
Adds the element constraint: variables[index] == target
.
Arguments:
- index: The index of the variable that's being constrained.
- variables: A list of variables.
- target: The value that the variable must be equal to.
Returns:
An instance of the
Constraint
class.
1617 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1618 """Adds Circuit(arcs). 1619 1620 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1621 1622 A circuit is a unique Hamiltonian path in a subgraph of the total 1623 graph. In case a node 'i' is not in the path, then there must be a 1624 loop arc 'i -> i' associated with a true literal. Otherwise 1625 this constraint will fail. 1626 1627 Args: 1628 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1629 literal). The arc is selected in the circuit if the literal is true. 1630 Both source_node and destination_node must be integers between 0 and the 1631 number of nodes - 1. 1632 1633 Returns: 1634 An instance of the `Constraint` class. 1635 1636 Raises: 1637 ValueError: If the list of arcs is empty. 1638 """ 1639 if not arcs: 1640 raise ValueError("add_circuit expects a non-empty array of arcs") 1641 ct = Constraint(self) 1642 model_ct = self.__model.constraints[ct.index] 1643 for arc in arcs: 1644 tail = cmh.assert_is_int32(arc[0]) 1645 head = cmh.assert_is_int32(arc[1]) 1646 lit = self.get_or_make_boolean_index(arc[2]) 1647 model_ct.circuit.tails.append(tail) 1648 model_ct.circuit.heads.append(head) 1649 model_ct.circuit.literals.append(lit) 1650 return ct
Adds Circuit(arcs).
Adds a circuit constraint from a sparse list of arcs that encode the graph.
A circuit is a unique Hamiltonian path in a subgraph of the total graph. In case a node 'i' is not in the path, then there must be a loop arc 'i -> i' associated with a true literal. Otherwise this constraint will fail.
Arguments:
- arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: If the list of arcs is empty.
1652 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1653 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1654 1655 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1656 literals[i] is true must satisfy this set of properties: 1657 - #incoming arcs == 1 except for node 0. 1658 - #outgoing arcs == 1 except for node 0. 1659 - for node zero, #incoming arcs == #outgoing arcs. 1660 - There are no duplicate arcs. 1661 - Self-arcs are allowed except for node 0. 1662 - There is no cycle in this graph, except through node 0. 1663 1664 Args: 1665 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1666 literal). The arc is selected in the circuit if the literal is true. 1667 Both source_node and destination_node must be integers between 0 and the 1668 number of nodes - 1. 1669 1670 Returns: 1671 An instance of the `Constraint` class. 1672 1673 Raises: 1674 ValueError: If the list of arcs is empty. 1675 """ 1676 if not arcs: 1677 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1678 ct = Constraint(self) 1679 model_ct = self.__model.constraints[ct.index] 1680 for arc in arcs: 1681 tail = cmh.assert_is_int32(arc[0]) 1682 head = cmh.assert_is_int32(arc[1]) 1683 lit = self.get_or_make_boolean_index(arc[2]) 1684 model_ct.routes.tails.append(tail) 1685 model_ct.routes.heads.append(head) 1686 model_ct.routes.literals.append(lit) 1687 return ct
Adds a multiple circuit constraint, aka the 'VRP' constraint.
The direct graph where arc #i (from tails[i] to head[i]) is present iff literals[i] is true must satisfy this set of properties:
- #incoming arcs == 1 except for node 0.
- #outgoing arcs == 1 except for node 0.
- for node zero, #incoming arcs == #outgoing arcs.
- There are no duplicate arcs.
- Self-arcs are allowed except for node 0.
- There is no cycle in this graph, except through node 0.
Arguments:
- arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: If the list of arcs is empty.
1689 def add_allowed_assignments( 1690 self, 1691 variables: Sequence[VariableT], 1692 tuples_list: Iterable[Sequence[IntegralT]], 1693 ) -> Constraint: 1694 """Adds AllowedAssignments(variables, tuples_list). 1695 1696 An AllowedAssignments constraint is a constraint on an array of variables, 1697 which requires that when all variables are assigned values, the resulting 1698 array equals one of the tuples in `tuple_list`. 1699 1700 Args: 1701 variables: A list of variables. 1702 tuples_list: A list of admissible tuples. Each tuple must have the same 1703 length as the variables, and the ith value of a tuple corresponds to the 1704 ith variable. 1705 1706 Returns: 1707 An instance of the `Constraint` class. 1708 1709 Raises: 1710 TypeError: If a tuple does not have the same size as the list of 1711 variables. 1712 ValueError: If the array of variables is empty. 1713 """ 1714 1715 if not variables: 1716 raise ValueError( 1717 "add_allowed_assignments expects a non-empty variables array" 1718 ) 1719 1720 ct: Constraint = Constraint(self) 1721 model_ct = self.__model.constraints[ct.index] 1722 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1723 arity: int = len(variables) 1724 for t in tuples_list: 1725 if len(t) != arity: 1726 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1727 1728 # duck-typing (no explicit type checks here) 1729 try: 1730 model_ct.table.values.extend(a for b in tuples_list for a in b) 1731 except ValueError as ex: 1732 raise TypeError(f"add_xxx_assignment: Not an integer or does not fit in an int64_t: {ex.args}") from ex 1733 1734 return ct
Adds AllowedAssignments(variables, tuples_list).
An AllowedAssignments constraint is a constraint on an array of variables,
which requires that when all variables are assigned values, the resulting
array equals one of the tuples in tuple_list
.
Arguments:
- variables: A list of variables.
- tuples_list: A list of admissible tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of variables.
- ValueError: If the array of variables is empty.
1736 def add_forbidden_assignments( 1737 self, 1738 variables: Sequence[VariableT], 1739 tuples_list: Iterable[Sequence[IntegralT]], 1740 ) -> Constraint: 1741 """Adds add_forbidden_assignments(variables, [tuples_list]). 1742 1743 A ForbiddenAssignments constraint is a constraint on an array of variables 1744 where the list of impossible combinations is provided in the tuples list. 1745 1746 Args: 1747 variables: A list of variables. 1748 tuples_list: A list of forbidden tuples. Each tuple must have the same 1749 length as the variables, and the *i*th value of a tuple corresponds to 1750 the *i*th variable. 1751 1752 Returns: 1753 An instance of the `Constraint` class. 1754 1755 Raises: 1756 TypeError: If a tuple does not have the same size as the list of 1757 variables. 1758 ValueError: If the array of variables is empty. 1759 """ 1760 1761 if not variables: 1762 raise ValueError( 1763 "add_forbidden_assignments expects a non-empty variables array" 1764 ) 1765 1766 index = len(self.__model.constraints) 1767 ct: Constraint = self.add_allowed_assignments(variables, tuples_list) 1768 self.__model.constraints[index].table.negated = True 1769 return ct
Adds add_forbidden_assignments(variables, [tuples_list]).
A ForbiddenAssignments constraint is a constraint on an array of variables where the list of impossible combinations is provided in the tuples list.
Arguments:
- variables: A list of variables.
- tuples_list: A list of forbidden tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of variables.
- ValueError: If the array of variables is empty.
1771 def add_automaton( 1772 self, 1773 transition_variables: Sequence[VariableT], 1774 starting_state: IntegralT, 1775 final_states: Sequence[IntegralT], 1776 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1777 ) -> Constraint: 1778 """Adds an automaton constraint. 1779 1780 An automaton constraint takes a list of variables (of size *n*), an initial 1781 state, a set of final states, and a set of transitions. A transition is a 1782 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1783 and *transition* is the label of an arc from *head* to *tail*, 1784 corresponding to the value of one variable in the list of variables. 1785 1786 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1787 contains the possible states of the automaton. The first state contains the 1788 initial state. The last phase contains the final states. 1789 1790 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1791 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1792 an arc from the state *tail* of phase *i* and the state *head* of phase 1793 *i* + 1. This arc is labeled by the value *transition* of the variables 1794 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1795 is assigned the value *transition*. 1796 1797 A feasible solution of this constraint is an assignment of variables such 1798 that, starting from the initial state in phase 0, there is a path labeled by 1799 the values of the variables that ends in one of the final states in the 1800 final phase. 1801 1802 Args: 1803 transition_variables: A non-empty list of variables whose values 1804 correspond to the labels of the arcs traversed by the automaton. 1805 starting_state: The initial state of the automaton. 1806 final_states: A non-empty list of admissible final states. 1807 transition_triples: A list of transitions for the automaton, in the 1808 following format (current_state, variable_value, next_state). 1809 1810 Returns: 1811 An instance of the `Constraint` class. 1812 1813 Raises: 1814 ValueError: if `transition_variables`, `final_states`, or 1815 `transition_triples` are empty. 1816 """ 1817 1818 if not transition_variables: 1819 raise ValueError( 1820 "add_automaton expects a non-empty transition_variables array" 1821 ) 1822 if not final_states: 1823 raise ValueError("add_automaton expects some final states") 1824 1825 if not transition_triples: 1826 raise ValueError("add_automaton expects some transition triples") 1827 1828 ct = Constraint(self) 1829 model_ct = self.__model.constraints[ct.index] 1830 model_ct.automaton.vars.extend( 1831 [self.get_or_make_index(x) for x in transition_variables] 1832 ) 1833 starting_state = cmh.assert_is_int64(starting_state) 1834 model_ct.automaton.starting_state = starting_state 1835 for v in final_states: 1836 v = cmh.assert_is_int64(v) 1837 model_ct.automaton.final_states.append(v) 1838 for t in transition_triples: 1839 if len(t) != 3: 1840 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1841 tail = cmh.assert_is_int64(t[0]) 1842 label = cmh.assert_is_int64(t[1]) 1843 head = cmh.assert_is_int64(t[2]) 1844 model_ct.automaton.transition_tail.append(tail) 1845 model_ct.automaton.transition_label.append(label) 1846 model_ct.automaton.transition_head.append(head) 1847 return ct
Adds an automaton constraint.
An automaton constraint takes a list of variables (of size n), an initial state, a set of final states, and a set of transitions. A transition is a triplet (tail, transition, head), where tail and head are states, and transition is the label of an arc from head to tail, corresponding to the value of one variable in the list of variables.
This automaton will be unrolled into a flow with n + 1 phases. Each phase contains the possible states of the automaton. The first state contains the initial state. The last phase contains the final states.
Between two consecutive phases i and i + 1, the automaton creates a set
of arcs. For each transition (tail, transition, head), it will add
an arc from the state tail of phase i and the state head of phase
i + 1. This arc is labeled by the value transition of the variables
variables[i]
. That is, this arc can only be selected if variables[i]
is assigned the value transition.
A feasible solution of this constraint is an assignment of variables such that, starting from the initial state in phase 0, there is a path labeled by the values of the variables that ends in one of the final states in the final phase.
Arguments:
- transition_variables: A non-empty list of variables whose values correspond to the labels of the arcs traversed by the automaton.
- starting_state: The initial state of the automaton.
- final_states: A non-empty list of admissible final states.
- transition_triples: A list of transitions for the automaton, in the following format (current_state, variable_value, next_state).
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: if
transition_variables
,final_states
, ortransition_triples
are empty.
1849 def add_inverse( 1850 self, 1851 variables: Sequence[VariableT], 1852 inverse_variables: Sequence[VariableT], 1853 ) -> Constraint: 1854 """Adds Inverse(variables, inverse_variables). 1855 1856 An inverse constraint enforces that if `variables[i]` is assigned a value 1857 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1858 1859 Args: 1860 variables: An array of integer variables. 1861 inverse_variables: An array of integer variables. 1862 1863 Returns: 1864 An instance of the `Constraint` class. 1865 1866 Raises: 1867 TypeError: if variables and inverse_variables have different lengths, or 1868 if they are empty. 1869 """ 1870 1871 if not variables or not inverse_variables: 1872 raise TypeError("The Inverse constraint does not accept empty arrays") 1873 if len(variables) != len(inverse_variables): 1874 raise TypeError( 1875 "In the inverse constraint, the two array variables and" 1876 " inverse_variables must have the same length." 1877 ) 1878 ct = Constraint(self) 1879 model_ct = self.__model.constraints[ct.index] 1880 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1881 model_ct.inverse.f_inverse.extend( 1882 [self.get_or_make_index(x) for x in inverse_variables] 1883 ) 1884 return ct
Adds Inverse(variables, inverse_variables).
An inverse constraint enforces that if variables[i]
is assigned a value
j
, then inverse_variables[j]
is assigned a value i
. And vice versa.
Arguments:
- variables: An array of integer variables.
- inverse_variables: An array of integer variables.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: if variables and inverse_variables have different lengths, or if they are empty.
1886 def add_reservoir_constraint( 1887 self, 1888 times: Iterable[LinearExprT], 1889 level_changes: Iterable[LinearExprT], 1890 min_level: int, 1891 max_level: int, 1892 ) -> Constraint: 1893 """Adds Reservoir(times, level_changes, min_level, max_level). 1894 1895 Maintains a reservoir level within bounds. The water level starts at 0, and 1896 at any time, it must be between min_level and max_level. 1897 1898 If the affine expression `times[i]` is assigned a value t, then the current 1899 level changes by `level_changes[i]`, which is constant, at time t. 1900 1901 Note that min level must be <= 0, and the max level must be >= 0. Please 1902 use fixed level_changes to simulate initial state. 1903 1904 Therefore, at any time: 1905 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1906 1907 Args: 1908 times: A list of 1-var affine expressions (a * x + b) which specify the 1909 time of the filling or emptying the reservoir. 1910 level_changes: A list of integer values that specifies the amount of the 1911 emptying or filling. Currently, variable demands are not supported. 1912 min_level: At any time, the level of the reservoir must be greater or 1913 equal than the min level. 1914 max_level: At any time, the level of the reservoir must be less or equal 1915 than the max level. 1916 1917 Returns: 1918 An instance of the `Constraint` class. 1919 1920 Raises: 1921 ValueError: if max_level < min_level. 1922 1923 ValueError: if max_level < 0. 1924 1925 ValueError: if min_level > 0 1926 """ 1927 1928 if max_level < min_level: 1929 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1930 1931 if max_level < 0: 1932 raise ValueError("Reservoir constraint must have a max_level >= 0") 1933 1934 if min_level > 0: 1935 raise ValueError("Reservoir constraint must have a min_level <= 0") 1936 1937 ct = Constraint(self) 1938 model_ct = self.__model.constraints[ct.index] 1939 model_ct.reservoir.time_exprs.extend( 1940 [self.parse_linear_expression(x) for x in times] 1941 ) 1942 model_ct.reservoir.level_changes.extend( 1943 [self.parse_linear_expression(x) for x in level_changes] 1944 ) 1945 model_ct.reservoir.min_level = min_level 1946 model_ct.reservoir.max_level = max_level 1947 return ct
Adds Reservoir(times, level_changes, min_level, max_level).
Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.
If the affine expression times[i]
is assigned a value t, then the current
level changes by level_changes[i]
, which is constant, at time t.
Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.
Therefore, at any time: sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
Arguments:
- times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
- level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
- min_level: At any time, the level of the reservoir must be greater or equal than the min level.
- max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: if max_level < min_level.
- ValueError: if max_level < 0.
- ValueError: if min_level > 0
1949 def add_reservoir_constraint_with_active( 1950 self, 1951 times: Iterable[LinearExprT], 1952 level_changes: Iterable[LinearExprT], 1953 actives: Iterable[LiteralT], 1954 min_level: int, 1955 max_level: int, 1956 ) -> Constraint: 1957 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1958 1959 Maintains a reservoir level within bounds. The water level starts at 0, and 1960 at any time, it must be between min_level and max_level. 1961 1962 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1963 `True`, then the current level changes by `level_changes[i]`, which is 1964 constant, 1965 at time t. 1966 1967 Note that min level must be <= 0, and the max level must be >= 0. Please 1968 use fixed level_changes to simulate initial state. 1969 1970 Therefore, at any time: 1971 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1972 max_level] 1973 1974 1975 The array of boolean variables 'actives', if defined, indicates which 1976 actions are actually performed. 1977 1978 Args: 1979 times: A list of 1-var affine expressions (a * x + b) which specify the 1980 time of the filling or emptying the reservoir. 1981 level_changes: A list of integer values that specifies the amount of the 1982 emptying or filling. Currently, variable demands are not supported. 1983 actives: a list of boolean variables. They indicates if the 1984 emptying/refilling events actually take place. 1985 min_level: At any time, the level of the reservoir must be greater or 1986 equal than the min level. 1987 max_level: At any time, the level of the reservoir must be less or equal 1988 than the max level. 1989 1990 Returns: 1991 An instance of the `Constraint` class. 1992 1993 Raises: 1994 ValueError: if max_level < min_level. 1995 1996 ValueError: if max_level < 0. 1997 1998 ValueError: if min_level > 0 1999 """ 2000 2001 if max_level < min_level: 2002 raise ValueError("Reservoir constraint must have a max_level >= min_level") 2003 2004 if max_level < 0: 2005 raise ValueError("Reservoir constraint must have a max_level >= 0") 2006 2007 if min_level > 0: 2008 raise ValueError("Reservoir constraint must have a min_level <= 0") 2009 2010 ct = Constraint(self) 2011 model_ct = self.__model.constraints[ct.index] 2012 model_ct.reservoir.time_exprs.extend( 2013 [self.parse_linear_expression(x) for x in times] 2014 ) 2015 model_ct.reservoir.level_changes.extend( 2016 [self.parse_linear_expression(x) for x in level_changes] 2017 ) 2018 model_ct.reservoir.active_literals.extend( 2019 [self.get_or_make_boolean_index(x) for x in actives] 2020 ) 2021 model_ct.reservoir.min_level = min_level 2022 model_ct.reservoir.max_level = max_level 2023 return ct
Adds Reservoir(times, level_changes, actives, min_level, max_level).
Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.
If the variable times[i]
is assigned a value t, and actives[i]
is
True
, then the current level changes by level_changes[i]
, which is
constant,
at time t.
Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.
Therefore, at any time: sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, max_level]
The array of boolean variables 'actives', if defined, indicates which actions are actually performed.
Arguments:
- times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
- level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
- actives: a list of boolean variables. They indicates if the emptying/refilling events actually take place.
- min_level: At any time, the level of the reservoir must be greater or equal than the min level.
- max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:
An instance of the
Constraint
class.
Raises:
- ValueError: if max_level < min_level.
- ValueError: if max_level < 0.
- ValueError: if min_level > 0
2049 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 2050 """Adds `a => b` (`a` implies `b`).""" 2051 ct = Constraint(self) 2052 model_ct = self.__model.constraints[ct.index] 2053 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2054 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2055 return ct
Adds a => b
(a
implies b
).
2063 def add_bool_or(self, *literals): 2064 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2065 ct = Constraint(self) 2066 model_ct = self.__model.constraints[ct.index] 2067 model_ct.bool_or.literals.extend( 2068 [ 2069 self.get_or_make_boolean_index(x) 2070 for x in expand_generator_or_tuple(literals) 2071 ] 2072 ) 2073 return ct
Adds Or(literals) == true
: sum(literals) >= 1.
2081 def add_at_least_one(self, *literals): 2082 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2083 return self.add_bool_or(*literals)
Same as add_bool_or
: sum(literals) >= 1
.
2091 def add_at_most_one(self, *literals): 2092 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2093 ct = Constraint(self) 2094 model_ct = self.__model.constraints[ct.index] 2095 model_ct.at_most_one.literals.extend( 2096 [ 2097 self.get_or_make_boolean_index(x) 2098 for x in expand_generator_or_tuple(literals) 2099 ] 2100 ) 2101 return ct
Adds AtMostOne(literals)
: sum(literals) <= 1
.
2109 def add_exactly_one(self, *literals): 2110 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2111 ct = Constraint(self) 2112 model_ct = self.__model.constraints[ct.index] 2113 model_ct.exactly_one.literals.extend( 2114 [ 2115 self.get_or_make_boolean_index(x) 2116 for x in expand_generator_or_tuple(literals) 2117 ] 2118 ) 2119 return ct
Adds ExactlyOne(literals)
: sum(literals) == 1
.
2127 def add_bool_and(self, *literals): 2128 """Adds `And(literals) == true`.""" 2129 ct = Constraint(self) 2130 model_ct = self.__model.constraints[ct.index] 2131 model_ct.bool_and.literals.extend( 2132 [ 2133 self.get_or_make_boolean_index(x) 2134 for x in expand_generator_or_tuple(literals) 2135 ] 2136 ) 2137 return ct
Adds And(literals) == true
.
2145 def add_bool_xor(self, *literals): 2146 """Adds `XOr(literals) == true`. 2147 2148 In contrast to add_bool_or and add_bool_and, it does not support 2149 .only_enforce_if(). 2150 2151 Args: 2152 *literals: the list of literals in the constraint. 2153 2154 Returns: 2155 An `Constraint` object. 2156 """ 2157 ct = Constraint(self) 2158 model_ct = self.__model.constraints[ct.index] 2159 model_ct.bool_xor.literals.extend( 2160 [ 2161 self.get_or_make_boolean_index(x) 2162 for x in expand_generator_or_tuple(literals) 2163 ] 2164 ) 2165 return ct
Adds XOr(literals) == true
.
In contrast to add_bool_or and add_bool_and, it does not support .only_enforce_if().
Arguments:
- *literals: the list of literals in the constraint.
Returns:
An
Constraint
object.
2167 def add_min_equality( 2168 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2169 ) -> Constraint: 2170 """Adds `target == Min(exprs)`.""" 2171 ct = Constraint(self) 2172 model_ct = self.__model.constraints[ct.index] 2173 model_ct.lin_max.exprs.extend( 2174 [self.parse_linear_expression(x, True) for x in exprs] 2175 ) 2176 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2177 return ct
Adds target == Min(exprs)
.
2179 def add_max_equality( 2180 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2181 ) -> Constraint: 2182 """Adds `target == Max(exprs)`.""" 2183 ct = Constraint(self) 2184 model_ct = self.__model.constraints[ct.index] 2185 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2186 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2187 return ct
Adds target == Max(exprs)
.
2189 def add_division_equality( 2190 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2191 ) -> Constraint: 2192 """Adds `target == num // denom` (integer division rounded towards 0).""" 2193 ct = Constraint(self) 2194 model_ct = self.__model.constraints[ct.index] 2195 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2196 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2197 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2198 return ct
Adds target == num // denom
(integer division rounded towards 0).
2200 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2201 """Adds `target == Abs(expr)`.""" 2202 ct = Constraint(self) 2203 model_ct = self.__model.constraints[ct.index] 2204 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2205 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2206 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2207 return ct
Adds target == Abs(expr)
.
2209 def add_modulo_equality( 2210 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2211 ) -> Constraint: 2212 """Adds `target = expr % mod`. 2213 2214 It uses the C convention, that is the result is the remainder of the 2215 integral division rounded towards 0. 2216 2217 For example: 2218 * 10 % 3 = 1 2219 * -10 % 3 = -1 2220 * 10 % -3 = 1 2221 * -10 % -3 = -1 2222 2223 Args: 2224 target: the target expression. 2225 expr: the expression to compute the modulo of. 2226 mod: the modulus expression. 2227 2228 Returns: 2229 A `Constraint` object. 2230 """ 2231 ct = Constraint(self) 2232 model_ct = self.__model.constraints[ct.index] 2233 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2234 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2235 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2236 return ct
Adds target = expr % mod
.
It uses the C convention, that is the result is the remainder of the integral division rounded towards 0.
For example:
* 10 % 3 = 1
* -10 % 3 = -1
* 10 % -3 = 1
* -10 % -3 = -1
Arguments:
- target: the target expression.
- expr: the expression to compute the modulo of.
- mod: the modulus expression.
Returns:
A
Constraint
object.
2238 def add_multiplication_equality( 2239 self, 2240 target: LinearExprT, 2241 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2242 ) -> Constraint: 2243 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2244 ct = Constraint(self) 2245 model_ct = self.__model.constraints[ct.index] 2246 model_ct.int_prod.exprs.extend( 2247 [ 2248 self.parse_linear_expression(expr) 2249 for expr in expand_generator_or_tuple(expressions) 2250 ] 2251 ) 2252 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2253 return ct
Adds target == expressions[0] * .. * expressions[n]
.
2257 def new_interval_var( 2258 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2259 ) -> IntervalVar: 2260 """Creates an interval variable from start, size, and end. 2261 2262 An interval variable is a constraint, that is itself used in other 2263 constraints like NoOverlap. 2264 2265 Internally, it ensures that `start + size == end`. 2266 2267 Args: 2268 start: The start of the interval. It must be of the form a * var + b. 2269 size: The size of the interval. It must be of the form a * var + b. 2270 end: The end of the interval. It must be of the form a * var + b. 2271 name: The name of the interval variable. 2272 2273 Returns: 2274 An `IntervalVar` object. 2275 """ 2276 2277 start_expr = self.parse_linear_expression(start) 2278 size_expr = self.parse_linear_expression(size) 2279 end_expr = self.parse_linear_expression(end) 2280 if len(start_expr.vars) > 1: 2281 raise TypeError( 2282 "cp_model.new_interval_var: start must be 1-var affine or constant." 2283 ) 2284 if len(size_expr.vars) > 1: 2285 raise TypeError( 2286 "cp_model.new_interval_var: size must be 1-var affine or constant." 2287 ) 2288 if len(end_expr.vars) > 1: 2289 raise TypeError( 2290 "cp_model.new_interval_var: end must be 1-var affine or constant." 2291 ) 2292 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)
Creates an interval variable from start, size, and end.
An interval variable is a constraint, that is itself used in other constraints like NoOverlap.
Internally, it ensures that start + size == end
.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be of the form a * var + b.
- end: The end of the interval. It must be of the form a * var + b.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2294 def new_interval_var_series( 2295 self, 2296 name: str, 2297 index: pd.Index, 2298 starts: Union[LinearExprT, pd.Series], 2299 sizes: Union[LinearExprT, pd.Series], 2300 ends: Union[LinearExprT, pd.Series], 2301 ) -> pd.Series: 2302 """Creates a series of interval variables with the given name. 2303 2304 Args: 2305 name (str): Required. The name of the variable set. 2306 index (pd.Index): Required. The index to use for the variable set. 2307 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2308 set. If a `pd.Series` is passed in, it will be based on the 2309 corresponding values of the pd.Series. 2310 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2311 set. If a `pd.Series` is passed in, it will be based on the 2312 corresponding values of the pd.Series. 2313 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2314 set. If a `pd.Series` is passed in, it will be based on the 2315 corresponding values of the pd.Series. 2316 2317 Returns: 2318 pd.Series: The interval variable set indexed by its corresponding 2319 dimensions. 2320 2321 Raises: 2322 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2323 ValueError: if the `name` is not a valid identifier or already exists. 2324 ValueError: if the all the indexes do not match. 2325 """ 2326 if not isinstance(index, pd.Index): 2327 raise TypeError("Non-index object is used as index") 2328 if not name.isidentifier(): 2329 raise ValueError("name={} is not a valid identifier".format(name)) 2330 2331 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2332 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2333 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2334 interval_array = [] 2335 for i in index: 2336 interval_array.append( 2337 self.new_interval_var( 2338 start=starts[i], 2339 size=sizes[i], 2340 end=ends[i], 2341 name=f"{name}[{i}]", 2342 ) 2343 ) 2344 return pd.Series(index=index, data=interval_array)
Creates a series of interval 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.
- starts (Union[LinearExprT, pd.Series]): The start of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series.
Returns:
pd.Series: The interval 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 all the indexes do not match.
2346 def new_fixed_size_interval_var( 2347 self, start: LinearExprT, size: IntegralT, name: str 2348 ) -> IntervalVar: 2349 """Creates an interval variable from start, and a fixed size. 2350 2351 An interval variable is a constraint, that is itself used in other 2352 constraints like NoOverlap. 2353 2354 Args: 2355 start: The start of the interval. It must be of the form a * var + b. 2356 size: The size of the interval. It must be an integer value. 2357 name: The name of the interval variable. 2358 2359 Returns: 2360 An `IntervalVar` object. 2361 """ 2362 size = cmh.assert_is_int64(size) 2363 start_expr = self.parse_linear_expression(start) 2364 size_expr = self.parse_linear_expression(size) 2365 end_expr = self.parse_linear_expression(start + size) 2366 if len(start_expr.vars) > 1: 2367 raise TypeError( 2368 "cp_model.new_interval_var: start must be affine or constant." 2369 ) 2370 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)
Creates an interval variable from start, and a fixed size.
An interval variable is a constraint, that is itself used in other constraints like NoOverlap.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be an integer value.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2418 def new_optional_interval_var( 2419 self, 2420 start: LinearExprT, 2421 size: LinearExprT, 2422 end: LinearExprT, 2423 is_present: LiteralT, 2424 name: str, 2425 ) -> IntervalVar: 2426 """Creates an optional interval var from start, size, end, and is_present. 2427 2428 An optional interval variable is a constraint, that is itself used in other 2429 constraints like NoOverlap. This constraint is protected by a presence 2430 literal that indicates if it is active or not. 2431 2432 Internally, it ensures that `is_present` implies `start + size == 2433 end`. 2434 2435 Args: 2436 start: The start of the interval. It must be of the form a * var + b. 2437 size: The size of the interval. It must be of the form a * var + b. 2438 end: The end of the interval. It must be of the form a * var + b. 2439 is_present: A literal that indicates if the interval is active or not. A 2440 inactive interval is simply ignored by all constraints. 2441 name: The name of the interval variable. 2442 2443 Returns: 2444 An `IntervalVar` object. 2445 """ 2446 2447 # Creates the IntervalConstraintProto object. 2448 is_present_index = self.get_or_make_boolean_index(is_present) 2449 start_expr = self.parse_linear_expression(start) 2450 size_expr = self.parse_linear_expression(size) 2451 end_expr = self.parse_linear_expression(end) 2452 if len(start_expr.vars) > 1: 2453 raise TypeError( 2454 "cp_model.new_interval_var: start must be affine or constant." 2455 ) 2456 if len(size_expr.vars) > 1: 2457 raise TypeError( 2458 "cp_model.new_interval_var: size must be affine or constant." 2459 ) 2460 if len(end_expr.vars) > 1: 2461 raise TypeError( 2462 "cp_model.new_interval_var: end must be affine or constant." 2463 ) 2464 return IntervalVar( 2465 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2466 )
Creates an optional interval var from start, size, end, and is_present.
An optional interval variable is a constraint, that is itself used in other constraints like NoOverlap. This constraint is protected by a presence literal that indicates if it is active or not.
Internally, it ensures that is_present
implies start + size ==
end
.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be of the form a * var + b.
- end: The end of the interval. It must be of the form a * var + b.
- is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2468 def new_optional_interval_var_series( 2469 self, 2470 name: str, 2471 index: pd.Index, 2472 starts: Union[LinearExprT, pd.Series], 2473 sizes: Union[LinearExprT, pd.Series], 2474 ends: Union[LinearExprT, pd.Series], 2475 are_present: Union[LiteralT, pd.Series], 2476 ) -> pd.Series: 2477 """Creates a series of interval variables with the given name. 2478 2479 Args: 2480 name (str): Required. The name of the variable set. 2481 index (pd.Index): Required. The index to use for the variable set. 2482 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2483 set. If a `pd.Series` is passed in, it will be based on the 2484 corresponding values of the pd.Series. 2485 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2486 set. If a `pd.Series` is passed in, it will be based on the 2487 corresponding values of the pd.Series. 2488 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2489 set. If a `pd.Series` is passed in, it will be based on the 2490 corresponding values of the pd.Series. 2491 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2492 interval in the set. If a `pd.Series` is passed in, it will be based on 2493 the corresponding values of the pd.Series. 2494 2495 Returns: 2496 pd.Series: The interval variable set indexed by its corresponding 2497 dimensions. 2498 2499 Raises: 2500 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2501 ValueError: if the `name` is not a valid identifier or already exists. 2502 ValueError: if the all the indexes do not match. 2503 """ 2504 if not isinstance(index, pd.Index): 2505 raise TypeError("Non-index object is used as index") 2506 if not name.isidentifier(): 2507 raise ValueError("name={} is not a valid identifier".format(name)) 2508 2509 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2510 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2511 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2512 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2513 2514 interval_array = [] 2515 for i in index: 2516 interval_array.append( 2517 self.new_optional_interval_var( 2518 start=starts[i], 2519 size=sizes[i], 2520 end=ends[i], 2521 is_present=are_present[i], 2522 name=f"{name}[{i}]", 2523 ) 2524 ) 2525 return pd.Series(index=index, data=interval_array)
Creates a series of interval 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.
- starts (Union[LinearExprT, pd.Series]): The start of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - are_present (Union[LiteralT, pd.Series]): The performed literal of each
interval in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series.
Returns:
pd.Series: The interval 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 all the indexes do not match.
2527 def new_optional_fixed_size_interval_var( 2528 self, 2529 start: LinearExprT, 2530 size: IntegralT, 2531 is_present: LiteralT, 2532 name: str, 2533 ) -> IntervalVar: 2534 """Creates an interval variable from start, and a fixed size. 2535 2536 An interval variable is a constraint, that is itself used in other 2537 constraints like NoOverlap. 2538 2539 Args: 2540 start: The start of the interval. It must be of the form a * var + b. 2541 size: The size of the interval. It must be an integer value. 2542 is_present: A literal that indicates if the interval is active or not. A 2543 inactive interval is simply ignored by all constraints. 2544 name: The name of the interval variable. 2545 2546 Returns: 2547 An `IntervalVar` object. 2548 """ 2549 size = cmh.assert_is_int64(size) 2550 start_expr = self.parse_linear_expression(start) 2551 size_expr = self.parse_linear_expression(size) 2552 end_expr = self.parse_linear_expression(start + size) 2553 if len(start_expr.vars) > 1: 2554 raise TypeError( 2555 "cp_model.new_interval_var: start must be affine or constant." 2556 ) 2557 is_present_index = self.get_or_make_boolean_index(is_present) 2558 return IntervalVar( 2559 self.__model, 2560 start_expr, 2561 size_expr, 2562 end_expr, 2563 is_present_index, 2564 name, 2565 )
Creates an interval variable from start, and a fixed size.
An interval variable is a constraint, that is itself used in other constraints like NoOverlap.
Arguments:
- start: The start of the interval. It must be of the form a * var + b.
- size: The size of the interval. It must be an integer value.
- is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
- name: The name of the interval variable.
Returns:
An
IntervalVar
object.
2567 def new_optional_fixed_size_interval_var_series( 2568 self, 2569 name: str, 2570 index: pd.Index, 2571 starts: Union[LinearExprT, pd.Series], 2572 sizes: Union[IntegralT, pd.Series], 2573 are_present: Union[LiteralT, pd.Series], 2574 ) -> pd.Series: 2575 """Creates a series of interval variables with the given name. 2576 2577 Args: 2578 name (str): Required. The name of the variable set. 2579 index (pd.Index): Required. The index to use for the variable set. 2580 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2581 set. If a `pd.Series` is passed in, it will be based on the 2582 corresponding values of the pd.Series. 2583 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2584 the set. If a `pd.Series` is passed in, it will be based on the 2585 corresponding values of the pd.Series. 2586 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2587 interval in the set. If a `pd.Series` is passed in, it will be based on 2588 the corresponding values of the pd.Series. 2589 2590 Returns: 2591 pd.Series: The interval variable set indexed by its corresponding 2592 dimensions. 2593 2594 Raises: 2595 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2596 ValueError: if the `name` is not a valid identifier or already exists. 2597 ValueError: if the all the indexes do not match. 2598 """ 2599 if not isinstance(index, pd.Index): 2600 raise TypeError("Non-index object is used as index") 2601 if not name.isidentifier(): 2602 raise ValueError("name={} is not a valid identifier".format(name)) 2603 2604 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2605 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2606 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2607 interval_array = [] 2608 for i in index: 2609 interval_array.append( 2610 self.new_optional_fixed_size_interval_var( 2611 start=starts[i], 2612 size=sizes[i], 2613 is_present=are_present[i], 2614 name=f"{name}[{i}]", 2615 ) 2616 ) 2617 return pd.Series(index=index, data=interval_array)
Creates a series of interval 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.
- starts (Union[LinearExprT, pd.Series]): The start of each interval in the
set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series. - are_present (Union[LiteralT, pd.Series]): The performed literal of each
interval in the set. If a
pd.Series
is passed in, it will be based on the corresponding values of the pd.Series.
Returns:
pd.Series: The interval 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 all the indexes do not match.
2619 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2620 """Adds NoOverlap(interval_vars). 2621 2622 A NoOverlap constraint ensures that all present intervals do not overlap 2623 in time. 2624 2625 Args: 2626 interval_vars: The list of interval variables to constrain. 2627 2628 Returns: 2629 An instance of the `Constraint` class. 2630 """ 2631 ct = Constraint(self) 2632 model_ct = self.__model.constraints[ct.index] 2633 model_ct.no_overlap.intervals.extend( 2634 [self.get_interval_index(x) for x in interval_vars] 2635 ) 2636 return ct
Adds NoOverlap(interval_vars).
A NoOverlap constraint ensures that all present intervals do not overlap in time.
Arguments:
- interval_vars: The list of interval variables to constrain.
Returns:
An instance of the
Constraint
class.
2638 def add_no_overlap_2d( 2639 self, 2640 x_intervals: Iterable[IntervalVar], 2641 y_intervals: Iterable[IntervalVar], 2642 ) -> Constraint: 2643 """Adds NoOverlap2D(x_intervals, y_intervals). 2644 2645 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2646 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2647 by two intervals which represent its projection onto the X and Y axis. 2648 2649 Furthermore, one box is optional if at least one of the x or y interval is 2650 optional. 2651 2652 Args: 2653 x_intervals: The X coordinates of the rectangles. 2654 y_intervals: The Y coordinates of the rectangles. 2655 2656 Returns: 2657 An instance of the `Constraint` class. 2658 """ 2659 ct = Constraint(self) 2660 model_ct = self.__model.constraints[ct.index] 2661 model_ct.no_overlap_2d.x_intervals.extend( 2662 [self.get_interval_index(x) for x in x_intervals] 2663 ) 2664 model_ct.no_overlap_2d.y_intervals.extend( 2665 [self.get_interval_index(x) for x in y_intervals] 2666 ) 2667 return ct
Adds NoOverlap2D(x_intervals, y_intervals).
A NoOverlap2D constraint ensures that all present rectangles do not overlap on a plane. Each rectangle is aligned with the X and Y axis, and is defined by two intervals which represent its projection onto the X and Y axis.
Furthermore, one box is optional if at least one of the x or y interval is optional.
Arguments:
- x_intervals: The X coordinates of the rectangles.
- y_intervals: The Y coordinates of the rectangles.
Returns:
An instance of the
Constraint
class.
2669 def add_cumulative( 2670 self, 2671 intervals: Iterable[IntervalVar], 2672 demands: Iterable[LinearExprT], 2673 capacity: LinearExprT, 2674 ) -> Constraint: 2675 """Adds Cumulative(intervals, demands, capacity). 2676 2677 This constraint enforces that: 2678 2679 for all t: 2680 sum(demands[i] 2681 if (start(intervals[i]) <= t < end(intervals[i])) and 2682 (intervals[i] is present)) <= capacity 2683 2684 Args: 2685 intervals: The list of intervals. 2686 demands: The list of demands for each interval. Each demand must be >= 0. 2687 Each demand can be a 1-var affine expression (a * x + b). 2688 capacity: The maximum capacity of the cumulative constraint. It can be a 2689 1-var affine expression (a * x + b). 2690 2691 Returns: 2692 An instance of the `Constraint` class. 2693 """ 2694 cumulative = Constraint(self) 2695 model_ct = self.__model.constraints[cumulative.index] 2696 model_ct.cumulative.intervals.extend( 2697 [self.get_interval_index(x) for x in intervals] 2698 ) 2699 for d in demands: 2700 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2701 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2702 return cumulative
Adds Cumulative(intervals, demands, capacity).
This constraint enforces that:
for all t: sum(demands[i] if (start(intervals[i]) <= t < end(intervals[i])) and (intervals[i] is present)) <= capacity
Arguments:
- intervals: The list of intervals.
- demands: The list of demands for each interval. Each demand must be >= 0. Each demand can be a 1-var affine expression (a * x + b).
- capacity: The maximum capacity of the cumulative constraint. It can be a 1-var affine expression (a * x + b).
Returns:
An instance of the
Constraint
class.
2705 def clone(self) -> "CpModel": 2706 """Reset the model, and creates a new one from a CpModelProto instance.""" 2707 clone = CpModel() 2708 clone.proto.CopyFrom(self.proto) 2709 clone.rebuild_constant_map() 2710 return clone
Reset the model, and creates a new one from a CpModelProto instance.
2718 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2719 """Returns an already created Boolean variable from its index.""" 2720 if index < 0 or index >= len(self.__model.variables): 2721 raise ValueError( 2722 f"get_bool_var_from_proto_index: out of bound index {index}" 2723 ) 2724 var = self.__model.variables[index] 2725 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2726 raise ValueError( 2727 f"get_bool_var_from_proto_index: index {index} does not reference" 2728 + " a Boolean variable" 2729 ) 2730 2731 return IntVar(self.__model, index, None)
Returns an already created Boolean variable from its index.
2733 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2734 """Returns an already created integer variable from its index.""" 2735 if index < 0 or index >= len(self.__model.variables): 2736 raise ValueError( 2737 f"get_int_var_from_proto_index: out of bound index {index}" 2738 ) 2739 return IntVar(self.__model, index, None)
Returns an already created integer variable from its index.
2741 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2742 """Returns an already created interval variable from its index.""" 2743 if index < 0 or index >= len(self.__model.constraints): 2744 raise ValueError( 2745 f"get_interval_var_from_proto_index: out of bound index {index}" 2746 ) 2747 ct = self.__model.constraints[index] 2748 if not ct.HasField("interval"): 2749 raise ValueError( 2750 f"get_interval_var_from_proto_index: index {index} does not" 2751 " reference an" + " interval variable" 2752 ) 2753 2754 return IntervalVar(self.__model, index, None, None, None, None)
Returns an already created interval variable from its index.
2891 def minimize(self, obj: ObjLinearExprT): 2892 """Sets the objective of the model to minimize(obj).""" 2893 self._set_objective(obj, minimize=True)
Sets the objective of the model to minimize(obj).
2895 def maximize(self, obj: ObjLinearExprT): 2896 """Sets the objective of the model to maximize(obj).""" 2897 self._set_objective(obj, minimize=False)
Sets the objective of the model to maximize(obj).
2908 def add_decision_strategy( 2909 self, 2910 variables: Sequence[IntVar], 2911 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2912 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2913 ) -> None: 2914 """Adds a search strategy to the model. 2915 2916 Args: 2917 variables: a list of variables this strategy will assign. 2918 var_strategy: heuristic to choose the next variable to assign. 2919 domain_strategy: heuristic to reduce the domain of the selected variable. 2920 Currently, this is advanced code: the union of all strategies added to 2921 the model must be complete, i.e. instantiates all variables. Otherwise, 2922 solve() will fail. 2923 """ 2924 2925 strategy: cp_model_pb2.DecisionStrategyProto = ( 2926 self.__model.search_strategy.add() 2927 ) 2928 for v in variables: 2929 expr = strategy.exprs.add() 2930 if v.index >= 0: 2931 expr.vars.append(v.index) 2932 expr.coeffs.append(1) 2933 else: 2934 expr.vars.append(self.negated(v.index)) 2935 expr.coeffs.append(-1) 2936 expr.offset = 1 2937 2938 strategy.variable_selection_strategy = var_strategy 2939 strategy.domain_reduction_strategy = domain_strategy
Adds a search strategy to the model.
Arguments:
- variables: a list of variables this strategy will assign.
- var_strategy: heuristic to choose the next variable to assign.
- domain_strategy: heuristic to reduce the domain of the selected variable. Currently, this is advanced code: the union of all strategies added to the model must be complete, i.e. instantiates all variables. Otherwise, solve() will fail.
2941 def model_stats(self) -> str: 2942 """Returns a string containing some model statistics.""" 2943 return swig_helper.CpSatHelper.model_stats(self.__model)
Returns a string containing some model statistics.
2945 def validate(self) -> str: 2946 """Returns a string indicating that the model is invalid.""" 2947 return swig_helper.CpSatHelper.validate_model(self.__model)
Returns a string indicating that the model is invalid.
2949 def export_to_file(self, file: str) -> bool: 2950 """Write the model as a protocol buffer to 'file'. 2951 2952 Args: 2953 file: file to write the model to. If the filename ends with 'txt', the 2954 model will be written as a text file, otherwise, the binary format will 2955 be used. 2956 2957 Returns: 2958 True if the model was correctly written. 2959 """ 2960 return swig_helper.CpSatHelper.write_model_to_file(self.__model, file)
Write the model as a protocol buffer to 'file'.
Arguments:
- file: file to write the model to. If the filename ends with 'txt', the model will be written as a text file, otherwise, the binary format will be used.
Returns:
True if the model was correctly written.
2962 def add_hint(self, var: IntVar, value: int) -> None: 2963 """Adds 'var == value' as a hint to the solver.""" 2964 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2965 self.__model.solution_hint.values.append(value)
Adds 'var == value' as a hint to the solver.
2967 def clear_hints(self): 2968 """Removes any solution hint from the model.""" 2969 self.__model.ClearField("solution_hint")
Removes any solution hint from the model.
2971 def add_assumption(self, lit: LiteralT) -> None: 2972 """Adds the literal to the model as assumptions.""" 2973 self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
Adds the literal to the model as assumptions.
2975 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2976 """Adds the literals to the model as assumptions.""" 2977 for lit in literals: 2978 self.add_assumption(lit)
Adds the literals to the model as assumptions.
3090def evaluate_linear_expr( 3091 expression: LinearExprT, solution: cp_model_pb2.CpSolverResponse 3092) -> int: 3093 """Evaluate a linear expression against a solution.""" 3094 if isinstance(expression, IntegralTypes): 3095 return int(expression) 3096 if not isinstance(expression, LinearExpr): 3097 raise TypeError("Cannot interpret %s as a linear expression." % expression) 3098 3099 value = 0 3100 to_process = [(expression, 1)] 3101 while to_process: 3102 expr, coeff = to_process.pop() 3103 if isinstance(expr, IntegralTypes): 3104 value += int(expr) * coeff 3105 elif isinstance(expr, _ProductCst): 3106 to_process.append((expr.expression(), coeff * expr.coefficient())) 3107 elif isinstance(expr, _Sum): 3108 to_process.append((expr.left(), coeff)) 3109 to_process.append((expr.right(), coeff)) 3110 elif isinstance(expr, _SumArray): 3111 for e in expr.expressions(): 3112 to_process.append((e, coeff)) 3113 value += expr.constant() * coeff 3114 elif isinstance(expr, _WeightedSum): 3115 for e, c in zip(expr.expressions(), expr.coefficients()): 3116 to_process.append((e, coeff * c)) 3117 value += expr.constant() * coeff 3118 elif isinstance(expr, IntVar): 3119 value += coeff * solution.solution[expr.index] 3120 elif isinstance(expr, _NotBooleanVariable): 3121 value += coeff * (1 - solution.solution[expr.negated().index]) 3122 else: 3123 raise TypeError(f"Cannot interpret {expr} as a linear expression.") 3124 3125 return value
Evaluate a linear expression against a solution.
3128def evaluate_boolean_expression( 3129 literal: LiteralT, solution: cp_model_pb2.CpSolverResponse 3130) -> bool: 3131 """Evaluate a boolean expression against a solution.""" 3132 if isinstance(literal, IntegralTypes): 3133 return bool(literal) 3134 elif isinstance(literal, IntVar) or isinstance(literal, _NotBooleanVariable): 3135 index: int = cast(Union[IntVar, _NotBooleanVariable], literal).index 3136 if index >= 0: 3137 return bool(solution.solution[index]) 3138 else: 3139 return not solution.solution[-index - 1] 3140 else: 3141 raise TypeError(f"Cannot interpret {literal} as a boolean expression.")
Evaluate a boolean expression against a solution.
3144class CpSolver: 3145 """Main solver class. 3146 3147 The purpose of this class is to search for a solution to the model provided 3148 to the solve() method. 3149 3150 Once solve() is called, this class allows inspecting the solution found 3151 with the value() and boolean_value() methods, as well as general statistics 3152 about the solve procedure. 3153 """ 3154 3155 def __init__(self) -> None: 3156 self.__solution: Optional[cp_model_pb2.CpSolverResponse] = None 3157 self.parameters: sat_parameters_pb2.SatParameters = ( 3158 sat_parameters_pb2.SatParameters() 3159 ) 3160 self.log_callback: Optional[Callable[[str], None]] = None 3161 self.best_bound_callback: Optional[Callable[[float], None]] = None 3162 self.__solve_wrapper: Optional[swig_helper.SolveWrapper] = None 3163 self.__lock: threading.Lock = threading.Lock() 3164 3165 def solve( 3166 self, 3167 model: CpModel, 3168 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3169 ) -> cp_model_pb2.CpSolverStatus: 3170 """Solves a problem and passes each solution to the callback if not null.""" 3171 with self.__lock: 3172 self.__solve_wrapper = swig_helper.SolveWrapper() 3173 3174 self.__solve_wrapper.set_parameters(self.parameters) 3175 if solution_callback is not None: 3176 self.__solve_wrapper.add_solution_callback(solution_callback) 3177 3178 if self.log_callback is not None: 3179 self.__solve_wrapper.add_log_callback(self.log_callback) 3180 3181 if self.best_bound_callback is not None: 3182 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) 3183 3184 solution: cp_model_pb2.CpSolverResponse = self.__solve_wrapper.solve( 3185 model.proto 3186 ) 3187 self.__solution = solution 3188 3189 if solution_callback is not None: 3190 self.__solve_wrapper.clear_solution_callback(solution_callback) 3191 3192 with self.__lock: 3193 self.__solve_wrapper = None 3194 3195 return solution.status 3196 3197 def stop_search(self) -> None: 3198 """Stops the current search asynchronously.""" 3199 with self.__lock: 3200 if self.__solve_wrapper: 3201 self.__solve_wrapper.stop_search() 3202 3203 def value(self, expression: LinearExprT) -> int: 3204 """Returns the value of a linear expression after solve.""" 3205 return evaluate_linear_expr(expression, self._solution) 3206 3207 def values(self, variables: _IndexOrSeries) -> pd.Series: 3208 """Returns the values of the input variables. 3209 3210 If `variables` is a `pd.Index`, then the output will be indexed by the 3211 variables. If `variables` is a `pd.Series` indexed by the underlying 3212 dimensions, then the output will be indexed by the same underlying 3213 dimensions. 3214 3215 Args: 3216 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3217 get the values. 3218 3219 Returns: 3220 pd.Series: The values of all variables in the set. 3221 """ 3222 solution = self._solution 3223 return _attribute_series( 3224 func=lambda v: solution.solution[v.index], 3225 values=variables, 3226 ) 3227 3228 def boolean_value(self, literal: LiteralT) -> bool: 3229 """Returns the boolean value of a literal after solve.""" 3230 return evaluate_boolean_expression(literal, self._solution) 3231 3232 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 3233 """Returns the values of the input variables. 3234 3235 If `variables` is a `pd.Index`, then the output will be indexed by the 3236 variables. If `variables` is a `pd.Series` indexed by the underlying 3237 dimensions, then the output will be indexed by the same underlying 3238 dimensions. 3239 3240 Args: 3241 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3242 get the values. 3243 3244 Returns: 3245 pd.Series: The values of all variables in the set. 3246 """ 3247 solution = self._solution 3248 return _attribute_series( 3249 func=lambda literal: evaluate_boolean_expression(literal, solution), 3250 values=variables, 3251 ) 3252 3253 @property 3254 def objective_value(self) -> float: 3255 """Returns the value of the objective after solve.""" 3256 return self._solution.objective_value 3257 3258 @property 3259 def best_objective_bound(self) -> float: 3260 """Returns the best lower (upper) bound found when min(max)imizing.""" 3261 return self._solution.best_objective_bound 3262 3263 @property 3264 def num_booleans(self) -> int: 3265 """Returns the number of boolean variables managed by the SAT solver.""" 3266 return self._solution.num_booleans 3267 3268 @property 3269 def num_conflicts(self) -> int: 3270 """Returns the number of conflicts since the creation of the solver.""" 3271 return self._solution.num_conflicts 3272 3273 @property 3274 def num_branches(self) -> int: 3275 """Returns the number of search branches explored by the solver.""" 3276 return self._solution.num_branches 3277 3278 @property 3279 def wall_time(self) -> float: 3280 """Returns the wall time in seconds since the creation of the solver.""" 3281 return self._solution.wall_time 3282 3283 @property 3284 def user_time(self) -> float: 3285 """Returns the user time in seconds since the creation of the solver.""" 3286 return self._solution.user_time 3287 3288 @property 3289 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3290 """Returns the response object.""" 3291 return self._solution 3292 3293 def response_stats(self) -> str: 3294 """Returns some statistics on the solution found as a string.""" 3295 return swig_helper.CpSatHelper.solver_response_stats(self._solution) 3296 3297 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 3298 """Returns the indices of the infeasible assumptions.""" 3299 return self._solution.sufficient_assumptions_for_infeasibility 3300 3301 def status_name(self, status: Optional[Any] = None) -> str: 3302 """Returns the name of the status returned by solve().""" 3303 if status is None: 3304 status = self._solution.status 3305 return cp_model_pb2.CpSolverStatus.Name(status) 3306 3307 def solution_info(self) -> str: 3308 """Returns some information on the solve process. 3309 3310 Returns some information on how the solution was found, or the reason 3311 why the model or the parameters are invalid. 3312 3313 Raises: 3314 RuntimeError: if solve() has not been called. 3315 """ 3316 return self._solution.solution_info 3317 3318 @property 3319 def _solution(self) -> cp_model_pb2.CpSolverResponse: 3320 """Checks solve() has been called, and returns the solution.""" 3321 if self.__solution is None: 3322 raise RuntimeError("solve() has not been called.") 3323 return self.__solution 3324 3325 # Compatibility with pre PEP8 3326 # pylint: disable=invalid-name 3327 3328 def BestObjectiveBound(self) -> float: 3329 return self.best_objective_bound 3330 3331 def BooleanValue(self, literal: LiteralT) -> bool: 3332 return self.boolean_value(literal) 3333 3334 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: 3335 return self.boolean_values(variables) 3336 3337 def NumBooleans(self) -> int: 3338 return self.num_booleans 3339 3340 def NumConflicts(self) -> int: 3341 return self.num_conflicts 3342 3343 def NumBranches(self) -> int: 3344 return self.num_branches 3345 3346 def ObjectiveValue(self) -> float: 3347 return self.objective_value 3348 3349 def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: 3350 return self.response_proto 3351 3352 def ResponseStats(self) -> str: 3353 return self.response_stats() 3354 3355 def Solve( 3356 self, 3357 model: CpModel, 3358 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3359 ) -> cp_model_pb2.CpSolverStatus: 3360 return self.solve(model, solution_callback) 3361 3362 def SolutionInfo(self) -> str: 3363 return self.solution_info() 3364 3365 def StatusName(self, status: Optional[Any] = None) -> str: 3366 return self.status_name(status) 3367 3368 def StopSearch(self) -> None: 3369 self.stop_search() 3370 3371 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: 3372 return self.sufficient_assumptions_for_infeasibility() 3373 3374 def UserTime(self) -> float: 3375 return self.user_time 3376 3377 def Value(self, expression: LinearExprT) -> int: 3378 return self.value(expression) 3379 3380 def Values(self, variables: _IndexOrSeries) -> pd.Series: 3381 return self.values(variables) 3382 3383 def WallTime(self) -> float: 3384 return self.wall_time 3385 3386 def SolveWithSolutionCallback( 3387 self, model: CpModel, callback: "CpSolverSolutionCallback" 3388 ) -> cp_model_pb2.CpSolverStatus: 3389 """DEPRECATED Use solve() with the callback argument.""" 3390 warnings.warn( 3391 "solve_with_solution_callback is deprecated; use solve() with" 3392 + "the callback argument.", 3393 DeprecationWarning, 3394 ) 3395 return self.solve(model, callback) 3396 3397 def SearchForAllSolutions( 3398 self, model: CpModel, callback: "CpSolverSolutionCallback" 3399 ) -> cp_model_pb2.CpSolverStatus: 3400 """DEPRECATED Use solve() with the right parameter. 3401 3402 Search for all solutions of a satisfiability problem. 3403 3404 This method searches for all feasible solutions of a given model. 3405 Then it feeds the solution to the callback. 3406 3407 Note that the model cannot contain an objective. 3408 3409 Args: 3410 model: The model to solve. 3411 callback: The callback that will be called at each solution. 3412 3413 Returns: 3414 The status of the solve: 3415 3416 * *FEASIBLE* if some solutions have been found 3417 * *INFEASIBLE* if the solver has proved there are no solution 3418 * *OPTIMAL* if all solutions have been found 3419 """ 3420 warnings.warn( 3421 "search_for_all_solutions is deprecated; use solve() with" 3422 + "enumerate_all_solutions = True.", 3423 DeprecationWarning, 3424 ) 3425 if model.has_objective(): 3426 raise TypeError( 3427 "Search for all solutions is only defined on satisfiability problems" 3428 ) 3429 # Store old parameter. 3430 enumerate_all = self.parameters.enumerate_all_solutions 3431 self.parameters.enumerate_all_solutions = True 3432 3433 status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) 3434 3435 # Restore parameter. 3436 self.parameters.enumerate_all_solutions = enumerate_all 3437 return status
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() and boolean_value() methods, as well as general statistics about the solve procedure.
3165 def solve( 3166 self, 3167 model: CpModel, 3168 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3169 ) -> cp_model_pb2.CpSolverStatus: 3170 """Solves a problem and passes each solution to the callback if not null.""" 3171 with self.__lock: 3172 self.__solve_wrapper = swig_helper.SolveWrapper() 3173 3174 self.__solve_wrapper.set_parameters(self.parameters) 3175 if solution_callback is not None: 3176 self.__solve_wrapper.add_solution_callback(solution_callback) 3177 3178 if self.log_callback is not None: 3179 self.__solve_wrapper.add_log_callback(self.log_callback) 3180 3181 if self.best_bound_callback is not None: 3182 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) 3183 3184 solution: cp_model_pb2.CpSolverResponse = self.__solve_wrapper.solve( 3185 model.proto 3186 ) 3187 self.__solution = solution 3188 3189 if solution_callback is not None: 3190 self.__solve_wrapper.clear_solution_callback(solution_callback) 3191 3192 with self.__lock: 3193 self.__solve_wrapper = None 3194 3195 return solution.status
Solves a problem and passes each solution to the callback if not null.
3197 def stop_search(self) -> None: 3198 """Stops the current search asynchronously.""" 3199 with self.__lock: 3200 if self.__solve_wrapper: 3201 self.__solve_wrapper.stop_search()
Stops the current search asynchronously.
3203 def value(self, expression: LinearExprT) -> int: 3204 """Returns the value of a linear expression after solve.""" 3205 return evaluate_linear_expr(expression, self._solution)
Returns the value of a linear expression after solve.
3207 def values(self, variables: _IndexOrSeries) -> pd.Series: 3208 """Returns the values of the input variables. 3209 3210 If `variables` is a `pd.Index`, then the output will be indexed by the 3211 variables. If `variables` is a `pd.Series` indexed by the underlying 3212 dimensions, then the output will be indexed by the same underlying 3213 dimensions. 3214 3215 Args: 3216 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3217 get the values. 3218 3219 Returns: 3220 pd.Series: The values of all variables in the set. 3221 """ 3222 solution = self._solution 3223 return _attribute_series( 3224 func=lambda v: solution.solution[v.index], 3225 values=variables, 3226 )
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.
3228 def boolean_value(self, literal: LiteralT) -> bool: 3229 """Returns the boolean value of a literal after solve.""" 3230 return evaluate_boolean_expression(literal, self._solution)
Returns the boolean value of a literal after solve.
3232 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 3233 """Returns the values of the input variables. 3234 3235 If `variables` is a `pd.Index`, then the output will be indexed by the 3236 variables. If `variables` is a `pd.Series` indexed by the underlying 3237 dimensions, then the output will be indexed by the same underlying 3238 dimensions. 3239 3240 Args: 3241 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3242 get the values. 3243 3244 Returns: 3245 pd.Series: The values of all variables in the set. 3246 """ 3247 solution = self._solution 3248 return _attribute_series( 3249 func=lambda literal: evaluate_boolean_expression(literal, solution), 3250 values=variables, 3251 )
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.
3253 @property 3254 def objective_value(self) -> float: 3255 """Returns the value of the objective after solve.""" 3256 return self._solution.objective_value
Returns the value of the objective after solve.
3258 @property 3259 def best_objective_bound(self) -> float: 3260 """Returns the best lower (upper) bound found when min(max)imizing.""" 3261 return self._solution.best_objective_bound
Returns the best lower (upper) bound found when min(max)imizing.
3263 @property 3264 def num_booleans(self) -> int: 3265 """Returns the number of boolean variables managed by the SAT solver.""" 3266 return self._solution.num_booleans
Returns the number of boolean variables managed by the SAT solver.
3268 @property 3269 def num_conflicts(self) -> int: 3270 """Returns the number of conflicts since the creation of the solver.""" 3271 return self._solution.num_conflicts
Returns the number of conflicts since the creation of the solver.
3273 @property 3274 def num_branches(self) -> int: 3275 """Returns the number of search branches explored by the solver.""" 3276 return self._solution.num_branches
Returns the number of search branches explored by the solver.
3278 @property 3279 def wall_time(self) -> float: 3280 """Returns the wall time in seconds since the creation of the solver.""" 3281 return self._solution.wall_time
Returns the wall time in seconds since the creation of the solver.
3283 @property 3284 def user_time(self) -> float: 3285 """Returns the user time in seconds since the creation of the solver.""" 3286 return self._solution.user_time
Returns the user time in seconds since the creation of the solver.
3288 @property 3289 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3290 """Returns the response object.""" 3291 return self._solution
Returns the response object.
3293 def response_stats(self) -> str: 3294 """Returns some statistics on the solution found as a string.""" 3295 return swig_helper.CpSatHelper.solver_response_stats(self._solution)
Returns some statistics on the solution found as a string.
3297 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 3298 """Returns the indices of the infeasible assumptions.""" 3299 return self._solution.sufficient_assumptions_for_infeasibility
Returns the indices of the infeasible assumptions.
3301 def status_name(self, status: Optional[Any] = None) -> str: 3302 """Returns the name of the status returned by solve().""" 3303 if status is None: 3304 status = self._solution.status 3305 return cp_model_pb2.CpSolverStatus.Name(status)
Returns the name of the status returned by solve().
3307 def solution_info(self) -> str: 3308 """Returns some information on the solve process. 3309 3310 Returns some information on how the solution was found, or the reason 3311 why the model or the parameters are invalid. 3312 3313 Raises: 3314 RuntimeError: if solve() has not been called. 3315 """ 3316 return self._solution.solution_info
Returns some information on the solve process.
Returns some information on how the solution was found, or the reason why the model or the parameters are invalid.
Raises:
- RuntimeError: if solve() has not been called.
3386 def SolveWithSolutionCallback( 3387 self, model: CpModel, callback: "CpSolverSolutionCallback" 3388 ) -> cp_model_pb2.CpSolverStatus: 3389 """DEPRECATED Use solve() with the callback argument.""" 3390 warnings.warn( 3391 "solve_with_solution_callback is deprecated; use solve() with" 3392 + "the callback argument.", 3393 DeprecationWarning, 3394 ) 3395 return self.solve(model, callback)
DEPRECATED Use solve() with the callback argument.
3397 def SearchForAllSolutions( 3398 self, model: CpModel, callback: "CpSolverSolutionCallback" 3399 ) -> cp_model_pb2.CpSolverStatus: 3400 """DEPRECATED Use solve() with the right parameter. 3401 3402 Search for all solutions of a satisfiability problem. 3403 3404 This method searches for all feasible solutions of a given model. 3405 Then it feeds the solution to the callback. 3406 3407 Note that the model cannot contain an objective. 3408 3409 Args: 3410 model: The model to solve. 3411 callback: The callback that will be called at each solution. 3412 3413 Returns: 3414 The status of the solve: 3415 3416 * *FEASIBLE* if some solutions have been found 3417 * *INFEASIBLE* if the solver has proved there are no solution 3418 * *OPTIMAL* if all solutions have been found 3419 """ 3420 warnings.warn( 3421 "search_for_all_solutions is deprecated; use solve() with" 3422 + "enumerate_all_solutions = True.", 3423 DeprecationWarning, 3424 ) 3425 if model.has_objective(): 3426 raise TypeError( 3427 "Search for all solutions is only defined on satisfiability problems" 3428 ) 3429 # Store old parameter. 3430 enumerate_all = self.parameters.enumerate_all_solutions 3431 self.parameters.enumerate_all_solutions = True 3432 3433 status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) 3434 3435 # Restore parameter. 3436 self.parameters.enumerate_all_solutions = enumerate_all 3437 return status
DEPRECATED Use solve() with the right parameter.
Search for all solutions of a satisfiability problem.
This method searches for all feasible solutions of a given model. Then it feeds the solution to the callback.
Note that the model cannot contain an objective.
Arguments:
- model: The model to solve.
- callback: The callback that will be called at each solution.
Returns:
The status of the solve:
- FEASIBLE if some solutions have been found
- INFEASIBLE if the solver has proved there are no solution
- OPTIMAL if all solutions have been found
3443class CpSolverSolutionCallback(swig_helper.SolutionCallback): 3444 """Solution callback. 3445 3446 This class implements a callback that will be called at each new solution 3447 found during search. 3448 3449 The method on_solution_callback() will be called by the solver, and must be 3450 implemented. The current solution can be queried using the boolean_value() 3451 and value() methods. 3452 3453 These methods returns the same information as their counterpart in the 3454 `CpSolver` class. 3455 """ 3456 3457 def __init__(self) -> None: 3458 swig_helper.SolutionCallback.__init__(self) 3459 3460 def OnSolutionCallback(self) -> None: 3461 """Proxy for the same method in snake case.""" 3462 self.on_solution_callback() 3463 3464 def boolean_value(self, lit: LiteralT) -> bool: 3465 """Returns the boolean value of a boolean literal. 3466 3467 Args: 3468 lit: A boolean variable or its negation. 3469 3470 Returns: 3471 The Boolean value of the literal in the solution. 3472 3473 Raises: 3474 RuntimeError: if `lit` is not a boolean variable or its negation. 3475 """ 3476 if not self.has_response(): 3477 raise RuntimeError("solve() has not been called.") 3478 if isinstance(lit, IntegralTypes): 3479 return bool(lit) 3480 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3481 return self.SolutionBooleanValue( 3482 cast(Union[IntVar, _NotBooleanVariable], lit).index 3483 ) 3484 if cmh.is_boolean(lit): 3485 return bool(lit) 3486 raise TypeError(f"Cannot interpret {lit} as a boolean expression.") 3487 3488 def value(self, expression: LinearExprT) -> int: 3489 """Evaluates an linear expression in the current solution. 3490 3491 Args: 3492 expression: a linear expression of the model. 3493 3494 Returns: 3495 An integer value equal to the evaluation of the linear expression 3496 against the current solution. 3497 3498 Raises: 3499 RuntimeError: if 'expression' is not a LinearExpr. 3500 """ 3501 if not self.has_response(): 3502 raise RuntimeError("solve() has not been called.") 3503 3504 value = 0 3505 to_process = [(expression, 1)] 3506 while to_process: 3507 expr, coeff = to_process.pop() 3508 if isinstance(expr, IntegralTypes): 3509 value += int(expr) * coeff 3510 elif isinstance(expr, _ProductCst): 3511 to_process.append((expr.expression(), coeff * expr.coefficient())) 3512 elif isinstance(expr, _Sum): 3513 to_process.append((expr.left(), coeff)) 3514 to_process.append((expr.right(), coeff)) 3515 elif isinstance(expr, _SumArray): 3516 for e in expr.expressions(): 3517 to_process.append((e, coeff)) 3518 value += expr.constant() * coeff 3519 elif isinstance(expr, _WeightedSum): 3520 for e, c in zip(expr.expressions(), expr.coefficients()): 3521 to_process.append((e, coeff * c)) 3522 value += expr.constant() * coeff 3523 elif isinstance(expr, IntVar): 3524 value += coeff * self.SolutionIntegerValue(expr.index) 3525 elif isinstance(expr, _NotBooleanVariable): 3526 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3527 else: 3528 raise TypeError( 3529 f"cannot interpret {expression} as a linear expression." 3530 ) 3531 3532 return value 3533 3534 def has_response(self) -> bool: 3535 return self.HasResponse() 3536 3537 def stop_search(self) -> None: 3538 """Stops the current search asynchronously.""" 3539 if not self.has_response(): 3540 raise RuntimeError("solve() has not been called.") 3541 self.StopSearch() 3542 3543 @property 3544 def objective_value(self) -> float: 3545 """Returns the value of the objective after solve.""" 3546 if not self.has_response(): 3547 raise RuntimeError("solve() has not been called.") 3548 return self.ObjectiveValue() 3549 3550 @property 3551 def best_objective_bound(self) -> float: 3552 """Returns the best lower (upper) bound found when min(max)imizing.""" 3553 if not self.has_response(): 3554 raise RuntimeError("solve() has not been called.") 3555 return self.BestObjectiveBound() 3556 3557 @property 3558 def num_booleans(self) -> int: 3559 """Returns the number of boolean variables managed by the SAT solver.""" 3560 if not self.has_response(): 3561 raise RuntimeError("solve() has not been called.") 3562 return self.NumBooleans() 3563 3564 @property 3565 def num_conflicts(self) -> int: 3566 """Returns the number of conflicts since the creation of the solver.""" 3567 if not self.has_response(): 3568 raise RuntimeError("solve() has not been called.") 3569 return self.NumConflicts() 3570 3571 @property 3572 def num_branches(self) -> int: 3573 """Returns the number of search branches explored by the solver.""" 3574 if not self.has_response(): 3575 raise RuntimeError("solve() has not been called.") 3576 return self.NumBranches() 3577 3578 @property 3579 def num_integer_propagations(self) -> int: 3580 """Returns the number of integer propagations done by the solver.""" 3581 if not self.has_response(): 3582 raise RuntimeError("solve() has not been called.") 3583 return self.NumIntegerPropagations() 3584 3585 @property 3586 def num_boolean_propagations(self) -> int: 3587 """Returns the number of Boolean propagations done by the solver.""" 3588 if not self.has_response(): 3589 raise RuntimeError("solve() has not been called.") 3590 return self.NumBooleanPropagations() 3591 3592 @property 3593 def deterministic_time(self) -> float: 3594 """Returns the determistic time in seconds since the creation of the solver.""" 3595 if not self.has_response(): 3596 raise RuntimeError("solve() has not been called.") 3597 return self.DeterministicTime() 3598 3599 @property 3600 def wall_time(self) -> float: 3601 """Returns the wall time in seconds since the creation of the solver.""" 3602 if not self.has_response(): 3603 raise RuntimeError("solve() has not been called.") 3604 return self.WallTime() 3605 3606 @property 3607 def user_time(self) -> float: 3608 """Returns the user time in seconds since the creation of the solver.""" 3609 if not self.has_response(): 3610 raise RuntimeError("solve() has not been called.") 3611 return self.UserTime() 3612 3613 @property 3614 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3615 """Returns the response object.""" 3616 if not self.has_response(): 3617 raise RuntimeError("solve() has not been called.") 3618 return self.Response() 3619 3620 # Compatibility with pre PEP8 3621 # pylint: disable=invalid-name 3622 Value = value 3623 BooleanValue = boolean_value 3624 # pylint: enable=invalid-name
Solution callback.
This class implements a callback that will be called at each new solution found during search.
The method on_solution_callback() will be called by the solver, and must be implemented. The current solution can be queried using the boolean_value() and value() methods.
These methods returns the same information as their counterpart in the
CpSolver
class.
3460 def OnSolutionCallback(self) -> None: 3461 """Proxy for the same method in snake case.""" 3462 self.on_solution_callback()
Proxy for the same method in snake case.
3464 def boolean_value(self, lit: LiteralT) -> bool: 3465 """Returns the boolean value of a boolean literal. 3466 3467 Args: 3468 lit: A boolean variable or its negation. 3469 3470 Returns: 3471 The Boolean value of the literal in the solution. 3472 3473 Raises: 3474 RuntimeError: if `lit` is not a boolean variable or its negation. 3475 """ 3476 if not self.has_response(): 3477 raise RuntimeError("solve() has not been called.") 3478 if isinstance(lit, IntegralTypes): 3479 return bool(lit) 3480 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3481 return self.SolutionBooleanValue( 3482 cast(Union[IntVar, _NotBooleanVariable], lit).index 3483 ) 3484 if cmh.is_boolean(lit): 3485 return bool(lit) 3486 raise TypeError(f"Cannot interpret {lit} as a boolean expression.")
Returns the boolean value of a boolean literal.
Arguments:
- lit: A boolean variable or its negation.
Returns:
The Boolean value of the literal in the solution.
Raises:
- RuntimeError: if
lit
is not a boolean variable or its negation.
3488 def value(self, expression: LinearExprT) -> int: 3489 """Evaluates an linear expression in the current solution. 3490 3491 Args: 3492 expression: a linear expression of the model. 3493 3494 Returns: 3495 An integer value equal to the evaluation of the linear expression 3496 against the current solution. 3497 3498 Raises: 3499 RuntimeError: if 'expression' is not a LinearExpr. 3500 """ 3501 if not self.has_response(): 3502 raise RuntimeError("solve() has not been called.") 3503 3504 value = 0 3505 to_process = [(expression, 1)] 3506 while to_process: 3507 expr, coeff = to_process.pop() 3508 if isinstance(expr, IntegralTypes): 3509 value += int(expr) * coeff 3510 elif isinstance(expr, _ProductCst): 3511 to_process.append((expr.expression(), coeff * expr.coefficient())) 3512 elif isinstance(expr, _Sum): 3513 to_process.append((expr.left(), coeff)) 3514 to_process.append((expr.right(), coeff)) 3515 elif isinstance(expr, _SumArray): 3516 for e in expr.expressions(): 3517 to_process.append((e, coeff)) 3518 value += expr.constant() * coeff 3519 elif isinstance(expr, _WeightedSum): 3520 for e, c in zip(expr.expressions(), expr.coefficients()): 3521 to_process.append((e, coeff * c)) 3522 value += expr.constant() * coeff 3523 elif isinstance(expr, IntVar): 3524 value += coeff * self.SolutionIntegerValue(expr.index) 3525 elif isinstance(expr, _NotBooleanVariable): 3526 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3527 else: 3528 raise TypeError( 3529 f"cannot interpret {expression} as a linear expression." 3530 ) 3531 3532 return value
Evaluates an linear expression in the current solution.
Arguments:
- expression: a linear expression of the model.
Returns:
An integer value equal to the evaluation of the linear expression against the current solution.
Raises:
- RuntimeError: if 'expression' is not a LinearExpr.
3537 def stop_search(self) -> None: 3538 """Stops the current search asynchronously.""" 3539 if not self.has_response(): 3540 raise RuntimeError("solve() has not been called.") 3541 self.StopSearch()
Stops the current search asynchronously.
3543 @property 3544 def objective_value(self) -> float: 3545 """Returns the value of the objective after solve.""" 3546 if not self.has_response(): 3547 raise RuntimeError("solve() has not been called.") 3548 return self.ObjectiveValue()
Returns the value of the objective after solve.
3550 @property 3551 def best_objective_bound(self) -> float: 3552 """Returns the best lower (upper) bound found when min(max)imizing.""" 3553 if not self.has_response(): 3554 raise RuntimeError("solve() has not been called.") 3555 return self.BestObjectiveBound()
Returns the best lower (upper) bound found when min(max)imizing.
3557 @property 3558 def num_booleans(self) -> int: 3559 """Returns the number of boolean variables managed by the SAT solver.""" 3560 if not self.has_response(): 3561 raise RuntimeError("solve() has not been called.") 3562 return self.NumBooleans()
Returns the number of boolean variables managed by the SAT solver.
3564 @property 3565 def num_conflicts(self) -> int: 3566 """Returns the number of conflicts since the creation of the solver.""" 3567 if not self.has_response(): 3568 raise RuntimeError("solve() has not been called.") 3569 return self.NumConflicts()
Returns the number of conflicts since the creation of the solver.
3571 @property 3572 def num_branches(self) -> int: 3573 """Returns the number of search branches explored by the solver.""" 3574 if not self.has_response(): 3575 raise RuntimeError("solve() has not been called.") 3576 return self.NumBranches()
Returns the number of search branches explored by the solver.
3578 @property 3579 def num_integer_propagations(self) -> int: 3580 """Returns the number of integer propagations done by the solver.""" 3581 if not self.has_response(): 3582 raise RuntimeError("solve() has not been called.") 3583 return self.NumIntegerPropagations()
Returns the number of integer propagations done by the solver.
3585 @property 3586 def num_boolean_propagations(self) -> int: 3587 """Returns the number of Boolean propagations done by the solver.""" 3588 if not self.has_response(): 3589 raise RuntimeError("solve() has not been called.") 3590 return self.NumBooleanPropagations()
Returns the number of Boolean propagations done by the solver.
3592 @property 3593 def deterministic_time(self) -> float: 3594 """Returns the determistic time in seconds since the creation of the solver.""" 3595 if not self.has_response(): 3596 raise RuntimeError("solve() has not been called.") 3597 return self.DeterministicTime()
Returns the determistic time in seconds since the creation of the solver.
3599 @property 3600 def wall_time(self) -> float: 3601 """Returns the wall time in seconds since the creation of the solver.""" 3602 if not self.has_response(): 3603 raise RuntimeError("solve() has not been called.") 3604 return self.WallTime()
Returns the wall time in seconds since the creation of the solver.
3606 @property 3607 def user_time(self) -> float: 3608 """Returns the user time in seconds since the creation of the solver.""" 3609 if not self.has_response(): 3610 raise RuntimeError("solve() has not been called.") 3611 return self.UserTime()
Returns the user time in seconds since the creation of the solver.
3613 @property 3614 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3615 """Returns the response object.""" 3616 if not self.has_response(): 3617 raise RuntimeError("solve() has not been called.") 3618 return self.Response()
Returns the response object.
3488 def value(self, expression: LinearExprT) -> int: 3489 """Evaluates an linear expression in the current solution. 3490 3491 Args: 3492 expression: a linear expression of the model. 3493 3494 Returns: 3495 An integer value equal to the evaluation of the linear expression 3496 against the current solution. 3497 3498 Raises: 3499 RuntimeError: if 'expression' is not a LinearExpr. 3500 """ 3501 if not self.has_response(): 3502 raise RuntimeError("solve() has not been called.") 3503 3504 value = 0 3505 to_process = [(expression, 1)] 3506 while to_process: 3507 expr, coeff = to_process.pop() 3508 if isinstance(expr, IntegralTypes): 3509 value += int(expr) * coeff 3510 elif isinstance(expr, _ProductCst): 3511 to_process.append((expr.expression(), coeff * expr.coefficient())) 3512 elif isinstance(expr, _Sum): 3513 to_process.append((expr.left(), coeff)) 3514 to_process.append((expr.right(), coeff)) 3515 elif isinstance(expr, _SumArray): 3516 for e in expr.expressions(): 3517 to_process.append((e, coeff)) 3518 value += expr.constant() * coeff 3519 elif isinstance(expr, _WeightedSum): 3520 for e, c in zip(expr.expressions(), expr.coefficients()): 3521 to_process.append((e, coeff * c)) 3522 value += expr.constant() * coeff 3523 elif isinstance(expr, IntVar): 3524 value += coeff * self.SolutionIntegerValue(expr.index) 3525 elif isinstance(expr, _NotBooleanVariable): 3526 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3527 else: 3528 raise TypeError( 3529 f"cannot interpret {expression} as a linear expression." 3530 ) 3531 3532 return value
Evaluates an linear expression in the current solution.
Arguments:
- expression: a linear expression of the model.
Returns:
An integer value equal to the evaluation of the linear expression against the current solution.
Raises:
- RuntimeError: if 'expression' is not a LinearExpr.
3464 def boolean_value(self, lit: LiteralT) -> bool: 3465 """Returns the boolean value of a boolean literal. 3466 3467 Args: 3468 lit: A boolean variable or its negation. 3469 3470 Returns: 3471 The Boolean value of the literal in the solution. 3472 3473 Raises: 3474 RuntimeError: if `lit` is not a boolean variable or its negation. 3475 """ 3476 if not self.has_response(): 3477 raise RuntimeError("solve() has not been called.") 3478 if isinstance(lit, IntegralTypes): 3479 return bool(lit) 3480 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3481 return self.SolutionBooleanValue( 3482 cast(Union[IntVar, _NotBooleanVariable], lit).index 3483 ) 3484 if cmh.is_boolean(lit): 3485 return bool(lit) 3486 raise TypeError(f"Cannot interpret {lit} as a boolean expression.")
Returns the boolean value of a boolean literal.
Arguments:
- lit: A boolean variable or its negation.
Returns:
The Boolean value of the literal in the solution.
Raises:
- RuntimeError: if
lit
is not a boolean variable or its negation.
3627class ObjectiveSolutionPrinter(CpSolverSolutionCallback): 3628 """Display the objective value and time of intermediate solutions.""" 3629 3630 def __init__(self) -> None: 3631 CpSolverSolutionCallback.__init__(self) 3632 self.__solution_count = 0 3633 self.__start_time = time.time() 3634 3635 def on_solution_callback(self) -> None: 3636 """Called on each new solution.""" 3637 current_time = time.time() 3638 obj = self.objective_value 3639 print( 3640 "Solution %i, time = %0.2f s, objective = %i" 3641 % (self.__solution_count, current_time - self.__start_time, obj) 3642 ) 3643 self.__solution_count += 1 3644 3645 def solution_count(self) -> int: 3646 """Returns the number of solutions found.""" 3647 return self.__solution_count
Display the objective value and time of intermediate solutions.
3630 def __init__(self) -> None: 3631 CpSolverSolutionCallback.__init__(self) 3632 self.__solution_count = 0 3633 self.__start_time = time.time()
__init__(self: ortools.sat.python.swig_helper.SolutionCallback) -> None
3635 def on_solution_callback(self) -> None: 3636 """Called on each new solution.""" 3637 current_time = time.time() 3638 obj = self.objective_value 3639 print( 3640 "Solution %i, time = %0.2f s, objective = %i" 3641 % (self.__solution_count, current_time - self.__start_time, obj) 3642 ) 3643 self.__solution_count += 1
Called on each new solution.
3645 def solution_count(self) -> int: 3646 """Returns the number of solutions found.""" 3647 return self.__solution_count
Returns the number of solutions found.
Inherited Members
3650class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): 3651 """Print intermediate solutions (objective, variable values, time).""" 3652 3653 def __init__(self, variables: Sequence[IntVar]) -> None: 3654 CpSolverSolutionCallback.__init__(self) 3655 self.__variables: Sequence[IntVar] = variables 3656 self.__solution_count: int = 0 3657 self.__start_time: float = time.time() 3658 3659 def on_solution_callback(self) -> None: 3660 """Called on each new solution.""" 3661 current_time = time.time() 3662 obj = self.objective_value 3663 print( 3664 "Solution %i, time = %0.2f s, objective = %i" 3665 % (self.__solution_count, current_time - self.__start_time, obj) 3666 ) 3667 for v in self.__variables: 3668 print(" %s = %i" % (v, self.value(v)), end=" ") 3669 print() 3670 self.__solution_count += 1 3671 3672 @property 3673 def solution_count(self) -> int: 3674 """Returns the number of solutions found.""" 3675 return self.__solution_count
Print intermediate solutions (objective, variable values, time).
3653 def __init__(self, variables: Sequence[IntVar]) -> None: 3654 CpSolverSolutionCallback.__init__(self) 3655 self.__variables: Sequence[IntVar] = variables 3656 self.__solution_count: int = 0 3657 self.__start_time: float = time.time()
__init__(self: ortools.sat.python.swig_helper.SolutionCallback) -> None
3659 def on_solution_callback(self) -> None: 3660 """Called on each new solution.""" 3661 current_time = time.time() 3662 obj = self.objective_value 3663 print( 3664 "Solution %i, time = %0.2f s, objective = %i" 3665 % (self.__solution_count, current_time - self.__start_time, obj) 3666 ) 3667 for v in self.__variables: 3668 print(" %s = %i" % (v, self.value(v)), end=" ") 3669 print() 3670 self.__solution_count += 1
Called on each new solution.
3672 @property 3673 def solution_count(self) -> int: 3674 """Returns the number of solutions found.""" 3675 return self.__solution_count
Returns the number of solutions found.
Inherited Members
3678class VarArraySolutionPrinter(CpSolverSolutionCallback): 3679 """Print intermediate solutions (variable values, time).""" 3680 3681 def __init__(self, variables: Sequence[IntVar]) -> None: 3682 CpSolverSolutionCallback.__init__(self) 3683 self.__variables: Sequence[IntVar] = variables 3684 self.__solution_count: int = 0 3685 self.__start_time: float = time.time() 3686 3687 def on_solution_callback(self) -> None: 3688 """Called on each new solution.""" 3689 current_time = time.time() 3690 print( 3691 "Solution %i, time = %0.2f s" 3692 % (self.__solution_count, current_time - self.__start_time) 3693 ) 3694 for v in self.__variables: 3695 print(" %s = %i" % (v, self.value(v)), end=" ") 3696 print() 3697 self.__solution_count += 1 3698 3699 @property 3700 def solution_count(self) -> int: 3701 """Returns the number of solutions found.""" 3702 return self.__solution_count
Print intermediate solutions (variable values, time).
3681 def __init__(self, variables: Sequence[IntVar]) -> None: 3682 CpSolverSolutionCallback.__init__(self) 3683 self.__variables: Sequence[IntVar] = variables 3684 self.__solution_count: int = 0 3685 self.__start_time: float = time.time()
__init__(self: ortools.sat.python.swig_helper.SolutionCallback) -> None
3687 def on_solution_callback(self) -> None: 3688 """Called on each new solution.""" 3689 current_time = time.time() 3690 print( 3691 "Solution %i, time = %0.2f s" 3692 % (self.__solution_count, current_time - self.__start_time) 3693 ) 3694 for v in self.__variables: 3695 print(" %s = %i" % (v, self.value(v)), end=" ") 3696 print() 3697 self.__solution_count += 1
Called on each new solution.
3699 @property 3700 def solution_count(self) -> int: 3701 """Returns the number of solutions found.""" 3702 return self.__solution_count
Returns the number of solutions found.