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
: Methods for creating models, including variables and constraints.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
: A general method for implementing callbacks.ObjectiveSolutionPrinter
: Print objective values and elapsed time for intermediate solutions.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
: A few utility methods for modifying constraints created byCpModel
.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 numbers 51import threading 52import time 53from typing import ( 54 Any, 55 Callable, 56 Dict, 57 Iterable, 58 List, 59 NoReturn, 60 Optional, 61 Sequence, 62 Tuple, 63 Union, 64 cast, 65 overload, 66) 67import warnings 68 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 122# We need to add int to numbers.Integral 123IntegralT = Union[numbers.Integral, int] 124# We need to add int and float, otherwise type checkers complain. 125NumberT = Union[numbers.Integral, int, numbers.Number, float] 126LiteralT = Union["IntVar", "_NotBooleanVariable", IntegralT, bool] 127BoolVarT = Union["IntVar", "_NotBooleanVariable"] 128VariableT = Union["IntVar", IntegralT] 129LinearExprT = Union["LinearExpr", "IntVar", IntegralT] 130ObjLinearExprT = Union["LinearExpr", "IntVar", NumberT] 131BoundedLinearExprT = Union["BoundedLinearExpression", bool] 132ArcT = Tuple[IntegralT, IntegralT, LiteralT] 133_IndexOrSeries = Union[pd.Index, pd.Series] 134 135 136def display_bounds(bounds: Sequence[int]) -> str: 137 """Displays a flattened list of intervals.""" 138 out = "" 139 for i in range(0, len(bounds), 2): 140 if i != 0: 141 out += ", " 142 if bounds[i] == bounds[i + 1]: 143 out += str(bounds[i]) 144 else: 145 out += str(bounds[i]) + ".." + str(bounds[i + 1]) 146 return out 147 148 149def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str: 150 """Returns a short name of an integer variable, or its negation.""" 151 if i < 0: 152 return "not(%s)" % short_name(model, -i - 1) 153 v = model.variables[i] 154 if v.name: 155 return v.name 156 elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: 157 return str(v.domain[0]) 158 else: 159 return "[%s]" % display_bounds(v.domain) 160 161 162def short_expr_name( 163 model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto 164) -> str: 165 """Pretty-print LinearExpressionProto instances.""" 166 if not e.vars: 167 return str(e.offset) 168 if len(e.vars) == 1: 169 var_name = short_name(model, e.vars[0]) 170 coeff = e.coeffs[0] 171 result = "" 172 if coeff == 1: 173 result = var_name 174 elif coeff == -1: 175 result = f"-{var_name}" 176 elif coeff != 0: 177 result = f"{coeff} * {var_name}" 178 if e.offset > 0: 179 result = f"{result} + {e.offset}" 180 elif e.offset < 0: 181 result = f"{result} - {-e.offset}" 182 return result 183 # TODO(user): Support more than affine expressions. 184 return str(e) 185 186 187class LinearExpr: 188 """Holds an integer linear expression. 189 190 A linear expression is built from integer constants and variables. 191 For example, `x + 2 * (y - z + 1)`. 192 193 Linear expressions are used in CP-SAT models in constraints and in the 194 objective: 195 196 * You can define linear constraints as in: 197 198 ``` 199 model.add(x + 2 * y <= 5) 200 model.add(sum(array_of_vars) == 5) 201 ``` 202 203 * In CP-SAT, the objective is a linear expression: 204 205 ``` 206 model.minimize(x + 2 * y + z) 207 ``` 208 209 * For large arrays, using the LinearExpr class is faster that using the python 210 `sum()` function. You can create constraints and the objective from lists of 211 linear expressions or coefficients as follows: 212 213 ``` 214 model.minimize(cp_model.LinearExpr.sum(expressions)) 215 model.add(cp_model.LinearExpr.weighted_sum(expressions, coefficients) >= 0) 216 ``` 217 """ 218 219 @classmethod 220 def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 221 """Creates the expression sum(expressions).""" 222 if len(expressions) == 1: 223 return expressions[0] 224 return _SumArray(expressions) 225 226 @overload 227 @classmethod 228 def weighted_sum( 229 cls, 230 expressions: Sequence[LinearExprT], 231 coefficients: Sequence[IntegralT], 232 ) -> LinearExprT: 233 ... 234 235 @overload 236 @classmethod 237 def weighted_sum( 238 cls, 239 expressions: Sequence[ObjLinearExprT], 240 coefficients: Sequence[NumberT], 241 ) -> ObjLinearExprT: 242 ... 243 244 @classmethod 245 def weighted_sum(cls, expressions, coefficients): 246 """Creates the expression sum(expressions[i] * coefficients[i]).""" 247 if LinearExpr.is_empty_or_all_null(coefficients): 248 return 0 249 elif len(expressions) == 1: 250 return expressions[0] * coefficients[0] 251 else: 252 return _WeightedSum(expressions, coefficients) 253 254 @overload 255 @classmethod 256 def term( 257 cls, 258 expressions: LinearExprT, 259 coefficients: IntegralT, 260 ) -> LinearExprT: 261 ... 262 263 @overload 264 @classmethod 265 def term( 266 cls, 267 expressions: ObjLinearExprT, 268 coefficients: NumberT, 269 ) -> ObjLinearExprT: 270 ... 271 272 @classmethod 273 def term(cls, expression, coefficient): 274 """Creates `expression * coefficient`.""" 275 if cmh.is_zero(coefficient): 276 return 0 277 else: 278 return expression * coefficient 279 280 @classmethod 281 def is_empty_or_all_null(cls, coefficients: Sequence[NumberT]) -> bool: 282 for c in coefficients: 283 if not cmh.is_zero(c): 284 return False 285 return True 286 287 @classmethod 288 def rebuild_from_linear_expression_proto( 289 cls, 290 model: cp_model_pb2.CpModelProto, 291 proto: cp_model_pb2.LinearExpressionProto, 292 ) -> LinearExprT: 293 """Recreate a LinearExpr from a LinearExpressionProto.""" 294 offset = proto.offset 295 num_elements = len(proto.vars) 296 if num_elements == 0: 297 return offset 298 elif num_elements == 1: 299 return IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset 300 else: 301 variables = [] 302 coeffs = [] 303 all_ones = True 304 for index, coeff in zip(proto.vars, proto.coeffs): 305 variables.append(IntVar(model, index, None)) 306 coeffs.append(coeff) 307 if not cmh.is_one(coeff): 308 all_ones = False 309 if all_ones: 310 return _SumArray(variables, offset) 311 else: 312 return _WeightedSum(variables, coeffs, offset) 313 314 def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", IntegralT], int]: 315 """Scans the expression, and returns (var_coef_map, constant).""" 316 coeffs = collections.defaultdict(int) 317 constant = 0 318 to_process: List[Tuple[LinearExprT, IntegralT]] = [(self, 1)] 319 while to_process: # Flatten to avoid recursion. 320 expr, coeff = to_process.pop() 321 if isinstance(expr, numbers.Integral): 322 constant += coeff * int(expr) 323 elif isinstance(expr, _ProductCst): 324 to_process.append((expr.expression(), coeff * expr.coefficient())) 325 elif isinstance(expr, _Sum): 326 to_process.append((expr.left(), coeff)) 327 to_process.append((expr.right(), coeff)) 328 elif isinstance(expr, _SumArray): 329 for e in expr.expressions(): 330 to_process.append((e, coeff)) 331 constant += expr.constant() * coeff 332 elif isinstance(expr, _WeightedSum): 333 for e, c in zip(expr.expressions(), expr.coefficients()): 334 to_process.append((e, coeff * c)) 335 constant += expr.constant() * coeff 336 elif isinstance(expr, IntVar): 337 coeffs[expr] += coeff 338 elif isinstance(expr, _NotBooleanVariable): 339 constant += coeff 340 coeffs[expr.negated()] -= coeff 341 else: 342 raise TypeError("Unrecognized linear expression: " + str(expr)) 343 344 return coeffs, constant 345 346 def get_float_var_value_map( 347 self, 348 ) -> Tuple[Dict["IntVar", float], float, bool]: 349 """Scans the expression. Returns (var_coef_map, constant, is_integer).""" 350 coeffs = {} 351 constant = 0 352 to_process: List[Tuple[LinearExprT, Union[IntegralT, float]]] = [(self, 1)] 353 while to_process: # Flatten to avoid recursion. 354 expr, coeff = to_process.pop() 355 if isinstance(expr, numbers.Integral): # Keep integrality. 356 constant += coeff * int(expr) 357 elif isinstance(expr, numbers.Number): 358 constant += coeff * float(expr) 359 elif isinstance(expr, _ProductCst): 360 to_process.append((expr.expression(), coeff * expr.coefficient())) 361 elif isinstance(expr, _Sum): 362 to_process.append((expr.left(), coeff)) 363 to_process.append((expr.right(), coeff)) 364 elif isinstance(expr, _SumArray): 365 for e in expr.expressions(): 366 to_process.append((e, coeff)) 367 constant += expr.constant() * coeff 368 elif isinstance(expr, _WeightedSum): 369 for e, c in zip(expr.expressions(), expr.coefficients()): 370 to_process.append((e, coeff * c)) 371 constant += expr.constant() * coeff 372 elif isinstance(expr, IntVar): 373 if expr in coeffs: 374 coeffs[expr] += coeff 375 else: 376 coeffs[expr] = coeff 377 elif isinstance(expr, _NotBooleanVariable): 378 constant += coeff 379 if expr.negated() in coeffs: 380 coeffs[expr.negated()] -= coeff 381 else: 382 coeffs[expr.negated()] = -coeff 383 else: 384 raise TypeError("Unrecognized linear expression: " + str(expr)) 385 is_integer = isinstance(constant, numbers.Integral) 386 if is_integer: 387 for coeff in coeffs.values(): 388 if not isinstance(coeff, numbers.Integral): 389 is_integer = False 390 break 391 return coeffs, constant, is_integer 392 393 def __hash__(self) -> int: 394 return object.__hash__(self) 395 396 def __abs__(self) -> NoReturn: 397 raise NotImplementedError( 398 "calling abs() on a linear expression is not supported, " 399 "please use CpModel.add_abs_equality" 400 ) 401 402 @overload 403 def __add__(self, arg: LinearExprT) -> LinearExprT: 404 ... 405 406 @overload 407 def __add__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 408 ... 409 410 def __add__(self, arg): 411 if cmh.is_zero(arg): 412 return self 413 return _Sum(self, arg) 414 415 @overload 416 def __radd__(self, arg: LinearExprT) -> LinearExprT: 417 ... 418 419 @overload 420 def __radd__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 421 ... 422 423 def __radd__(self, arg): 424 if cmh.is_zero(arg): 425 return self 426 return _Sum(self, arg) 427 428 @overload 429 def __sub__(self, arg: LinearExprT) -> LinearExprT: 430 ... 431 432 @overload 433 def __sub__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 434 ... 435 436 def __sub__(self, arg): 437 if cmh.is_zero(arg): 438 return self 439 if isinstance(arg, numbers.Number): 440 arg = cmh.assert_is_a_number(arg) 441 return _Sum(self, -arg) 442 else: 443 return _Sum(self, -arg) 444 445 @overload 446 def __rsub__(self, arg: LinearExprT) -> LinearExprT: 447 ... 448 449 @overload 450 def __rsub__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 451 ... 452 453 def __rsub__(self, arg): 454 return _Sum(-self, arg) 455 456 @overload 457 def __mul__(self, arg: LinearExprT) -> LinearExprT: 458 ... 459 460 @overload 461 def __mul__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 462 ... 463 464 def __mul__(self, arg): 465 arg = cmh.assert_is_a_number(arg) 466 if cmh.is_one(arg): 467 return self 468 elif cmh.is_zero(arg): 469 return 0 470 return _ProductCst(self, arg) 471 472 @overload 473 def __rmul__(self, arg: LinearExprT) -> LinearExprT: 474 ... 475 476 @overload 477 def __rmul__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 478 ... 479 480 def __rmul__(self, arg): 481 arg = cmh.assert_is_a_number(arg) 482 if cmh.is_one(arg): 483 return self 484 elif cmh.is_zero(arg): 485 return 0 486 return _ProductCst(self, arg) 487 488 def __div__(self, _) -> NoReturn: 489 raise NotImplementedError( 490 "calling / on a linear expression is not supported, " 491 "please use CpModel.add_division_equality" 492 ) 493 494 def __truediv__(self, _) -> NoReturn: 495 raise NotImplementedError( 496 "calling // on a linear expression is not supported, " 497 "please use CpModel.add_division_equality" 498 ) 499 500 def __mod__(self, _) -> NoReturn: 501 raise NotImplementedError( 502 "calling %% on a linear expression is not supported, " 503 "please use CpModel.add_modulo_equality" 504 ) 505 506 def __pow__(self, _) -> NoReturn: 507 raise NotImplementedError( 508 "calling ** on a linear expression is not supported, " 509 "please use CpModel.add_multiplication_equality" 510 ) 511 512 def __lshift__(self, _) -> NoReturn: 513 raise NotImplementedError( 514 "calling left shift on a linear expression is not supported" 515 ) 516 517 def __rshift__(self, _) -> NoReturn: 518 raise NotImplementedError( 519 "calling right shift on a linear expression is not supported" 520 ) 521 522 def __and__(self, _) -> NoReturn: 523 raise NotImplementedError( 524 "calling and on a linear expression is not supported, " 525 "please use CpModel.add_bool_and" 526 ) 527 528 def __or__(self, _) -> NoReturn: 529 raise NotImplementedError( 530 "calling or on a linear expression is not supported, " 531 "please use CpModel.add_bool_or" 532 ) 533 534 def __xor__(self, _) -> NoReturn: 535 raise NotImplementedError( 536 "calling xor on a linear expression is not supported, " 537 "please use CpModel.add_bool_xor" 538 ) 539 540 def __neg__(self) -> LinearExprT: 541 return _ProductCst(self, -1) 542 543 def __bool__(self) -> NoReturn: 544 raise NotImplementedError( 545 "Evaluating a LinearExpr instance as a Boolean is not implemented." 546 ) 547 548 def __eq__(self, arg: LinearExprT) -> BoundedLinearExprT: 549 if arg is None: 550 return False 551 if isinstance(arg, numbers.Integral): 552 arg = cmh.assert_is_int64(arg) 553 return BoundedLinearExpression(self, [arg, arg]) 554 else: 555 return BoundedLinearExpression(self - arg, [0, 0]) 556 557 def __ge__(self, arg: LinearExprT) -> BoundedLinearExprT: 558 if isinstance(arg, numbers.Integral): 559 arg = cmh.assert_is_int64(arg) 560 return BoundedLinearExpression(self, [arg, INT_MAX]) 561 else: 562 return BoundedLinearExpression(self - arg, [0, INT_MAX]) 563 564 def __le__(self, arg: LinearExprT) -> BoundedLinearExprT: 565 if isinstance(arg, numbers.Integral): 566 arg = cmh.assert_is_int64(arg) 567 return BoundedLinearExpression(self, [INT_MIN, arg]) 568 else: 569 return BoundedLinearExpression(self - arg, [INT_MIN, 0]) 570 571 def __lt__(self, arg: LinearExprT) -> BoundedLinearExprT: 572 if isinstance(arg, numbers.Integral): 573 arg = cmh.assert_is_int64(arg) 574 if arg == INT_MIN: 575 raise ArithmeticError("< INT_MIN is not supported") 576 return BoundedLinearExpression(self, [INT_MIN, arg - 1]) 577 else: 578 return BoundedLinearExpression(self - arg, [INT_MIN, -1]) 579 580 def __gt__(self, arg: LinearExprT) -> BoundedLinearExprT: 581 if isinstance(arg, numbers.Integral): 582 arg = cmh.assert_is_int64(arg) 583 if arg == INT_MAX: 584 raise ArithmeticError("> INT_MAX is not supported") 585 return BoundedLinearExpression(self, [arg + 1, INT_MAX]) 586 else: 587 return BoundedLinearExpression(self - arg, [1, INT_MAX]) 588 589 def __ne__(self, arg: LinearExprT) -> BoundedLinearExprT: 590 if arg is None: 591 return True 592 if isinstance(arg, numbers.Integral): 593 arg = cmh.assert_is_int64(arg) 594 if arg == INT_MAX: 595 return BoundedLinearExpression(self, [INT_MIN, INT_MAX - 1]) 596 elif arg == INT_MIN: 597 return BoundedLinearExpression(self, [INT_MIN + 1, INT_MAX]) 598 else: 599 return BoundedLinearExpression( 600 self, [INT_MIN, arg - 1, arg + 1, INT_MAX] 601 ) 602 else: 603 return BoundedLinearExpression(self - arg, [INT_MIN, -1, 1, INT_MAX]) 604 605 # Compatibility with pre PEP8 606 # pylint: disable=invalid-name 607 @classmethod 608 def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 609 """Creates the expression sum(expressions).""" 610 return cls.sum(expressions) 611 612 @overload 613 @classmethod 614 def WeightedSum( 615 cls, 616 expressions: Sequence[LinearExprT], 617 coefficients: Sequence[IntegralT], 618 ) -> LinearExprT: 619 ... 620 621 @overload 622 @classmethod 623 def WeightedSum( 624 cls, 625 expressions: Sequence[ObjLinearExprT], 626 coefficients: Sequence[NumberT], 627 ) -> ObjLinearExprT: 628 ... 629 630 @classmethod 631 def WeightedSum(cls, expressions, coefficients): 632 """Creates the expression sum(expressions[i] * coefficients[i]).""" 633 return cls.weighted_sum(expressions, coefficients) 634 635 @overload 636 @classmethod 637 def Term( 638 cls, 639 expressions: LinearExprT, 640 coefficients: IntegralT, 641 ) -> LinearExprT: 642 ... 643 644 @overload 645 @classmethod 646 def Term( 647 cls, 648 expressions: ObjLinearExprT, 649 coefficients: NumberT, 650 ) -> ObjLinearExprT: 651 ... 652 653 @classmethod 654 def Term(cls, expression, coefficient): 655 """Creates `expression * coefficient`.""" 656 return cls.term(expression, coefficient) 657 658 # pylint: enable=invalid-name 659 660 661class _Sum(LinearExpr): 662 """Represents the sum of two LinearExprs.""" 663 664 def __init__(self, left, right): 665 for x in [left, right]: 666 if not isinstance(x, (numbers.Number, LinearExpr)): 667 raise TypeError("not an linear expression: " + str(x)) 668 self.__left = left 669 self.__right = right 670 671 def left(self): 672 return self.__left 673 674 def right(self): 675 return self.__right 676 677 def __str__(self): 678 return f"({self.__left} + {self.__right})" 679 680 def __repr__(self): 681 return f"sum({self.__left!r}, {self.__right!r})" 682 683 684class _ProductCst(LinearExpr): 685 """Represents the product of a LinearExpr by a constant.""" 686 687 def __init__(self, expr, coeff): 688 coeff = cmh.assert_is_a_number(coeff) 689 if isinstance(expr, _ProductCst): 690 self.__expr = expr.expression() 691 self.__coef = expr.coefficient() * coeff 692 else: 693 self.__expr = expr 694 self.__coef = coeff 695 696 def __str__(self): 697 if self.__coef == -1: 698 return "-" + str(self.__expr) 699 else: 700 return "(" + str(self.__coef) + " * " + str(self.__expr) + ")" 701 702 def __repr__(self): 703 return f"ProductCst({self.__expr!r}, {self.__coef!r})" 704 705 def coefficient(self): 706 return self.__coef 707 708 def expression(self): 709 return self.__expr 710 711 712class _SumArray(LinearExpr): 713 """Represents the sum of a list of LinearExpr and a constant.""" 714 715 def __init__(self, expressions, constant=0): 716 self.__expressions = [] 717 self.__constant = constant 718 for x in expressions: 719 if isinstance(x, numbers.Number): 720 if cmh.is_zero(x): 721 continue 722 x = cmh.assert_is_a_number(x) 723 self.__constant += x 724 elif isinstance(x, LinearExpr): 725 self.__expressions.append(x) 726 else: 727 raise TypeError("not an linear expression: " + str(x)) 728 729 def __str__(self): 730 constant_terms = (self.__constant,) if self.__constant != 0 else () 731 exprs_str = " + ".join( 732 map(repr, itertools.chain(self.__expressions, constant_terms)) 733 ) 734 if not exprs_str: 735 return "0" 736 return f"({exprs_str})" 737 738 def __repr__(self): 739 exprs_str = ", ".join(map(repr, self.__expressions)) 740 return f"SumArray({exprs_str}, {self.__constant})" 741 742 def expressions(self): 743 return self.__expressions 744 745 def constant(self): 746 return self.__constant 747 748 749class _WeightedSum(LinearExpr): 750 """Represents sum(ai * xi) + b.""" 751 752 def __init__(self, expressions, coefficients, constant=0): 753 self.__expressions = [] 754 self.__coefficients = [] 755 self.__constant = constant 756 if len(expressions) != len(coefficients): 757 raise TypeError( 758 "In the LinearExpr.weighted_sum method, the expression array and the " 759 " coefficient array must have the same length." 760 ) 761 for e, c in zip(expressions, coefficients): 762 c = cmh.assert_is_a_number(c) 763 if cmh.is_zero(c): 764 continue 765 if isinstance(e, numbers.Number): 766 e = cmh.assert_is_a_number(e) 767 self.__constant += e * c 768 elif isinstance(e, LinearExpr): 769 self.__expressions.append(e) 770 self.__coefficients.append(c) 771 else: 772 raise TypeError("not an linear expression: " + str(e)) 773 774 def __str__(self): 775 output = None 776 for expr, coeff in zip(self.__expressions, self.__coefficients): 777 if not output and cmh.is_one(coeff): 778 output = str(expr) 779 elif not output and cmh.is_minus_one(coeff): 780 output = "-" + str(expr) 781 elif not output: 782 output = f"{coeff} * {expr}" 783 elif cmh.is_one(coeff): 784 output += f" + {expr}" 785 elif cmh.is_minus_one(coeff): 786 output += f" - {expr}" 787 elif coeff > 1: 788 output += f" + {coeff} * {expr}" 789 elif coeff < -1: 790 output += f" - {-coeff} * {expr}" 791 if self.__constant > 0: 792 output += f" + {self.__constant}" 793 elif self.__constant < 0: 794 output += f" - {-self.__constant}" 795 if output is None: 796 output = "0" 797 return output 798 799 def __repr__(self): 800 return ( 801 f"weighted_sum({self.__expressions!r}, {self.__coefficients!r}," 802 f" {self.__constant})" 803 ) 804 805 def expressions(self): 806 return self.__expressions 807 808 def coefficients(self): 809 return self.__coefficients 810 811 def constant(self): 812 return self.__constant 813 814 815class IntVar(LinearExpr): 816 """An integer variable. 817 818 An IntVar is an object that can take on any integer value within defined 819 ranges. Variables appear in constraint like: 820 821 x + y >= 5 822 AllDifferent([x, y, z]) 823 824 Solving a model is equivalent to finding, for each variable, a single value 825 from the set of initial values (called the initial domain), such that the 826 model is feasible, or optimal if you provided an objective function. 827 """ 828 829 def __init__( 830 self, 831 model: cp_model_pb2.CpModelProto, 832 domain: Union[int, Domain], 833 name: Optional[str], 834 ): 835 """See CpModel.new_int_var below.""" 836 self.__negation: Optional[_NotBooleanVariable] = None 837 # Python do not support multiple __init__ methods. 838 # This method is only called from the CpModel class. 839 # We hack the parameter to support the two cases: 840 # case 1: 841 # model is a CpModelProto, domain is a Domain, and name is a string. 842 # case 2: 843 # model is a CpModelProto, domain is an index (int), and name is None. 844 if isinstance(domain, numbers.Integral) and name is None: 845 self.__index: int = int(domain) 846 self.__var: cp_model_pb2.IntegerVariableProto = model.variables[domain] 847 else: 848 self.__index: int = len(model.variables) 849 self.__var: cp_model_pb2.IntegerVariableProto = model.variables.add() 850 self.__var.domain.extend(cast(Domain, domain).flattened_intervals()) 851 self.__var.name = name 852 853 @property 854 def index(self) -> int: 855 """Returns the index of the variable in the model.""" 856 return self.__index 857 858 @property 859 def proto(self) -> cp_model_pb2.IntegerVariableProto: 860 """Returns the variable protobuf.""" 861 return self.__var 862 863 def is_equal_to(self, other: Any) -> bool: 864 """Returns true if self == other in the python sense.""" 865 if not isinstance(other, IntVar): 866 return False 867 return self.index == other.index 868 869 def __str__(self) -> str: 870 if not self.__var.name: 871 if ( 872 len(self.__var.domain) == 2 873 and self.__var.domain[0] == self.__var.domain[1] 874 ): 875 # Special case for constants. 876 return str(self.__var.domain[0]) 877 else: 878 return "unnamed_var_%i" % self.__index 879 return self.__var.name 880 881 def __repr__(self) -> str: 882 return "%s(%s)" % (self.__var.name, display_bounds(self.__var.domain)) 883 884 @property 885 def name(self) -> str: 886 if not self.__var or not self.__var.name: 887 return "" 888 return self.__var.name 889 890 def negated(self) -> "_NotBooleanVariable": 891 """Returns the negation of a Boolean variable. 892 893 This method implements the logical negation of a Boolean variable. 894 It is only valid if the variable has a Boolean domain (0 or 1). 895 896 Note that this method is nilpotent: `x.negated().negated() == x`. 897 """ 898 899 for bound in self.__var.domain: 900 if bound < 0 or bound > 1: 901 raise TypeError( 902 f"cannot call negated on a non boolean variable: {self}" 903 ) 904 if self.__negation is None: 905 self.__negation = _NotBooleanVariable(self) 906 return self.__negation 907 908 def __invert__(self) -> "_NotBooleanVariable": 909 """Returns the logical negation of a Boolean variable.""" 910 return self.negated() 911 912 # Pre PEP8 compatibility. 913 # pylint: disable=invalid-name 914 Not = negated 915 916 def Name(self) -> str: 917 return self.name 918 919 def Proto(self) -> cp_model_pb2.IntegerVariableProto: 920 return self.proto 921 922 def Index(self) -> int: 923 return self.index 924 925 # pylint: enable=invalid-name 926 927 928class _NotBooleanVariable(LinearExpr): 929 """Negation of a boolean variable.""" 930 931 def __init__(self, boolvar: IntVar): 932 self.__boolvar: IntVar = boolvar 933 934 @property 935 def index(self) -> int: 936 return -self.__boolvar.index - 1 937 938 def negated(self) -> IntVar: 939 return self.__boolvar 940 941 def __invert__(self) -> IntVar: 942 """Returns the logical negation of a Boolean literal.""" 943 return self.negated() 944 945 def __str__(self) -> str: 946 return self.name 947 948 @property 949 def name(self) -> str: 950 return "not(%s)" % str(self.__boolvar) 951 952 def __bool__(self) -> NoReturn: 953 raise NotImplementedError( 954 "Evaluating a literal as a Boolean value is not implemented." 955 ) 956 957 # Pre PEP8 compatibility. 958 # pylint: disable=invalid-name 959 def Not(self) -> "IntVar": 960 return self.negated() 961 962 def Index(self) -> int: 963 return self.index 964 965 # pylint: enable=invalid-name 966 967 968class BoundedLinearExpression: 969 """Represents a linear constraint: `lb <= linear expression <= ub`. 970 971 The only use of this class is to be added to the CpModel through 972 `CpModel.add(expression)`, as in: 973 974 model.add(x + 2 * y -1 >= z) 975 """ 976 977 def __init__(self, expr: LinearExprT, bounds: Sequence[int]): 978 self.__expr: LinearExprT = expr 979 self.__bounds: Sequence[int] = bounds 980 981 def __str__(self): 982 if len(self.__bounds) == 2: 983 lb, ub = self.__bounds 984 if lb > INT_MIN and ub < INT_MAX: 985 if lb == ub: 986 return str(self.__expr) + " == " + str(lb) 987 else: 988 return str(lb) + " <= " + str(self.__expr) + " <= " + str(ub) 989 elif lb > INT_MIN: 990 return str(self.__expr) + " >= " + str(lb) 991 elif ub < INT_MAX: 992 return str(self.__expr) + " <= " + str(ub) 993 else: 994 return "True (unbounded expr " + str(self.__expr) + ")" 995 elif ( 996 len(self.__bounds) == 4 997 and self.__bounds[0] == INT_MIN 998 and self.__bounds[1] + 2 == self.__bounds[2] 999 and self.__bounds[3] == INT_MAX 1000 ): 1001 return str(self.__expr) + " != " + str(self.__bounds[1] + 1) 1002 else: 1003 return str(self.__expr) + " in [" + display_bounds(self.__bounds) + "]" 1004 1005 def expression(self) -> LinearExprT: 1006 return self.__expr 1007 1008 def bounds(self) -> Sequence[int]: 1009 return self.__bounds 1010 1011 def __bool__(self) -> bool: 1012 expr = self.__expr 1013 if isinstance(expr, LinearExpr): 1014 coeffs_map, constant = expr.get_integer_var_value_map() 1015 all_coeffs = set(coeffs_map.values()) 1016 same_var = set([0]) 1017 eq_bounds = [0, 0] 1018 different_vars = set([-1, 1]) 1019 ne_bounds = [INT_MIN, -1, 1, INT_MAX] 1020 if ( 1021 len(coeffs_map) == 1 1022 and all_coeffs == same_var 1023 and constant == 0 1024 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1025 ): 1026 return self.__bounds == eq_bounds 1027 if ( 1028 len(coeffs_map) == 2 1029 and all_coeffs == different_vars 1030 and constant == 0 1031 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1032 ): 1033 return self.__bounds == ne_bounds 1034 1035 raise NotImplementedError( 1036 f'Evaluating a BoundedLinearExpression "{self}" as a Boolean value' 1037 + " is not supported." 1038 ) 1039 1040 1041class Constraint: 1042 """Base class for constraints. 1043 1044 Constraints are built by the CpModel through the add<XXX> methods. 1045 Once created by the CpModel class, they are automatically added to the model. 1046 The purpose of this class is to allow specification of enforcement literals 1047 for this constraint. 1048 1049 b = model.new_bool_var('b') 1050 x = model.new_int_var(0, 10, 'x') 1051 y = model.new_int_var(0, 10, 'y') 1052 1053 model.add(x + 2 * y == 5).only_enforce_if(b.negated()) 1054 """ 1055 1056 def __init__( 1057 self, 1058 cp_model: "CpModel", 1059 ): 1060 self.__index: int = len(cp_model.proto.constraints) 1061 self.__cp_model: "CpModel" = cp_model 1062 self.__constraint: cp_model_pb2.ConstraintProto = ( 1063 cp_model.proto.constraints.add() 1064 ) 1065 1066 @overload 1067 def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": 1068 ... 1069 1070 @overload 1071 def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": 1072 ... 1073 1074 def only_enforce_if(self, *boolvar) -> "Constraint": 1075 """Adds an enforcement literal to the constraint. 1076 1077 This method adds one or more literals (that is, a boolean variable or its 1078 negation) as enforcement literals. The conjunction of all these literals 1079 determines whether the constraint is active or not. It acts as an 1080 implication, so if the conjunction is true, it implies that the constraint 1081 must be enforced. If it is false, then the constraint is ignored. 1082 1083 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1084 1085 Args: 1086 *boolvar: One or more Boolean literals. 1087 1088 Returns: 1089 self. 1090 """ 1091 for lit in expand_generator_or_tuple(boolvar): 1092 if (cmh.is_boolean(lit) and lit) or ( 1093 isinstance(lit, numbers.Integral) and lit == 1 1094 ): 1095 # Always true. Do nothing. 1096 pass 1097 elif (cmh.is_boolean(lit) and not lit) or ( 1098 isinstance(lit, numbers.Integral) and lit == 0 1099 ): 1100 self.__constraint.enforcement_literal.append( 1101 self.__cp_model.new_constant(0).index 1102 ) 1103 else: 1104 self.__constraint.enforcement_literal.append( 1105 cast(Union[IntVar, _NotBooleanVariable], lit).index 1106 ) 1107 return self 1108 1109 def with_name(self, name: str) -> "Constraint": 1110 """Sets the name of the constraint.""" 1111 if name: 1112 self.__constraint.name = name 1113 else: 1114 self.__constraint.ClearField("name") 1115 return self 1116 1117 @property 1118 def name(self) -> str: 1119 """Returns the name of the constraint.""" 1120 if not self.__constraint or not self.__constraint.name: 1121 return "" 1122 return self.__constraint.name 1123 1124 @property 1125 def index(self) -> int: 1126 """Returns the index of the constraint in the model.""" 1127 return self.__index 1128 1129 @property 1130 def proto(self) -> cp_model_pb2.ConstraintProto: 1131 """Returns the constraint protobuf.""" 1132 return self.__constraint 1133 1134 # Pre PEP8 compatibility. 1135 # pylint: disable=invalid-name 1136 OnlyEnforceIf = only_enforce_if 1137 WithName = with_name 1138 1139 def Name(self) -> str: 1140 return self.name 1141 1142 def Index(self) -> int: 1143 return self.index 1144 1145 def Proto(self) -> cp_model_pb2.ConstraintProto: 1146 return self.proto 1147 1148 # pylint: enable=invalid-name 1149 1150 1151class IntervalVar: 1152 """Represents an Interval variable. 1153 1154 An interval variable is both a constraint and a variable. It is defined by 1155 three integer variables: start, size, and end. 1156 1157 It is a constraint because, internally, it enforces that start + size == end. 1158 1159 It is also a variable as it can appear in specific scheduling constraints: 1160 NoOverlap, NoOverlap2D, Cumulative. 1161 1162 Optionally, an enforcement literal can be added to this constraint, in which 1163 case these scheduling constraints will ignore interval variables with 1164 enforcement literals assigned to false. Conversely, these constraints will 1165 also set these enforcement literals to false if they cannot fit these 1166 intervals into the schedule. 1167 """ 1168 1169 def __init__( 1170 self, 1171 model: cp_model_pb2.CpModelProto, 1172 start: Union[cp_model_pb2.LinearExpressionProto, int], 1173 size: Optional[cp_model_pb2.LinearExpressionProto], 1174 end: Optional[cp_model_pb2.LinearExpressionProto], 1175 is_present_index: Optional[int], 1176 name: Optional[str], 1177 ): 1178 self.__model: cp_model_pb2.CpModelProto = model 1179 # As with the IntVar::__init__ method, we hack the __init__ method to 1180 # support two use cases: 1181 # case 1: called when creating a new interval variable. 1182 # {start|size|end} are linear expressions, is_present_index is either 1183 # None or the index of a Boolean literal. name is a string 1184 # case 2: called when querying an existing interval variable. 1185 # start_index is an int, all parameters after are None. 1186 if size is None and end is None and is_present_index is None and name is None: 1187 self.__index: int = cast(int, start) 1188 self.__ct: cp_model_pb2.ConstraintProto = model.constraints[self.__index] 1189 else: 1190 self.__index: int = len(model.constraints) 1191 self.__ct: cp_model_pb2.ConstraintProto = self.__model.constraints.add() 1192 self.__ct.interval.start.CopyFrom(start) 1193 self.__ct.interval.size.CopyFrom(size) 1194 self.__ct.interval.end.CopyFrom(end) 1195 if is_present_index is not None: 1196 self.__ct.enforcement_literal.append(is_present_index) 1197 if name: 1198 self.__ct.name = name 1199 1200 @property 1201 def index(self) -> int: 1202 """Returns the index of the interval constraint in the model.""" 1203 return self.__index 1204 1205 @property 1206 def proto(self) -> cp_model_pb2.IntervalConstraintProto: 1207 """Returns the interval protobuf.""" 1208 return self.__ct.interval 1209 1210 def __str__(self): 1211 return self.__ct.name 1212 1213 def __repr__(self): 1214 interval = self.__ct.interval 1215 if self.__ct.enforcement_literal: 1216 return "%s(start = %s, size = %s, end = %s, is_present = %s)" % ( 1217 self.__ct.name, 1218 short_expr_name(self.__model, interval.start), 1219 short_expr_name(self.__model, interval.size), 1220 short_expr_name(self.__model, interval.end), 1221 short_name(self.__model, self.__ct.enforcement_literal[0]), 1222 ) 1223 else: 1224 return "%s(start = %s, size = %s, end = %s)" % ( 1225 self.__ct.name, 1226 short_expr_name(self.__model, interval.start), 1227 short_expr_name(self.__model, interval.size), 1228 short_expr_name(self.__model, interval.end), 1229 ) 1230 1231 @property 1232 def name(self) -> str: 1233 if not self.__ct or not self.__ct.name: 1234 return "" 1235 return self.__ct.name 1236 1237 def start_expr(self) -> LinearExprT: 1238 return LinearExpr.rebuild_from_linear_expression_proto( 1239 self.__model, self.__ct.interval.start 1240 ) 1241 1242 def size_expr(self) -> LinearExprT: 1243 return LinearExpr.rebuild_from_linear_expression_proto( 1244 self.__model, self.__ct.interval.size 1245 ) 1246 1247 def end_expr(self) -> LinearExprT: 1248 return LinearExpr.rebuild_from_linear_expression_proto( 1249 self.__model, self.__ct.interval.end 1250 ) 1251 1252 # Pre PEP8 compatibility. 1253 # pylint: disable=invalid-name 1254 def Name(self) -> str: 1255 return self.name 1256 1257 def Index(self) -> int: 1258 return self.index 1259 1260 def Proto(self) -> cp_model_pb2.IntervalConstraintProto: 1261 return self.proto 1262 1263 StartExpr = start_expr 1264 SizeExpr = size_expr 1265 EndExpr = end_expr 1266 1267 # pylint: enable=invalid-name 1268 1269 1270def object_is_a_true_literal(literal: LiteralT) -> bool: 1271 """Checks if literal is either True, or a Boolean literals fixed to True.""" 1272 if isinstance(literal, IntVar): 1273 proto = literal.proto 1274 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1275 if isinstance(literal, _NotBooleanVariable): 1276 proto = literal.negated().proto 1277 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1278 if isinstance(literal, numbers.Integral): 1279 return int(literal) == 1 1280 return False 1281 1282 1283def object_is_a_false_literal(literal: LiteralT) -> bool: 1284 """Checks if literal is either False, or a Boolean literals fixed to False.""" 1285 if isinstance(literal, IntVar): 1286 proto = literal.proto 1287 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1288 if isinstance(literal, _NotBooleanVariable): 1289 proto = literal.negated().proto 1290 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1291 if isinstance(literal, numbers.Integral): 1292 return int(literal) == 0 1293 return False 1294 1295 1296class CpModel: 1297 """Methods for building a CP model. 1298 1299 Methods beginning with: 1300 1301 * ```New``` create integer, boolean, or interval variables. 1302 * ```add``` create new constraints and add them to the model. 1303 """ 1304 1305 def __init__(self): 1306 self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() 1307 self.__constant_map = {} 1308 1309 # Naming. 1310 @property 1311 def name(self) -> str: 1312 """Returns the name of the model.""" 1313 if not self.__model or not self.__model.name: 1314 return "" 1315 return self.__model.name 1316 1317 @name.setter 1318 def name(self, name: str): 1319 """Sets the name of the model.""" 1320 self.__model.name = name 1321 1322 # Integer variable. 1323 1324 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1325 """Create an integer variable with domain [lb, ub]. 1326 1327 The CP-SAT solver is limited to integer variables. If you have fractional 1328 values, scale them up so that they become integers; if you have strings, 1329 encode them as integers. 1330 1331 Args: 1332 lb: Lower bound for the variable. 1333 ub: Upper bound for the variable. 1334 name: The name of the variable. 1335 1336 Returns: 1337 a variable whose domain is [lb, ub]. 1338 """ 1339 1340 return IntVar(self.__model, Domain(lb, ub), name) 1341 1342 def new_int_var_from_domain(self, domain: Domain, name: str) -> IntVar: 1343 """Create an integer variable from a domain. 1344 1345 A domain is a set of integers specified by a collection of intervals. 1346 For example, `model.new_int_var_from_domain(cp_model. 1347 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1348 1349 Args: 1350 domain: An instance of the Domain class. 1351 name: The name of the variable. 1352 1353 Returns: 1354 a variable whose domain is the given domain. 1355 """ 1356 return IntVar(self.__model, domain, name) 1357 1358 def new_bool_var(self, name: str) -> IntVar: 1359 """Creates a 0-1 variable with the given name.""" 1360 return IntVar(self.__model, Domain(0, 1), name) 1361 1362 def new_constant(self, value: IntegralT) -> IntVar: 1363 """Declares a constant integer.""" 1364 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None) 1365 1366 def new_int_var_series( 1367 self, 1368 name: str, 1369 index: pd.Index, 1370 lower_bounds: Union[IntegralT, pd.Series], 1371 upper_bounds: Union[IntegralT, pd.Series], 1372 ) -> pd.Series: 1373 """Creates a series of (scalar-valued) variables with the given name. 1374 1375 Args: 1376 name (str): Required. The name of the variable set. 1377 index (pd.Index): Required. The index to use for the variable set. 1378 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1379 set. If a `pd.Series` is passed in, it will be based on the 1380 corresponding values of the pd.Series. 1381 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1382 set. If a `pd.Series` is passed in, it will be based on the 1383 corresponding values of the pd.Series. 1384 1385 Returns: 1386 pd.Series: The variable set indexed by its corresponding dimensions. 1387 1388 Raises: 1389 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1390 ValueError: if the `name` is not a valid identifier or already exists. 1391 ValueError: if the `lowerbound` is greater than the `upperbound`. 1392 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1393 the input index. 1394 """ 1395 if not isinstance(index, pd.Index): 1396 raise TypeError("Non-index object is used as index") 1397 if not name.isidentifier(): 1398 raise ValueError("name={} is not a valid identifier".format(name)) 1399 if ( 1400 isinstance(lower_bounds, numbers.Integral) 1401 and isinstance(upper_bounds, numbers.Integral) 1402 and lower_bounds > upper_bounds 1403 ): 1404 raise ValueError( 1405 f"lower_bound={lower_bounds} is greater than" 1406 f" upper_bound={upper_bounds} for variable set={name}" 1407 ) 1408 1409 lower_bounds = _convert_to_integral_series_and_validate_index( 1410 lower_bounds, index 1411 ) 1412 upper_bounds = _convert_to_integral_series_and_validate_index( 1413 upper_bounds, index 1414 ) 1415 return pd.Series( 1416 index=index, 1417 data=[ 1418 # pylint: disable=g-complex-comprehension 1419 IntVar( 1420 model=self.__model, 1421 name=f"{name}[{i}]", 1422 domain=Domain(lower_bounds[i], upper_bounds[i]), 1423 ) 1424 for i in index 1425 ], 1426 ) 1427 1428 def new_bool_var_series( 1429 self, 1430 name: str, 1431 index: pd.Index, 1432 ) -> pd.Series: 1433 """Creates a series of (scalar-valued) variables with the given name. 1434 1435 Args: 1436 name (str): Required. The name of the variable set. 1437 index (pd.Index): Required. The index to use for the variable set. 1438 1439 Returns: 1440 pd.Series: The variable set indexed by its corresponding dimensions. 1441 1442 Raises: 1443 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1444 ValueError: if the `name` is not a valid identifier or already exists. 1445 """ 1446 return self.new_int_var_series( 1447 name=name, index=index, lower_bounds=0, upper_bounds=1 1448 ) 1449 1450 # Linear constraints. 1451 1452 def add_linear_constraint( 1453 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1454 ) -> Constraint: 1455 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1456 return self.add_linear_expression_in_domain(linear_expr, Domain(lb, ub)) 1457 1458 def add_linear_expression_in_domain( 1459 self, linear_expr: LinearExprT, domain: Domain 1460 ) -> Constraint: 1461 """Adds the constraint: `linear_expr` in `domain`.""" 1462 if isinstance(linear_expr, LinearExpr): 1463 ct = Constraint(self) 1464 model_ct = self.__model.constraints[ct.index] 1465 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1466 for t in coeffs_map.items(): 1467 if not isinstance(t[0], IntVar): 1468 raise TypeError("Wrong argument" + str(t)) 1469 c = cmh.assert_is_int64(t[1]) 1470 model_ct.linear.vars.append(t[0].index) 1471 model_ct.linear.coeffs.append(c) 1472 model_ct.linear.domain.extend( 1473 [ 1474 cmh.capped_subtraction(x, constant) 1475 for x in domain.flattened_intervals() 1476 ] 1477 ) 1478 return ct 1479 if isinstance(linear_expr, numbers.Integral): 1480 if not domain.contains(int(linear_expr)): 1481 return self.add_bool_or([]) # Evaluate to false. 1482 else: 1483 return self.add_bool_and([]) # Evaluate to true. 1484 raise TypeError( 1485 "not supported: CpModel.add_linear_expression_in_domain(" 1486 + str(linear_expr) 1487 + " " 1488 + str(domain) 1489 + ")" 1490 ) 1491 1492 def add(self, ct: Union[BoundedLinearExpression, bool]) -> Constraint: 1493 """Adds a `BoundedLinearExpression` to the model. 1494 1495 Args: 1496 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1497 1498 Returns: 1499 An instance of the `Constraint` class. 1500 """ 1501 if isinstance(ct, BoundedLinearExpression): 1502 return self.add_linear_expression_in_domain( 1503 ct.expression(), Domain.from_flat_intervals(ct.bounds()) 1504 ) 1505 if ct and cmh.is_boolean(ct): 1506 return self.add_bool_or([True]) 1507 if not ct and cmh.is_boolean(ct): 1508 return self.add_bool_or([]) # Evaluate to false. 1509 raise TypeError("not supported: CpModel.add(" + str(ct) + ")") 1510 1511 # General Integer Constraints. 1512 1513 @overload 1514 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: 1515 ... 1516 1517 @overload 1518 def add_all_different(self, *expressions: LinearExprT) -> Constraint: 1519 ... 1520 1521 def add_all_different(self, *expressions): 1522 """Adds AllDifferent(expressions). 1523 1524 This constraint forces all expressions to have different values. 1525 1526 Args: 1527 *expressions: simple expressions of the form a * var + constant. 1528 1529 Returns: 1530 An instance of the `Constraint` class. 1531 """ 1532 ct = Constraint(self) 1533 model_ct = self.__model.constraints[ct.index] 1534 expanded = expand_generator_or_tuple(expressions) 1535 model_ct.all_diff.exprs.extend( 1536 self.parse_linear_expression(x) for x in expanded 1537 ) 1538 return ct 1539 1540 def add_element( 1541 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1542 ) -> Constraint: 1543 """Adds the element constraint: `variables[index] == target`. 1544 1545 Args: 1546 index: The index of the variable that's being constrained. 1547 variables: A list of variables. 1548 target: The value that the variable must be equal to. 1549 1550 Returns: 1551 An instance of the `Constraint` class. 1552 """ 1553 1554 if not variables: 1555 raise ValueError("add_element expects a non-empty variables array") 1556 1557 if isinstance(index, numbers.Integral): 1558 return self.add(list(variables)[int(index)] == target) 1559 1560 ct = Constraint(self) 1561 model_ct = self.__model.constraints[ct.index] 1562 model_ct.element.index = self.get_or_make_index(index) 1563 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1564 model_ct.element.target = self.get_or_make_index(target) 1565 return ct 1566 1567 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1568 """Adds Circuit(arcs). 1569 1570 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1571 1572 A circuit is a unique Hamiltonian path in a subgraph of the total 1573 graph. In case a node 'i' is not in the path, then there must be a 1574 loop arc 'i -> i' associated with a true literal. Otherwise 1575 this constraint will fail. 1576 1577 Args: 1578 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1579 literal). The arc is selected in the circuit if the literal is true. 1580 Both source_node and destination_node must be integers between 0 and the 1581 number of nodes - 1. 1582 1583 Returns: 1584 An instance of the `Constraint` class. 1585 1586 Raises: 1587 ValueError: If the list of arcs is empty. 1588 """ 1589 if not arcs: 1590 raise ValueError("add_circuit expects a non-empty array of arcs") 1591 ct = Constraint(self) 1592 model_ct = self.__model.constraints[ct.index] 1593 for arc in arcs: 1594 tail = cmh.assert_is_int32(arc[0]) 1595 head = cmh.assert_is_int32(arc[1]) 1596 lit = self.get_or_make_boolean_index(arc[2]) 1597 model_ct.circuit.tails.append(tail) 1598 model_ct.circuit.heads.append(head) 1599 model_ct.circuit.literals.append(lit) 1600 return ct 1601 1602 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1603 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1604 1605 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1606 literals[i] is true must satisfy this set of properties: 1607 - #incoming arcs == 1 except for node 0. 1608 - #outgoing arcs == 1 except for node 0. 1609 - for node zero, #incoming arcs == #outgoing arcs. 1610 - There are no duplicate arcs. 1611 - Self-arcs are allowed except for node 0. 1612 - There is no cycle in this graph, except through node 0. 1613 1614 Args: 1615 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1616 literal). The arc is selected in the circuit if the literal is true. 1617 Both source_node and destination_node must be integers between 0 and the 1618 number of nodes - 1. 1619 1620 Returns: 1621 An instance of the `Constraint` class. 1622 1623 Raises: 1624 ValueError: If the list of arcs is empty. 1625 """ 1626 if not arcs: 1627 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1628 ct = Constraint(self) 1629 model_ct = self.__model.constraints[ct.index] 1630 for arc in arcs: 1631 tail = cmh.assert_is_int32(arc[0]) 1632 head = cmh.assert_is_int32(arc[1]) 1633 lit = self.get_or_make_boolean_index(arc[2]) 1634 model_ct.routes.tails.append(tail) 1635 model_ct.routes.heads.append(head) 1636 model_ct.routes.literals.append(lit) 1637 return ct 1638 1639 def add_allowed_assignments( 1640 self, 1641 variables: Sequence[VariableT], 1642 tuples_list: Iterable[Sequence[IntegralT]], 1643 ) -> Constraint: 1644 """Adds AllowedAssignments(variables, tuples_list). 1645 1646 An AllowedAssignments constraint is a constraint on an array of variables, 1647 which requires that when all variables are assigned values, the resulting 1648 array equals one of the tuples in `tuple_list`. 1649 1650 Args: 1651 variables: A list of variables. 1652 tuples_list: A list of admissible tuples. Each tuple must have the same 1653 length as the variables, and the ith value of a tuple corresponds to the 1654 ith variable. 1655 1656 Returns: 1657 An instance of the `Constraint` class. 1658 1659 Raises: 1660 TypeError: If a tuple does not have the same size as the list of 1661 variables. 1662 ValueError: If the array of variables is empty. 1663 """ 1664 1665 if not variables: 1666 raise ValueError( 1667 "add_allowed_assignments expects a non-empty variables array" 1668 ) 1669 1670 ct = Constraint(self) 1671 model_ct = self.__model.constraints[ct.index] 1672 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1673 arity = len(variables) 1674 for t in tuples_list: 1675 if len(t) != arity: 1676 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1677 ar = [] 1678 for v in t: 1679 ar.append(cmh.assert_is_int64(v)) 1680 model_ct.table.values.extend(ar) 1681 return ct 1682 1683 def add_forbidden_assignments( 1684 self, 1685 variables: Sequence[VariableT], 1686 tuples_list: Iterable[Sequence[IntegralT]], 1687 ) -> Constraint: 1688 """Adds add_forbidden_assignments(variables, [tuples_list]). 1689 1690 A ForbiddenAssignments constraint is a constraint on an array of variables 1691 where the list of impossible combinations is provided in the tuples list. 1692 1693 Args: 1694 variables: A list of variables. 1695 tuples_list: A list of forbidden tuples. Each tuple must have the same 1696 length as the variables, and the *i*th value of a tuple corresponds to 1697 the *i*th variable. 1698 1699 Returns: 1700 An instance of the `Constraint` class. 1701 1702 Raises: 1703 TypeError: If a tuple does not have the same size as the list of 1704 variables. 1705 ValueError: If the array of variables is empty. 1706 """ 1707 1708 if not variables: 1709 raise ValueError( 1710 "add_forbidden_assignments expects a non-empty variables array" 1711 ) 1712 1713 index = len(self.__model.constraints) 1714 ct = self.add_allowed_assignments(variables, tuples_list) 1715 self.__model.constraints[index].table.negated = True 1716 return ct 1717 1718 def add_automaton( 1719 self, 1720 transition_variables: Sequence[VariableT], 1721 starting_state: IntegralT, 1722 final_states: Sequence[IntegralT], 1723 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1724 ) -> Constraint: 1725 """Adds an automaton constraint. 1726 1727 An automaton constraint takes a list of variables (of size *n*), an initial 1728 state, a set of final states, and a set of transitions. A transition is a 1729 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1730 and *transition* is the label of an arc from *head* to *tail*, 1731 corresponding to the value of one variable in the list of variables. 1732 1733 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1734 contains the possible states of the automaton. The first state contains the 1735 initial state. The last phase contains the final states. 1736 1737 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1738 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1739 an arc from the state *tail* of phase *i* and the state *head* of phase 1740 *i* + 1. This arc is labeled by the value *transition* of the variables 1741 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1742 is assigned the value *transition*. 1743 1744 A feasible solution of this constraint is an assignment of variables such 1745 that, starting from the initial state in phase 0, there is a path labeled by 1746 the values of the variables that ends in one of the final states in the 1747 final phase. 1748 1749 Args: 1750 transition_variables: A non-empty list of variables whose values 1751 correspond to the labels of the arcs traversed by the automaton. 1752 starting_state: The initial state of the automaton. 1753 final_states: A non-empty list of admissible final states. 1754 transition_triples: A list of transitions for the automaton, in the 1755 following format (current_state, variable_value, next_state). 1756 1757 Returns: 1758 An instance of the `Constraint` class. 1759 1760 Raises: 1761 ValueError: if `transition_variables`, `final_states`, or 1762 `transition_triples` are empty. 1763 """ 1764 1765 if not transition_variables: 1766 raise ValueError( 1767 "add_automaton expects a non-empty transition_variables array" 1768 ) 1769 if not final_states: 1770 raise ValueError("add_automaton expects some final states") 1771 1772 if not transition_triples: 1773 raise ValueError("add_automaton expects some transition triples") 1774 1775 ct = Constraint(self) 1776 model_ct = self.__model.constraints[ct.index] 1777 model_ct.automaton.vars.extend( 1778 [self.get_or_make_index(x) for x in transition_variables] 1779 ) 1780 starting_state = cmh.assert_is_int64(starting_state) 1781 model_ct.automaton.starting_state = starting_state 1782 for v in final_states: 1783 v = cmh.assert_is_int64(v) 1784 model_ct.automaton.final_states.append(v) 1785 for t in transition_triples: 1786 if len(t) != 3: 1787 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1788 tail = cmh.assert_is_int64(t[0]) 1789 label = cmh.assert_is_int64(t[1]) 1790 head = cmh.assert_is_int64(t[2]) 1791 model_ct.automaton.transition_tail.append(tail) 1792 model_ct.automaton.transition_label.append(label) 1793 model_ct.automaton.transition_head.append(head) 1794 return ct 1795 1796 def add_inverse( 1797 self, 1798 variables: Sequence[VariableT], 1799 inverse_variables: Sequence[VariableT], 1800 ) -> Constraint: 1801 """Adds Inverse(variables, inverse_variables). 1802 1803 An inverse constraint enforces that if `variables[i]` is assigned a value 1804 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1805 1806 Args: 1807 variables: An array of integer variables. 1808 inverse_variables: An array of integer variables. 1809 1810 Returns: 1811 An instance of the `Constraint` class. 1812 1813 Raises: 1814 TypeError: if variables and inverse_variables have different lengths, or 1815 if they are empty. 1816 """ 1817 1818 if not variables or not inverse_variables: 1819 raise TypeError("The Inverse constraint does not accept empty arrays") 1820 if len(variables) != len(inverse_variables): 1821 raise TypeError( 1822 "In the inverse constraint, the two array variables and" 1823 " inverse_variables must have the same length." 1824 ) 1825 ct = Constraint(self) 1826 model_ct = self.__model.constraints[ct.index] 1827 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1828 model_ct.inverse.f_inverse.extend( 1829 [self.get_or_make_index(x) for x in inverse_variables] 1830 ) 1831 return ct 1832 1833 def add_reservoir_constraint( 1834 self, 1835 times: Iterable[LinearExprT], 1836 level_changes: Iterable[LinearExprT], 1837 min_level: int, 1838 max_level: int, 1839 ) -> Constraint: 1840 """Adds Reservoir(times, level_changes, min_level, max_level). 1841 1842 Maintains a reservoir level within bounds. The water level starts at 0, and 1843 at any time, it must be between min_level and max_level. 1844 1845 If the affine expression `times[i]` is assigned a value t, then the current 1846 level changes by `level_changes[i]`, which is constant, at time t. 1847 1848 Note that min level must be <= 0, and the max level must be >= 0. Please 1849 use fixed level_changes to simulate initial state. 1850 1851 Therefore, at any time: 1852 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1853 1854 Args: 1855 times: A list of 1-var affine expressions (a * x + b) which specify the 1856 time of the filling or emptying the reservoir. 1857 level_changes: A list of integer values that specifies the amount of the 1858 emptying or filling. Currently, variable demands are not supported. 1859 min_level: At any time, the level of the reservoir must be greater or 1860 equal than the min level. 1861 max_level: At any time, the level of the reservoir must be less or equal 1862 than the max level. 1863 1864 Returns: 1865 An instance of the `Constraint` class. 1866 1867 Raises: 1868 ValueError: if max_level < min_level. 1869 1870 ValueError: if max_level < 0. 1871 1872 ValueError: if min_level > 0 1873 """ 1874 1875 if max_level < min_level: 1876 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1877 1878 if max_level < 0: 1879 raise ValueError("Reservoir constraint must have a max_level >= 0") 1880 1881 if min_level > 0: 1882 raise ValueError("Reservoir constraint must have a min_level <= 0") 1883 1884 ct = Constraint(self) 1885 model_ct = self.__model.constraints[ct.index] 1886 model_ct.reservoir.time_exprs.extend( 1887 [self.parse_linear_expression(x) for x in times] 1888 ) 1889 model_ct.reservoir.level_changes.extend( 1890 [self.parse_linear_expression(x) for x in level_changes] 1891 ) 1892 model_ct.reservoir.min_level = min_level 1893 model_ct.reservoir.max_level = max_level 1894 return ct 1895 1896 def add_reservoir_constraint_with_active( 1897 self, 1898 times: Iterable[LinearExprT], 1899 level_changes: Iterable[LinearExprT], 1900 actives: Iterable[LiteralT], 1901 min_level: int, 1902 max_level: int, 1903 ) -> Constraint: 1904 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1905 1906 Maintains a reservoir level within bounds. The water level starts at 0, and 1907 at any time, it must be between min_level and max_level. 1908 1909 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1910 `True`, then the current level changes by `level_changes[i]`, which is 1911 constant, 1912 at time t. 1913 1914 Note that min level must be <= 0, and the max level must be >= 0. Please 1915 use fixed level_changes to simulate initial state. 1916 1917 Therefore, at any time: 1918 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1919 max_level] 1920 1921 1922 The array of boolean variables 'actives', if defined, indicates which 1923 actions are actually performed. 1924 1925 Args: 1926 times: A list of 1-var affine expressions (a * x + b) which specify the 1927 time of the filling or emptying the reservoir. 1928 level_changes: A list of integer values that specifies the amount of the 1929 emptying or filling. Currently, variable demands are not supported. 1930 actives: a list of boolean variables. They indicates if the 1931 emptying/refilling events actually take place. 1932 min_level: At any time, the level of the reservoir must be greater or 1933 equal than the min level. 1934 max_level: At any time, the level of the reservoir must be less or equal 1935 than the max level. 1936 1937 Returns: 1938 An instance of the `Constraint` class. 1939 1940 Raises: 1941 ValueError: if max_level < min_level. 1942 1943 ValueError: if max_level < 0. 1944 1945 ValueError: if min_level > 0 1946 """ 1947 1948 if max_level < min_level: 1949 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1950 1951 if max_level < 0: 1952 raise ValueError("Reservoir constraint must have a max_level >= 0") 1953 1954 if min_level > 0: 1955 raise ValueError("Reservoir constraint must have a min_level <= 0") 1956 1957 ct = Constraint(self) 1958 model_ct = self.__model.constraints[ct.index] 1959 model_ct.reservoir.time_exprs.extend( 1960 [self.parse_linear_expression(x) for x in times] 1961 ) 1962 model_ct.reservoir.level_changes.extend( 1963 [self.parse_linear_expression(x) for x in level_changes] 1964 ) 1965 model_ct.reservoir.active_literals.extend( 1966 [self.get_or_make_boolean_index(x) for x in actives] 1967 ) 1968 model_ct.reservoir.min_level = min_level 1969 model_ct.reservoir.max_level = max_level 1970 return ct 1971 1972 def add_map_domain( 1973 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 1974 ): 1975 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 1976 1977 for i, bool_var in enumerate(bool_var_array): 1978 b_index = bool_var.index 1979 var_index = var.index 1980 model_ct = self.__model.constraints.add() 1981 model_ct.linear.vars.append(var_index) 1982 model_ct.linear.coeffs.append(1) 1983 model_ct.linear.domain.extend([offset + i, offset + i]) 1984 model_ct.enforcement_literal.append(b_index) 1985 1986 model_ct = self.__model.constraints.add() 1987 model_ct.linear.vars.append(var_index) 1988 model_ct.linear.coeffs.append(1) 1989 model_ct.enforcement_literal.append(-b_index - 1) 1990 if offset + i - 1 >= INT_MIN: 1991 model_ct.linear.domain.extend([INT_MIN, offset + i - 1]) 1992 if offset + i + 1 <= INT_MAX: 1993 model_ct.linear.domain.extend([offset + i + 1, INT_MAX]) 1994 1995 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1996 """Adds `a => b` (`a` implies `b`).""" 1997 ct = Constraint(self) 1998 model_ct = self.__model.constraints[ct.index] 1999 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2000 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2001 return ct 2002 2003 @overload 2004 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: 2005 ... 2006 2007 @overload 2008 def add_bool_or(self, *literals: LiteralT) -> Constraint: 2009 ... 2010 2011 def add_bool_or(self, *literals): 2012 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2013 ct = Constraint(self) 2014 model_ct = self.__model.constraints[ct.index] 2015 model_ct.bool_or.literals.extend( 2016 [ 2017 self.get_or_make_boolean_index(x) 2018 for x in expand_generator_or_tuple(literals) 2019 ] 2020 ) 2021 return ct 2022 2023 @overload 2024 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: 2025 ... 2026 2027 @overload 2028 def add_at_least_one(self, *literals: LiteralT) -> Constraint: 2029 ... 2030 2031 def add_at_least_one(self, *literals): 2032 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2033 return self.add_bool_or(*literals) 2034 2035 @overload 2036 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: 2037 ... 2038 2039 @overload 2040 def add_at_most_one(self, *literals: LiteralT) -> Constraint: 2041 ... 2042 2043 def add_at_most_one(self, *literals): 2044 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2045 ct = Constraint(self) 2046 model_ct = self.__model.constraints[ct.index] 2047 model_ct.at_most_one.literals.extend( 2048 [ 2049 self.get_or_make_boolean_index(x) 2050 for x in expand_generator_or_tuple(literals) 2051 ] 2052 ) 2053 return ct 2054 2055 @overload 2056 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: 2057 ... 2058 2059 @overload 2060 def add_exactly_one(self, *literals: LiteralT) -> Constraint: 2061 ... 2062 2063 def add_exactly_one(self, *literals): 2064 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2065 ct = Constraint(self) 2066 model_ct = self.__model.constraints[ct.index] 2067 model_ct.exactly_one.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_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: 2077 ... 2078 2079 @overload 2080 def add_bool_and(self, *literals: LiteralT) -> Constraint: 2081 ... 2082 2083 def add_bool_and(self, *literals): 2084 """Adds `And(literals) == true`.""" 2085 ct = Constraint(self) 2086 model_ct = self.__model.constraints[ct.index] 2087 model_ct.bool_and.literals.extend( 2088 [ 2089 self.get_or_make_boolean_index(x) 2090 for x in expand_generator_or_tuple(literals) 2091 ] 2092 ) 2093 return ct 2094 2095 @overload 2096 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: 2097 ... 2098 2099 @overload 2100 def add_bool_xor(self, *literals: LiteralT) -> Constraint: 2101 ... 2102 2103 def add_bool_xor(self, *literals): 2104 """Adds `XOr(literals) == true`. 2105 2106 In contrast to add_bool_or and add_bool_and, it does not support 2107 .only_enforce_if(). 2108 2109 Args: 2110 *literals: the list of literals in the constraint. 2111 2112 Returns: 2113 An `Constraint` object. 2114 """ 2115 ct = Constraint(self) 2116 model_ct = self.__model.constraints[ct.index] 2117 model_ct.bool_xor.literals.extend( 2118 [ 2119 self.get_or_make_boolean_index(x) 2120 for x in expand_generator_or_tuple(literals) 2121 ] 2122 ) 2123 return ct 2124 2125 def add_min_equality( 2126 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2127 ) -> Constraint: 2128 """Adds `target == Min(exprs)`.""" 2129 ct = Constraint(self) 2130 model_ct = self.__model.constraints[ct.index] 2131 model_ct.lin_max.exprs.extend( 2132 [self.parse_linear_expression(x, True) for x in exprs] 2133 ) 2134 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2135 return ct 2136 2137 def add_max_equality( 2138 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2139 ) -> Constraint: 2140 """Adds `target == Max(exprs)`.""" 2141 ct = Constraint(self) 2142 model_ct = self.__model.constraints[ct.index] 2143 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2144 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2145 return ct 2146 2147 def add_division_equality( 2148 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2149 ) -> Constraint: 2150 """Adds `target == num // denom` (integer division rounded towards 0).""" 2151 ct = Constraint(self) 2152 model_ct = self.__model.constraints[ct.index] 2153 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2154 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2155 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2156 return ct 2157 2158 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2159 """Adds `target == Abs(expr)`.""" 2160 ct = Constraint(self) 2161 model_ct = self.__model.constraints[ct.index] 2162 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2163 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2164 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2165 return ct 2166 2167 def add_modulo_equality( 2168 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2169 ) -> Constraint: 2170 """Adds `target = expr % mod`.""" 2171 ct = Constraint(self) 2172 model_ct = self.__model.constraints[ct.index] 2173 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2174 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2175 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2176 return ct 2177 2178 def add_multiplication_equality( 2179 self, 2180 target: LinearExprT, 2181 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2182 ) -> Constraint: 2183 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2184 ct = Constraint(self) 2185 model_ct = self.__model.constraints[ct.index] 2186 model_ct.int_prod.exprs.extend( 2187 [ 2188 self.parse_linear_expression(expr) 2189 for expr in expand_generator_or_tuple(expressions) 2190 ] 2191 ) 2192 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2193 return ct 2194 2195 # Scheduling support 2196 2197 def new_interval_var( 2198 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2199 ) -> IntervalVar: 2200 """Creates an interval variable from start, size, and end. 2201 2202 An interval variable is a constraint, that is itself used in other 2203 constraints like NoOverlap. 2204 2205 Internally, it ensures that `start + size == end`. 2206 2207 Args: 2208 start: The start of the interval. It must be of the form a * var + b. 2209 size: The size of the interval. It must be of the form a * var + b. 2210 end: The end of the interval. It must be of the form a * var + b. 2211 name: The name of the interval variable. 2212 2213 Returns: 2214 An `IntervalVar` object. 2215 """ 2216 2217 lin = self.add(start + size == end) 2218 if name: 2219 lin.with_name("lin_" + name) 2220 2221 start_expr = self.parse_linear_expression(start) 2222 size_expr = self.parse_linear_expression(size) 2223 end_expr = self.parse_linear_expression(end) 2224 if len(start_expr.vars) > 1: 2225 raise TypeError( 2226 "cp_model.new_interval_var: start must be 1-var affine or constant." 2227 ) 2228 if len(size_expr.vars) > 1: 2229 raise TypeError( 2230 "cp_model.new_interval_var: size must be 1-var affine or constant." 2231 ) 2232 if len(end_expr.vars) > 1: 2233 raise TypeError( 2234 "cp_model.new_interval_var: end must be 1-var affine or constant." 2235 ) 2236 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2237 2238 def new_interval_var_series( 2239 self, 2240 name: str, 2241 index: pd.Index, 2242 starts: Union[LinearExprT, pd.Series], 2243 sizes: Union[LinearExprT, pd.Series], 2244 ends: Union[LinearExprT, pd.Series], 2245 ) -> pd.Series: 2246 """Creates a series of interval variables with the given name. 2247 2248 Args: 2249 name (str): Required. The name of the variable set. 2250 index (pd.Index): Required. The index to use for the variable set. 2251 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2252 set. If a `pd.Series` is passed in, it will be based on the 2253 corresponding values of the pd.Series. 2254 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2255 set. If a `pd.Series` is passed in, it will be based on the 2256 corresponding values of the pd.Series. 2257 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2258 set. If a `pd.Series` is passed in, it will be based on the 2259 corresponding values of the pd.Series. 2260 2261 Returns: 2262 pd.Series: The interval variable set indexed by its corresponding 2263 dimensions. 2264 2265 Raises: 2266 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2267 ValueError: if the `name` is not a valid identifier or already exists. 2268 ValueError: if the all the indexes do not match. 2269 """ 2270 if not isinstance(index, pd.Index): 2271 raise TypeError("Non-index object is used as index") 2272 if not name.isidentifier(): 2273 raise ValueError("name={} is not a valid identifier".format(name)) 2274 2275 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2276 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2277 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2278 interval_array = [] 2279 for i in index: 2280 interval_array.append( 2281 self.new_interval_var( 2282 start=starts[i], 2283 size=sizes[i], 2284 end=ends[i], 2285 name=f"{name}[{i}]", 2286 ) 2287 ) 2288 return pd.Series(index=index, data=interval_array) 2289 2290 def new_fixed_size_interval_var( 2291 self, start: LinearExprT, size: IntegralT, name: str 2292 ) -> IntervalVar: 2293 """Creates an interval variable from start, and a fixed size. 2294 2295 An interval variable is a constraint, that is itself used in other 2296 constraints like NoOverlap. 2297 2298 Args: 2299 start: The start of the interval. It must be of the form a * var + b. 2300 size: The size of the interval. It must be an integer value. 2301 name: The name of the interval variable. 2302 2303 Returns: 2304 An `IntervalVar` object. 2305 """ 2306 size = cmh.assert_is_int64(size) 2307 start_expr = self.parse_linear_expression(start) 2308 size_expr = self.parse_linear_expression(size) 2309 end_expr = self.parse_linear_expression(start + size) 2310 if len(start_expr.vars) > 1: 2311 raise TypeError( 2312 "cp_model.new_interval_var: start must be affine or constant." 2313 ) 2314 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2315 2316 def new_fixed_size_interval_var_series( 2317 self, 2318 name: str, 2319 index: pd.Index, 2320 starts: Union[LinearExprT, pd.Series], 2321 sizes: Union[IntegralT, pd.Series], 2322 ) -> pd.Series: 2323 """Creates a series of interval variables with the given name. 2324 2325 Args: 2326 name (str): Required. The name of the variable set. 2327 index (pd.Index): Required. The index to use for the variable set. 2328 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2329 set. If a `pd.Series` is passed in, it will be based on the 2330 corresponding values of the pd.Series. 2331 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2332 the set. If a `pd.Series` is passed in, it will be based on the 2333 corresponding values of the pd.Series. 2334 2335 Returns: 2336 pd.Series: The interval variable set indexed by its corresponding 2337 dimensions. 2338 2339 Raises: 2340 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2341 ValueError: if the `name` is not a valid identifier or already exists. 2342 ValueError: if the all the indexes do not match. 2343 """ 2344 if not isinstance(index, pd.Index): 2345 raise TypeError("Non-index object is used as index") 2346 if not name.isidentifier(): 2347 raise ValueError("name={} is not a valid identifier".format(name)) 2348 2349 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2350 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2351 interval_array = [] 2352 for i in index: 2353 interval_array.append( 2354 self.new_fixed_size_interval_var( 2355 start=starts[i], 2356 size=sizes[i], 2357 name=f"{name}[{i}]", 2358 ) 2359 ) 2360 return pd.Series(index=index, data=interval_array) 2361 2362 def new_optional_interval_var( 2363 self, 2364 start: LinearExprT, 2365 size: LinearExprT, 2366 end: LinearExprT, 2367 is_present: LiteralT, 2368 name: str, 2369 ) -> IntervalVar: 2370 """Creates an optional interval var from start, size, end, and is_present. 2371 2372 An optional interval variable is a constraint, that is itself used in other 2373 constraints like NoOverlap. This constraint is protected by a presence 2374 literal that indicates if it is active or not. 2375 2376 Internally, it ensures that `is_present` implies `start + size == 2377 end`. 2378 2379 Args: 2380 start: The start of the interval. It must be of the form a * var + b. 2381 size: The size of the interval. It must be of the form a * var + b. 2382 end: The end of the interval. It must be of the form a * var + b. 2383 is_present: A literal that indicates if the interval is active or not. A 2384 inactive interval is simply ignored by all constraints. 2385 name: The name of the interval variable. 2386 2387 Returns: 2388 An `IntervalVar` object. 2389 """ 2390 2391 # add the linear constraint. 2392 lin = self.add(start + size == end).only_enforce_if(is_present) 2393 if name: 2394 lin.with_name("lin_opt_" + name) 2395 2396 # Creates the IntervalConstraintProto object. 2397 is_present_index = self.get_or_make_boolean_index(is_present) 2398 start_expr = self.parse_linear_expression(start) 2399 size_expr = self.parse_linear_expression(size) 2400 end_expr = self.parse_linear_expression(end) 2401 if len(start_expr.vars) > 1: 2402 raise TypeError( 2403 "cp_model.new_interval_var: start must be affine or constant." 2404 ) 2405 if len(size_expr.vars) > 1: 2406 raise TypeError( 2407 "cp_model.new_interval_var: size must be affine or constant." 2408 ) 2409 if len(end_expr.vars) > 1: 2410 raise TypeError( 2411 "cp_model.new_interval_var: end must be affine or constant." 2412 ) 2413 return IntervalVar( 2414 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2415 ) 2416 2417 def new_optional_interval_var_series( 2418 self, 2419 name: str, 2420 index: pd.Index, 2421 starts: Union[LinearExprT, pd.Series], 2422 sizes: Union[LinearExprT, pd.Series], 2423 ends: Union[LinearExprT, pd.Series], 2424 are_present: Union[LiteralT, pd.Series], 2425 ) -> pd.Series: 2426 """Creates a series of interval variables with the given name. 2427 2428 Args: 2429 name (str): Required. The name of the variable set. 2430 index (pd.Index): Required. The index to use for the variable set. 2431 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2432 set. If a `pd.Series` is passed in, it will be based on the 2433 corresponding values of the pd.Series. 2434 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2435 set. If a `pd.Series` is passed in, it will be based on the 2436 corresponding values of the pd.Series. 2437 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2438 set. If a `pd.Series` is passed in, it will be based on the 2439 corresponding values of the pd.Series. 2440 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2441 interval in the set. If a `pd.Series` is passed in, it will be based on 2442 the corresponding values of the pd.Series. 2443 2444 Returns: 2445 pd.Series: The interval variable set indexed by its corresponding 2446 dimensions. 2447 2448 Raises: 2449 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2450 ValueError: if the `name` is not a valid identifier or already exists. 2451 ValueError: if the all the indexes do not match. 2452 """ 2453 if not isinstance(index, pd.Index): 2454 raise TypeError("Non-index object is used as index") 2455 if not name.isidentifier(): 2456 raise ValueError("name={} is not a valid identifier".format(name)) 2457 2458 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2459 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2460 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2461 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2462 2463 interval_array = [] 2464 for i in index: 2465 interval_array.append( 2466 self.new_optional_interval_var( 2467 start=starts[i], 2468 size=sizes[i], 2469 end=ends[i], 2470 is_present=are_present[i], 2471 name=f"{name}[{i}]", 2472 ) 2473 ) 2474 return pd.Series(index=index, data=interval_array) 2475 2476 def new_optional_fixed_size_interval_var( 2477 self, 2478 start: LinearExprT, 2479 size: IntegralT, 2480 is_present: LiteralT, 2481 name: str, 2482 ) -> IntervalVar: 2483 """Creates an interval variable from start, and a fixed size. 2484 2485 An interval variable is a constraint, that is itself used in other 2486 constraints like NoOverlap. 2487 2488 Args: 2489 start: The start of the interval. It must be of the form a * var + b. 2490 size: The size of the interval. It must be an integer value. 2491 is_present: A literal that indicates if the interval is active or not. A 2492 inactive interval is simply ignored by all constraints. 2493 name: The name of the interval variable. 2494 2495 Returns: 2496 An `IntervalVar` object. 2497 """ 2498 size = cmh.assert_is_int64(size) 2499 start_expr = self.parse_linear_expression(start) 2500 size_expr = self.parse_linear_expression(size) 2501 end_expr = self.parse_linear_expression(start + size) 2502 if len(start_expr.vars) > 1: 2503 raise TypeError( 2504 "cp_model.new_interval_var: start must be affine or constant." 2505 ) 2506 is_present_index = self.get_or_make_boolean_index(is_present) 2507 return IntervalVar( 2508 self.__model, 2509 start_expr, 2510 size_expr, 2511 end_expr, 2512 is_present_index, 2513 name, 2514 ) 2515 2516 def new_optional_fixed_size_interval_var_series( 2517 self, 2518 name: str, 2519 index: pd.Index, 2520 starts: Union[LinearExprT, pd.Series], 2521 sizes: Union[IntegralT, pd.Series], 2522 are_present: Union[LiteralT, pd.Series], 2523 ) -> pd.Series: 2524 """Creates a series of interval variables with the given name. 2525 2526 Args: 2527 name (str): Required. The name of the variable set. 2528 index (pd.Index): Required. The index to use for the variable set. 2529 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2530 set. If a `pd.Series` is passed in, it will be based on the 2531 corresponding values of the pd.Series. 2532 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2533 the set. If a `pd.Series` is passed in, it will be based on the 2534 corresponding values of the pd.Series. 2535 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2536 interval in the set. If a `pd.Series` is passed in, it will be based on 2537 the corresponding values of the pd.Series. 2538 2539 Returns: 2540 pd.Series: The interval variable set indexed by its corresponding 2541 dimensions. 2542 2543 Raises: 2544 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2545 ValueError: if the `name` is not a valid identifier or already exists. 2546 ValueError: if the all the indexes do not match. 2547 """ 2548 if not isinstance(index, pd.Index): 2549 raise TypeError("Non-index object is used as index") 2550 if not name.isidentifier(): 2551 raise ValueError("name={} is not a valid identifier".format(name)) 2552 2553 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2554 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2555 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2556 interval_array = [] 2557 for i in index: 2558 interval_array.append( 2559 self.new_optional_fixed_size_interval_var( 2560 start=starts[i], 2561 size=sizes[i], 2562 is_present=are_present[i], 2563 name=f"{name}[{i}]", 2564 ) 2565 ) 2566 return pd.Series(index=index, data=interval_array) 2567 2568 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2569 """Adds NoOverlap(interval_vars). 2570 2571 A NoOverlap constraint ensures that all present intervals do not overlap 2572 in time. 2573 2574 Args: 2575 interval_vars: The list of interval variables to constrain. 2576 2577 Returns: 2578 An instance of the `Constraint` class. 2579 """ 2580 ct = Constraint(self) 2581 model_ct = self.__model.constraints[ct.index] 2582 model_ct.no_overlap.intervals.extend( 2583 [self.get_interval_index(x) for x in interval_vars] 2584 ) 2585 return ct 2586 2587 def add_no_overlap_2d( 2588 self, 2589 x_intervals: Iterable[IntervalVar], 2590 y_intervals: Iterable[IntervalVar], 2591 ) -> Constraint: 2592 """Adds NoOverlap2D(x_intervals, y_intervals). 2593 2594 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2595 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2596 by two intervals which represent its projection onto the X and Y axis. 2597 2598 Furthermore, one box is optional if at least one of the x or y interval is 2599 optional. 2600 2601 Args: 2602 x_intervals: The X coordinates of the rectangles. 2603 y_intervals: The Y coordinates of the rectangles. 2604 2605 Returns: 2606 An instance of the `Constraint` class. 2607 """ 2608 ct = Constraint(self) 2609 model_ct = self.__model.constraints[ct.index] 2610 model_ct.no_overlap_2d.x_intervals.extend( 2611 [self.get_interval_index(x) for x in x_intervals] 2612 ) 2613 model_ct.no_overlap_2d.y_intervals.extend( 2614 [self.get_interval_index(x) for x in y_intervals] 2615 ) 2616 return ct 2617 2618 def add_cumulative( 2619 self, 2620 intervals: Iterable[IntervalVar], 2621 demands: Iterable[LinearExprT], 2622 capacity: LinearExprT, 2623 ) -> Constraint: 2624 """Adds Cumulative(intervals, demands, capacity). 2625 2626 This constraint enforces that: 2627 2628 for all t: 2629 sum(demands[i] 2630 if (start(intervals[i]) <= t < end(intervals[i])) and 2631 (intervals[i] is present)) <= capacity 2632 2633 Args: 2634 intervals: The list of intervals. 2635 demands: The list of demands for each interval. Each demand must be >= 0. 2636 Each demand can be a 1-var affine expression (a * x + b). 2637 capacity: The maximum capacity of the cumulative constraint. It can be a 2638 1-var affine expression (a * x + b). 2639 2640 Returns: 2641 An instance of the `Constraint` class. 2642 """ 2643 cumulative = Constraint(self) 2644 model_ct = self.__model.constraints[cumulative.index] 2645 model_ct.cumulative.intervals.extend( 2646 [self.get_interval_index(x) for x in intervals] 2647 ) 2648 for d in demands: 2649 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2650 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2651 return cumulative 2652 2653 # Support for model cloning. 2654 def clone(self) -> "CpModel": 2655 """Reset the model, and creates a new one from a CpModelProto instance.""" 2656 clone = CpModel() 2657 clone.proto.CopyFrom(self.proto) 2658 clone.rebuild_constant_map() 2659 return clone 2660 2661 def rebuild_constant_map(self): 2662 """Internal method used during model cloning.""" 2663 for i, var in enumerate(self.__model.variables): 2664 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2665 self.__constant_map[var.domain[0]] = i 2666 2667 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2668 """Returns an already created Boolean variable from its index.""" 2669 if index < 0 or index >= len(self.__model.variables): 2670 raise ValueError( 2671 f"get_bool_var_from_proto_index: out of bound index {index}" 2672 ) 2673 var = self.__model.variables[index] 2674 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2675 raise ValueError( 2676 f"get_bool_var_from_proto_index: index {index} does not reference" 2677 + " a Boolean variable" 2678 ) 2679 2680 return IntVar(self.__model, index, None) 2681 2682 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2683 """Returns an already created integer variable from its index.""" 2684 if index < 0 or index >= len(self.__model.variables): 2685 raise ValueError( 2686 f"get_int_var_from_proto_index: out of bound index {index}" 2687 ) 2688 return IntVar(self.__model, index, None) 2689 2690 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2691 """Returns an already created interval variable from its index.""" 2692 if index < 0 or index >= len(self.__model.constraints): 2693 raise ValueError( 2694 f"get_interval_var_from_proto_index: out of bound index {index}" 2695 ) 2696 ct = self.__model.constraints[index] 2697 if not ct.HasField("interval"): 2698 raise ValueError( 2699 f"get_interval_var_from_proto_index: index {index} does not" 2700 " reference an" + " interval variable" 2701 ) 2702 2703 return IntervalVar(self.__model, index, None, None, None, None) 2704 2705 # Helpers. 2706 2707 def __str__(self): 2708 return str(self.__model) 2709 2710 @property 2711 def proto(self) -> cp_model_pb2.CpModelProto: 2712 """Returns the underlying CpModelProto.""" 2713 return self.__model 2714 2715 def negated(self, index: int) -> int: 2716 return -index - 1 2717 2718 def get_or_make_index(self, arg: VariableT) -> int: 2719 """Returns the index of a variable, its negation, or a number.""" 2720 if isinstance(arg, IntVar): 2721 return arg.index 2722 if ( 2723 isinstance(arg, _ProductCst) 2724 and isinstance(arg.expression(), IntVar) 2725 and arg.coefficient() == -1 2726 ): 2727 return -arg.expression().index - 1 2728 if isinstance(arg, numbers.Integral): 2729 arg = cmh.assert_is_int64(arg) 2730 return self.get_or_make_index_from_constant(arg) 2731 raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")") 2732 2733 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2734 """Returns an index from a boolean expression.""" 2735 if isinstance(arg, IntVar): 2736 self.assert_is_boolean_variable(arg) 2737 return arg.index 2738 if isinstance(arg, _NotBooleanVariable): 2739 self.assert_is_boolean_variable(arg.negated()) 2740 return arg.index 2741 if isinstance(arg, numbers.Integral): 2742 arg = cmh.assert_is_zero_or_one(arg) 2743 return self.get_or_make_index_from_constant(arg) 2744 if cmh.is_boolean(arg): 2745 return self.get_or_make_index_from_constant(int(arg)) 2746 raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})") 2747 2748 def get_interval_index(self, arg: IntervalVar) -> int: 2749 if not isinstance(arg, IntervalVar): 2750 raise TypeError("NotSupported: model.get_interval_index(%s)" % arg) 2751 return arg.index 2752 2753 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2754 if value in self.__constant_map: 2755 return self.__constant_map[value] 2756 index = len(self.__model.variables) 2757 self.__model.variables.add(domain=[value, value]) 2758 self.__constant_map[value] = index 2759 return index 2760 2761 def var_index_to_var_proto( 2762 self, var_index: int 2763 ) -> cp_model_pb2.IntegerVariableProto: 2764 if var_index >= 0: 2765 return self.__model.variables[var_index] 2766 else: 2767 return self.__model.variables[-var_index - 1] 2768 2769 def parse_linear_expression( 2770 self, linear_expr: LinearExprT, negate: bool = False 2771 ) -> cp_model_pb2.LinearExpressionProto: 2772 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2773 result: cp_model_pb2.LinearExpressionProto = ( 2774 cp_model_pb2.LinearExpressionProto() 2775 ) 2776 mult = -1 if negate else 1 2777 if isinstance(linear_expr, numbers.Integral): 2778 result.offset = int(linear_expr) * mult 2779 return result 2780 2781 if isinstance(linear_expr, IntVar): 2782 result.vars.append(self.get_or_make_index(linear_expr)) 2783 result.coeffs.append(mult) 2784 return result 2785 2786 coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map() 2787 result.offset = constant * mult 2788 for t in coeffs_map.items(): 2789 if not isinstance(t[0], IntVar): 2790 raise TypeError("Wrong argument" + str(t)) 2791 c = cmh.assert_is_int64(t[1]) 2792 result.vars.append(t[0].index) 2793 result.coeffs.append(c * mult) 2794 return result 2795 2796 def _set_objective(self, obj: ObjLinearExprT, minimize: bool): 2797 """Sets the objective of the model.""" 2798 self.clear_objective() 2799 if isinstance(obj, IntVar): 2800 self.__model.objective.coeffs.append(1) 2801 self.__model.objective.offset = 0 2802 if minimize: 2803 self.__model.objective.vars.append(obj.index) 2804 self.__model.objective.scaling_factor = 1 2805 else: 2806 self.__model.objective.vars.append(self.negated(obj.index)) 2807 self.__model.objective.scaling_factor = -1 2808 elif isinstance(obj, LinearExpr): 2809 coeffs_map, constant, is_integer = obj.get_float_var_value_map() 2810 if is_integer: 2811 if minimize: 2812 self.__model.objective.scaling_factor = 1 2813 self.__model.objective.offset = constant 2814 else: 2815 self.__model.objective.scaling_factor = -1 2816 self.__model.objective.offset = -constant 2817 for v, c in coeffs_map.items(): 2818 self.__model.objective.coeffs.append(c) 2819 if minimize: 2820 self.__model.objective.vars.append(v.index) 2821 else: 2822 self.__model.objective.vars.append(self.negated(v.index)) 2823 else: 2824 self.__model.floating_point_objective.maximize = not minimize 2825 self.__model.floating_point_objective.offset = constant 2826 for v, c in coeffs_map.items(): 2827 self.__model.floating_point_objective.coeffs.append(c) 2828 self.__model.floating_point_objective.vars.append(v.index) 2829 elif isinstance(obj, numbers.Integral): 2830 self.__model.objective.offset = int(obj) 2831 self.__model.objective.scaling_factor = 1 2832 else: 2833 raise TypeError("TypeError: " + str(obj) + " is not a valid objective") 2834 2835 def minimize(self, obj: ObjLinearExprT): 2836 """Sets the objective of the model to minimize(obj).""" 2837 self._set_objective(obj, minimize=True) 2838 2839 def maximize(self, obj: ObjLinearExprT): 2840 """Sets the objective of the model to maximize(obj).""" 2841 self._set_objective(obj, minimize=False) 2842 2843 def has_objective(self) -> bool: 2844 return self.__model.HasField("objective") or self.__model.HasField( 2845 "floating_point_objective" 2846 ) 2847 2848 def clear_objective(self): 2849 self.__model.ClearField("objective") 2850 self.__model.ClearField("floating_point_objective") 2851 2852 def add_decision_strategy( 2853 self, 2854 variables: Sequence[IntVar], 2855 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2856 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2857 ) -> None: 2858 """Adds a search strategy to the model. 2859 2860 Args: 2861 variables: a list of variables this strategy will assign. 2862 var_strategy: heuristic to choose the next variable to assign. 2863 domain_strategy: heuristic to reduce the domain of the selected variable. 2864 Currently, this is advanced code: the union of all strategies added to 2865 the model must be complete, i.e. instantiates all variables. Otherwise, 2866 solve() will fail. 2867 """ 2868 2869 strategy = self.__model.search_strategy.add() 2870 for v in variables: 2871 expr = strategy.exprs.add() 2872 if v.index >= 0: 2873 expr.vars.append(v.index) 2874 expr.coeffs.append(1) 2875 else: 2876 expr.vars.append(self.negated(v.index)) 2877 expr.coeffs.append(-1) 2878 expr.offset = 1 2879 2880 strategy.variable_selection_strategy = var_strategy 2881 strategy.domain_reduction_strategy = domain_strategy 2882 2883 def model_stats(self) -> str: 2884 """Returns a string containing some model statistics.""" 2885 return swig_helper.CpSatHelper.model_stats(self.__model) 2886 2887 def validate(self) -> str: 2888 """Returns a string indicating that the model is invalid.""" 2889 return swig_helper.CpSatHelper.validate_model(self.__model) 2890 2891 def export_to_file(self, file: str) -> bool: 2892 """Write the model as a protocol buffer to 'file'. 2893 2894 Args: 2895 file: file to write the model to. If the filename ends with 'txt', the 2896 model will be written as a text file, otherwise, the binary format will 2897 be used. 2898 2899 Returns: 2900 True if the model was correctly written. 2901 """ 2902 return swig_helper.CpSatHelper.write_model_to_file(self.__model, file) 2903 2904 def add_hint(self, var: IntVar, value: int) -> None: 2905 """Adds 'var == value' as a hint to the solver.""" 2906 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2907 self.__model.solution_hint.values.append(value) 2908 2909 def clear_hints(self): 2910 """Removes any solution hint from the model.""" 2911 self.__model.ClearField("solution_hint") 2912 2913 def add_assumption(self, lit: LiteralT) -> None: 2914 """Adds the literal to the model as assumptions.""" 2915 self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) 2916 2917 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2918 """Adds the literals to the model as assumptions.""" 2919 for lit in literals: 2920 self.add_assumption(lit) 2921 2922 def clear_assumptions(self) -> None: 2923 """Removes all assumptions from the model.""" 2924 self.__model.ClearField("assumptions") 2925 2926 # Helpers. 2927 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2928 if isinstance(x, IntVar): 2929 var = self.__model.variables[x.index] 2930 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2931 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2932 elif not isinstance(x, _NotBooleanVariable): 2933 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2934 2935 # Compatibility with pre PEP8 2936 # pylint: disable=invalid-name 2937 2938 def Name(self) -> str: 2939 return self.name 2940 2941 def SetName(self, name: str) -> None: 2942 self.name = name 2943 2944 def Proto(self) -> cp_model_pb2.CpModelProto: 2945 return self.proto 2946 2947 NewIntVar = new_int_var 2948 NewIntVarFromDomain = new_int_var_from_domain 2949 NewBoolVar = new_bool_var 2950 NewConstant = new_constant 2951 NewIntVarSeries = new_int_var_series 2952 NewBoolVarSeries = new_bool_var_series 2953 AddLinearConstraint = add_linear_constraint 2954 AddLinearExpressionInDomain = add_linear_expression_in_domain 2955 Add = add 2956 AddAllDifferent = add_all_different 2957 AddElement = add_element 2958 AddCircuit = add_circuit 2959 AddMultipleCircuit = add_multiple_circuit 2960 AddAllowedAssignments = add_allowed_assignments 2961 AddForbiddenAssignments = add_forbidden_assignments 2962 AddAutomaton = add_automaton 2963 AddInverse = add_inverse 2964 AddReservoirConstraint = add_reservoir_constraint 2965 AddImplication = add_implication 2966 AddBoolOr = add_bool_or 2967 AddAtLeastOne = add_at_least_one 2968 AddAtMostOne = add_at_most_one 2969 AddExactlyOne = add_exactly_one 2970 AddBoolAnd = add_bool_and 2971 AddBoolXOr = add_bool_xor 2972 AddMinEquality = add_min_equality 2973 AddMaxEquality = add_max_equality 2974 AddDivisionEquality = add_division_equality 2975 AddAbsEquality = add_abs_equality 2976 AddModuloEquality = add_modulo_equality 2977 AddMultiplicationEquality = add_multiplication_equality 2978 NewIntervalVar = new_interval_var 2979 NewIntervalVarSeries = new_interval_var_series 2980 NewFixedSizedIntervalVar = new_fixed_size_interval_var 2981 NewOptionalIntervalVar = new_optional_interval_var 2982 NewOptionalIntervalVarSeries = new_optional_interval_var_series 2983 NewOptionalFixedSizedIntervalVar = new_optional_fixed_size_interval_var 2984 NewOptionalFixedSizedIntervalVarSeries = new_optional_fixed_size_interval_var_series 2985 AddNoOverlap = add_no_overlap 2986 AddNoOverlap2D = add_no_overlap_2d 2987 AddCumulative = add_cumulative 2988 Clone = clone 2989 GetBoolVarFromProtoIndex = get_bool_var_from_proto_index 2990 GetIntVarFromProtoIndex = get_int_var_from_proto_index 2991 GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index 2992 Minimize = minimize 2993 Maximize = maximize 2994 HasObjective = has_objective 2995 ClearObjective = clear_objective 2996 AddDecisionStrategy = add_decision_strategy 2997 ModelStats = model_stats 2998 Validate = validate 2999 ExportToFile = export_to_file 3000 AddHint = add_hint 3001 ClearHints = clear_hints 3002 AddAssumption = add_assumption 3003 AddAssumptions = add_assumptions 3004 ClearAssumptions = clear_assumptions 3005 3006 # pylint: enable=invalid-name 3007 3008 3009@overload 3010def expand_generator_or_tuple( 3011 args: Union[Tuple[LiteralT, ...], Iterable[LiteralT]] 3012) -> Union[Iterable[LiteralT], LiteralT]: 3013 ... 3014 3015 3016@overload 3017def expand_generator_or_tuple( 3018 args: Union[Tuple[LinearExprT, ...], Iterable[LinearExprT]] 3019) -> Union[Iterable[LinearExprT], LinearExprT]: 3020 ... 3021 3022 3023def expand_generator_or_tuple(args): 3024 if hasattr(args, "__len__"): # Tuple 3025 if len(args) != 1: 3026 return args 3027 if isinstance(args[0], (numbers.Number, LinearExpr)): 3028 return args 3029 # Generator 3030 return args[0] 3031 3032 3033def evaluate_linear_expr( 3034 expression: LinearExprT, solution: cp_model_pb2.CpSolverResponse 3035) -> int: 3036 """Evaluate a linear expression against a solution.""" 3037 if isinstance(expression, numbers.Integral): 3038 return int(expression) 3039 if not isinstance(expression, LinearExpr): 3040 raise TypeError("Cannot interpret %s as a linear expression." % expression) 3041 3042 value = 0 3043 to_process = [(expression, 1)] 3044 while to_process: 3045 expr, coeff = to_process.pop() 3046 if isinstance(expr, numbers.Integral): 3047 value += int(expr) * coeff 3048 elif isinstance(expr, _ProductCst): 3049 to_process.append((expr.expression(), coeff * expr.coefficient())) 3050 elif isinstance(expr, _Sum): 3051 to_process.append((expr.left(), coeff)) 3052 to_process.append((expr.right(), coeff)) 3053 elif isinstance(expr, _SumArray): 3054 for e in expr.expressions(): 3055 to_process.append((e, coeff)) 3056 value += expr.constant() * coeff 3057 elif isinstance(expr, _WeightedSum): 3058 for e, c in zip(expr.expressions(), expr.coefficients()): 3059 to_process.append((e, coeff * c)) 3060 value += expr.constant() * coeff 3061 elif isinstance(expr, IntVar): 3062 value += coeff * solution.solution[expr.index] 3063 elif isinstance(expr, _NotBooleanVariable): 3064 value += coeff * (1 - solution.solution[expr.negated().index]) 3065 else: 3066 raise TypeError(f"Cannot interpret {expr} as a linear expression.") 3067 3068 return value 3069 3070 3071def evaluate_boolean_expression( 3072 literal: LiteralT, solution: cp_model_pb2.CpSolverResponse 3073) -> bool: 3074 """Evaluate a boolean expression against a solution.""" 3075 if isinstance(literal, numbers.Integral): 3076 return bool(literal) 3077 elif isinstance(literal, IntVar) or isinstance(literal, _NotBooleanVariable): 3078 index: int = cast(Union[IntVar, _NotBooleanVariable], literal).index 3079 if index >= 0: 3080 return bool(solution.solution[index]) 3081 else: 3082 return not solution.solution[-index - 1] 3083 else: 3084 raise TypeError(f"Cannot interpret {literal} as a boolean expression.") 3085 3086 3087class CpSolver: 3088 """Main solver class. 3089 3090 The purpose of this class is to search for a solution to the model provided 3091 to the solve() method. 3092 3093 Once solve() is called, this class allows inspecting the solution found 3094 with the value() and boolean_value() methods, as well as general statistics 3095 about the solve procedure. 3096 """ 3097 3098 def __init__(self): 3099 self.__solution: Optional[cp_model_pb2.CpSolverResponse] = None 3100 self.parameters: sat_parameters_pb2.SatParameters = ( 3101 sat_parameters_pb2.SatParameters() 3102 ) 3103 self.log_callback: Optional[swig_helper.LogCallback] = None 3104 self.__solve_wrapper: Optional[swig_helper.SolveWrapper] = None 3105 self.__lock: threading.Lock = threading.Lock() 3106 3107 def solve( 3108 self, 3109 model: CpModel, 3110 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3111 ) -> cp_model_pb2.CpSolverStatus: 3112 """Solves a problem and passes each solution to the callback if not null.""" 3113 with self.__lock: 3114 self.__solve_wrapper = swig_helper.SolveWrapper() 3115 3116 self.__solve_wrapper.set_parameters(self.parameters) 3117 if solution_callback is not None: 3118 self.__solve_wrapper.add_solution_callback(solution_callback) 3119 3120 if self.log_callback is not None: 3121 self.__solve_wrapper.add_log_callback(self.log_callback) 3122 3123 self.__solution = self.__solve_wrapper.solve(model.proto) 3124 3125 if solution_callback is not None: 3126 self.__solve_wrapper.clear_solution_callback(solution_callback) 3127 3128 with self.__lock: 3129 self.__solve_wrapper = None 3130 3131 return self.__solution.status 3132 3133 def stop_search(self) -> None: 3134 """Stops the current search asynchronously.""" 3135 with self.__lock: 3136 if self.__solve_wrapper: 3137 self.__solve_wrapper.stop_search() 3138 3139 def value(self, expression: LinearExprT) -> int: 3140 """Returns the value of a linear expression after solve.""" 3141 return evaluate_linear_expr(expression, self._solution) 3142 3143 def values(self, variables: _IndexOrSeries) -> pd.Series: 3144 """Returns the values of the input variables. 3145 3146 If `variables` is a `pd.Index`, then the output will be indexed by the 3147 variables. If `variables` is a `pd.Series` indexed by the underlying 3148 dimensions, then the output will be indexed by the same underlying 3149 dimensions. 3150 3151 Args: 3152 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3153 get the values. 3154 3155 Returns: 3156 pd.Series: The values of all variables in the set. 3157 """ 3158 solution = self._solution 3159 return _attribute_series( 3160 func=lambda v: solution.solution[v.index], 3161 values=variables, 3162 ) 3163 3164 def boolean_value(self, literal: LiteralT) -> bool: 3165 """Returns the boolean value of a literal after solve.""" 3166 return evaluate_boolean_expression(literal, self._solution) 3167 3168 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 3169 """Returns the values of the input variables. 3170 3171 If `variables` is a `pd.Index`, then the output will be indexed by the 3172 variables. If `variables` is a `pd.Series` indexed by the underlying 3173 dimensions, then the output will be indexed by the same underlying 3174 dimensions. 3175 3176 Args: 3177 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3178 get the values. 3179 3180 Returns: 3181 pd.Series: The values of all variables in the set. 3182 """ 3183 solution = self._solution 3184 return _attribute_series( 3185 func=lambda literal: evaluate_boolean_expression(literal, solution), 3186 values=variables, 3187 ) 3188 3189 @property 3190 def objective_value(self) -> float: 3191 """Returns the value of the objective after solve.""" 3192 return self._solution.objective_value 3193 3194 @property 3195 def best_objective_bound(self) -> float: 3196 """Returns the best lower (upper) bound found when min(max)imizing.""" 3197 return self._solution.best_objective_bound 3198 3199 @property 3200 def num_booleans(self) -> int: 3201 """Returns the number of boolean variables managed by the SAT solver.""" 3202 return self._solution.num_booleans 3203 3204 @property 3205 def num_conflicts(self) -> int: 3206 """Returns the number of conflicts since the creation of the solver.""" 3207 return self._solution.num_conflicts 3208 3209 @property 3210 def num_branches(self) -> int: 3211 """Returns the number of search branches explored by the solver.""" 3212 return self._solution.num_branches 3213 3214 @property 3215 def wall_time(self) -> float: 3216 """Returns the wall time in seconds since the creation of the solver.""" 3217 return self._solution.wall_time 3218 3219 @property 3220 def user_time(self) -> float: 3221 """Returns the user time in seconds since the creation of the solver.""" 3222 return self._solution.user_time 3223 3224 @property 3225 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3226 """Returns the response object.""" 3227 return self._solution 3228 3229 def response_stats(self) -> str: 3230 """Returns some statistics on the solution found as a string.""" 3231 return swig_helper.CpSatHelper.solver_response_stats(self._solution) 3232 3233 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 3234 """Returns the indices of the infeasible assumptions.""" 3235 return self._solution.sufficient_assumptions_for_infeasibility 3236 3237 def status_name(self, status: Optional[Any] = None) -> str: 3238 """Returns the name of the status returned by solve().""" 3239 if status is None: 3240 status = self._solution.status 3241 return cp_model_pb2.CpSolverStatus.Name(status) 3242 3243 def solution_info(self) -> str: 3244 """Returns some information on the solve process. 3245 3246 Returns some information on how the solution was found, or the reason 3247 why the model or the parameters are invalid. 3248 3249 Raises: 3250 RuntimeError: if solve() has not been called. 3251 """ 3252 return self._solution.solution_info 3253 3254 @property 3255 def _solution(self) -> cp_model_pb2.CpSolverResponse: 3256 """Checks solve() has been called, and returns the solution.""" 3257 if self.__solution is None: 3258 raise RuntimeError("solve() has not been called.") 3259 return self.__solution 3260 3261 # Compatibility with pre PEP8 3262 # pylint: disable=invalid-name 3263 3264 def BestObjectiveBound(self) -> float: 3265 return self.best_objective_bound 3266 3267 def BooleanValue(self, literal: LiteralT) -> bool: 3268 return self.boolean_value(literal) 3269 3270 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: 3271 return self.boolean_values(variables) 3272 3273 def NumBooleans(self) -> int: 3274 return self.num_booleans 3275 3276 def NumConflicts(self) -> int: 3277 return self.num_conflicts 3278 3279 def NumBranches(self) -> int: 3280 return self.num_branches 3281 3282 def ObjectiveValue(self) -> float: 3283 return self.objective_value 3284 3285 def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: 3286 return self.response_proto 3287 3288 def ResponseStats(self) -> str: 3289 return self.response_stats() 3290 3291 def Solve( 3292 self, 3293 model: CpModel, 3294 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3295 ) -> cp_model_pb2.CpSolverStatus: 3296 return self.solve(model, solution_callback) 3297 3298 def SolutionInfo(self) -> str: 3299 return self.solution_info() 3300 3301 def StatusName(self, status: Optional[Any] = None) -> str: 3302 return self.status_name(status) 3303 3304 def StopSearch(self) -> None: 3305 self.stop_search() 3306 3307 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: 3308 return self.sufficient_assumptions_for_infeasibility() 3309 3310 def UserTime(self) -> float: 3311 return self.user_time 3312 3313 def Value(self, expression: LinearExprT) -> int: 3314 return self.value(expression) 3315 3316 def Values(self, variables: _IndexOrSeries) -> pd.Series: 3317 return self.values(variables) 3318 3319 def WallTime(self) -> float: 3320 return self.wall_time 3321 3322 def SolveWithSolutionCallback( 3323 self, model: CpModel, callback: "CpSolverSolutionCallback" 3324 ) -> cp_model_pb2.CpSolverStatus: 3325 """DEPRECATED Use solve() with the callback argument.""" 3326 warnings.warn( 3327 "solve_with_solution_callback is deprecated; use solve() with" 3328 + "the callback argument.", 3329 DeprecationWarning, 3330 ) 3331 return self.solve(model, callback) 3332 3333 def SearchForAllSolutions( 3334 self, model: CpModel, callback: "CpSolverSolutionCallback" 3335 ) -> cp_model_pb2.CpSolverStatus: 3336 """DEPRECATED Use solve() with the right parameter. 3337 3338 Search for all solutions of a satisfiability problem. 3339 3340 This method searches for all feasible solutions of a given model. 3341 Then it feeds the solution to the callback. 3342 3343 Note that the model cannot contain an objective. 3344 3345 Args: 3346 model: The model to solve. 3347 callback: The callback that will be called at each solution. 3348 3349 Returns: 3350 The status of the solve: 3351 3352 * *FEASIBLE* if some solutions have been found 3353 * *INFEASIBLE* if the solver has proved there are no solution 3354 * *OPTIMAL* if all solutions have been found 3355 """ 3356 warnings.warn( 3357 "search_for_all_solutions is deprecated; use solve() with" 3358 + "enumerate_all_solutions = True.", 3359 DeprecationWarning, 3360 ) 3361 if model.has_objective(): 3362 raise TypeError( 3363 "Search for all solutions is only defined on satisfiability problems" 3364 ) 3365 # Store old parameter. 3366 enumerate_all = self.parameters.enumerate_all_solutions 3367 self.parameters.enumerate_all_solutions = True 3368 3369 self.solve(model, callback) 3370 3371 # Restore parameter. 3372 self.parameters.enumerate_all_solutions = enumerate_all 3373 return self.__solution.status 3374 3375 3376# pylint: enable=invalid-name 3377 3378 3379class CpSolverSolutionCallback(swig_helper.SolutionCallback): 3380 """Solution callback. 3381 3382 This class implements a callback that will be called at each new solution 3383 found during search. 3384 3385 The method on_solution_callback() will be called by the solver, and must be 3386 implemented. The current solution can be queried using the boolean_value() 3387 and value() methods. 3388 3389 These methods returns the same information as their counterpart in the 3390 `CpSolver` class. 3391 """ 3392 3393 def __init__(self): 3394 swig_helper.SolutionCallback.__init__(self) 3395 3396 def OnSolutionCallback(self) -> None: 3397 """Proxy for the same method in snake case.""" 3398 self.on_solution_callback() 3399 3400 def boolean_value(self, lit: LiteralT) -> bool: 3401 """Returns the boolean value of a boolean literal. 3402 3403 Args: 3404 lit: A boolean variable or its negation. 3405 3406 Returns: 3407 The Boolean value of the literal in the solution. 3408 3409 Raises: 3410 RuntimeError: if `lit` is not a boolean variable or its negation. 3411 """ 3412 if not self.has_response(): 3413 raise RuntimeError("solve() has not been called.") 3414 if isinstance(lit, numbers.Integral): 3415 return bool(lit) 3416 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3417 return self.SolutionBooleanValue( 3418 cast(Union[IntVar, _NotBooleanVariable], lit).index 3419 ) 3420 if cmh.is_boolean(lit): 3421 return bool(lit) 3422 raise TypeError(f"Cannot interpret {lit} as a boolean expression.") 3423 3424 def value(self, expression: LinearExprT) -> int: 3425 """Evaluates an linear expression in the current solution. 3426 3427 Args: 3428 expression: a linear expression of the model. 3429 3430 Returns: 3431 An integer value equal to the evaluation of the linear expression 3432 against the current solution. 3433 3434 Raises: 3435 RuntimeError: if 'expression' is not a LinearExpr. 3436 """ 3437 if not self.has_response(): 3438 raise RuntimeError("solve() has not been called.") 3439 3440 value = 0 3441 to_process = [(expression, 1)] 3442 while to_process: 3443 expr, coeff = to_process.pop() 3444 if isinstance(expr, numbers.Integral): 3445 value += int(expr) * coeff 3446 elif isinstance(expr, _ProductCst): 3447 to_process.append((expr.expression(), coeff * expr.coefficient())) 3448 elif isinstance(expr, _Sum): 3449 to_process.append((expr.left(), coeff)) 3450 to_process.append((expr.right(), coeff)) 3451 elif isinstance(expr, _SumArray): 3452 for e in expr.expressions(): 3453 to_process.append((e, coeff)) 3454 value += expr.constant() * coeff 3455 elif isinstance(expr, _WeightedSum): 3456 for e, c in zip(expr.expressions(), expr.coefficients()): 3457 to_process.append((e, coeff * c)) 3458 value += expr.constant() * coeff 3459 elif isinstance(expr, IntVar): 3460 value += coeff * self.SolutionIntegerValue(expr.index) 3461 elif isinstance(expr, _NotBooleanVariable): 3462 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3463 else: 3464 raise TypeError( 3465 f"cannot interpret {expression} as a linear expression." 3466 ) 3467 3468 return value 3469 3470 def has_response(self) -> bool: 3471 return self.HasResponse() 3472 3473 def stop_search(self) -> None: 3474 """Stops the current search asynchronously.""" 3475 if not self.has_response(): 3476 raise RuntimeError("solve() has not been called.") 3477 self.StopSearch() 3478 3479 @property 3480 def objective_value(self) -> float: 3481 """Returns the value of the objective after solve.""" 3482 if not self.has_response(): 3483 raise RuntimeError("solve() has not been called.") 3484 return self.ObjectiveValue() 3485 3486 @property 3487 def best_objective_bound(self) -> float: 3488 """Returns the best lower (upper) bound found when min(max)imizing.""" 3489 if not self.has_response(): 3490 raise RuntimeError("solve() has not been called.") 3491 return self.BestObjectiveBound() 3492 3493 @property 3494 def num_booleans(self) -> int: 3495 """Returns the number of boolean variables managed by the SAT solver.""" 3496 if not self.has_response(): 3497 raise RuntimeError("solve() has not been called.") 3498 return self.NumBooleans() 3499 3500 @property 3501 def num_conflicts(self) -> int: 3502 """Returns the number of conflicts since the creation of the solver.""" 3503 if not self.has_response(): 3504 raise RuntimeError("solve() has not been called.") 3505 return self.NumConflicts() 3506 3507 @property 3508 def num_branches(self) -> int: 3509 """Returns the number of search branches explored by the solver.""" 3510 if not self.has_response(): 3511 raise RuntimeError("solve() has not been called.") 3512 return self.NumBranches() 3513 3514 @property 3515 def num_integer_propagations(self) -> int: 3516 """Returns the number of integer propagations done by the solver.""" 3517 if not self.has_response(): 3518 raise RuntimeError("solve() has not been called.") 3519 return self.NumIntegerPropagations() 3520 3521 @property 3522 def num_boolean_propagations(self) -> int: 3523 """Returns the number of Boolean propagations done by the solver.""" 3524 if not self.has_response(): 3525 raise RuntimeError("solve() has not been called.") 3526 return self.NumBooleanPropagations() 3527 3528 @property 3529 def deterministic_time(self) -> float: 3530 """Returns the determistic time in seconds since the creation of the solver.""" 3531 if not self.has_response(): 3532 raise RuntimeError("solve() has not been called.") 3533 return self.DeterministicTime() 3534 3535 @property 3536 def wall_time(self) -> float: 3537 """Returns the wall time in seconds since the creation of the solver.""" 3538 if not self.has_response(): 3539 raise RuntimeError("solve() has not been called.") 3540 return self.WallTime() 3541 3542 @property 3543 def user_time(self) -> float: 3544 """Returns the user time in seconds since the creation of the solver.""" 3545 if not self.has_response(): 3546 raise RuntimeError("solve() has not been called.") 3547 return self.UserTime() 3548 3549 @property 3550 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3551 """Returns the response object.""" 3552 if not self.has_response(): 3553 raise RuntimeError("solve() has not been called.") 3554 return self.Response() 3555 3556 # Compatibility with pre PEP8 3557 # pylint: disable=invalid-name 3558 Value = value 3559 BooleanValue = boolean_value 3560 # pylint: enable=invalid-name 3561 3562 3563class ObjectiveSolutionPrinter(CpSolverSolutionCallback): 3564 """Display the objective value and time of intermediate solutions.""" 3565 3566 def __init__(self): 3567 CpSolverSolutionCallback.__init__(self) 3568 self.__solution_count = 0 3569 self.__start_time = time.time() 3570 3571 def on_solution_callback(self) -> None: 3572 """Called on each new solution.""" 3573 current_time = time.time() 3574 obj = self.objective_value 3575 print( 3576 "Solution %i, time = %0.2f s, objective = %i" 3577 % (self.__solution_count, current_time - self.__start_time, obj) 3578 ) 3579 self.__solution_count += 1 3580 3581 def solution_count(self) -> int: 3582 """Returns the number of solutions found.""" 3583 return self.__solution_count 3584 3585 3586class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): 3587 """Print intermediate solutions (objective, variable values, time).""" 3588 3589 def __init__(self, variables): 3590 CpSolverSolutionCallback.__init__(self) 3591 self.__variables: Sequence[IntVar] = variables 3592 self.__solution_count: int = 0 3593 self.__start_time: float = time.time() 3594 3595 def on_solution_callback(self) -> None: 3596 """Called on each new solution.""" 3597 current_time = time.time() 3598 obj = self.objective_value 3599 print( 3600 "Solution %i, time = %0.2f s, objective = %i" 3601 % (self.__solution_count, current_time - self.__start_time, obj) 3602 ) 3603 for v in self.__variables: 3604 print(" %s = %i" % (v, self.value(v)), end=" ") 3605 print() 3606 self.__solution_count += 1 3607 3608 @property 3609 def solution_count(self) -> int: 3610 """Returns the number of solutions found.""" 3611 return self.__solution_count 3612 3613 3614class VarArraySolutionPrinter(CpSolverSolutionCallback): 3615 """Print intermediate solutions (variable values, time).""" 3616 3617 def __init__(self, variables: Sequence[IntVar]): 3618 CpSolverSolutionCallback.__init__(self) 3619 self.__variables: Sequence[IntVar] = variables 3620 self.__solution_count: int = 0 3621 self.__start_time: float = time.time() 3622 3623 def on_solution_callback(self) -> None: 3624 """Called on each new solution.""" 3625 current_time = time.time() 3626 print( 3627 "Solution %i, time = %0.2f s" 3628 % (self.__solution_count, current_time - self.__start_time) 3629 ) 3630 for v in self.__variables: 3631 print(" %s = %i" % (v, self.value(v)), end=" ") 3632 print() 3633 self.__solution_count += 1 3634 3635 @property 3636 def solution_count(self) -> int: 3637 """Returns the number of solutions found.""" 3638 return self.__solution_count 3639 3640 3641def _get_index(obj: _IndexOrSeries) -> pd.Index: 3642 """Returns the indices of `obj` as a `pd.Index`.""" 3643 if isinstance(obj, pd.Series): 3644 return obj.index 3645 return obj 3646 3647 3648def _attribute_series( 3649 *, 3650 func: Callable[[IntVar], IntegralT], 3651 values: _IndexOrSeries, 3652) -> pd.Series: 3653 """Returns the attributes of `values`. 3654 3655 Args: 3656 func: The function to call for getting the attribute data. 3657 values: The values that the function will be applied (element-wise) to. 3658 3659 Returns: 3660 pd.Series: The attribute values. 3661 """ 3662 return pd.Series( 3663 data=[func(v) for v in values], 3664 index=_get_index(values), 3665 ) 3666 3667 3668def _convert_to_integral_series_and_validate_index( 3669 value_or_series: Union[IntegralT, pd.Series], index: pd.Index 3670) -> pd.Series: 3671 """Returns a pd.Series of the given index with the corresponding values. 3672 3673 Args: 3674 value_or_series: the values to be converted (if applicable). 3675 index: the index of the resulting pd.Series. 3676 3677 Returns: 3678 pd.Series: The set of values with the given index. 3679 3680 Raises: 3681 TypeError: If the type of `value_or_series` is not recognized. 3682 ValueError: If the index does not match. 3683 """ 3684 if isinstance(value_or_series, numbers.Integral): 3685 result = pd.Series(data=value_or_series, index=index) 3686 elif isinstance(value_or_series, pd.Series): 3687 if value_or_series.index.equals(index): 3688 result = value_or_series 3689 else: 3690 raise ValueError("index does not match") 3691 else: 3692 raise TypeError("invalid type={}".format(type(value_or_series))) 3693 return result 3694 3695 3696def _convert_to_linear_expr_series_and_validate_index( 3697 value_or_series: Union[LinearExprT, pd.Series], index: pd.Index 3698) -> pd.Series: 3699 """Returns a pd.Series of the given index with the corresponding values. 3700 3701 Args: 3702 value_or_series: the values to be converted (if applicable). 3703 index: the index of the resulting pd.Series. 3704 3705 Returns: 3706 pd.Series: The set of values with the given index. 3707 3708 Raises: 3709 TypeError: If the type of `value_or_series` is not recognized. 3710 ValueError: If the index does not match. 3711 """ 3712 if isinstance(value_or_series, numbers.Integral): 3713 result = pd.Series(data=value_or_series, index=index) 3714 elif isinstance(value_or_series, pd.Series): 3715 if value_or_series.index.equals(index): 3716 result = value_or_series 3717 else: 3718 raise ValueError("index does not match") 3719 else: 3720 raise TypeError("invalid type={}".format(type(value_or_series))) 3721 return result 3722 3723 3724def _convert_to_literal_series_and_validate_index( 3725 value_or_series: Union[LiteralT, pd.Series], index: pd.Index 3726) -> pd.Series: 3727 """Returns a pd.Series of the given index with the corresponding values. 3728 3729 Args: 3730 value_or_series: the values to be converted (if applicable). 3731 index: the index of the resulting pd.Series. 3732 3733 Returns: 3734 pd.Series: The set of values with the given index. 3735 3736 Raises: 3737 TypeError: If the type of `value_or_series` is not recognized. 3738 ValueError: If the index does not match. 3739 """ 3740 if isinstance(value_or_series, numbers.Integral): 3741 result = pd.Series(data=value_or_series, index=index) 3742 elif isinstance(value_or_series, pd.Series): 3743 if value_or_series.index.equals(index): 3744 result = value_or_series 3745 else: 3746 raise ValueError("index does not match") 3747 else: 3748 raise TypeError("invalid type={}".format(type(value_or_series))) 3749 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.
from_values(values: List[int]) -> 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]]) -> 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]) -> 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).
contains(self: Domain, value: int) -> bool
Returns true iff value is in Domain.
flattened_intervals(self: 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
size(self: Domain) -> int
Returns the number of elements in the domain. It is capped at kint64max
max(self: Domain) -> int
Returns the max value of the domain. The domain must not be empty.
min(self: Domain) -> int
Returns the min value of the domain. The domain must not be empty.
FromValues(values: List[int]) -> 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]]) -> 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]) -> 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: 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
137def display_bounds(bounds: Sequence[int]) -> str: 138 """Displays a flattened list of intervals.""" 139 out = "" 140 for i in range(0, len(bounds), 2): 141 if i != 0: 142 out += ", " 143 if bounds[i] == bounds[i + 1]: 144 out += str(bounds[i]) 145 else: 146 out += str(bounds[i]) + ".." + str(bounds[i + 1]) 147 return out
Displays a flattened list of intervals.
150def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str: 151 """Returns a short name of an integer variable, or its negation.""" 152 if i < 0: 153 return "not(%s)" % short_name(model, -i - 1) 154 v = model.variables[i] 155 if v.name: 156 return v.name 157 elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: 158 return str(v.domain[0]) 159 else: 160 return "[%s]" % display_bounds(v.domain)
Returns a short name of an integer variable, or its negation.
163def short_expr_name( 164 model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto 165) -> str: 166 """Pretty-print LinearExpressionProto instances.""" 167 if not e.vars: 168 return str(e.offset) 169 if len(e.vars) == 1: 170 var_name = short_name(model, e.vars[0]) 171 coeff = e.coeffs[0] 172 result = "" 173 if coeff == 1: 174 result = var_name 175 elif coeff == -1: 176 result = f"-{var_name}" 177 elif coeff != 0: 178 result = f"{coeff} * {var_name}" 179 if e.offset > 0: 180 result = f"{result} + {e.offset}" 181 elif e.offset < 0: 182 result = f"{result} - {-e.offset}" 183 return result 184 # TODO(user): Support more than affine expressions. 185 return str(e)
Pretty-print LinearExpressionProto instances.
188class LinearExpr: 189 """Holds an integer linear expression. 190 191 A linear expression is built from integer constants and variables. 192 For example, `x + 2 * (y - z + 1)`. 193 194 Linear expressions are used in CP-SAT models in constraints and in the 195 objective: 196 197 * You can define linear constraints as in: 198 199 ``` 200 model.add(x + 2 * y <= 5) 201 model.add(sum(array_of_vars) == 5) 202 ``` 203 204 * In CP-SAT, the objective is a linear expression: 205 206 ``` 207 model.minimize(x + 2 * y + z) 208 ``` 209 210 * For large arrays, using the LinearExpr class is faster that using the python 211 `sum()` function. You can create constraints and the objective from lists of 212 linear expressions or coefficients as follows: 213 214 ``` 215 model.minimize(cp_model.LinearExpr.sum(expressions)) 216 model.add(cp_model.LinearExpr.weighted_sum(expressions, coefficients) >= 0) 217 ``` 218 """ 219 220 @classmethod 221 def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 222 """Creates the expression sum(expressions).""" 223 if len(expressions) == 1: 224 return expressions[0] 225 return _SumArray(expressions) 226 227 @overload 228 @classmethod 229 def weighted_sum( 230 cls, 231 expressions: Sequence[LinearExprT], 232 coefficients: Sequence[IntegralT], 233 ) -> LinearExprT: 234 ... 235 236 @overload 237 @classmethod 238 def weighted_sum( 239 cls, 240 expressions: Sequence[ObjLinearExprT], 241 coefficients: Sequence[NumberT], 242 ) -> ObjLinearExprT: 243 ... 244 245 @classmethod 246 def weighted_sum(cls, expressions, coefficients): 247 """Creates the expression sum(expressions[i] * coefficients[i]).""" 248 if LinearExpr.is_empty_or_all_null(coefficients): 249 return 0 250 elif len(expressions) == 1: 251 return expressions[0] * coefficients[0] 252 else: 253 return _WeightedSum(expressions, coefficients) 254 255 @overload 256 @classmethod 257 def term( 258 cls, 259 expressions: LinearExprT, 260 coefficients: IntegralT, 261 ) -> LinearExprT: 262 ... 263 264 @overload 265 @classmethod 266 def term( 267 cls, 268 expressions: ObjLinearExprT, 269 coefficients: NumberT, 270 ) -> ObjLinearExprT: 271 ... 272 273 @classmethod 274 def term(cls, expression, coefficient): 275 """Creates `expression * coefficient`.""" 276 if cmh.is_zero(coefficient): 277 return 0 278 else: 279 return expression * coefficient 280 281 @classmethod 282 def is_empty_or_all_null(cls, coefficients: Sequence[NumberT]) -> bool: 283 for c in coefficients: 284 if not cmh.is_zero(c): 285 return False 286 return True 287 288 @classmethod 289 def rebuild_from_linear_expression_proto( 290 cls, 291 model: cp_model_pb2.CpModelProto, 292 proto: cp_model_pb2.LinearExpressionProto, 293 ) -> LinearExprT: 294 """Recreate a LinearExpr from a LinearExpressionProto.""" 295 offset = proto.offset 296 num_elements = len(proto.vars) 297 if num_elements == 0: 298 return offset 299 elif num_elements == 1: 300 return IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset 301 else: 302 variables = [] 303 coeffs = [] 304 all_ones = True 305 for index, coeff in zip(proto.vars, proto.coeffs): 306 variables.append(IntVar(model, index, None)) 307 coeffs.append(coeff) 308 if not cmh.is_one(coeff): 309 all_ones = False 310 if all_ones: 311 return _SumArray(variables, offset) 312 else: 313 return _WeightedSum(variables, coeffs, offset) 314 315 def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", IntegralT], int]: 316 """Scans the expression, and returns (var_coef_map, constant).""" 317 coeffs = collections.defaultdict(int) 318 constant = 0 319 to_process: List[Tuple[LinearExprT, IntegralT]] = [(self, 1)] 320 while to_process: # Flatten to avoid recursion. 321 expr, coeff = to_process.pop() 322 if isinstance(expr, numbers.Integral): 323 constant += coeff * int(expr) 324 elif isinstance(expr, _ProductCst): 325 to_process.append((expr.expression(), coeff * expr.coefficient())) 326 elif isinstance(expr, _Sum): 327 to_process.append((expr.left(), coeff)) 328 to_process.append((expr.right(), coeff)) 329 elif isinstance(expr, _SumArray): 330 for e in expr.expressions(): 331 to_process.append((e, coeff)) 332 constant += expr.constant() * coeff 333 elif isinstance(expr, _WeightedSum): 334 for e, c in zip(expr.expressions(), expr.coefficients()): 335 to_process.append((e, coeff * c)) 336 constant += expr.constant() * coeff 337 elif isinstance(expr, IntVar): 338 coeffs[expr] += coeff 339 elif isinstance(expr, _NotBooleanVariable): 340 constant += coeff 341 coeffs[expr.negated()] -= coeff 342 else: 343 raise TypeError("Unrecognized linear expression: " + str(expr)) 344 345 return coeffs, constant 346 347 def get_float_var_value_map( 348 self, 349 ) -> Tuple[Dict["IntVar", float], float, bool]: 350 """Scans the expression. Returns (var_coef_map, constant, is_integer).""" 351 coeffs = {} 352 constant = 0 353 to_process: List[Tuple[LinearExprT, Union[IntegralT, float]]] = [(self, 1)] 354 while to_process: # Flatten to avoid recursion. 355 expr, coeff = to_process.pop() 356 if isinstance(expr, numbers.Integral): # Keep integrality. 357 constant += coeff * int(expr) 358 elif isinstance(expr, numbers.Number): 359 constant += coeff * float(expr) 360 elif isinstance(expr, _ProductCst): 361 to_process.append((expr.expression(), coeff * expr.coefficient())) 362 elif isinstance(expr, _Sum): 363 to_process.append((expr.left(), coeff)) 364 to_process.append((expr.right(), coeff)) 365 elif isinstance(expr, _SumArray): 366 for e in expr.expressions(): 367 to_process.append((e, coeff)) 368 constant += expr.constant() * coeff 369 elif isinstance(expr, _WeightedSum): 370 for e, c in zip(expr.expressions(), expr.coefficients()): 371 to_process.append((e, coeff * c)) 372 constant += expr.constant() * coeff 373 elif isinstance(expr, IntVar): 374 if expr in coeffs: 375 coeffs[expr] += coeff 376 else: 377 coeffs[expr] = coeff 378 elif isinstance(expr, _NotBooleanVariable): 379 constant += coeff 380 if expr.negated() in coeffs: 381 coeffs[expr.negated()] -= coeff 382 else: 383 coeffs[expr.negated()] = -coeff 384 else: 385 raise TypeError("Unrecognized linear expression: " + str(expr)) 386 is_integer = isinstance(constant, numbers.Integral) 387 if is_integer: 388 for coeff in coeffs.values(): 389 if not isinstance(coeff, numbers.Integral): 390 is_integer = False 391 break 392 return coeffs, constant, is_integer 393 394 def __hash__(self) -> int: 395 return object.__hash__(self) 396 397 def __abs__(self) -> NoReturn: 398 raise NotImplementedError( 399 "calling abs() on a linear expression is not supported, " 400 "please use CpModel.add_abs_equality" 401 ) 402 403 @overload 404 def __add__(self, arg: LinearExprT) -> LinearExprT: 405 ... 406 407 @overload 408 def __add__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 409 ... 410 411 def __add__(self, arg): 412 if cmh.is_zero(arg): 413 return self 414 return _Sum(self, arg) 415 416 @overload 417 def __radd__(self, arg: LinearExprT) -> LinearExprT: 418 ... 419 420 @overload 421 def __radd__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 422 ... 423 424 def __radd__(self, arg): 425 if cmh.is_zero(arg): 426 return self 427 return _Sum(self, arg) 428 429 @overload 430 def __sub__(self, arg: LinearExprT) -> LinearExprT: 431 ... 432 433 @overload 434 def __sub__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 435 ... 436 437 def __sub__(self, arg): 438 if cmh.is_zero(arg): 439 return self 440 if isinstance(arg, numbers.Number): 441 arg = cmh.assert_is_a_number(arg) 442 return _Sum(self, -arg) 443 else: 444 return _Sum(self, -arg) 445 446 @overload 447 def __rsub__(self, arg: LinearExprT) -> LinearExprT: 448 ... 449 450 @overload 451 def __rsub__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 452 ... 453 454 def __rsub__(self, arg): 455 return _Sum(-self, arg) 456 457 @overload 458 def __mul__(self, arg: LinearExprT) -> LinearExprT: 459 ... 460 461 @overload 462 def __mul__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 463 ... 464 465 def __mul__(self, arg): 466 arg = cmh.assert_is_a_number(arg) 467 if cmh.is_one(arg): 468 return self 469 elif cmh.is_zero(arg): 470 return 0 471 return _ProductCst(self, arg) 472 473 @overload 474 def __rmul__(self, arg: LinearExprT) -> LinearExprT: 475 ... 476 477 @overload 478 def __rmul__(self, arg: ObjLinearExprT) -> ObjLinearExprT: 479 ... 480 481 def __rmul__(self, arg): 482 arg = cmh.assert_is_a_number(arg) 483 if cmh.is_one(arg): 484 return self 485 elif cmh.is_zero(arg): 486 return 0 487 return _ProductCst(self, arg) 488 489 def __div__(self, _) -> NoReturn: 490 raise NotImplementedError( 491 "calling / on a linear expression is not supported, " 492 "please use CpModel.add_division_equality" 493 ) 494 495 def __truediv__(self, _) -> NoReturn: 496 raise NotImplementedError( 497 "calling // on a linear expression is not supported, " 498 "please use CpModel.add_division_equality" 499 ) 500 501 def __mod__(self, _) -> NoReturn: 502 raise NotImplementedError( 503 "calling %% on a linear expression is not supported, " 504 "please use CpModel.add_modulo_equality" 505 ) 506 507 def __pow__(self, _) -> NoReturn: 508 raise NotImplementedError( 509 "calling ** on a linear expression is not supported, " 510 "please use CpModel.add_multiplication_equality" 511 ) 512 513 def __lshift__(self, _) -> NoReturn: 514 raise NotImplementedError( 515 "calling left shift on a linear expression is not supported" 516 ) 517 518 def __rshift__(self, _) -> NoReturn: 519 raise NotImplementedError( 520 "calling right shift on a linear expression is not supported" 521 ) 522 523 def __and__(self, _) -> NoReturn: 524 raise NotImplementedError( 525 "calling and on a linear expression is not supported, " 526 "please use CpModel.add_bool_and" 527 ) 528 529 def __or__(self, _) -> NoReturn: 530 raise NotImplementedError( 531 "calling or on a linear expression is not supported, " 532 "please use CpModel.add_bool_or" 533 ) 534 535 def __xor__(self, _) -> NoReturn: 536 raise NotImplementedError( 537 "calling xor on a linear expression is not supported, " 538 "please use CpModel.add_bool_xor" 539 ) 540 541 def __neg__(self) -> LinearExprT: 542 return _ProductCst(self, -1) 543 544 def __bool__(self) -> NoReturn: 545 raise NotImplementedError( 546 "Evaluating a LinearExpr instance as a Boolean is not implemented." 547 ) 548 549 def __eq__(self, arg: LinearExprT) -> BoundedLinearExprT: 550 if arg is None: 551 return False 552 if isinstance(arg, numbers.Integral): 553 arg = cmh.assert_is_int64(arg) 554 return BoundedLinearExpression(self, [arg, arg]) 555 else: 556 return BoundedLinearExpression(self - arg, [0, 0]) 557 558 def __ge__(self, arg: LinearExprT) -> BoundedLinearExprT: 559 if isinstance(arg, numbers.Integral): 560 arg = cmh.assert_is_int64(arg) 561 return BoundedLinearExpression(self, [arg, INT_MAX]) 562 else: 563 return BoundedLinearExpression(self - arg, [0, INT_MAX]) 564 565 def __le__(self, arg: LinearExprT) -> BoundedLinearExprT: 566 if isinstance(arg, numbers.Integral): 567 arg = cmh.assert_is_int64(arg) 568 return BoundedLinearExpression(self, [INT_MIN, arg]) 569 else: 570 return BoundedLinearExpression(self - arg, [INT_MIN, 0]) 571 572 def __lt__(self, arg: LinearExprT) -> BoundedLinearExprT: 573 if isinstance(arg, numbers.Integral): 574 arg = cmh.assert_is_int64(arg) 575 if arg == INT_MIN: 576 raise ArithmeticError("< INT_MIN is not supported") 577 return BoundedLinearExpression(self, [INT_MIN, arg - 1]) 578 else: 579 return BoundedLinearExpression(self - arg, [INT_MIN, -1]) 580 581 def __gt__(self, arg: LinearExprT) -> BoundedLinearExprT: 582 if isinstance(arg, numbers.Integral): 583 arg = cmh.assert_is_int64(arg) 584 if arg == INT_MAX: 585 raise ArithmeticError("> INT_MAX is not supported") 586 return BoundedLinearExpression(self, [arg + 1, INT_MAX]) 587 else: 588 return BoundedLinearExpression(self - arg, [1, INT_MAX]) 589 590 def __ne__(self, arg: LinearExprT) -> BoundedLinearExprT: 591 if arg is None: 592 return True 593 if isinstance(arg, numbers.Integral): 594 arg = cmh.assert_is_int64(arg) 595 if arg == INT_MAX: 596 return BoundedLinearExpression(self, [INT_MIN, INT_MAX - 1]) 597 elif arg == INT_MIN: 598 return BoundedLinearExpression(self, [INT_MIN + 1, INT_MAX]) 599 else: 600 return BoundedLinearExpression( 601 self, [INT_MIN, arg - 1, arg + 1, INT_MAX] 602 ) 603 else: 604 return BoundedLinearExpression(self - arg, [INT_MIN, -1, 1, INT_MAX]) 605 606 # Compatibility with pre PEP8 607 # pylint: disable=invalid-name 608 @classmethod 609 def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 610 """Creates the expression sum(expressions).""" 611 return cls.sum(expressions) 612 613 @overload 614 @classmethod 615 def WeightedSum( 616 cls, 617 expressions: Sequence[LinearExprT], 618 coefficients: Sequence[IntegralT], 619 ) -> LinearExprT: 620 ... 621 622 @overload 623 @classmethod 624 def WeightedSum( 625 cls, 626 expressions: Sequence[ObjLinearExprT], 627 coefficients: Sequence[NumberT], 628 ) -> ObjLinearExprT: 629 ... 630 631 @classmethod 632 def WeightedSum(cls, expressions, coefficients): 633 """Creates the expression sum(expressions[i] * coefficients[i]).""" 634 return cls.weighted_sum(expressions, coefficients) 635 636 @overload 637 @classmethod 638 def Term( 639 cls, 640 expressions: LinearExprT, 641 coefficients: IntegralT, 642 ) -> LinearExprT: 643 ... 644 645 @overload 646 @classmethod 647 def Term( 648 cls, 649 expressions: ObjLinearExprT, 650 coefficients: NumberT, 651 ) -> ObjLinearExprT: 652 ... 653 654 @classmethod 655 def Term(cls, expression, coefficient): 656 """Creates `expression * coefficient`.""" 657 return cls.term(expression, coefficient) 658 659 # 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)
220 @classmethod 221 def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 222 """Creates the expression sum(expressions).""" 223 if len(expressions) == 1: 224 return expressions[0] 225 return _SumArray(expressions)
Creates the expression sum(expressions).
245 @classmethod 246 def weighted_sum(cls, expressions, coefficients): 247 """Creates the expression sum(expressions[i] * coefficients[i]).""" 248 if LinearExpr.is_empty_or_all_null(coefficients): 249 return 0 250 elif len(expressions) == 1: 251 return expressions[0] * coefficients[0] 252 else: 253 return _WeightedSum(expressions, coefficients)
Creates the expression sum(expressions[i] * coefficients[i]).
273 @classmethod 274 def term(cls, expression, coefficient): 275 """Creates `expression * coefficient`.""" 276 if cmh.is_zero(coefficient): 277 return 0 278 else: 279 return expression * coefficient
Creates expression * coefficient
.
288 @classmethod 289 def rebuild_from_linear_expression_proto( 290 cls, 291 model: cp_model_pb2.CpModelProto, 292 proto: cp_model_pb2.LinearExpressionProto, 293 ) -> LinearExprT: 294 """Recreate a LinearExpr from a LinearExpressionProto.""" 295 offset = proto.offset 296 num_elements = len(proto.vars) 297 if num_elements == 0: 298 return offset 299 elif num_elements == 1: 300 return IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset 301 else: 302 variables = [] 303 coeffs = [] 304 all_ones = True 305 for index, coeff in zip(proto.vars, proto.coeffs): 306 variables.append(IntVar(model, index, None)) 307 coeffs.append(coeff) 308 if not cmh.is_one(coeff): 309 all_ones = False 310 if all_ones: 311 return _SumArray(variables, offset) 312 else: 313 return _WeightedSum(variables, coeffs, offset)
Recreate a LinearExpr from a LinearExpressionProto.
315 def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", IntegralT], int]: 316 """Scans the expression, and returns (var_coef_map, constant).""" 317 coeffs = collections.defaultdict(int) 318 constant = 0 319 to_process: List[Tuple[LinearExprT, IntegralT]] = [(self, 1)] 320 while to_process: # Flatten to avoid recursion. 321 expr, coeff = to_process.pop() 322 if isinstance(expr, numbers.Integral): 323 constant += coeff * int(expr) 324 elif isinstance(expr, _ProductCst): 325 to_process.append((expr.expression(), coeff * expr.coefficient())) 326 elif isinstance(expr, _Sum): 327 to_process.append((expr.left(), coeff)) 328 to_process.append((expr.right(), coeff)) 329 elif isinstance(expr, _SumArray): 330 for e in expr.expressions(): 331 to_process.append((e, coeff)) 332 constant += expr.constant() * coeff 333 elif isinstance(expr, _WeightedSum): 334 for e, c in zip(expr.expressions(), expr.coefficients()): 335 to_process.append((e, coeff * c)) 336 constant += expr.constant() * coeff 337 elif isinstance(expr, IntVar): 338 coeffs[expr] += coeff 339 elif isinstance(expr, _NotBooleanVariable): 340 constant += coeff 341 coeffs[expr.negated()] -= coeff 342 else: 343 raise TypeError("Unrecognized linear expression: " + str(expr)) 344 345 return coeffs, constant
Scans the expression, and returns (var_coef_map, constant).
347 def get_float_var_value_map( 348 self, 349 ) -> Tuple[Dict["IntVar", float], float, bool]: 350 """Scans the expression. Returns (var_coef_map, constant, is_integer).""" 351 coeffs = {} 352 constant = 0 353 to_process: List[Tuple[LinearExprT, Union[IntegralT, float]]] = [(self, 1)] 354 while to_process: # Flatten to avoid recursion. 355 expr, coeff = to_process.pop() 356 if isinstance(expr, numbers.Integral): # Keep integrality. 357 constant += coeff * int(expr) 358 elif isinstance(expr, numbers.Number): 359 constant += coeff * float(expr) 360 elif isinstance(expr, _ProductCst): 361 to_process.append((expr.expression(), coeff * expr.coefficient())) 362 elif isinstance(expr, _Sum): 363 to_process.append((expr.left(), coeff)) 364 to_process.append((expr.right(), coeff)) 365 elif isinstance(expr, _SumArray): 366 for e in expr.expressions(): 367 to_process.append((e, coeff)) 368 constant += expr.constant() * coeff 369 elif isinstance(expr, _WeightedSum): 370 for e, c in zip(expr.expressions(), expr.coefficients()): 371 to_process.append((e, coeff * c)) 372 constant += expr.constant() * coeff 373 elif isinstance(expr, IntVar): 374 if expr in coeffs: 375 coeffs[expr] += coeff 376 else: 377 coeffs[expr] = coeff 378 elif isinstance(expr, _NotBooleanVariable): 379 constant += coeff 380 if expr.negated() in coeffs: 381 coeffs[expr.negated()] -= coeff 382 else: 383 coeffs[expr.negated()] = -coeff 384 else: 385 raise TypeError("Unrecognized linear expression: " + str(expr)) 386 is_integer = isinstance(constant, numbers.Integral) 387 if is_integer: 388 for coeff in coeffs.values(): 389 if not isinstance(coeff, numbers.Integral): 390 is_integer = False 391 break 392 return coeffs, constant, is_integer
Scans the expression. Returns (var_coef_map, constant, is_integer).
608 @classmethod 609 def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT: 610 """Creates the expression sum(expressions).""" 611 return cls.sum(expressions)
Creates the expression sum(expressions).
631 @classmethod 632 def WeightedSum(cls, expressions, coefficients): 633 """Creates the expression sum(expressions[i] * coefficients[i]).""" 634 return cls.weighted_sum(expressions, coefficients)
Creates the expression sum(expressions[i] * coefficients[i]).
816class IntVar(LinearExpr): 817 """An integer variable. 818 819 An IntVar is an object that can take on any integer value within defined 820 ranges. Variables appear in constraint like: 821 822 x + y >= 5 823 AllDifferent([x, y, z]) 824 825 Solving a model is equivalent to finding, for each variable, a single value 826 from the set of initial values (called the initial domain), such that the 827 model is feasible, or optimal if you provided an objective function. 828 """ 829 830 def __init__( 831 self, 832 model: cp_model_pb2.CpModelProto, 833 domain: Union[int, Domain], 834 name: Optional[str], 835 ): 836 """See CpModel.new_int_var below.""" 837 self.__negation: Optional[_NotBooleanVariable] = None 838 # Python do not support multiple __init__ methods. 839 # This method is only called from the CpModel class. 840 # We hack the parameter to support the two cases: 841 # case 1: 842 # model is a CpModelProto, domain is a Domain, and name is a string. 843 # case 2: 844 # model is a CpModelProto, domain is an index (int), and name is None. 845 if isinstance(domain, numbers.Integral) and name is None: 846 self.__index: int = int(domain) 847 self.__var: cp_model_pb2.IntegerVariableProto = model.variables[domain] 848 else: 849 self.__index: int = len(model.variables) 850 self.__var: cp_model_pb2.IntegerVariableProto = model.variables.add() 851 self.__var.domain.extend(cast(Domain, domain).flattened_intervals()) 852 self.__var.name = name 853 854 @property 855 def index(self) -> int: 856 """Returns the index of the variable in the model.""" 857 return self.__index 858 859 @property 860 def proto(self) -> cp_model_pb2.IntegerVariableProto: 861 """Returns the variable protobuf.""" 862 return self.__var 863 864 def is_equal_to(self, other: Any) -> bool: 865 """Returns true if self == other in the python sense.""" 866 if not isinstance(other, IntVar): 867 return False 868 return self.index == other.index 869 870 def __str__(self) -> str: 871 if not self.__var.name: 872 if ( 873 len(self.__var.domain) == 2 874 and self.__var.domain[0] == self.__var.domain[1] 875 ): 876 # Special case for constants. 877 return str(self.__var.domain[0]) 878 else: 879 return "unnamed_var_%i" % self.__index 880 return self.__var.name 881 882 def __repr__(self) -> str: 883 return "%s(%s)" % (self.__var.name, display_bounds(self.__var.domain)) 884 885 @property 886 def name(self) -> str: 887 if not self.__var or not self.__var.name: 888 return "" 889 return self.__var.name 890 891 def negated(self) -> "_NotBooleanVariable": 892 """Returns the negation of a Boolean variable. 893 894 This method implements the logical negation of a Boolean variable. 895 It is only valid if the variable has a Boolean domain (0 or 1). 896 897 Note that this method is nilpotent: `x.negated().negated() == x`. 898 """ 899 900 for bound in self.__var.domain: 901 if bound < 0 or bound > 1: 902 raise TypeError( 903 f"cannot call negated on a non boolean variable: {self}" 904 ) 905 if self.__negation is None: 906 self.__negation = _NotBooleanVariable(self) 907 return self.__negation 908 909 def __invert__(self) -> "_NotBooleanVariable": 910 """Returns the logical negation of a Boolean variable.""" 911 return self.negated() 912 913 # Pre PEP8 compatibility. 914 # pylint: disable=invalid-name 915 Not = negated 916 917 def Name(self) -> str: 918 return self.name 919 920 def Proto(self) -> cp_model_pb2.IntegerVariableProto: 921 return self.proto 922 923 def Index(self) -> int: 924 return self.index 925 926 # 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.
830 def __init__( 831 self, 832 model: cp_model_pb2.CpModelProto, 833 domain: Union[int, Domain], 834 name: Optional[str], 835 ): 836 """See CpModel.new_int_var below.""" 837 self.__negation: Optional[_NotBooleanVariable] = None 838 # Python do not support multiple __init__ methods. 839 # This method is only called from the CpModel class. 840 # We hack the parameter to support the two cases: 841 # case 1: 842 # model is a CpModelProto, domain is a Domain, and name is a string. 843 # case 2: 844 # model is a CpModelProto, domain is an index (int), and name is None. 845 if isinstance(domain, numbers.Integral) and name is None: 846 self.__index: int = int(domain) 847 self.__var: cp_model_pb2.IntegerVariableProto = model.variables[domain] 848 else: 849 self.__index: int = len(model.variables) 850 self.__var: cp_model_pb2.IntegerVariableProto = model.variables.add() 851 self.__var.domain.extend(cast(Domain, domain).flattened_intervals()) 852 self.__var.name = name
See CpModel.new_int_var below.
854 @property 855 def index(self) -> int: 856 """Returns the index of the variable in the model.""" 857 return self.__index
Returns the index of the variable in the model.
859 @property 860 def proto(self) -> cp_model_pb2.IntegerVariableProto: 861 """Returns the variable protobuf.""" 862 return self.__var
Returns the variable protobuf.
864 def is_equal_to(self, other: Any) -> bool: 865 """Returns true if self == other in the python sense.""" 866 if not isinstance(other, IntVar): 867 return False 868 return self.index == other.index
Returns true if self == other in the python sense.
891 def negated(self) -> "_NotBooleanVariable": 892 """Returns the negation of a Boolean variable. 893 894 This method implements the logical negation of a Boolean variable. 895 It is only valid if the variable has a Boolean domain (0 or 1). 896 897 Note that this method is nilpotent: `x.negated().negated() == x`. 898 """ 899 900 for bound in self.__var.domain: 901 if bound < 0 or bound > 1: 902 raise TypeError( 903 f"cannot call negated on a non boolean variable: {self}" 904 ) 905 if self.__negation is None: 906 self.__negation = _NotBooleanVariable(self) 907 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
.
891 def negated(self) -> "_NotBooleanVariable": 892 """Returns the negation of a Boolean variable. 893 894 This method implements the logical negation of a Boolean variable. 895 It is only valid if the variable has a Boolean domain (0 or 1). 896 897 Note that this method is nilpotent: `x.negated().negated() == x`. 898 """ 899 900 for bound in self.__var.domain: 901 if bound < 0 or bound > 1: 902 raise TypeError( 903 f"cannot call negated on a non boolean variable: {self}" 904 ) 905 if self.__negation is None: 906 self.__negation = _NotBooleanVariable(self) 907 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
.
969class BoundedLinearExpression: 970 """Represents a linear constraint: `lb <= linear expression <= ub`. 971 972 The only use of this class is to be added to the CpModel through 973 `CpModel.add(expression)`, as in: 974 975 model.add(x + 2 * y -1 >= z) 976 """ 977 978 def __init__(self, expr: LinearExprT, bounds: Sequence[int]): 979 self.__expr: LinearExprT = expr 980 self.__bounds: Sequence[int] = bounds 981 982 def __str__(self): 983 if len(self.__bounds) == 2: 984 lb, ub = self.__bounds 985 if lb > INT_MIN and ub < INT_MAX: 986 if lb == ub: 987 return str(self.__expr) + " == " + str(lb) 988 else: 989 return str(lb) + " <= " + str(self.__expr) + " <= " + str(ub) 990 elif lb > INT_MIN: 991 return str(self.__expr) + " >= " + str(lb) 992 elif ub < INT_MAX: 993 return str(self.__expr) + " <= " + str(ub) 994 else: 995 return "True (unbounded expr " + str(self.__expr) + ")" 996 elif ( 997 len(self.__bounds) == 4 998 and self.__bounds[0] == INT_MIN 999 and self.__bounds[1] + 2 == self.__bounds[2] 1000 and self.__bounds[3] == INT_MAX 1001 ): 1002 return str(self.__expr) + " != " + str(self.__bounds[1] + 1) 1003 else: 1004 return str(self.__expr) + " in [" + display_bounds(self.__bounds) + "]" 1005 1006 def expression(self) -> LinearExprT: 1007 return self.__expr 1008 1009 def bounds(self) -> Sequence[int]: 1010 return self.__bounds 1011 1012 def __bool__(self) -> bool: 1013 expr = self.__expr 1014 if isinstance(expr, LinearExpr): 1015 coeffs_map, constant = expr.get_integer_var_value_map() 1016 all_coeffs = set(coeffs_map.values()) 1017 same_var = set([0]) 1018 eq_bounds = [0, 0] 1019 different_vars = set([-1, 1]) 1020 ne_bounds = [INT_MIN, -1, 1, INT_MAX] 1021 if ( 1022 len(coeffs_map) == 1 1023 and all_coeffs == same_var 1024 and constant == 0 1025 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1026 ): 1027 return self.__bounds == eq_bounds 1028 if ( 1029 len(coeffs_map) == 2 1030 and all_coeffs == different_vars 1031 and constant == 0 1032 and (self.__bounds == eq_bounds or self.__bounds == ne_bounds) 1033 ): 1034 return self.__bounds == ne_bounds 1035 1036 raise NotImplementedError( 1037 f'Evaluating a BoundedLinearExpression "{self}" as a Boolean value' 1038 + " is not supported." 1039 )
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)
1042class Constraint: 1043 """Base class for constraints. 1044 1045 Constraints are built by the CpModel through the add<XXX> methods. 1046 Once created by the CpModel class, they are automatically added to the model. 1047 The purpose of this class is to allow specification of enforcement literals 1048 for this constraint. 1049 1050 b = model.new_bool_var('b') 1051 x = model.new_int_var(0, 10, 'x') 1052 y = model.new_int_var(0, 10, 'y') 1053 1054 model.add(x + 2 * y == 5).only_enforce_if(b.negated()) 1055 """ 1056 1057 def __init__( 1058 self, 1059 cp_model: "CpModel", 1060 ): 1061 self.__index: int = len(cp_model.proto.constraints) 1062 self.__cp_model: "CpModel" = cp_model 1063 self.__constraint: cp_model_pb2.ConstraintProto = ( 1064 cp_model.proto.constraints.add() 1065 ) 1066 1067 @overload 1068 def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": 1069 ... 1070 1071 @overload 1072 def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": 1073 ... 1074 1075 def only_enforce_if(self, *boolvar) -> "Constraint": 1076 """Adds an enforcement literal to the constraint. 1077 1078 This method adds one or more literals (that is, a boolean variable or its 1079 negation) as enforcement literals. The conjunction of all these literals 1080 determines whether the constraint is active or not. It acts as an 1081 implication, so if the conjunction is true, it implies that the constraint 1082 must be enforced. If it is false, then the constraint is ignored. 1083 1084 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1085 1086 Args: 1087 *boolvar: One or more Boolean literals. 1088 1089 Returns: 1090 self. 1091 """ 1092 for lit in expand_generator_or_tuple(boolvar): 1093 if (cmh.is_boolean(lit) and lit) or ( 1094 isinstance(lit, numbers.Integral) and lit == 1 1095 ): 1096 # Always true. Do nothing. 1097 pass 1098 elif (cmh.is_boolean(lit) and not lit) or ( 1099 isinstance(lit, numbers.Integral) and lit == 0 1100 ): 1101 self.__constraint.enforcement_literal.append( 1102 self.__cp_model.new_constant(0).index 1103 ) 1104 else: 1105 self.__constraint.enforcement_literal.append( 1106 cast(Union[IntVar, _NotBooleanVariable], lit).index 1107 ) 1108 return self 1109 1110 def with_name(self, name: str) -> "Constraint": 1111 """Sets the name of the constraint.""" 1112 if name: 1113 self.__constraint.name = name 1114 else: 1115 self.__constraint.ClearField("name") 1116 return self 1117 1118 @property 1119 def name(self) -> str: 1120 """Returns the name of the constraint.""" 1121 if not self.__constraint or not self.__constraint.name: 1122 return "" 1123 return self.__constraint.name 1124 1125 @property 1126 def index(self) -> int: 1127 """Returns the index of the constraint in the model.""" 1128 return self.__index 1129 1130 @property 1131 def proto(self) -> cp_model_pb2.ConstraintProto: 1132 """Returns the constraint protobuf.""" 1133 return self.__constraint 1134 1135 # Pre PEP8 compatibility. 1136 # pylint: disable=invalid-name 1137 OnlyEnforceIf = only_enforce_if 1138 WithName = with_name 1139 1140 def Name(self) -> str: 1141 return self.name 1142 1143 def Index(self) -> int: 1144 return self.index 1145 1146 def Proto(self) -> cp_model_pb2.ConstraintProto: 1147 return self.proto 1148 1149 # 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())
1075 def only_enforce_if(self, *boolvar) -> "Constraint": 1076 """Adds an enforcement literal to the constraint. 1077 1078 This method adds one or more literals (that is, a boolean variable or its 1079 negation) as enforcement literals. The conjunction of all these literals 1080 determines whether the constraint is active or not. It acts as an 1081 implication, so if the conjunction is true, it implies that the constraint 1082 must be enforced. If it is false, then the constraint is ignored. 1083 1084 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1085 1086 Args: 1087 *boolvar: One or more Boolean literals. 1088 1089 Returns: 1090 self. 1091 """ 1092 for lit in expand_generator_or_tuple(boolvar): 1093 if (cmh.is_boolean(lit) and lit) or ( 1094 isinstance(lit, numbers.Integral) and lit == 1 1095 ): 1096 # Always true. Do nothing. 1097 pass 1098 elif (cmh.is_boolean(lit) and not lit) or ( 1099 isinstance(lit, numbers.Integral) and lit == 0 1100 ): 1101 self.__constraint.enforcement_literal.append( 1102 self.__cp_model.new_constant(0).index 1103 ) 1104 else: 1105 self.__constraint.enforcement_literal.append( 1106 cast(Union[IntVar, _NotBooleanVariable], lit).index 1107 ) 1108 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.
1110 def with_name(self, name: str) -> "Constraint": 1111 """Sets the name of the constraint.""" 1112 if name: 1113 self.__constraint.name = name 1114 else: 1115 self.__constraint.ClearField("name") 1116 return self
Sets the name of the constraint.
1118 @property 1119 def name(self) -> str: 1120 """Returns the name of the constraint.""" 1121 if not self.__constraint or not self.__constraint.name: 1122 return "" 1123 return self.__constraint.name
Returns the name of the constraint.
1125 @property 1126 def index(self) -> int: 1127 """Returns the index of the constraint in the model.""" 1128 return self.__index
Returns the index of the constraint in the model.
1130 @property 1131 def proto(self) -> cp_model_pb2.ConstraintProto: 1132 """Returns the constraint protobuf.""" 1133 return self.__constraint
Returns the constraint protobuf.
1075 def only_enforce_if(self, *boolvar) -> "Constraint": 1076 """Adds an enforcement literal to the constraint. 1077 1078 This method adds one or more literals (that is, a boolean variable or its 1079 negation) as enforcement literals. The conjunction of all these literals 1080 determines whether the constraint is active or not. It acts as an 1081 implication, so if the conjunction is true, it implies that the constraint 1082 must be enforced. If it is false, then the constraint is ignored. 1083 1084 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 1085 1086 Args: 1087 *boolvar: One or more Boolean literals. 1088 1089 Returns: 1090 self. 1091 """ 1092 for lit in expand_generator_or_tuple(boolvar): 1093 if (cmh.is_boolean(lit) and lit) or ( 1094 isinstance(lit, numbers.Integral) and lit == 1 1095 ): 1096 # Always true. Do nothing. 1097 pass 1098 elif (cmh.is_boolean(lit) and not lit) or ( 1099 isinstance(lit, numbers.Integral) and lit == 0 1100 ): 1101 self.__constraint.enforcement_literal.append( 1102 self.__cp_model.new_constant(0).index 1103 ) 1104 else: 1105 self.__constraint.enforcement_literal.append( 1106 cast(Union[IntVar, _NotBooleanVariable], lit).index 1107 ) 1108 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.
1110 def with_name(self, name: str) -> "Constraint": 1111 """Sets the name of the constraint.""" 1112 if name: 1113 self.__constraint.name = name 1114 else: 1115 self.__constraint.ClearField("name") 1116 return self
Sets the name of the constraint.
1152class IntervalVar: 1153 """Represents an Interval variable. 1154 1155 An interval variable is both a constraint and a variable. It is defined by 1156 three integer variables: start, size, and end. 1157 1158 It is a constraint because, internally, it enforces that start + size == end. 1159 1160 It is also a variable as it can appear in specific scheduling constraints: 1161 NoOverlap, NoOverlap2D, Cumulative. 1162 1163 Optionally, an enforcement literal can be added to this constraint, in which 1164 case these scheduling constraints will ignore interval variables with 1165 enforcement literals assigned to false. Conversely, these constraints will 1166 also set these enforcement literals to false if they cannot fit these 1167 intervals into the schedule. 1168 """ 1169 1170 def __init__( 1171 self, 1172 model: cp_model_pb2.CpModelProto, 1173 start: Union[cp_model_pb2.LinearExpressionProto, int], 1174 size: Optional[cp_model_pb2.LinearExpressionProto], 1175 end: Optional[cp_model_pb2.LinearExpressionProto], 1176 is_present_index: Optional[int], 1177 name: Optional[str], 1178 ): 1179 self.__model: cp_model_pb2.CpModelProto = model 1180 # As with the IntVar::__init__ method, we hack the __init__ method to 1181 # support two use cases: 1182 # case 1: called when creating a new interval variable. 1183 # {start|size|end} are linear expressions, is_present_index is either 1184 # None or the index of a Boolean literal. name is a string 1185 # case 2: called when querying an existing interval variable. 1186 # start_index is an int, all parameters after are None. 1187 if size is None and end is None and is_present_index is None and name is None: 1188 self.__index: int = cast(int, start) 1189 self.__ct: cp_model_pb2.ConstraintProto = model.constraints[self.__index] 1190 else: 1191 self.__index: int = len(model.constraints) 1192 self.__ct: cp_model_pb2.ConstraintProto = self.__model.constraints.add() 1193 self.__ct.interval.start.CopyFrom(start) 1194 self.__ct.interval.size.CopyFrom(size) 1195 self.__ct.interval.end.CopyFrom(end) 1196 if is_present_index is not None: 1197 self.__ct.enforcement_literal.append(is_present_index) 1198 if name: 1199 self.__ct.name = name 1200 1201 @property 1202 def index(self) -> int: 1203 """Returns the index of the interval constraint in the model.""" 1204 return self.__index 1205 1206 @property 1207 def proto(self) -> cp_model_pb2.IntervalConstraintProto: 1208 """Returns the interval protobuf.""" 1209 return self.__ct.interval 1210 1211 def __str__(self): 1212 return self.__ct.name 1213 1214 def __repr__(self): 1215 interval = self.__ct.interval 1216 if self.__ct.enforcement_literal: 1217 return "%s(start = %s, size = %s, end = %s, is_present = %s)" % ( 1218 self.__ct.name, 1219 short_expr_name(self.__model, interval.start), 1220 short_expr_name(self.__model, interval.size), 1221 short_expr_name(self.__model, interval.end), 1222 short_name(self.__model, self.__ct.enforcement_literal[0]), 1223 ) 1224 else: 1225 return "%s(start = %s, size = %s, end = %s)" % ( 1226 self.__ct.name, 1227 short_expr_name(self.__model, interval.start), 1228 short_expr_name(self.__model, interval.size), 1229 short_expr_name(self.__model, interval.end), 1230 ) 1231 1232 @property 1233 def name(self) -> str: 1234 if not self.__ct or not self.__ct.name: 1235 return "" 1236 return self.__ct.name 1237 1238 def start_expr(self) -> LinearExprT: 1239 return LinearExpr.rebuild_from_linear_expression_proto( 1240 self.__model, self.__ct.interval.start 1241 ) 1242 1243 def size_expr(self) -> LinearExprT: 1244 return LinearExpr.rebuild_from_linear_expression_proto( 1245 self.__model, self.__ct.interval.size 1246 ) 1247 1248 def end_expr(self) -> LinearExprT: 1249 return LinearExpr.rebuild_from_linear_expression_proto( 1250 self.__model, self.__ct.interval.end 1251 ) 1252 1253 # Pre PEP8 compatibility. 1254 # pylint: disable=invalid-name 1255 def Name(self) -> str: 1256 return self.name 1257 1258 def Index(self) -> int: 1259 return self.index 1260 1261 def Proto(self) -> cp_model_pb2.IntervalConstraintProto: 1262 return self.proto 1263 1264 StartExpr = start_expr 1265 SizeExpr = size_expr 1266 EndExpr = end_expr 1267 1268 # 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.
1170 def __init__( 1171 self, 1172 model: cp_model_pb2.CpModelProto, 1173 start: Union[cp_model_pb2.LinearExpressionProto, int], 1174 size: Optional[cp_model_pb2.LinearExpressionProto], 1175 end: Optional[cp_model_pb2.LinearExpressionProto], 1176 is_present_index: Optional[int], 1177 name: Optional[str], 1178 ): 1179 self.__model: cp_model_pb2.CpModelProto = model 1180 # As with the IntVar::__init__ method, we hack the __init__ method to 1181 # support two use cases: 1182 # case 1: called when creating a new interval variable. 1183 # {start|size|end} are linear expressions, is_present_index is either 1184 # None or the index of a Boolean literal. name is a string 1185 # case 2: called when querying an existing interval variable. 1186 # start_index is an int, all parameters after are None. 1187 if size is None and end is None and is_present_index is None and name is None: 1188 self.__index: int = cast(int, start) 1189 self.__ct: cp_model_pb2.ConstraintProto = model.constraints[self.__index] 1190 else: 1191 self.__index: int = len(model.constraints) 1192 self.__ct: cp_model_pb2.ConstraintProto = self.__model.constraints.add() 1193 self.__ct.interval.start.CopyFrom(start) 1194 self.__ct.interval.size.CopyFrom(size) 1195 self.__ct.interval.end.CopyFrom(end) 1196 if is_present_index is not None: 1197 self.__ct.enforcement_literal.append(is_present_index) 1198 if name: 1199 self.__ct.name = name
1201 @property 1202 def index(self) -> int: 1203 """Returns the index of the interval constraint in the model.""" 1204 return self.__index
Returns the index of the interval constraint in the model.
1206 @property 1207 def proto(self) -> cp_model_pb2.IntervalConstraintProto: 1208 """Returns the interval protobuf.""" 1209 return self.__ct.interval
Returns the interval protobuf.
1271def object_is_a_true_literal(literal: LiteralT) -> bool: 1272 """Checks if literal is either True, or a Boolean literals fixed to True.""" 1273 if isinstance(literal, IntVar): 1274 proto = literal.proto 1275 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1276 if isinstance(literal, _NotBooleanVariable): 1277 proto = literal.negated().proto 1278 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1279 if isinstance(literal, numbers.Integral): 1280 return int(literal) == 1 1281 return False
Checks if literal is either True, or a Boolean literals fixed to True.
1284def object_is_a_false_literal(literal: LiteralT) -> bool: 1285 """Checks if literal is either False, or a Boolean literals fixed to False.""" 1286 if isinstance(literal, IntVar): 1287 proto = literal.proto 1288 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 1289 if isinstance(literal, _NotBooleanVariable): 1290 proto = literal.negated().proto 1291 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 1292 if isinstance(literal, numbers.Integral): 1293 return int(literal) == 0 1294 return False
Checks if literal is either False, or a Boolean literals fixed to False.
1297class CpModel: 1298 """Methods for building a CP model. 1299 1300 Methods beginning with: 1301 1302 * ```New``` create integer, boolean, or interval variables. 1303 * ```add``` create new constraints and add them to the model. 1304 """ 1305 1306 def __init__(self): 1307 self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() 1308 self.__constant_map = {} 1309 1310 # Naming. 1311 @property 1312 def name(self) -> str: 1313 """Returns the name of the model.""" 1314 if not self.__model or not self.__model.name: 1315 return "" 1316 return self.__model.name 1317 1318 @name.setter 1319 def name(self, name: str): 1320 """Sets the name of the model.""" 1321 self.__model.name = name 1322 1323 # Integer variable. 1324 1325 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1326 """Create an integer variable with domain [lb, ub]. 1327 1328 The CP-SAT solver is limited to integer variables. If you have fractional 1329 values, scale them up so that they become integers; if you have strings, 1330 encode them as integers. 1331 1332 Args: 1333 lb: Lower bound for the variable. 1334 ub: Upper bound for the variable. 1335 name: The name of the variable. 1336 1337 Returns: 1338 a variable whose domain is [lb, ub]. 1339 """ 1340 1341 return IntVar(self.__model, Domain(lb, ub), name) 1342 1343 def new_int_var_from_domain(self, domain: Domain, name: str) -> IntVar: 1344 """Create an integer variable from a domain. 1345 1346 A domain is a set of integers specified by a collection of intervals. 1347 For example, `model.new_int_var_from_domain(cp_model. 1348 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1349 1350 Args: 1351 domain: An instance of the Domain class. 1352 name: The name of the variable. 1353 1354 Returns: 1355 a variable whose domain is the given domain. 1356 """ 1357 return IntVar(self.__model, domain, name) 1358 1359 def new_bool_var(self, name: str) -> IntVar: 1360 """Creates a 0-1 variable with the given name.""" 1361 return IntVar(self.__model, Domain(0, 1), name) 1362 1363 def new_constant(self, value: IntegralT) -> IntVar: 1364 """Declares a constant integer.""" 1365 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None) 1366 1367 def new_int_var_series( 1368 self, 1369 name: str, 1370 index: pd.Index, 1371 lower_bounds: Union[IntegralT, pd.Series], 1372 upper_bounds: Union[IntegralT, pd.Series], 1373 ) -> pd.Series: 1374 """Creates a series of (scalar-valued) variables with the given name. 1375 1376 Args: 1377 name (str): Required. The name of the variable set. 1378 index (pd.Index): Required. The index to use for the variable set. 1379 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1380 set. If a `pd.Series` is passed in, it will be based on the 1381 corresponding values of the pd.Series. 1382 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1383 set. If a `pd.Series` is passed in, it will be based on the 1384 corresponding values of the pd.Series. 1385 1386 Returns: 1387 pd.Series: The variable set indexed by its corresponding dimensions. 1388 1389 Raises: 1390 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1391 ValueError: if the `name` is not a valid identifier or already exists. 1392 ValueError: if the `lowerbound` is greater than the `upperbound`. 1393 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1394 the input index. 1395 """ 1396 if not isinstance(index, pd.Index): 1397 raise TypeError("Non-index object is used as index") 1398 if not name.isidentifier(): 1399 raise ValueError("name={} is not a valid identifier".format(name)) 1400 if ( 1401 isinstance(lower_bounds, numbers.Integral) 1402 and isinstance(upper_bounds, numbers.Integral) 1403 and lower_bounds > upper_bounds 1404 ): 1405 raise ValueError( 1406 f"lower_bound={lower_bounds} is greater than" 1407 f" upper_bound={upper_bounds} for variable set={name}" 1408 ) 1409 1410 lower_bounds = _convert_to_integral_series_and_validate_index( 1411 lower_bounds, index 1412 ) 1413 upper_bounds = _convert_to_integral_series_and_validate_index( 1414 upper_bounds, index 1415 ) 1416 return pd.Series( 1417 index=index, 1418 data=[ 1419 # pylint: disable=g-complex-comprehension 1420 IntVar( 1421 model=self.__model, 1422 name=f"{name}[{i}]", 1423 domain=Domain(lower_bounds[i], upper_bounds[i]), 1424 ) 1425 for i in index 1426 ], 1427 ) 1428 1429 def new_bool_var_series( 1430 self, 1431 name: str, 1432 index: pd.Index, 1433 ) -> pd.Series: 1434 """Creates a series of (scalar-valued) variables with the given name. 1435 1436 Args: 1437 name (str): Required. The name of the variable set. 1438 index (pd.Index): Required. The index to use for the variable set. 1439 1440 Returns: 1441 pd.Series: The variable set indexed by its corresponding dimensions. 1442 1443 Raises: 1444 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1445 ValueError: if the `name` is not a valid identifier or already exists. 1446 """ 1447 return self.new_int_var_series( 1448 name=name, index=index, lower_bounds=0, upper_bounds=1 1449 ) 1450 1451 # Linear constraints. 1452 1453 def add_linear_constraint( 1454 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1455 ) -> Constraint: 1456 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1457 return self.add_linear_expression_in_domain(linear_expr, Domain(lb, ub)) 1458 1459 def add_linear_expression_in_domain( 1460 self, linear_expr: LinearExprT, domain: Domain 1461 ) -> Constraint: 1462 """Adds the constraint: `linear_expr` in `domain`.""" 1463 if isinstance(linear_expr, LinearExpr): 1464 ct = Constraint(self) 1465 model_ct = self.__model.constraints[ct.index] 1466 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1467 for t in coeffs_map.items(): 1468 if not isinstance(t[0], IntVar): 1469 raise TypeError("Wrong argument" + str(t)) 1470 c = cmh.assert_is_int64(t[1]) 1471 model_ct.linear.vars.append(t[0].index) 1472 model_ct.linear.coeffs.append(c) 1473 model_ct.linear.domain.extend( 1474 [ 1475 cmh.capped_subtraction(x, constant) 1476 for x in domain.flattened_intervals() 1477 ] 1478 ) 1479 return ct 1480 if isinstance(linear_expr, numbers.Integral): 1481 if not domain.contains(int(linear_expr)): 1482 return self.add_bool_or([]) # Evaluate to false. 1483 else: 1484 return self.add_bool_and([]) # Evaluate to true. 1485 raise TypeError( 1486 "not supported: CpModel.add_linear_expression_in_domain(" 1487 + str(linear_expr) 1488 + " " 1489 + str(domain) 1490 + ")" 1491 ) 1492 1493 def add(self, ct: Union[BoundedLinearExpression, bool]) -> Constraint: 1494 """Adds a `BoundedLinearExpression` to the model. 1495 1496 Args: 1497 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1498 1499 Returns: 1500 An instance of the `Constraint` class. 1501 """ 1502 if isinstance(ct, BoundedLinearExpression): 1503 return self.add_linear_expression_in_domain( 1504 ct.expression(), Domain.from_flat_intervals(ct.bounds()) 1505 ) 1506 if ct and cmh.is_boolean(ct): 1507 return self.add_bool_or([True]) 1508 if not ct and cmh.is_boolean(ct): 1509 return self.add_bool_or([]) # Evaluate to false. 1510 raise TypeError("not supported: CpModel.add(" + str(ct) + ")") 1511 1512 # General Integer Constraints. 1513 1514 @overload 1515 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: 1516 ... 1517 1518 @overload 1519 def add_all_different(self, *expressions: LinearExprT) -> Constraint: 1520 ... 1521 1522 def add_all_different(self, *expressions): 1523 """Adds AllDifferent(expressions). 1524 1525 This constraint forces all expressions to have different values. 1526 1527 Args: 1528 *expressions: simple expressions of the form a * var + constant. 1529 1530 Returns: 1531 An instance of the `Constraint` class. 1532 """ 1533 ct = Constraint(self) 1534 model_ct = self.__model.constraints[ct.index] 1535 expanded = expand_generator_or_tuple(expressions) 1536 model_ct.all_diff.exprs.extend( 1537 self.parse_linear_expression(x) for x in expanded 1538 ) 1539 return ct 1540 1541 def add_element( 1542 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1543 ) -> Constraint: 1544 """Adds the element constraint: `variables[index] == target`. 1545 1546 Args: 1547 index: The index of the variable that's being constrained. 1548 variables: A list of variables. 1549 target: The value that the variable must be equal to. 1550 1551 Returns: 1552 An instance of the `Constraint` class. 1553 """ 1554 1555 if not variables: 1556 raise ValueError("add_element expects a non-empty variables array") 1557 1558 if isinstance(index, numbers.Integral): 1559 return self.add(list(variables)[int(index)] == target) 1560 1561 ct = Constraint(self) 1562 model_ct = self.__model.constraints[ct.index] 1563 model_ct.element.index = self.get_or_make_index(index) 1564 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1565 model_ct.element.target = self.get_or_make_index(target) 1566 return ct 1567 1568 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1569 """Adds Circuit(arcs). 1570 1571 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1572 1573 A circuit is a unique Hamiltonian path in a subgraph of the total 1574 graph. In case a node 'i' is not in the path, then there must be a 1575 loop arc 'i -> i' associated with a true literal. Otherwise 1576 this constraint will fail. 1577 1578 Args: 1579 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1580 literal). The arc is selected in the circuit if the literal is true. 1581 Both source_node and destination_node must be integers between 0 and the 1582 number of nodes - 1. 1583 1584 Returns: 1585 An instance of the `Constraint` class. 1586 1587 Raises: 1588 ValueError: If the list of arcs is empty. 1589 """ 1590 if not arcs: 1591 raise ValueError("add_circuit expects a non-empty array of arcs") 1592 ct = Constraint(self) 1593 model_ct = self.__model.constraints[ct.index] 1594 for arc in arcs: 1595 tail = cmh.assert_is_int32(arc[0]) 1596 head = cmh.assert_is_int32(arc[1]) 1597 lit = self.get_or_make_boolean_index(arc[2]) 1598 model_ct.circuit.tails.append(tail) 1599 model_ct.circuit.heads.append(head) 1600 model_ct.circuit.literals.append(lit) 1601 return ct 1602 1603 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1604 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1605 1606 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1607 literals[i] is true must satisfy this set of properties: 1608 - #incoming arcs == 1 except for node 0. 1609 - #outgoing arcs == 1 except for node 0. 1610 - for node zero, #incoming arcs == #outgoing arcs. 1611 - There are no duplicate arcs. 1612 - Self-arcs are allowed except for node 0. 1613 - There is no cycle in this graph, except through node 0. 1614 1615 Args: 1616 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1617 literal). The arc is selected in the circuit if the literal is true. 1618 Both source_node and destination_node must be integers between 0 and the 1619 number of nodes - 1. 1620 1621 Returns: 1622 An instance of the `Constraint` class. 1623 1624 Raises: 1625 ValueError: If the list of arcs is empty. 1626 """ 1627 if not arcs: 1628 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1629 ct = Constraint(self) 1630 model_ct = self.__model.constraints[ct.index] 1631 for arc in arcs: 1632 tail = cmh.assert_is_int32(arc[0]) 1633 head = cmh.assert_is_int32(arc[1]) 1634 lit = self.get_or_make_boolean_index(arc[2]) 1635 model_ct.routes.tails.append(tail) 1636 model_ct.routes.heads.append(head) 1637 model_ct.routes.literals.append(lit) 1638 return ct 1639 1640 def add_allowed_assignments( 1641 self, 1642 variables: Sequence[VariableT], 1643 tuples_list: Iterable[Sequence[IntegralT]], 1644 ) -> Constraint: 1645 """Adds AllowedAssignments(variables, tuples_list). 1646 1647 An AllowedAssignments constraint is a constraint on an array of variables, 1648 which requires that when all variables are assigned values, the resulting 1649 array equals one of the tuples in `tuple_list`. 1650 1651 Args: 1652 variables: A list of variables. 1653 tuples_list: A list of admissible tuples. Each tuple must have the same 1654 length as the variables, and the ith value of a tuple corresponds to the 1655 ith variable. 1656 1657 Returns: 1658 An instance of the `Constraint` class. 1659 1660 Raises: 1661 TypeError: If a tuple does not have the same size as the list of 1662 variables. 1663 ValueError: If the array of variables is empty. 1664 """ 1665 1666 if not variables: 1667 raise ValueError( 1668 "add_allowed_assignments expects a non-empty variables array" 1669 ) 1670 1671 ct = Constraint(self) 1672 model_ct = self.__model.constraints[ct.index] 1673 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1674 arity = len(variables) 1675 for t in tuples_list: 1676 if len(t) != arity: 1677 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1678 ar = [] 1679 for v in t: 1680 ar.append(cmh.assert_is_int64(v)) 1681 model_ct.table.values.extend(ar) 1682 return ct 1683 1684 def add_forbidden_assignments( 1685 self, 1686 variables: Sequence[VariableT], 1687 tuples_list: Iterable[Sequence[IntegralT]], 1688 ) -> Constraint: 1689 """Adds add_forbidden_assignments(variables, [tuples_list]). 1690 1691 A ForbiddenAssignments constraint is a constraint on an array of variables 1692 where the list of impossible combinations is provided in the tuples list. 1693 1694 Args: 1695 variables: A list of variables. 1696 tuples_list: A list of forbidden tuples. Each tuple must have the same 1697 length as the variables, and the *i*th value of a tuple corresponds to 1698 the *i*th variable. 1699 1700 Returns: 1701 An instance of the `Constraint` class. 1702 1703 Raises: 1704 TypeError: If a tuple does not have the same size as the list of 1705 variables. 1706 ValueError: If the array of variables is empty. 1707 """ 1708 1709 if not variables: 1710 raise ValueError( 1711 "add_forbidden_assignments expects a non-empty variables array" 1712 ) 1713 1714 index = len(self.__model.constraints) 1715 ct = self.add_allowed_assignments(variables, tuples_list) 1716 self.__model.constraints[index].table.negated = True 1717 return ct 1718 1719 def add_automaton( 1720 self, 1721 transition_variables: Sequence[VariableT], 1722 starting_state: IntegralT, 1723 final_states: Sequence[IntegralT], 1724 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1725 ) -> Constraint: 1726 """Adds an automaton constraint. 1727 1728 An automaton constraint takes a list of variables (of size *n*), an initial 1729 state, a set of final states, and a set of transitions. A transition is a 1730 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1731 and *transition* is the label of an arc from *head* to *tail*, 1732 corresponding to the value of one variable in the list of variables. 1733 1734 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1735 contains the possible states of the automaton. The first state contains the 1736 initial state. The last phase contains the final states. 1737 1738 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1739 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1740 an arc from the state *tail* of phase *i* and the state *head* of phase 1741 *i* + 1. This arc is labeled by the value *transition* of the variables 1742 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1743 is assigned the value *transition*. 1744 1745 A feasible solution of this constraint is an assignment of variables such 1746 that, starting from the initial state in phase 0, there is a path labeled by 1747 the values of the variables that ends in one of the final states in the 1748 final phase. 1749 1750 Args: 1751 transition_variables: A non-empty list of variables whose values 1752 correspond to the labels of the arcs traversed by the automaton. 1753 starting_state: The initial state of the automaton. 1754 final_states: A non-empty list of admissible final states. 1755 transition_triples: A list of transitions for the automaton, in the 1756 following format (current_state, variable_value, next_state). 1757 1758 Returns: 1759 An instance of the `Constraint` class. 1760 1761 Raises: 1762 ValueError: if `transition_variables`, `final_states`, or 1763 `transition_triples` are empty. 1764 """ 1765 1766 if not transition_variables: 1767 raise ValueError( 1768 "add_automaton expects a non-empty transition_variables array" 1769 ) 1770 if not final_states: 1771 raise ValueError("add_automaton expects some final states") 1772 1773 if not transition_triples: 1774 raise ValueError("add_automaton expects some transition triples") 1775 1776 ct = Constraint(self) 1777 model_ct = self.__model.constraints[ct.index] 1778 model_ct.automaton.vars.extend( 1779 [self.get_or_make_index(x) for x in transition_variables] 1780 ) 1781 starting_state = cmh.assert_is_int64(starting_state) 1782 model_ct.automaton.starting_state = starting_state 1783 for v in final_states: 1784 v = cmh.assert_is_int64(v) 1785 model_ct.automaton.final_states.append(v) 1786 for t in transition_triples: 1787 if len(t) != 3: 1788 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1789 tail = cmh.assert_is_int64(t[0]) 1790 label = cmh.assert_is_int64(t[1]) 1791 head = cmh.assert_is_int64(t[2]) 1792 model_ct.automaton.transition_tail.append(tail) 1793 model_ct.automaton.transition_label.append(label) 1794 model_ct.automaton.transition_head.append(head) 1795 return ct 1796 1797 def add_inverse( 1798 self, 1799 variables: Sequence[VariableT], 1800 inverse_variables: Sequence[VariableT], 1801 ) -> Constraint: 1802 """Adds Inverse(variables, inverse_variables). 1803 1804 An inverse constraint enforces that if `variables[i]` is assigned a value 1805 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1806 1807 Args: 1808 variables: An array of integer variables. 1809 inverse_variables: An array of integer variables. 1810 1811 Returns: 1812 An instance of the `Constraint` class. 1813 1814 Raises: 1815 TypeError: if variables and inverse_variables have different lengths, or 1816 if they are empty. 1817 """ 1818 1819 if not variables or not inverse_variables: 1820 raise TypeError("The Inverse constraint does not accept empty arrays") 1821 if len(variables) != len(inverse_variables): 1822 raise TypeError( 1823 "In the inverse constraint, the two array variables and" 1824 " inverse_variables must have the same length." 1825 ) 1826 ct = Constraint(self) 1827 model_ct = self.__model.constraints[ct.index] 1828 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1829 model_ct.inverse.f_inverse.extend( 1830 [self.get_or_make_index(x) for x in inverse_variables] 1831 ) 1832 return ct 1833 1834 def add_reservoir_constraint( 1835 self, 1836 times: Iterable[LinearExprT], 1837 level_changes: Iterable[LinearExprT], 1838 min_level: int, 1839 max_level: int, 1840 ) -> Constraint: 1841 """Adds Reservoir(times, level_changes, min_level, max_level). 1842 1843 Maintains a reservoir level within bounds. The water level starts at 0, and 1844 at any time, it must be between min_level and max_level. 1845 1846 If the affine expression `times[i]` is assigned a value t, then the current 1847 level changes by `level_changes[i]`, which is constant, at time t. 1848 1849 Note that min level must be <= 0, and the max level must be >= 0. Please 1850 use fixed level_changes to simulate initial state. 1851 1852 Therefore, at any time: 1853 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1854 1855 Args: 1856 times: A list of 1-var affine expressions (a * x + b) which specify the 1857 time of the filling or emptying the reservoir. 1858 level_changes: A list of integer values that specifies the amount of the 1859 emptying or filling. Currently, variable demands are not supported. 1860 min_level: At any time, the level of the reservoir must be greater or 1861 equal than the min level. 1862 max_level: At any time, the level of the reservoir must be less or equal 1863 than the max level. 1864 1865 Returns: 1866 An instance of the `Constraint` class. 1867 1868 Raises: 1869 ValueError: if max_level < min_level. 1870 1871 ValueError: if max_level < 0. 1872 1873 ValueError: if min_level > 0 1874 """ 1875 1876 if max_level < min_level: 1877 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1878 1879 if max_level < 0: 1880 raise ValueError("Reservoir constraint must have a max_level >= 0") 1881 1882 if min_level > 0: 1883 raise ValueError("Reservoir constraint must have a min_level <= 0") 1884 1885 ct = Constraint(self) 1886 model_ct = self.__model.constraints[ct.index] 1887 model_ct.reservoir.time_exprs.extend( 1888 [self.parse_linear_expression(x) for x in times] 1889 ) 1890 model_ct.reservoir.level_changes.extend( 1891 [self.parse_linear_expression(x) for x in level_changes] 1892 ) 1893 model_ct.reservoir.min_level = min_level 1894 model_ct.reservoir.max_level = max_level 1895 return ct 1896 1897 def add_reservoir_constraint_with_active( 1898 self, 1899 times: Iterable[LinearExprT], 1900 level_changes: Iterable[LinearExprT], 1901 actives: Iterable[LiteralT], 1902 min_level: int, 1903 max_level: int, 1904 ) -> Constraint: 1905 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1906 1907 Maintains a reservoir level within bounds. The water level starts at 0, and 1908 at any time, it must be between min_level and max_level. 1909 1910 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1911 `True`, then the current level changes by `level_changes[i]`, which is 1912 constant, 1913 at time t. 1914 1915 Note that min level must be <= 0, and the max level must be >= 0. Please 1916 use fixed level_changes to simulate initial state. 1917 1918 Therefore, at any time: 1919 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1920 max_level] 1921 1922 1923 The array of boolean variables 'actives', if defined, indicates which 1924 actions are actually performed. 1925 1926 Args: 1927 times: A list of 1-var affine expressions (a * x + b) which specify the 1928 time of the filling or emptying the reservoir. 1929 level_changes: A list of integer values that specifies the amount of the 1930 emptying or filling. Currently, variable demands are not supported. 1931 actives: a list of boolean variables. They indicates if the 1932 emptying/refilling events actually take place. 1933 min_level: At any time, the level of the reservoir must be greater or 1934 equal than the min level. 1935 max_level: At any time, the level of the reservoir must be less or equal 1936 than the max level. 1937 1938 Returns: 1939 An instance of the `Constraint` class. 1940 1941 Raises: 1942 ValueError: if max_level < min_level. 1943 1944 ValueError: if max_level < 0. 1945 1946 ValueError: if min_level > 0 1947 """ 1948 1949 if max_level < min_level: 1950 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1951 1952 if max_level < 0: 1953 raise ValueError("Reservoir constraint must have a max_level >= 0") 1954 1955 if min_level > 0: 1956 raise ValueError("Reservoir constraint must have a min_level <= 0") 1957 1958 ct = Constraint(self) 1959 model_ct = self.__model.constraints[ct.index] 1960 model_ct.reservoir.time_exprs.extend( 1961 [self.parse_linear_expression(x) for x in times] 1962 ) 1963 model_ct.reservoir.level_changes.extend( 1964 [self.parse_linear_expression(x) for x in level_changes] 1965 ) 1966 model_ct.reservoir.active_literals.extend( 1967 [self.get_or_make_boolean_index(x) for x in actives] 1968 ) 1969 model_ct.reservoir.min_level = min_level 1970 model_ct.reservoir.max_level = max_level 1971 return ct 1972 1973 def add_map_domain( 1974 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 1975 ): 1976 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 1977 1978 for i, bool_var in enumerate(bool_var_array): 1979 b_index = bool_var.index 1980 var_index = var.index 1981 model_ct = self.__model.constraints.add() 1982 model_ct.linear.vars.append(var_index) 1983 model_ct.linear.coeffs.append(1) 1984 model_ct.linear.domain.extend([offset + i, offset + i]) 1985 model_ct.enforcement_literal.append(b_index) 1986 1987 model_ct = self.__model.constraints.add() 1988 model_ct.linear.vars.append(var_index) 1989 model_ct.linear.coeffs.append(1) 1990 model_ct.enforcement_literal.append(-b_index - 1) 1991 if offset + i - 1 >= INT_MIN: 1992 model_ct.linear.domain.extend([INT_MIN, offset + i - 1]) 1993 if offset + i + 1 <= INT_MAX: 1994 model_ct.linear.domain.extend([offset + i + 1, INT_MAX]) 1995 1996 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1997 """Adds `a => b` (`a` implies `b`).""" 1998 ct = Constraint(self) 1999 model_ct = self.__model.constraints[ct.index] 2000 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2001 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2002 return ct 2003 2004 @overload 2005 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: 2006 ... 2007 2008 @overload 2009 def add_bool_or(self, *literals: LiteralT) -> Constraint: 2010 ... 2011 2012 def add_bool_or(self, *literals): 2013 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2014 ct = Constraint(self) 2015 model_ct = self.__model.constraints[ct.index] 2016 model_ct.bool_or.literals.extend( 2017 [ 2018 self.get_or_make_boolean_index(x) 2019 for x in expand_generator_or_tuple(literals) 2020 ] 2021 ) 2022 return ct 2023 2024 @overload 2025 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: 2026 ... 2027 2028 @overload 2029 def add_at_least_one(self, *literals: LiteralT) -> Constraint: 2030 ... 2031 2032 def add_at_least_one(self, *literals): 2033 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2034 return self.add_bool_or(*literals) 2035 2036 @overload 2037 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: 2038 ... 2039 2040 @overload 2041 def add_at_most_one(self, *literals: LiteralT) -> Constraint: 2042 ... 2043 2044 def add_at_most_one(self, *literals): 2045 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2046 ct = Constraint(self) 2047 model_ct = self.__model.constraints[ct.index] 2048 model_ct.at_most_one.literals.extend( 2049 [ 2050 self.get_or_make_boolean_index(x) 2051 for x in expand_generator_or_tuple(literals) 2052 ] 2053 ) 2054 return ct 2055 2056 @overload 2057 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: 2058 ... 2059 2060 @overload 2061 def add_exactly_one(self, *literals: LiteralT) -> Constraint: 2062 ... 2063 2064 def add_exactly_one(self, *literals): 2065 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2066 ct = Constraint(self) 2067 model_ct = self.__model.constraints[ct.index] 2068 model_ct.exactly_one.literals.extend( 2069 [ 2070 self.get_or_make_boolean_index(x) 2071 for x in expand_generator_or_tuple(literals) 2072 ] 2073 ) 2074 return ct 2075 2076 @overload 2077 def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: 2078 ... 2079 2080 @overload 2081 def add_bool_and(self, *literals: LiteralT) -> Constraint: 2082 ... 2083 2084 def add_bool_and(self, *literals): 2085 """Adds `And(literals) == true`.""" 2086 ct = Constraint(self) 2087 model_ct = self.__model.constraints[ct.index] 2088 model_ct.bool_and.literals.extend( 2089 [ 2090 self.get_or_make_boolean_index(x) 2091 for x in expand_generator_or_tuple(literals) 2092 ] 2093 ) 2094 return ct 2095 2096 @overload 2097 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: 2098 ... 2099 2100 @overload 2101 def add_bool_xor(self, *literals: LiteralT) -> Constraint: 2102 ... 2103 2104 def add_bool_xor(self, *literals): 2105 """Adds `XOr(literals) == true`. 2106 2107 In contrast to add_bool_or and add_bool_and, it does not support 2108 .only_enforce_if(). 2109 2110 Args: 2111 *literals: the list of literals in the constraint. 2112 2113 Returns: 2114 An `Constraint` object. 2115 """ 2116 ct = Constraint(self) 2117 model_ct = self.__model.constraints[ct.index] 2118 model_ct.bool_xor.literals.extend( 2119 [ 2120 self.get_or_make_boolean_index(x) 2121 for x in expand_generator_or_tuple(literals) 2122 ] 2123 ) 2124 return ct 2125 2126 def add_min_equality( 2127 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2128 ) -> Constraint: 2129 """Adds `target == Min(exprs)`.""" 2130 ct = Constraint(self) 2131 model_ct = self.__model.constraints[ct.index] 2132 model_ct.lin_max.exprs.extend( 2133 [self.parse_linear_expression(x, True) for x in exprs] 2134 ) 2135 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2136 return ct 2137 2138 def add_max_equality( 2139 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2140 ) -> Constraint: 2141 """Adds `target == Max(exprs)`.""" 2142 ct = Constraint(self) 2143 model_ct = self.__model.constraints[ct.index] 2144 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2145 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2146 return ct 2147 2148 def add_division_equality( 2149 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2150 ) -> Constraint: 2151 """Adds `target == num // denom` (integer division rounded towards 0).""" 2152 ct = Constraint(self) 2153 model_ct = self.__model.constraints[ct.index] 2154 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2155 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2156 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2157 return ct 2158 2159 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2160 """Adds `target == Abs(expr)`.""" 2161 ct = Constraint(self) 2162 model_ct = self.__model.constraints[ct.index] 2163 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2164 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2165 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2166 return ct 2167 2168 def add_modulo_equality( 2169 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2170 ) -> Constraint: 2171 """Adds `target = expr % mod`.""" 2172 ct = Constraint(self) 2173 model_ct = self.__model.constraints[ct.index] 2174 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2175 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2176 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2177 return ct 2178 2179 def add_multiplication_equality( 2180 self, 2181 target: LinearExprT, 2182 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2183 ) -> Constraint: 2184 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2185 ct = Constraint(self) 2186 model_ct = self.__model.constraints[ct.index] 2187 model_ct.int_prod.exprs.extend( 2188 [ 2189 self.parse_linear_expression(expr) 2190 for expr in expand_generator_or_tuple(expressions) 2191 ] 2192 ) 2193 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2194 return ct 2195 2196 # Scheduling support 2197 2198 def new_interval_var( 2199 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2200 ) -> IntervalVar: 2201 """Creates an interval variable from start, size, and end. 2202 2203 An interval variable is a constraint, that is itself used in other 2204 constraints like NoOverlap. 2205 2206 Internally, it ensures that `start + size == end`. 2207 2208 Args: 2209 start: The start of the interval. It must be of the form a * var + b. 2210 size: The size of the interval. It must be of the form a * var + b. 2211 end: The end of the interval. It must be of the form a * var + b. 2212 name: The name of the interval variable. 2213 2214 Returns: 2215 An `IntervalVar` object. 2216 """ 2217 2218 lin = self.add(start + size == end) 2219 if name: 2220 lin.with_name("lin_" + name) 2221 2222 start_expr = self.parse_linear_expression(start) 2223 size_expr = self.parse_linear_expression(size) 2224 end_expr = self.parse_linear_expression(end) 2225 if len(start_expr.vars) > 1: 2226 raise TypeError( 2227 "cp_model.new_interval_var: start must be 1-var affine or constant." 2228 ) 2229 if len(size_expr.vars) > 1: 2230 raise TypeError( 2231 "cp_model.new_interval_var: size must be 1-var affine or constant." 2232 ) 2233 if len(end_expr.vars) > 1: 2234 raise TypeError( 2235 "cp_model.new_interval_var: end must be 1-var affine or constant." 2236 ) 2237 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2238 2239 def new_interval_var_series( 2240 self, 2241 name: str, 2242 index: pd.Index, 2243 starts: Union[LinearExprT, pd.Series], 2244 sizes: Union[LinearExprT, pd.Series], 2245 ends: Union[LinearExprT, pd.Series], 2246 ) -> pd.Series: 2247 """Creates a series of interval variables with the given name. 2248 2249 Args: 2250 name (str): Required. The name of the variable set. 2251 index (pd.Index): Required. The index to use for the variable set. 2252 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2253 set. If a `pd.Series` is passed in, it will be based on the 2254 corresponding values of the pd.Series. 2255 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2256 set. If a `pd.Series` is passed in, it will be based on the 2257 corresponding values of the pd.Series. 2258 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2259 set. If a `pd.Series` is passed in, it will be based on the 2260 corresponding values of the pd.Series. 2261 2262 Returns: 2263 pd.Series: The interval variable set indexed by its corresponding 2264 dimensions. 2265 2266 Raises: 2267 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2268 ValueError: if the `name` is not a valid identifier or already exists. 2269 ValueError: if the all the indexes do not match. 2270 """ 2271 if not isinstance(index, pd.Index): 2272 raise TypeError("Non-index object is used as index") 2273 if not name.isidentifier(): 2274 raise ValueError("name={} is not a valid identifier".format(name)) 2275 2276 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2277 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2278 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2279 interval_array = [] 2280 for i in index: 2281 interval_array.append( 2282 self.new_interval_var( 2283 start=starts[i], 2284 size=sizes[i], 2285 end=ends[i], 2286 name=f"{name}[{i}]", 2287 ) 2288 ) 2289 return pd.Series(index=index, data=interval_array) 2290 2291 def new_fixed_size_interval_var( 2292 self, start: LinearExprT, size: IntegralT, name: str 2293 ) -> IntervalVar: 2294 """Creates an interval variable from start, and a fixed size. 2295 2296 An interval variable is a constraint, that is itself used in other 2297 constraints like NoOverlap. 2298 2299 Args: 2300 start: The start of the interval. It must be of the form a * var + b. 2301 size: The size of the interval. It must be an integer value. 2302 name: The name of the interval variable. 2303 2304 Returns: 2305 An `IntervalVar` object. 2306 """ 2307 size = cmh.assert_is_int64(size) 2308 start_expr = self.parse_linear_expression(start) 2309 size_expr = self.parse_linear_expression(size) 2310 end_expr = self.parse_linear_expression(start + size) 2311 if len(start_expr.vars) > 1: 2312 raise TypeError( 2313 "cp_model.new_interval_var: start must be affine or constant." 2314 ) 2315 return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name) 2316 2317 def new_fixed_size_interval_var_series( 2318 self, 2319 name: str, 2320 index: pd.Index, 2321 starts: Union[LinearExprT, pd.Series], 2322 sizes: Union[IntegralT, pd.Series], 2323 ) -> pd.Series: 2324 """Creates a series of interval variables with the given name. 2325 2326 Args: 2327 name (str): Required. The name of the variable set. 2328 index (pd.Index): Required. The index to use for the variable set. 2329 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2330 set. If a `pd.Series` is passed in, it will be based on the 2331 corresponding values of the pd.Series. 2332 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2333 the set. If a `pd.Series` is passed in, it will be based on the 2334 corresponding values of the pd.Series. 2335 2336 Returns: 2337 pd.Series: The interval variable set indexed by its corresponding 2338 dimensions. 2339 2340 Raises: 2341 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2342 ValueError: if the `name` is not a valid identifier or already exists. 2343 ValueError: if the all the indexes do not match. 2344 """ 2345 if not isinstance(index, pd.Index): 2346 raise TypeError("Non-index object is used as index") 2347 if not name.isidentifier(): 2348 raise ValueError("name={} is not a valid identifier".format(name)) 2349 2350 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2351 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2352 interval_array = [] 2353 for i in index: 2354 interval_array.append( 2355 self.new_fixed_size_interval_var( 2356 start=starts[i], 2357 size=sizes[i], 2358 name=f"{name}[{i}]", 2359 ) 2360 ) 2361 return pd.Series(index=index, data=interval_array) 2362 2363 def new_optional_interval_var( 2364 self, 2365 start: LinearExprT, 2366 size: LinearExprT, 2367 end: LinearExprT, 2368 is_present: LiteralT, 2369 name: str, 2370 ) -> IntervalVar: 2371 """Creates an optional interval var from start, size, end, and is_present. 2372 2373 An optional interval variable is a constraint, that is itself used in other 2374 constraints like NoOverlap. This constraint is protected by a presence 2375 literal that indicates if it is active or not. 2376 2377 Internally, it ensures that `is_present` implies `start + size == 2378 end`. 2379 2380 Args: 2381 start: The start of the interval. It must be of the form a * var + b. 2382 size: The size of the interval. It must be of the form a * var + b. 2383 end: The end of the interval. It must be of the form a * var + b. 2384 is_present: A literal that indicates if the interval is active or not. A 2385 inactive interval is simply ignored by all constraints. 2386 name: The name of the interval variable. 2387 2388 Returns: 2389 An `IntervalVar` object. 2390 """ 2391 2392 # add the linear constraint. 2393 lin = self.add(start + size == end).only_enforce_if(is_present) 2394 if name: 2395 lin.with_name("lin_opt_" + name) 2396 2397 # Creates the IntervalConstraintProto object. 2398 is_present_index = self.get_or_make_boolean_index(is_present) 2399 start_expr = self.parse_linear_expression(start) 2400 size_expr = self.parse_linear_expression(size) 2401 end_expr = self.parse_linear_expression(end) 2402 if len(start_expr.vars) > 1: 2403 raise TypeError( 2404 "cp_model.new_interval_var: start must be affine or constant." 2405 ) 2406 if len(size_expr.vars) > 1: 2407 raise TypeError( 2408 "cp_model.new_interval_var: size must be affine or constant." 2409 ) 2410 if len(end_expr.vars) > 1: 2411 raise TypeError( 2412 "cp_model.new_interval_var: end must be affine or constant." 2413 ) 2414 return IntervalVar( 2415 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2416 ) 2417 2418 def new_optional_interval_var_series( 2419 self, 2420 name: str, 2421 index: pd.Index, 2422 starts: Union[LinearExprT, pd.Series], 2423 sizes: Union[LinearExprT, pd.Series], 2424 ends: Union[LinearExprT, pd.Series], 2425 are_present: Union[LiteralT, pd.Series], 2426 ) -> pd.Series: 2427 """Creates a series of interval variables with the given name. 2428 2429 Args: 2430 name (str): Required. The name of the variable set. 2431 index (pd.Index): Required. The index to use for the variable set. 2432 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2433 set. If a `pd.Series` is passed in, it will be based on the 2434 corresponding values of the pd.Series. 2435 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2436 set. If a `pd.Series` is passed in, it will be based on the 2437 corresponding values of the pd.Series. 2438 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2439 set. If a `pd.Series` is passed in, it will be based on the 2440 corresponding values of the pd.Series. 2441 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2442 interval in the set. If a `pd.Series` is passed in, it will be based on 2443 the corresponding values of the pd.Series. 2444 2445 Returns: 2446 pd.Series: The interval variable set indexed by its corresponding 2447 dimensions. 2448 2449 Raises: 2450 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2451 ValueError: if the `name` is not a valid identifier or already exists. 2452 ValueError: if the all the indexes do not match. 2453 """ 2454 if not isinstance(index, pd.Index): 2455 raise TypeError("Non-index object is used as index") 2456 if not name.isidentifier(): 2457 raise ValueError("name={} is not a valid identifier".format(name)) 2458 2459 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2460 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2461 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2462 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2463 2464 interval_array = [] 2465 for i in index: 2466 interval_array.append( 2467 self.new_optional_interval_var( 2468 start=starts[i], 2469 size=sizes[i], 2470 end=ends[i], 2471 is_present=are_present[i], 2472 name=f"{name}[{i}]", 2473 ) 2474 ) 2475 return pd.Series(index=index, data=interval_array) 2476 2477 def new_optional_fixed_size_interval_var( 2478 self, 2479 start: LinearExprT, 2480 size: IntegralT, 2481 is_present: LiteralT, 2482 name: str, 2483 ) -> IntervalVar: 2484 """Creates an interval variable from start, and a fixed size. 2485 2486 An interval variable is a constraint, that is itself used in other 2487 constraints like NoOverlap. 2488 2489 Args: 2490 start: The start of the interval. It must be of the form a * var + b. 2491 size: The size of the interval. It must be an integer value. 2492 is_present: A literal that indicates if the interval is active or not. A 2493 inactive interval is simply ignored by all constraints. 2494 name: The name of the interval variable. 2495 2496 Returns: 2497 An `IntervalVar` object. 2498 """ 2499 size = cmh.assert_is_int64(size) 2500 start_expr = self.parse_linear_expression(start) 2501 size_expr = self.parse_linear_expression(size) 2502 end_expr = self.parse_linear_expression(start + size) 2503 if len(start_expr.vars) > 1: 2504 raise TypeError( 2505 "cp_model.new_interval_var: start must be affine or constant." 2506 ) 2507 is_present_index = self.get_or_make_boolean_index(is_present) 2508 return IntervalVar( 2509 self.__model, 2510 start_expr, 2511 size_expr, 2512 end_expr, 2513 is_present_index, 2514 name, 2515 ) 2516 2517 def new_optional_fixed_size_interval_var_series( 2518 self, 2519 name: str, 2520 index: pd.Index, 2521 starts: Union[LinearExprT, pd.Series], 2522 sizes: Union[IntegralT, pd.Series], 2523 are_present: Union[LiteralT, pd.Series], 2524 ) -> pd.Series: 2525 """Creates a series of interval variables with the given name. 2526 2527 Args: 2528 name (str): Required. The name of the variable set. 2529 index (pd.Index): Required. The index to use for the variable set. 2530 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2531 set. If a `pd.Series` is passed in, it will be based on the 2532 corresponding values of the pd.Series. 2533 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2534 the set. If a `pd.Series` is passed in, it will be based on the 2535 corresponding values of the pd.Series. 2536 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2537 interval in the set. If a `pd.Series` is passed in, it will be based on 2538 the corresponding values of the pd.Series. 2539 2540 Returns: 2541 pd.Series: The interval variable set indexed by its corresponding 2542 dimensions. 2543 2544 Raises: 2545 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2546 ValueError: if the `name` is not a valid identifier or already exists. 2547 ValueError: if the all the indexes do not match. 2548 """ 2549 if not isinstance(index, pd.Index): 2550 raise TypeError("Non-index object is used as index") 2551 if not name.isidentifier(): 2552 raise ValueError("name={} is not a valid identifier".format(name)) 2553 2554 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2555 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2556 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2557 interval_array = [] 2558 for i in index: 2559 interval_array.append( 2560 self.new_optional_fixed_size_interval_var( 2561 start=starts[i], 2562 size=sizes[i], 2563 is_present=are_present[i], 2564 name=f"{name}[{i}]", 2565 ) 2566 ) 2567 return pd.Series(index=index, data=interval_array) 2568 2569 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2570 """Adds NoOverlap(interval_vars). 2571 2572 A NoOverlap constraint ensures that all present intervals do not overlap 2573 in time. 2574 2575 Args: 2576 interval_vars: The list of interval variables to constrain. 2577 2578 Returns: 2579 An instance of the `Constraint` class. 2580 """ 2581 ct = Constraint(self) 2582 model_ct = self.__model.constraints[ct.index] 2583 model_ct.no_overlap.intervals.extend( 2584 [self.get_interval_index(x) for x in interval_vars] 2585 ) 2586 return ct 2587 2588 def add_no_overlap_2d( 2589 self, 2590 x_intervals: Iterable[IntervalVar], 2591 y_intervals: Iterable[IntervalVar], 2592 ) -> Constraint: 2593 """Adds NoOverlap2D(x_intervals, y_intervals). 2594 2595 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2596 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2597 by two intervals which represent its projection onto the X and Y axis. 2598 2599 Furthermore, one box is optional if at least one of the x or y interval is 2600 optional. 2601 2602 Args: 2603 x_intervals: The X coordinates of the rectangles. 2604 y_intervals: The Y coordinates of the rectangles. 2605 2606 Returns: 2607 An instance of the `Constraint` class. 2608 """ 2609 ct = Constraint(self) 2610 model_ct = self.__model.constraints[ct.index] 2611 model_ct.no_overlap_2d.x_intervals.extend( 2612 [self.get_interval_index(x) for x in x_intervals] 2613 ) 2614 model_ct.no_overlap_2d.y_intervals.extend( 2615 [self.get_interval_index(x) for x in y_intervals] 2616 ) 2617 return ct 2618 2619 def add_cumulative( 2620 self, 2621 intervals: Iterable[IntervalVar], 2622 demands: Iterable[LinearExprT], 2623 capacity: LinearExprT, 2624 ) -> Constraint: 2625 """Adds Cumulative(intervals, demands, capacity). 2626 2627 This constraint enforces that: 2628 2629 for all t: 2630 sum(demands[i] 2631 if (start(intervals[i]) <= t < end(intervals[i])) and 2632 (intervals[i] is present)) <= capacity 2633 2634 Args: 2635 intervals: The list of intervals. 2636 demands: The list of demands for each interval. Each demand must be >= 0. 2637 Each demand can be a 1-var affine expression (a * x + b). 2638 capacity: The maximum capacity of the cumulative constraint. It can be a 2639 1-var affine expression (a * x + b). 2640 2641 Returns: 2642 An instance of the `Constraint` class. 2643 """ 2644 cumulative = Constraint(self) 2645 model_ct = self.__model.constraints[cumulative.index] 2646 model_ct.cumulative.intervals.extend( 2647 [self.get_interval_index(x) for x in intervals] 2648 ) 2649 for d in demands: 2650 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2651 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2652 return cumulative 2653 2654 # Support for model cloning. 2655 def clone(self) -> "CpModel": 2656 """Reset the model, and creates a new one from a CpModelProto instance.""" 2657 clone = CpModel() 2658 clone.proto.CopyFrom(self.proto) 2659 clone.rebuild_constant_map() 2660 return clone 2661 2662 def rebuild_constant_map(self): 2663 """Internal method used during model cloning.""" 2664 for i, var in enumerate(self.__model.variables): 2665 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2666 self.__constant_map[var.domain[0]] = i 2667 2668 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2669 """Returns an already created Boolean variable from its index.""" 2670 if index < 0 or index >= len(self.__model.variables): 2671 raise ValueError( 2672 f"get_bool_var_from_proto_index: out of bound index {index}" 2673 ) 2674 var = self.__model.variables[index] 2675 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2676 raise ValueError( 2677 f"get_bool_var_from_proto_index: index {index} does not reference" 2678 + " a Boolean variable" 2679 ) 2680 2681 return IntVar(self.__model, index, None) 2682 2683 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2684 """Returns an already created integer variable from its index.""" 2685 if index < 0 or index >= len(self.__model.variables): 2686 raise ValueError( 2687 f"get_int_var_from_proto_index: out of bound index {index}" 2688 ) 2689 return IntVar(self.__model, index, None) 2690 2691 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2692 """Returns an already created interval variable from its index.""" 2693 if index < 0 or index >= len(self.__model.constraints): 2694 raise ValueError( 2695 f"get_interval_var_from_proto_index: out of bound index {index}" 2696 ) 2697 ct = self.__model.constraints[index] 2698 if not ct.HasField("interval"): 2699 raise ValueError( 2700 f"get_interval_var_from_proto_index: index {index} does not" 2701 " reference an" + " interval variable" 2702 ) 2703 2704 return IntervalVar(self.__model, index, None, None, None, None) 2705 2706 # Helpers. 2707 2708 def __str__(self): 2709 return str(self.__model) 2710 2711 @property 2712 def proto(self) -> cp_model_pb2.CpModelProto: 2713 """Returns the underlying CpModelProto.""" 2714 return self.__model 2715 2716 def negated(self, index: int) -> int: 2717 return -index - 1 2718 2719 def get_or_make_index(self, arg: VariableT) -> int: 2720 """Returns the index of a variable, its negation, or a number.""" 2721 if isinstance(arg, IntVar): 2722 return arg.index 2723 if ( 2724 isinstance(arg, _ProductCst) 2725 and isinstance(arg.expression(), IntVar) 2726 and arg.coefficient() == -1 2727 ): 2728 return -arg.expression().index - 1 2729 if isinstance(arg, numbers.Integral): 2730 arg = cmh.assert_is_int64(arg) 2731 return self.get_or_make_index_from_constant(arg) 2732 raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")") 2733 2734 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2735 """Returns an index from a boolean expression.""" 2736 if isinstance(arg, IntVar): 2737 self.assert_is_boolean_variable(arg) 2738 return arg.index 2739 if isinstance(arg, _NotBooleanVariable): 2740 self.assert_is_boolean_variable(arg.negated()) 2741 return arg.index 2742 if isinstance(arg, numbers.Integral): 2743 arg = cmh.assert_is_zero_or_one(arg) 2744 return self.get_or_make_index_from_constant(arg) 2745 if cmh.is_boolean(arg): 2746 return self.get_or_make_index_from_constant(int(arg)) 2747 raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})") 2748 2749 def get_interval_index(self, arg: IntervalVar) -> int: 2750 if not isinstance(arg, IntervalVar): 2751 raise TypeError("NotSupported: model.get_interval_index(%s)" % arg) 2752 return arg.index 2753 2754 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2755 if value in self.__constant_map: 2756 return self.__constant_map[value] 2757 index = len(self.__model.variables) 2758 self.__model.variables.add(domain=[value, value]) 2759 self.__constant_map[value] = index 2760 return index 2761 2762 def var_index_to_var_proto( 2763 self, var_index: int 2764 ) -> cp_model_pb2.IntegerVariableProto: 2765 if var_index >= 0: 2766 return self.__model.variables[var_index] 2767 else: 2768 return self.__model.variables[-var_index - 1] 2769 2770 def parse_linear_expression( 2771 self, linear_expr: LinearExprT, negate: bool = False 2772 ) -> cp_model_pb2.LinearExpressionProto: 2773 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2774 result: cp_model_pb2.LinearExpressionProto = ( 2775 cp_model_pb2.LinearExpressionProto() 2776 ) 2777 mult = -1 if negate else 1 2778 if isinstance(linear_expr, numbers.Integral): 2779 result.offset = int(linear_expr) * mult 2780 return result 2781 2782 if isinstance(linear_expr, IntVar): 2783 result.vars.append(self.get_or_make_index(linear_expr)) 2784 result.coeffs.append(mult) 2785 return result 2786 2787 coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map() 2788 result.offset = constant * mult 2789 for t in coeffs_map.items(): 2790 if not isinstance(t[0], IntVar): 2791 raise TypeError("Wrong argument" + str(t)) 2792 c = cmh.assert_is_int64(t[1]) 2793 result.vars.append(t[0].index) 2794 result.coeffs.append(c * mult) 2795 return result 2796 2797 def _set_objective(self, obj: ObjLinearExprT, minimize: bool): 2798 """Sets the objective of the model.""" 2799 self.clear_objective() 2800 if isinstance(obj, IntVar): 2801 self.__model.objective.coeffs.append(1) 2802 self.__model.objective.offset = 0 2803 if minimize: 2804 self.__model.objective.vars.append(obj.index) 2805 self.__model.objective.scaling_factor = 1 2806 else: 2807 self.__model.objective.vars.append(self.negated(obj.index)) 2808 self.__model.objective.scaling_factor = -1 2809 elif isinstance(obj, LinearExpr): 2810 coeffs_map, constant, is_integer = obj.get_float_var_value_map() 2811 if is_integer: 2812 if minimize: 2813 self.__model.objective.scaling_factor = 1 2814 self.__model.objective.offset = constant 2815 else: 2816 self.__model.objective.scaling_factor = -1 2817 self.__model.objective.offset = -constant 2818 for v, c in coeffs_map.items(): 2819 self.__model.objective.coeffs.append(c) 2820 if minimize: 2821 self.__model.objective.vars.append(v.index) 2822 else: 2823 self.__model.objective.vars.append(self.negated(v.index)) 2824 else: 2825 self.__model.floating_point_objective.maximize = not minimize 2826 self.__model.floating_point_objective.offset = constant 2827 for v, c in coeffs_map.items(): 2828 self.__model.floating_point_objective.coeffs.append(c) 2829 self.__model.floating_point_objective.vars.append(v.index) 2830 elif isinstance(obj, numbers.Integral): 2831 self.__model.objective.offset = int(obj) 2832 self.__model.objective.scaling_factor = 1 2833 else: 2834 raise TypeError("TypeError: " + str(obj) + " is not a valid objective") 2835 2836 def minimize(self, obj: ObjLinearExprT): 2837 """Sets the objective of the model to minimize(obj).""" 2838 self._set_objective(obj, minimize=True) 2839 2840 def maximize(self, obj: ObjLinearExprT): 2841 """Sets the objective of the model to maximize(obj).""" 2842 self._set_objective(obj, minimize=False) 2843 2844 def has_objective(self) -> bool: 2845 return self.__model.HasField("objective") or self.__model.HasField( 2846 "floating_point_objective" 2847 ) 2848 2849 def clear_objective(self): 2850 self.__model.ClearField("objective") 2851 self.__model.ClearField("floating_point_objective") 2852 2853 def add_decision_strategy( 2854 self, 2855 variables: Sequence[IntVar], 2856 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2857 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2858 ) -> None: 2859 """Adds a search strategy to the model. 2860 2861 Args: 2862 variables: a list of variables this strategy will assign. 2863 var_strategy: heuristic to choose the next variable to assign. 2864 domain_strategy: heuristic to reduce the domain of the selected variable. 2865 Currently, this is advanced code: the union of all strategies added to 2866 the model must be complete, i.e. instantiates all variables. Otherwise, 2867 solve() will fail. 2868 """ 2869 2870 strategy = self.__model.search_strategy.add() 2871 for v in variables: 2872 expr = strategy.exprs.add() 2873 if v.index >= 0: 2874 expr.vars.append(v.index) 2875 expr.coeffs.append(1) 2876 else: 2877 expr.vars.append(self.negated(v.index)) 2878 expr.coeffs.append(-1) 2879 expr.offset = 1 2880 2881 strategy.variable_selection_strategy = var_strategy 2882 strategy.domain_reduction_strategy = domain_strategy 2883 2884 def model_stats(self) -> str: 2885 """Returns a string containing some model statistics.""" 2886 return swig_helper.CpSatHelper.model_stats(self.__model) 2887 2888 def validate(self) -> str: 2889 """Returns a string indicating that the model is invalid.""" 2890 return swig_helper.CpSatHelper.validate_model(self.__model) 2891 2892 def export_to_file(self, file: str) -> bool: 2893 """Write the model as a protocol buffer to 'file'. 2894 2895 Args: 2896 file: file to write the model to. If the filename ends with 'txt', the 2897 model will be written as a text file, otherwise, the binary format will 2898 be used. 2899 2900 Returns: 2901 True if the model was correctly written. 2902 """ 2903 return swig_helper.CpSatHelper.write_model_to_file(self.__model, file) 2904 2905 def add_hint(self, var: IntVar, value: int) -> None: 2906 """Adds 'var == value' as a hint to the solver.""" 2907 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2908 self.__model.solution_hint.values.append(value) 2909 2910 def clear_hints(self): 2911 """Removes any solution hint from the model.""" 2912 self.__model.ClearField("solution_hint") 2913 2914 def add_assumption(self, lit: LiteralT) -> None: 2915 """Adds the literal to the model as assumptions.""" 2916 self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) 2917 2918 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2919 """Adds the literals to the model as assumptions.""" 2920 for lit in literals: 2921 self.add_assumption(lit) 2922 2923 def clear_assumptions(self) -> None: 2924 """Removes all assumptions from the model.""" 2925 self.__model.ClearField("assumptions") 2926 2927 # Helpers. 2928 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2929 if isinstance(x, IntVar): 2930 var = self.__model.variables[x.index] 2931 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2932 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2933 elif not isinstance(x, _NotBooleanVariable): 2934 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2935 2936 # Compatibility with pre PEP8 2937 # pylint: disable=invalid-name 2938 2939 def Name(self) -> str: 2940 return self.name 2941 2942 def SetName(self, name: str) -> None: 2943 self.name = name 2944 2945 def Proto(self) -> cp_model_pb2.CpModelProto: 2946 return self.proto 2947 2948 NewIntVar = new_int_var 2949 NewIntVarFromDomain = new_int_var_from_domain 2950 NewBoolVar = new_bool_var 2951 NewConstant = new_constant 2952 NewIntVarSeries = new_int_var_series 2953 NewBoolVarSeries = new_bool_var_series 2954 AddLinearConstraint = add_linear_constraint 2955 AddLinearExpressionInDomain = add_linear_expression_in_domain 2956 Add = add 2957 AddAllDifferent = add_all_different 2958 AddElement = add_element 2959 AddCircuit = add_circuit 2960 AddMultipleCircuit = add_multiple_circuit 2961 AddAllowedAssignments = add_allowed_assignments 2962 AddForbiddenAssignments = add_forbidden_assignments 2963 AddAutomaton = add_automaton 2964 AddInverse = add_inverse 2965 AddReservoirConstraint = add_reservoir_constraint 2966 AddImplication = add_implication 2967 AddBoolOr = add_bool_or 2968 AddAtLeastOne = add_at_least_one 2969 AddAtMostOne = add_at_most_one 2970 AddExactlyOne = add_exactly_one 2971 AddBoolAnd = add_bool_and 2972 AddBoolXOr = add_bool_xor 2973 AddMinEquality = add_min_equality 2974 AddMaxEquality = add_max_equality 2975 AddDivisionEquality = add_division_equality 2976 AddAbsEquality = add_abs_equality 2977 AddModuloEquality = add_modulo_equality 2978 AddMultiplicationEquality = add_multiplication_equality 2979 NewIntervalVar = new_interval_var 2980 NewIntervalVarSeries = new_interval_var_series 2981 NewFixedSizedIntervalVar = new_fixed_size_interval_var 2982 NewOptionalIntervalVar = new_optional_interval_var 2983 NewOptionalIntervalVarSeries = new_optional_interval_var_series 2984 NewOptionalFixedSizedIntervalVar = new_optional_fixed_size_interval_var 2985 NewOptionalFixedSizedIntervalVarSeries = new_optional_fixed_size_interval_var_series 2986 AddNoOverlap = add_no_overlap 2987 AddNoOverlap2D = add_no_overlap_2d 2988 AddCumulative = add_cumulative 2989 Clone = clone 2990 GetBoolVarFromProtoIndex = get_bool_var_from_proto_index 2991 GetIntVarFromProtoIndex = get_int_var_from_proto_index 2992 GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index 2993 Minimize = minimize 2994 Maximize = maximize 2995 HasObjective = has_objective 2996 ClearObjective = clear_objective 2997 AddDecisionStrategy = add_decision_strategy 2998 ModelStats = model_stats 2999 Validate = validate 3000 ExportToFile = export_to_file 3001 AddHint = add_hint 3002 ClearHints = clear_hints 3003 AddAssumption = add_assumption 3004 AddAssumptions = add_assumptions 3005 ClearAssumptions = clear_assumptions 3006 3007 # 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.
1311 @property 1312 def name(self) -> str: 1313 """Returns the name of the model.""" 1314 if not self.__model or not self.__model.name: 1315 return "" 1316 return self.__model.name
Returns the name of the model.
1325 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1326 """Create an integer variable with domain [lb, ub]. 1327 1328 The CP-SAT solver is limited to integer variables. If you have fractional 1329 values, scale them up so that they become integers; if you have strings, 1330 encode them as integers. 1331 1332 Args: 1333 lb: Lower bound for the variable. 1334 ub: Upper bound for the variable. 1335 name: The name of the variable. 1336 1337 Returns: 1338 a variable whose domain is [lb, ub]. 1339 """ 1340 1341 return IntVar(self.__model, 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].
1343 def new_int_var_from_domain(self, domain: Domain, name: str) -> IntVar: 1344 """Create an integer variable from a domain. 1345 1346 A domain is a set of integers specified by a collection of intervals. 1347 For example, `model.new_int_var_from_domain(cp_model. 1348 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1349 1350 Args: 1351 domain: An instance of the Domain class. 1352 name: The name of the variable. 1353 1354 Returns: 1355 a variable whose domain is the given domain. 1356 """ 1357 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.
1359 def new_bool_var(self, name: str) -> IntVar: 1360 """Creates a 0-1 variable with the given name.""" 1361 return IntVar(self.__model, Domain(0, 1), name)
Creates a 0-1 variable with the given name.
1363 def new_constant(self, value: IntegralT) -> IntVar: 1364 """Declares a constant integer.""" 1365 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None)
Declares a constant integer.
1367 def new_int_var_series( 1368 self, 1369 name: str, 1370 index: pd.Index, 1371 lower_bounds: Union[IntegralT, pd.Series], 1372 upper_bounds: Union[IntegralT, pd.Series], 1373 ) -> pd.Series: 1374 """Creates a series of (scalar-valued) variables with the given name. 1375 1376 Args: 1377 name (str): Required. The name of the variable set. 1378 index (pd.Index): Required. The index to use for the variable set. 1379 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1380 set. If a `pd.Series` is passed in, it will be based on the 1381 corresponding values of the pd.Series. 1382 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1383 set. If a `pd.Series` is passed in, it will be based on the 1384 corresponding values of the pd.Series. 1385 1386 Returns: 1387 pd.Series: The variable set indexed by its corresponding dimensions. 1388 1389 Raises: 1390 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1391 ValueError: if the `name` is not a valid identifier or already exists. 1392 ValueError: if the `lowerbound` is greater than the `upperbound`. 1393 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1394 the input index. 1395 """ 1396 if not isinstance(index, pd.Index): 1397 raise TypeError("Non-index object is used as index") 1398 if not name.isidentifier(): 1399 raise ValueError("name={} is not a valid identifier".format(name)) 1400 if ( 1401 isinstance(lower_bounds, numbers.Integral) 1402 and isinstance(upper_bounds, numbers.Integral) 1403 and lower_bounds > upper_bounds 1404 ): 1405 raise ValueError( 1406 f"lower_bound={lower_bounds} is greater than" 1407 f" upper_bound={upper_bounds} for variable set={name}" 1408 ) 1409 1410 lower_bounds = _convert_to_integral_series_and_validate_index( 1411 lower_bounds, index 1412 ) 1413 upper_bounds = _convert_to_integral_series_and_validate_index( 1414 upper_bounds, index 1415 ) 1416 return pd.Series( 1417 index=index, 1418 data=[ 1419 # pylint: disable=g-complex-comprehension 1420 IntVar( 1421 model=self.__model, 1422 name=f"{name}[{i}]", 1423 domain=Domain(lower_bounds[i], upper_bounds[i]), 1424 ) 1425 for i in index 1426 ], 1427 )
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.
1429 def new_bool_var_series( 1430 self, 1431 name: str, 1432 index: pd.Index, 1433 ) -> pd.Series: 1434 """Creates a series of (scalar-valued) variables with the given name. 1435 1436 Args: 1437 name (str): Required. The name of the variable set. 1438 index (pd.Index): Required. The index to use for the variable set. 1439 1440 Returns: 1441 pd.Series: The variable set indexed by its corresponding dimensions. 1442 1443 Raises: 1444 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1445 ValueError: if the `name` is not a valid identifier or already exists. 1446 """ 1447 return self.new_int_var_series( 1448 name=name, index=index, lower_bounds=0, upper_bounds=1 1449 )
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.
1453 def add_linear_constraint( 1454 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1455 ) -> Constraint: 1456 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1457 return self.add_linear_expression_in_domain(linear_expr, Domain(lb, ub))
Adds the constraint: lb <= linear_expr <= ub
.
1459 def add_linear_expression_in_domain( 1460 self, linear_expr: LinearExprT, domain: Domain 1461 ) -> Constraint: 1462 """Adds the constraint: `linear_expr` in `domain`.""" 1463 if isinstance(linear_expr, LinearExpr): 1464 ct = Constraint(self) 1465 model_ct = self.__model.constraints[ct.index] 1466 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1467 for t in coeffs_map.items(): 1468 if not isinstance(t[0], IntVar): 1469 raise TypeError("Wrong argument" + str(t)) 1470 c = cmh.assert_is_int64(t[1]) 1471 model_ct.linear.vars.append(t[0].index) 1472 model_ct.linear.coeffs.append(c) 1473 model_ct.linear.domain.extend( 1474 [ 1475 cmh.capped_subtraction(x, constant) 1476 for x in domain.flattened_intervals() 1477 ] 1478 ) 1479 return ct 1480 if isinstance(linear_expr, numbers.Integral): 1481 if not domain.contains(int(linear_expr)): 1482 return self.add_bool_or([]) # Evaluate to false. 1483 else: 1484 return self.add_bool_and([]) # Evaluate to true. 1485 raise TypeError( 1486 "not supported: CpModel.add_linear_expression_in_domain(" 1487 + str(linear_expr) 1488 + " " 1489 + str(domain) 1490 + ")" 1491 )
Adds the constraint: linear_expr
in domain
.
1493 def add(self, ct: Union[BoundedLinearExpression, bool]) -> Constraint: 1494 """Adds a `BoundedLinearExpression` to the model. 1495 1496 Args: 1497 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1498 1499 Returns: 1500 An instance of the `Constraint` class. 1501 """ 1502 if isinstance(ct, BoundedLinearExpression): 1503 return self.add_linear_expression_in_domain( 1504 ct.expression(), Domain.from_flat_intervals(ct.bounds()) 1505 ) 1506 if ct and cmh.is_boolean(ct): 1507 return self.add_bool_or([True]) 1508 if not ct and cmh.is_boolean(ct): 1509 return self.add_bool_or([]) # Evaluate to false. 1510 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.
1522 def add_all_different(self, *expressions): 1523 """Adds AllDifferent(expressions). 1524 1525 This constraint forces all expressions to have different values. 1526 1527 Args: 1528 *expressions: simple expressions of the form a * var + constant. 1529 1530 Returns: 1531 An instance of the `Constraint` class. 1532 """ 1533 ct = Constraint(self) 1534 model_ct = self.__model.constraints[ct.index] 1535 expanded = expand_generator_or_tuple(expressions) 1536 model_ct.all_diff.exprs.extend( 1537 self.parse_linear_expression(x) for x in expanded 1538 ) 1539 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.
1541 def add_element( 1542 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1543 ) -> Constraint: 1544 """Adds the element constraint: `variables[index] == target`. 1545 1546 Args: 1547 index: The index of the variable that's being constrained. 1548 variables: A list of variables. 1549 target: The value that the variable must be equal to. 1550 1551 Returns: 1552 An instance of the `Constraint` class. 1553 """ 1554 1555 if not variables: 1556 raise ValueError("add_element expects a non-empty variables array") 1557 1558 if isinstance(index, numbers.Integral): 1559 return self.add(list(variables)[int(index)] == target) 1560 1561 ct = Constraint(self) 1562 model_ct = self.__model.constraints[ct.index] 1563 model_ct.element.index = self.get_or_make_index(index) 1564 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1565 model_ct.element.target = self.get_or_make_index(target) 1566 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.
1568 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1569 """Adds Circuit(arcs). 1570 1571 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1572 1573 A circuit is a unique Hamiltonian path in a subgraph of the total 1574 graph. In case a node 'i' is not in the path, then there must be a 1575 loop arc 'i -> i' associated with a true literal. Otherwise 1576 this constraint will fail. 1577 1578 Args: 1579 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1580 literal). The arc is selected in the circuit if the literal is true. 1581 Both source_node and destination_node must be integers between 0 and the 1582 number of nodes - 1. 1583 1584 Returns: 1585 An instance of the `Constraint` class. 1586 1587 Raises: 1588 ValueError: If the list of arcs is empty. 1589 """ 1590 if not arcs: 1591 raise ValueError("add_circuit expects a non-empty array of arcs") 1592 ct = Constraint(self) 1593 model_ct = self.__model.constraints[ct.index] 1594 for arc in arcs: 1595 tail = cmh.assert_is_int32(arc[0]) 1596 head = cmh.assert_is_int32(arc[1]) 1597 lit = self.get_or_make_boolean_index(arc[2]) 1598 model_ct.circuit.tails.append(tail) 1599 model_ct.circuit.heads.append(head) 1600 model_ct.circuit.literals.append(lit) 1601 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.
1603 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1604 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1605 1606 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1607 literals[i] is true must satisfy this set of properties: 1608 - #incoming arcs == 1 except for node 0. 1609 - #outgoing arcs == 1 except for node 0. 1610 - for node zero, #incoming arcs == #outgoing arcs. 1611 - There are no duplicate arcs. 1612 - Self-arcs are allowed except for node 0. 1613 - There is no cycle in this graph, except through node 0. 1614 1615 Args: 1616 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1617 literal). The arc is selected in the circuit if the literal is true. 1618 Both source_node and destination_node must be integers between 0 and the 1619 number of nodes - 1. 1620 1621 Returns: 1622 An instance of the `Constraint` class. 1623 1624 Raises: 1625 ValueError: If the list of arcs is empty. 1626 """ 1627 if not arcs: 1628 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1629 ct = Constraint(self) 1630 model_ct = self.__model.constraints[ct.index] 1631 for arc in arcs: 1632 tail = cmh.assert_is_int32(arc[0]) 1633 head = cmh.assert_is_int32(arc[1]) 1634 lit = self.get_or_make_boolean_index(arc[2]) 1635 model_ct.routes.tails.append(tail) 1636 model_ct.routes.heads.append(head) 1637 model_ct.routes.literals.append(lit) 1638 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.
1640 def add_allowed_assignments( 1641 self, 1642 variables: Sequence[VariableT], 1643 tuples_list: Iterable[Sequence[IntegralT]], 1644 ) -> Constraint: 1645 """Adds AllowedAssignments(variables, tuples_list). 1646 1647 An AllowedAssignments constraint is a constraint on an array of variables, 1648 which requires that when all variables are assigned values, the resulting 1649 array equals one of the tuples in `tuple_list`. 1650 1651 Args: 1652 variables: A list of variables. 1653 tuples_list: A list of admissible tuples. Each tuple must have the same 1654 length as the variables, and the ith value of a tuple corresponds to the 1655 ith variable. 1656 1657 Returns: 1658 An instance of the `Constraint` class. 1659 1660 Raises: 1661 TypeError: If a tuple does not have the same size as the list of 1662 variables. 1663 ValueError: If the array of variables is empty. 1664 """ 1665 1666 if not variables: 1667 raise ValueError( 1668 "add_allowed_assignments expects a non-empty variables array" 1669 ) 1670 1671 ct = Constraint(self) 1672 model_ct = self.__model.constraints[ct.index] 1673 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1674 arity = len(variables) 1675 for t in tuples_list: 1676 if len(t) != arity: 1677 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1678 ar = [] 1679 for v in t: 1680 ar.append(cmh.assert_is_int64(v)) 1681 model_ct.table.values.extend(ar) 1682 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.
1684 def add_forbidden_assignments( 1685 self, 1686 variables: Sequence[VariableT], 1687 tuples_list: Iterable[Sequence[IntegralT]], 1688 ) -> Constraint: 1689 """Adds add_forbidden_assignments(variables, [tuples_list]). 1690 1691 A ForbiddenAssignments constraint is a constraint on an array of variables 1692 where the list of impossible combinations is provided in the tuples list. 1693 1694 Args: 1695 variables: A list of variables. 1696 tuples_list: A list of forbidden tuples. Each tuple must have the same 1697 length as the variables, and the *i*th value of a tuple corresponds to 1698 the *i*th variable. 1699 1700 Returns: 1701 An instance of the `Constraint` class. 1702 1703 Raises: 1704 TypeError: If a tuple does not have the same size as the list of 1705 variables. 1706 ValueError: If the array of variables is empty. 1707 """ 1708 1709 if not variables: 1710 raise ValueError( 1711 "add_forbidden_assignments expects a non-empty variables array" 1712 ) 1713 1714 index = len(self.__model.constraints) 1715 ct = self.add_allowed_assignments(variables, tuples_list) 1716 self.__model.constraints[index].table.negated = True 1717 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.
1719 def add_automaton( 1720 self, 1721 transition_variables: Sequence[VariableT], 1722 starting_state: IntegralT, 1723 final_states: Sequence[IntegralT], 1724 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1725 ) -> Constraint: 1726 """Adds an automaton constraint. 1727 1728 An automaton constraint takes a list of variables (of size *n*), an initial 1729 state, a set of final states, and a set of transitions. A transition is a 1730 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1731 and *transition* is the label of an arc from *head* to *tail*, 1732 corresponding to the value of one variable in the list of variables. 1733 1734 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1735 contains the possible states of the automaton. The first state contains the 1736 initial state. The last phase contains the final states. 1737 1738 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1739 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1740 an arc from the state *tail* of phase *i* and the state *head* of phase 1741 *i* + 1. This arc is labeled by the value *transition* of the variables 1742 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1743 is assigned the value *transition*. 1744 1745 A feasible solution of this constraint is an assignment of variables such 1746 that, starting from the initial state in phase 0, there is a path labeled by 1747 the values of the variables that ends in one of the final states in the 1748 final phase. 1749 1750 Args: 1751 transition_variables: A non-empty list of variables whose values 1752 correspond to the labels of the arcs traversed by the automaton. 1753 starting_state: The initial state of the automaton. 1754 final_states: A non-empty list of admissible final states. 1755 transition_triples: A list of transitions for the automaton, in the 1756 following format (current_state, variable_value, next_state). 1757 1758 Returns: 1759 An instance of the `Constraint` class. 1760 1761 Raises: 1762 ValueError: if `transition_variables`, `final_states`, or 1763 `transition_triples` are empty. 1764 """ 1765 1766 if not transition_variables: 1767 raise ValueError( 1768 "add_automaton expects a non-empty transition_variables array" 1769 ) 1770 if not final_states: 1771 raise ValueError("add_automaton expects some final states") 1772 1773 if not transition_triples: 1774 raise ValueError("add_automaton expects some transition triples") 1775 1776 ct = Constraint(self) 1777 model_ct = self.__model.constraints[ct.index] 1778 model_ct.automaton.vars.extend( 1779 [self.get_or_make_index(x) for x in transition_variables] 1780 ) 1781 starting_state = cmh.assert_is_int64(starting_state) 1782 model_ct.automaton.starting_state = starting_state 1783 for v in final_states: 1784 v = cmh.assert_is_int64(v) 1785 model_ct.automaton.final_states.append(v) 1786 for t in transition_triples: 1787 if len(t) != 3: 1788 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1789 tail = cmh.assert_is_int64(t[0]) 1790 label = cmh.assert_is_int64(t[1]) 1791 head = cmh.assert_is_int64(t[2]) 1792 model_ct.automaton.transition_tail.append(tail) 1793 model_ct.automaton.transition_label.append(label) 1794 model_ct.automaton.transition_head.append(head) 1795 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.
1797 def add_inverse( 1798 self, 1799 variables: Sequence[VariableT], 1800 inverse_variables: Sequence[VariableT], 1801 ) -> Constraint: 1802 """Adds Inverse(variables, inverse_variables). 1803 1804 An inverse constraint enforces that if `variables[i]` is assigned a value 1805 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1806 1807 Args: 1808 variables: An array of integer variables. 1809 inverse_variables: An array of integer variables. 1810 1811 Returns: 1812 An instance of the `Constraint` class. 1813 1814 Raises: 1815 TypeError: if variables and inverse_variables have different lengths, or 1816 if they are empty. 1817 """ 1818 1819 if not variables or not inverse_variables: 1820 raise TypeError("The Inverse constraint does not accept empty arrays") 1821 if len(variables) != len(inverse_variables): 1822 raise TypeError( 1823 "In the inverse constraint, the two array variables and" 1824 " inverse_variables must have the same length." 1825 ) 1826 ct = Constraint(self) 1827 model_ct = self.__model.constraints[ct.index] 1828 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1829 model_ct.inverse.f_inverse.extend( 1830 [self.get_or_make_index(x) for x in inverse_variables] 1831 ) 1832 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.
1834 def add_reservoir_constraint( 1835 self, 1836 times: Iterable[LinearExprT], 1837 level_changes: Iterable[LinearExprT], 1838 min_level: int, 1839 max_level: int, 1840 ) -> Constraint: 1841 """Adds Reservoir(times, level_changes, min_level, max_level). 1842 1843 Maintains a reservoir level within bounds. The water level starts at 0, and 1844 at any time, it must be between min_level and max_level. 1845 1846 If the affine expression `times[i]` is assigned a value t, then the current 1847 level changes by `level_changes[i]`, which is constant, at time t. 1848 1849 Note that min level must be <= 0, and the max level must be >= 0. Please 1850 use fixed level_changes to simulate initial state. 1851 1852 Therefore, at any time: 1853 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1854 1855 Args: 1856 times: A list of 1-var affine expressions (a * x + b) which specify the 1857 time of the filling or emptying the reservoir. 1858 level_changes: A list of integer values that specifies the amount of the 1859 emptying or filling. Currently, variable demands are not supported. 1860 min_level: At any time, the level of the reservoir must be greater or 1861 equal than the min level. 1862 max_level: At any time, the level of the reservoir must be less or equal 1863 than the max level. 1864 1865 Returns: 1866 An instance of the `Constraint` class. 1867 1868 Raises: 1869 ValueError: if max_level < min_level. 1870 1871 ValueError: if max_level < 0. 1872 1873 ValueError: if min_level > 0 1874 """ 1875 1876 if max_level < min_level: 1877 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1878 1879 if max_level < 0: 1880 raise ValueError("Reservoir constraint must have a max_level >= 0") 1881 1882 if min_level > 0: 1883 raise ValueError("Reservoir constraint must have a min_level <= 0") 1884 1885 ct = Constraint(self) 1886 model_ct = self.__model.constraints[ct.index] 1887 model_ct.reservoir.time_exprs.extend( 1888 [self.parse_linear_expression(x) for x in times] 1889 ) 1890 model_ct.reservoir.level_changes.extend( 1891 [self.parse_linear_expression(x) for x in level_changes] 1892 ) 1893 model_ct.reservoir.min_level = min_level 1894 model_ct.reservoir.max_level = max_level 1895 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
1897 def add_reservoir_constraint_with_active( 1898 self, 1899 times: Iterable[LinearExprT], 1900 level_changes: Iterable[LinearExprT], 1901 actives: Iterable[LiteralT], 1902 min_level: int, 1903 max_level: int, 1904 ) -> Constraint: 1905 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1906 1907 Maintains a reservoir level within bounds. The water level starts at 0, and 1908 at any time, it must be between min_level and max_level. 1909 1910 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1911 `True`, then the current level changes by `level_changes[i]`, which is 1912 constant, 1913 at time t. 1914 1915 Note that min level must be <= 0, and the max level must be >= 0. Please 1916 use fixed level_changes to simulate initial state. 1917 1918 Therefore, at any time: 1919 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1920 max_level] 1921 1922 1923 The array of boolean variables 'actives', if defined, indicates which 1924 actions are actually performed. 1925 1926 Args: 1927 times: A list of 1-var affine expressions (a * x + b) which specify the 1928 time of the filling or emptying the reservoir. 1929 level_changes: A list of integer values that specifies the amount of the 1930 emptying or filling. Currently, variable demands are not supported. 1931 actives: a list of boolean variables. They indicates if the 1932 emptying/refilling events actually take place. 1933 min_level: At any time, the level of the reservoir must be greater or 1934 equal than the min level. 1935 max_level: At any time, the level of the reservoir must be less or equal 1936 than the max level. 1937 1938 Returns: 1939 An instance of the `Constraint` class. 1940 1941 Raises: 1942 ValueError: if max_level < min_level. 1943 1944 ValueError: if max_level < 0. 1945 1946 ValueError: if min_level > 0 1947 """ 1948 1949 if max_level < min_level: 1950 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1951 1952 if max_level < 0: 1953 raise ValueError("Reservoir constraint must have a max_level >= 0") 1954 1955 if min_level > 0: 1956 raise ValueError("Reservoir constraint must have a min_level <= 0") 1957 1958 ct = Constraint(self) 1959 model_ct = self.__model.constraints[ct.index] 1960 model_ct.reservoir.time_exprs.extend( 1961 [self.parse_linear_expression(x) for x in times] 1962 ) 1963 model_ct.reservoir.level_changes.extend( 1964 [self.parse_linear_expression(x) for x in level_changes] 1965 ) 1966 model_ct.reservoir.active_literals.extend( 1967 [self.get_or_make_boolean_index(x) for x in actives] 1968 ) 1969 model_ct.reservoir.min_level = min_level 1970 model_ct.reservoir.max_level = max_level 1971 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
1973 def add_map_domain( 1974 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 1975 ): 1976 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 1977 1978 for i, bool_var in enumerate(bool_var_array): 1979 b_index = bool_var.index 1980 var_index = var.index 1981 model_ct = self.__model.constraints.add() 1982 model_ct.linear.vars.append(var_index) 1983 model_ct.linear.coeffs.append(1) 1984 model_ct.linear.domain.extend([offset + i, offset + i]) 1985 model_ct.enforcement_literal.append(b_index) 1986 1987 model_ct = self.__model.constraints.add() 1988 model_ct.linear.vars.append(var_index) 1989 model_ct.linear.coeffs.append(1) 1990 model_ct.enforcement_literal.append(-b_index - 1) 1991 if offset + i - 1 >= INT_MIN: 1992 model_ct.linear.domain.extend([INT_MIN, offset + i - 1]) 1993 if offset + i + 1 <= INT_MAX: 1994 model_ct.linear.domain.extend([offset + i + 1, INT_MAX])
Adds var == i + offset <=> bool_var_array[i] == true for all i
.
1996 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1997 """Adds `a => b` (`a` implies `b`).""" 1998 ct = Constraint(self) 1999 model_ct = self.__model.constraints[ct.index] 2000 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2001 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2002 return ct
Adds a => b
(a
implies b
).
2012 def add_bool_or(self, *literals): 2013 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2014 ct = Constraint(self) 2015 model_ct = self.__model.constraints[ct.index] 2016 model_ct.bool_or.literals.extend( 2017 [ 2018 self.get_or_make_boolean_index(x) 2019 for x in expand_generator_or_tuple(literals) 2020 ] 2021 ) 2022 return ct
Adds Or(literals) == true
: sum(literals) >= 1.
2032 def add_at_least_one(self, *literals): 2033 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2034 return self.add_bool_or(*literals)
Same as add_bool_or
: sum(literals) >= 1
.
2044 def add_at_most_one(self, *literals): 2045 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2046 ct = Constraint(self) 2047 model_ct = self.__model.constraints[ct.index] 2048 model_ct.at_most_one.literals.extend( 2049 [ 2050 self.get_or_make_boolean_index(x) 2051 for x in expand_generator_or_tuple(literals) 2052 ] 2053 ) 2054 return ct
Adds AtMostOne(literals)
: sum(literals) <= 1
.
2064 def add_exactly_one(self, *literals): 2065 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2066 ct = Constraint(self) 2067 model_ct = self.__model.constraints[ct.index] 2068 model_ct.exactly_one.literals.extend( 2069 [ 2070 self.get_or_make_boolean_index(x) 2071 for x in expand_generator_or_tuple(literals) 2072 ] 2073 ) 2074 return ct
Adds ExactlyOne(literals)
: sum(literals) == 1
.
2084 def add_bool_and(self, *literals): 2085 """Adds `And(literals) == true`.""" 2086 ct = Constraint(self) 2087 model_ct = self.__model.constraints[ct.index] 2088 model_ct.bool_and.literals.extend( 2089 [ 2090 self.get_or_make_boolean_index(x) 2091 for x in expand_generator_or_tuple(literals) 2092 ] 2093 ) 2094 return ct
Adds And(literals) == true
.
2104 def add_bool_xor(self, *literals): 2105 """Adds `XOr(literals) == true`. 2106 2107 In contrast to add_bool_or and add_bool_and, it does not support 2108 .only_enforce_if(). 2109 2110 Args: 2111 *literals: the list of literals in the constraint. 2112 2113 Returns: 2114 An `Constraint` object. 2115 """ 2116 ct = Constraint(self) 2117 model_ct = self.__model.constraints[ct.index] 2118 model_ct.bool_xor.literals.extend( 2119 [ 2120 self.get_or_make_boolean_index(x) 2121 for x in expand_generator_or_tuple(literals) 2122 ] 2123 ) 2124 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.
2126 def add_min_equality( 2127 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2128 ) -> Constraint: 2129 """Adds `target == Min(exprs)`.""" 2130 ct = Constraint(self) 2131 model_ct = self.__model.constraints[ct.index] 2132 model_ct.lin_max.exprs.extend( 2133 [self.parse_linear_expression(x, True) for x in exprs] 2134 ) 2135 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2136 return ct
Adds target == Min(exprs)
.
2138 def add_max_equality( 2139 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2140 ) -> Constraint: 2141 """Adds `target == Max(exprs)`.""" 2142 ct = Constraint(self) 2143 model_ct = self.__model.constraints[ct.index] 2144 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2145 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2146 return ct
Adds target == Max(exprs)
.
2148 def add_division_equality( 2149 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2150 ) -> Constraint: 2151 """Adds `target == num // denom` (integer division rounded towards 0).""" 2152 ct = Constraint(self) 2153 model_ct = self.__model.constraints[ct.index] 2154 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2155 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2156 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2157 return ct
Adds target == num // denom
(integer division rounded towards 0).
2159 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2160 """Adds `target == Abs(expr)`.""" 2161 ct = Constraint(self) 2162 model_ct = self.__model.constraints[ct.index] 2163 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2164 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2165 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2166 return ct
Adds target == Abs(expr)
.
2168 def add_modulo_equality( 2169 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2170 ) -> Constraint: 2171 """Adds `target = expr % mod`.""" 2172 ct = Constraint(self) 2173 model_ct = self.__model.constraints[ct.index] 2174 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2175 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2176 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2177 return ct
Adds target = expr % mod
.
2179 def add_multiplication_equality( 2180 self, 2181 target: LinearExprT, 2182 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2183 ) -> Constraint: 2184 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2185 ct = Constraint(self) 2186 model_ct = self.__model.constraints[ct.index] 2187 model_ct.int_prod.exprs.extend( 2188 [ 2189 self.parse_linear_expression(expr) 2190 for expr in expand_generator_or_tuple(expressions) 2191 ] 2192 ) 2193 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2194 return ct
Adds target == expressions[0] * .. * expressions[n]
.
2198 def new_interval_var( 2199 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2200 ) -> IntervalVar: 2201 """Creates an interval variable from start, size, and end. 2202 2203 An interval variable is a constraint, that is itself used in other 2204 constraints like NoOverlap. 2205 2206 Internally, it ensures that `start + size == end`. 2207 2208 Args: 2209 start: The start of the interval. It must be of the form a * var + b. 2210 size: The size of the interval. It must be of the form a * var + b. 2211 end: The end of the interval. It must be of the form a * var + b. 2212 name: The name of the interval variable. 2213 2214 Returns: 2215 An `IntervalVar` object. 2216 """ 2217 2218 lin = self.add(start + size == end) 2219 if name: 2220 lin.with_name("lin_" + name) 2221 2222 start_expr = self.parse_linear_expression(start) 2223 size_expr = self.parse_linear_expression(size) 2224 end_expr = self.parse_linear_expression(end) 2225 if len(start_expr.vars) > 1: 2226 raise TypeError( 2227 "cp_model.new_interval_var: start must be 1-var affine or constant." 2228 ) 2229 if len(size_expr.vars) > 1: 2230 raise TypeError( 2231 "cp_model.new_interval_var: size must be 1-var affine or constant." 2232 ) 2233 if len(end_expr.vars) > 1: 2234 raise TypeError( 2235 "cp_model.new_interval_var: end must be 1-var affine or constant." 2236 ) 2237 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.
2239 def new_interval_var_series( 2240 self, 2241 name: str, 2242 index: pd.Index, 2243 starts: Union[LinearExprT, pd.Series], 2244 sizes: Union[LinearExprT, pd.Series], 2245 ends: Union[LinearExprT, pd.Series], 2246 ) -> pd.Series: 2247 """Creates a series of interval variables with the given name. 2248 2249 Args: 2250 name (str): Required. The name of the variable set. 2251 index (pd.Index): Required. The index to use for the variable set. 2252 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2253 set. If a `pd.Series` is passed in, it will be based on the 2254 corresponding values of the pd.Series. 2255 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2256 set. If a `pd.Series` is passed in, it will be based on the 2257 corresponding values of the pd.Series. 2258 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2259 set. If a `pd.Series` is passed in, it will be based on the 2260 corresponding values of the pd.Series. 2261 2262 Returns: 2263 pd.Series: The interval variable set indexed by its corresponding 2264 dimensions. 2265 2266 Raises: 2267 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2268 ValueError: if the `name` is not a valid identifier or already exists. 2269 ValueError: if the all the indexes do not match. 2270 """ 2271 if not isinstance(index, pd.Index): 2272 raise TypeError("Non-index object is used as index") 2273 if not name.isidentifier(): 2274 raise ValueError("name={} is not a valid identifier".format(name)) 2275 2276 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2277 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2278 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2279 interval_array = [] 2280 for i in index: 2281 interval_array.append( 2282 self.new_interval_var( 2283 start=starts[i], 2284 size=sizes[i], 2285 end=ends[i], 2286 name=f"{name}[{i}]", 2287 ) 2288 ) 2289 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.
2291 def new_fixed_size_interval_var( 2292 self, start: LinearExprT, size: IntegralT, name: str 2293 ) -> IntervalVar: 2294 """Creates an interval variable from start, and a fixed size. 2295 2296 An interval variable is a constraint, that is itself used in other 2297 constraints like NoOverlap. 2298 2299 Args: 2300 start: The start of the interval. It must be of the form a * var + b. 2301 size: The size of the interval. It must be an integer value. 2302 name: The name of the interval variable. 2303 2304 Returns: 2305 An `IntervalVar` object. 2306 """ 2307 size = cmh.assert_is_int64(size) 2308 start_expr = self.parse_linear_expression(start) 2309 size_expr = self.parse_linear_expression(size) 2310 end_expr = self.parse_linear_expression(start + size) 2311 if len(start_expr.vars) > 1: 2312 raise TypeError( 2313 "cp_model.new_interval_var: start must be affine or constant." 2314 ) 2315 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.
2317 def new_fixed_size_interval_var_series( 2318 self, 2319 name: str, 2320 index: pd.Index, 2321 starts: Union[LinearExprT, pd.Series], 2322 sizes: Union[IntegralT, pd.Series], 2323 ) -> pd.Series: 2324 """Creates a series of interval variables with the given name. 2325 2326 Args: 2327 name (str): Required. The name of the variable set. 2328 index (pd.Index): Required. The index to use for the variable set. 2329 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2330 set. If a `pd.Series` is passed in, it will be based on the 2331 corresponding values of the pd.Series. 2332 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2333 the set. If a `pd.Series` is passed in, it will be based on the 2334 corresponding values of the pd.Series. 2335 2336 Returns: 2337 pd.Series: The interval variable set indexed by its corresponding 2338 dimensions. 2339 2340 Raises: 2341 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2342 ValueError: if the `name` is not a valid identifier or already exists. 2343 ValueError: if the all the indexes do not match. 2344 """ 2345 if not isinstance(index, pd.Index): 2346 raise TypeError("Non-index object is used as index") 2347 if not name.isidentifier(): 2348 raise ValueError("name={} is not a valid identifier".format(name)) 2349 2350 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2351 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2352 interval_array = [] 2353 for i in index: 2354 interval_array.append( 2355 self.new_fixed_size_interval_var( 2356 start=starts[i], 2357 size=sizes[i], 2358 name=f"{name}[{i}]", 2359 ) 2360 ) 2361 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.
2363 def new_optional_interval_var( 2364 self, 2365 start: LinearExprT, 2366 size: LinearExprT, 2367 end: LinearExprT, 2368 is_present: LiteralT, 2369 name: str, 2370 ) -> IntervalVar: 2371 """Creates an optional interval var from start, size, end, and is_present. 2372 2373 An optional interval variable is a constraint, that is itself used in other 2374 constraints like NoOverlap. This constraint is protected by a presence 2375 literal that indicates if it is active or not. 2376 2377 Internally, it ensures that `is_present` implies `start + size == 2378 end`. 2379 2380 Args: 2381 start: The start of the interval. It must be of the form a * var + b. 2382 size: The size of the interval. It must be of the form a * var + b. 2383 end: The end of the interval. It must be of the form a * var + b. 2384 is_present: A literal that indicates if the interval is active or not. A 2385 inactive interval is simply ignored by all constraints. 2386 name: The name of the interval variable. 2387 2388 Returns: 2389 An `IntervalVar` object. 2390 """ 2391 2392 # add the linear constraint. 2393 lin = self.add(start + size == end).only_enforce_if(is_present) 2394 if name: 2395 lin.with_name("lin_opt_" + name) 2396 2397 # Creates the IntervalConstraintProto object. 2398 is_present_index = self.get_or_make_boolean_index(is_present) 2399 start_expr = self.parse_linear_expression(start) 2400 size_expr = self.parse_linear_expression(size) 2401 end_expr = self.parse_linear_expression(end) 2402 if len(start_expr.vars) > 1: 2403 raise TypeError( 2404 "cp_model.new_interval_var: start must be affine or constant." 2405 ) 2406 if len(size_expr.vars) > 1: 2407 raise TypeError( 2408 "cp_model.new_interval_var: size must be affine or constant." 2409 ) 2410 if len(end_expr.vars) > 1: 2411 raise TypeError( 2412 "cp_model.new_interval_var: end must be affine or constant." 2413 ) 2414 return IntervalVar( 2415 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2416 )
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.
2418 def new_optional_interval_var_series( 2419 self, 2420 name: str, 2421 index: pd.Index, 2422 starts: Union[LinearExprT, pd.Series], 2423 sizes: Union[LinearExprT, pd.Series], 2424 ends: Union[LinearExprT, pd.Series], 2425 are_present: Union[LiteralT, pd.Series], 2426 ) -> pd.Series: 2427 """Creates a series of interval variables with the given name. 2428 2429 Args: 2430 name (str): Required. The name of the variable set. 2431 index (pd.Index): Required. The index to use for the variable set. 2432 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2433 set. If a `pd.Series` is passed in, it will be based on the 2434 corresponding values of the pd.Series. 2435 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2436 set. If a `pd.Series` is passed in, it will be based on the 2437 corresponding values of the pd.Series. 2438 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2439 set. If a `pd.Series` is passed in, it will be based on the 2440 corresponding values of the pd.Series. 2441 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2442 interval in the set. If a `pd.Series` is passed in, it will be based on 2443 the corresponding values of the pd.Series. 2444 2445 Returns: 2446 pd.Series: The interval variable set indexed by its corresponding 2447 dimensions. 2448 2449 Raises: 2450 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2451 ValueError: if the `name` is not a valid identifier or already exists. 2452 ValueError: if the all the indexes do not match. 2453 """ 2454 if not isinstance(index, pd.Index): 2455 raise TypeError("Non-index object is used as index") 2456 if not name.isidentifier(): 2457 raise ValueError("name={} is not a valid identifier".format(name)) 2458 2459 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2460 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2461 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2462 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2463 2464 interval_array = [] 2465 for i in index: 2466 interval_array.append( 2467 self.new_optional_interval_var( 2468 start=starts[i], 2469 size=sizes[i], 2470 end=ends[i], 2471 is_present=are_present[i], 2472 name=f"{name}[{i}]", 2473 ) 2474 ) 2475 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.
2477 def new_optional_fixed_size_interval_var( 2478 self, 2479 start: LinearExprT, 2480 size: IntegralT, 2481 is_present: LiteralT, 2482 name: str, 2483 ) -> IntervalVar: 2484 """Creates an interval variable from start, and a fixed size. 2485 2486 An interval variable is a constraint, that is itself used in other 2487 constraints like NoOverlap. 2488 2489 Args: 2490 start: The start of the interval. It must be of the form a * var + b. 2491 size: The size of the interval. It must be an integer value. 2492 is_present: A literal that indicates if the interval is active or not. A 2493 inactive interval is simply ignored by all constraints. 2494 name: The name of the interval variable. 2495 2496 Returns: 2497 An `IntervalVar` object. 2498 """ 2499 size = cmh.assert_is_int64(size) 2500 start_expr = self.parse_linear_expression(start) 2501 size_expr = self.parse_linear_expression(size) 2502 end_expr = self.parse_linear_expression(start + size) 2503 if len(start_expr.vars) > 1: 2504 raise TypeError( 2505 "cp_model.new_interval_var: start must be affine or constant." 2506 ) 2507 is_present_index = self.get_or_make_boolean_index(is_present) 2508 return IntervalVar( 2509 self.__model, 2510 start_expr, 2511 size_expr, 2512 end_expr, 2513 is_present_index, 2514 name, 2515 )
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.
2517 def new_optional_fixed_size_interval_var_series( 2518 self, 2519 name: str, 2520 index: pd.Index, 2521 starts: Union[LinearExprT, pd.Series], 2522 sizes: Union[IntegralT, pd.Series], 2523 are_present: Union[LiteralT, pd.Series], 2524 ) -> pd.Series: 2525 """Creates a series of interval variables with the given name. 2526 2527 Args: 2528 name (str): Required. The name of the variable set. 2529 index (pd.Index): Required. The index to use for the variable set. 2530 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2531 set. If a `pd.Series` is passed in, it will be based on the 2532 corresponding values of the pd.Series. 2533 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2534 the set. If a `pd.Series` is passed in, it will be based on the 2535 corresponding values of the pd.Series. 2536 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2537 interval in the set. If a `pd.Series` is passed in, it will be based on 2538 the corresponding values of the pd.Series. 2539 2540 Returns: 2541 pd.Series: The interval variable set indexed by its corresponding 2542 dimensions. 2543 2544 Raises: 2545 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2546 ValueError: if the `name` is not a valid identifier or already exists. 2547 ValueError: if the all the indexes do not match. 2548 """ 2549 if not isinstance(index, pd.Index): 2550 raise TypeError("Non-index object is used as index") 2551 if not name.isidentifier(): 2552 raise ValueError("name={} is not a valid identifier".format(name)) 2553 2554 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2555 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2556 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2557 interval_array = [] 2558 for i in index: 2559 interval_array.append( 2560 self.new_optional_fixed_size_interval_var( 2561 start=starts[i], 2562 size=sizes[i], 2563 is_present=are_present[i], 2564 name=f"{name}[{i}]", 2565 ) 2566 ) 2567 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.
2569 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2570 """Adds NoOverlap(interval_vars). 2571 2572 A NoOverlap constraint ensures that all present intervals do not overlap 2573 in time. 2574 2575 Args: 2576 interval_vars: The list of interval variables to constrain. 2577 2578 Returns: 2579 An instance of the `Constraint` class. 2580 """ 2581 ct = Constraint(self) 2582 model_ct = self.__model.constraints[ct.index] 2583 model_ct.no_overlap.intervals.extend( 2584 [self.get_interval_index(x) for x in interval_vars] 2585 ) 2586 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.
2588 def add_no_overlap_2d( 2589 self, 2590 x_intervals: Iterable[IntervalVar], 2591 y_intervals: Iterable[IntervalVar], 2592 ) -> Constraint: 2593 """Adds NoOverlap2D(x_intervals, y_intervals). 2594 2595 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2596 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2597 by two intervals which represent its projection onto the X and Y axis. 2598 2599 Furthermore, one box is optional if at least one of the x or y interval is 2600 optional. 2601 2602 Args: 2603 x_intervals: The X coordinates of the rectangles. 2604 y_intervals: The Y coordinates of the rectangles. 2605 2606 Returns: 2607 An instance of the `Constraint` class. 2608 """ 2609 ct = Constraint(self) 2610 model_ct = self.__model.constraints[ct.index] 2611 model_ct.no_overlap_2d.x_intervals.extend( 2612 [self.get_interval_index(x) for x in x_intervals] 2613 ) 2614 model_ct.no_overlap_2d.y_intervals.extend( 2615 [self.get_interval_index(x) for x in y_intervals] 2616 ) 2617 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.
2619 def add_cumulative( 2620 self, 2621 intervals: Iterable[IntervalVar], 2622 demands: Iterable[LinearExprT], 2623 capacity: LinearExprT, 2624 ) -> Constraint: 2625 """Adds Cumulative(intervals, demands, capacity). 2626 2627 This constraint enforces that: 2628 2629 for all t: 2630 sum(demands[i] 2631 if (start(intervals[i]) <= t < end(intervals[i])) and 2632 (intervals[i] is present)) <= capacity 2633 2634 Args: 2635 intervals: The list of intervals. 2636 demands: The list of demands for each interval. Each demand must be >= 0. 2637 Each demand can be a 1-var affine expression (a * x + b). 2638 capacity: The maximum capacity of the cumulative constraint. It can be a 2639 1-var affine expression (a * x + b). 2640 2641 Returns: 2642 An instance of the `Constraint` class. 2643 """ 2644 cumulative = Constraint(self) 2645 model_ct = self.__model.constraints[cumulative.index] 2646 model_ct.cumulative.intervals.extend( 2647 [self.get_interval_index(x) for x in intervals] 2648 ) 2649 for d in demands: 2650 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2651 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2652 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.
2655 def clone(self) -> "CpModel": 2656 """Reset the model, and creates a new one from a CpModelProto instance.""" 2657 clone = CpModel() 2658 clone.proto.CopyFrom(self.proto) 2659 clone.rebuild_constant_map() 2660 return clone
Reset the model, and creates a new one from a CpModelProto instance.
2662 def rebuild_constant_map(self): 2663 """Internal method used during model cloning.""" 2664 for i, var in enumerate(self.__model.variables): 2665 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2666 self.__constant_map[var.domain[0]] = i
Internal method used during model cloning.
2668 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2669 """Returns an already created Boolean variable from its index.""" 2670 if index < 0 or index >= len(self.__model.variables): 2671 raise ValueError( 2672 f"get_bool_var_from_proto_index: out of bound index {index}" 2673 ) 2674 var = self.__model.variables[index] 2675 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2676 raise ValueError( 2677 f"get_bool_var_from_proto_index: index {index} does not reference" 2678 + " a Boolean variable" 2679 ) 2680 2681 return IntVar(self.__model, index, None)
Returns an already created Boolean variable from its index.
2683 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2684 """Returns an already created integer variable from its index.""" 2685 if index < 0 or index >= len(self.__model.variables): 2686 raise ValueError( 2687 f"get_int_var_from_proto_index: out of bound index {index}" 2688 ) 2689 return IntVar(self.__model, index, None)
Returns an already created integer variable from its index.
2691 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2692 """Returns an already created interval variable from its index.""" 2693 if index < 0 or index >= len(self.__model.constraints): 2694 raise ValueError( 2695 f"get_interval_var_from_proto_index: out of bound index {index}" 2696 ) 2697 ct = self.__model.constraints[index] 2698 if not ct.HasField("interval"): 2699 raise ValueError( 2700 f"get_interval_var_from_proto_index: index {index} does not" 2701 " reference an" + " interval variable" 2702 ) 2703 2704 return IntervalVar(self.__model, index, None, None, None, None)
Returns an already created interval variable from its index.
2711 @property 2712 def proto(self) -> cp_model_pb2.CpModelProto: 2713 """Returns the underlying CpModelProto.""" 2714 return self.__model
Returns the underlying CpModelProto.
2719 def get_or_make_index(self, arg: VariableT) -> int: 2720 """Returns the index of a variable, its negation, or a number.""" 2721 if isinstance(arg, IntVar): 2722 return arg.index 2723 if ( 2724 isinstance(arg, _ProductCst) 2725 and isinstance(arg.expression(), IntVar) 2726 and arg.coefficient() == -1 2727 ): 2728 return -arg.expression().index - 1 2729 if isinstance(arg, numbers.Integral): 2730 arg = cmh.assert_is_int64(arg) 2731 return self.get_or_make_index_from_constant(arg) 2732 raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")")
Returns the index of a variable, its negation, or a number.
2734 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2735 """Returns an index from a boolean expression.""" 2736 if isinstance(arg, IntVar): 2737 self.assert_is_boolean_variable(arg) 2738 return arg.index 2739 if isinstance(arg, _NotBooleanVariable): 2740 self.assert_is_boolean_variable(arg.negated()) 2741 return arg.index 2742 if isinstance(arg, numbers.Integral): 2743 arg = cmh.assert_is_zero_or_one(arg) 2744 return self.get_or_make_index_from_constant(arg) 2745 if cmh.is_boolean(arg): 2746 return self.get_or_make_index_from_constant(int(arg)) 2747 raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})")
Returns an index from a boolean expression.
2754 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2755 if value in self.__constant_map: 2756 return self.__constant_map[value] 2757 index = len(self.__model.variables) 2758 self.__model.variables.add(domain=[value, value]) 2759 self.__constant_map[value] = index 2760 return index
2770 def parse_linear_expression( 2771 self, linear_expr: LinearExprT, negate: bool = False 2772 ) -> cp_model_pb2.LinearExpressionProto: 2773 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2774 result: cp_model_pb2.LinearExpressionProto = ( 2775 cp_model_pb2.LinearExpressionProto() 2776 ) 2777 mult = -1 if negate else 1 2778 if isinstance(linear_expr, numbers.Integral): 2779 result.offset = int(linear_expr) * mult 2780 return result 2781 2782 if isinstance(linear_expr, IntVar): 2783 result.vars.append(self.get_or_make_index(linear_expr)) 2784 result.coeffs.append(mult) 2785 return result 2786 2787 coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map() 2788 result.offset = constant * mult 2789 for t in coeffs_map.items(): 2790 if not isinstance(t[0], IntVar): 2791 raise TypeError("Wrong argument" + str(t)) 2792 c = cmh.assert_is_int64(t[1]) 2793 result.vars.append(t[0].index) 2794 result.coeffs.append(c * mult) 2795 return result
Returns a LinearExpressionProto built from a LinearExpr instance.
2836 def minimize(self, obj: ObjLinearExprT): 2837 """Sets the objective of the model to minimize(obj).""" 2838 self._set_objective(obj, minimize=True)
Sets the objective of the model to minimize(obj).
2840 def maximize(self, obj: ObjLinearExprT): 2841 """Sets the objective of the model to maximize(obj).""" 2842 self._set_objective(obj, minimize=False)
Sets the objective of the model to maximize(obj).
2853 def add_decision_strategy( 2854 self, 2855 variables: Sequence[IntVar], 2856 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2857 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2858 ) -> None: 2859 """Adds a search strategy to the model. 2860 2861 Args: 2862 variables: a list of variables this strategy will assign. 2863 var_strategy: heuristic to choose the next variable to assign. 2864 domain_strategy: heuristic to reduce the domain of the selected variable. 2865 Currently, this is advanced code: the union of all strategies added to 2866 the model must be complete, i.e. instantiates all variables. Otherwise, 2867 solve() will fail. 2868 """ 2869 2870 strategy = self.__model.search_strategy.add() 2871 for v in variables: 2872 expr = strategy.exprs.add() 2873 if v.index >= 0: 2874 expr.vars.append(v.index) 2875 expr.coeffs.append(1) 2876 else: 2877 expr.vars.append(self.negated(v.index)) 2878 expr.coeffs.append(-1) 2879 expr.offset = 1 2880 2881 strategy.variable_selection_strategy = var_strategy 2882 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.
2884 def model_stats(self) -> str: 2885 """Returns a string containing some model statistics.""" 2886 return swig_helper.CpSatHelper.model_stats(self.__model)
Returns a string containing some model statistics.
2888 def validate(self) -> str: 2889 """Returns a string indicating that the model is invalid.""" 2890 return swig_helper.CpSatHelper.validate_model(self.__model)
Returns a string indicating that the model is invalid.
2892 def export_to_file(self, file: str) -> bool: 2893 """Write the model as a protocol buffer to 'file'. 2894 2895 Args: 2896 file: file to write the model to. If the filename ends with 'txt', the 2897 model will be written as a text file, otherwise, the binary format will 2898 be used. 2899 2900 Returns: 2901 True if the model was correctly written. 2902 """ 2903 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.
2905 def add_hint(self, var: IntVar, value: int) -> None: 2906 """Adds 'var == value' as a hint to the solver.""" 2907 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2908 self.__model.solution_hint.values.append(value)
Adds 'var == value' as a hint to the solver.
2910 def clear_hints(self): 2911 """Removes any solution hint from the model.""" 2912 self.__model.ClearField("solution_hint")
Removes any solution hint from the model.
2914 def add_assumption(self, lit: LiteralT) -> None: 2915 """Adds the literal to the model as assumptions.""" 2916 self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
Adds the literal to the model as assumptions.
2918 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2919 """Adds the literals to the model as assumptions.""" 2920 for lit in literals: 2921 self.add_assumption(lit)
Adds the literals to the model as assumptions.
2923 def clear_assumptions(self) -> None: 2924 """Removes all assumptions from the model.""" 2925 self.__model.ClearField("assumptions")
Removes all assumptions from the model.
2928 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2929 if isinstance(x, IntVar): 2930 var = self.__model.variables[x.index] 2931 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2932 raise TypeError("TypeError: " + str(x) + " is not a boolean variable") 2933 elif not isinstance(x, _NotBooleanVariable): 2934 raise TypeError("TypeError: " + str(x) + " is not a boolean variable")
1325 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 1326 """Create an integer variable with domain [lb, ub]. 1327 1328 The CP-SAT solver is limited to integer variables. If you have fractional 1329 values, scale them up so that they become integers; if you have strings, 1330 encode them as integers. 1331 1332 Args: 1333 lb: Lower bound for the variable. 1334 ub: Upper bound for the variable. 1335 name: The name of the variable. 1336 1337 Returns: 1338 a variable whose domain is [lb, ub]. 1339 """ 1340 1341 return IntVar(self.__model, 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].
1343 def new_int_var_from_domain(self, domain: Domain, name: str) -> IntVar: 1344 """Create an integer variable from a domain. 1345 1346 A domain is a set of integers specified by a collection of intervals. 1347 For example, `model.new_int_var_from_domain(cp_model. 1348 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 1349 1350 Args: 1351 domain: An instance of the Domain class. 1352 name: The name of the variable. 1353 1354 Returns: 1355 a variable whose domain is the given domain. 1356 """ 1357 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.
1359 def new_bool_var(self, name: str) -> IntVar: 1360 """Creates a 0-1 variable with the given name.""" 1361 return IntVar(self.__model, Domain(0, 1), name)
Creates a 0-1 variable with the given name.
1363 def new_constant(self, value: IntegralT) -> IntVar: 1364 """Declares a constant integer.""" 1365 return IntVar(self.__model, self.get_or_make_index_from_constant(value), None)
Declares a constant integer.
1367 def new_int_var_series( 1368 self, 1369 name: str, 1370 index: pd.Index, 1371 lower_bounds: Union[IntegralT, pd.Series], 1372 upper_bounds: Union[IntegralT, pd.Series], 1373 ) -> pd.Series: 1374 """Creates a series of (scalar-valued) variables with the given name. 1375 1376 Args: 1377 name (str): Required. The name of the variable set. 1378 index (pd.Index): Required. The index to use for the variable set. 1379 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 1380 set. If a `pd.Series` is passed in, it will be based on the 1381 corresponding values of the pd.Series. 1382 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 1383 set. If a `pd.Series` is passed in, it will be based on the 1384 corresponding values of the pd.Series. 1385 1386 Returns: 1387 pd.Series: The variable set indexed by its corresponding dimensions. 1388 1389 Raises: 1390 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1391 ValueError: if the `name` is not a valid identifier or already exists. 1392 ValueError: if the `lowerbound` is greater than the `upperbound`. 1393 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 1394 the input index. 1395 """ 1396 if not isinstance(index, pd.Index): 1397 raise TypeError("Non-index object is used as index") 1398 if not name.isidentifier(): 1399 raise ValueError("name={} is not a valid identifier".format(name)) 1400 if ( 1401 isinstance(lower_bounds, numbers.Integral) 1402 and isinstance(upper_bounds, numbers.Integral) 1403 and lower_bounds > upper_bounds 1404 ): 1405 raise ValueError( 1406 f"lower_bound={lower_bounds} is greater than" 1407 f" upper_bound={upper_bounds} for variable set={name}" 1408 ) 1409 1410 lower_bounds = _convert_to_integral_series_and_validate_index( 1411 lower_bounds, index 1412 ) 1413 upper_bounds = _convert_to_integral_series_and_validate_index( 1414 upper_bounds, index 1415 ) 1416 return pd.Series( 1417 index=index, 1418 data=[ 1419 # pylint: disable=g-complex-comprehension 1420 IntVar( 1421 model=self.__model, 1422 name=f"{name}[{i}]", 1423 domain=Domain(lower_bounds[i], upper_bounds[i]), 1424 ) 1425 for i in index 1426 ], 1427 )
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.
1429 def new_bool_var_series( 1430 self, 1431 name: str, 1432 index: pd.Index, 1433 ) -> pd.Series: 1434 """Creates a series of (scalar-valued) variables with the given name. 1435 1436 Args: 1437 name (str): Required. The name of the variable set. 1438 index (pd.Index): Required. The index to use for the variable set. 1439 1440 Returns: 1441 pd.Series: The variable set indexed by its corresponding dimensions. 1442 1443 Raises: 1444 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1445 ValueError: if the `name` is not a valid identifier or already exists. 1446 """ 1447 return self.new_int_var_series( 1448 name=name, index=index, lower_bounds=0, upper_bounds=1 1449 )
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.
1453 def add_linear_constraint( 1454 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 1455 ) -> Constraint: 1456 """Adds the constraint: `lb <= linear_expr <= ub`.""" 1457 return self.add_linear_expression_in_domain(linear_expr, Domain(lb, ub))
Adds the constraint: lb <= linear_expr <= ub
.
1459 def add_linear_expression_in_domain( 1460 self, linear_expr: LinearExprT, domain: Domain 1461 ) -> Constraint: 1462 """Adds the constraint: `linear_expr` in `domain`.""" 1463 if isinstance(linear_expr, LinearExpr): 1464 ct = Constraint(self) 1465 model_ct = self.__model.constraints[ct.index] 1466 coeffs_map, constant = linear_expr.get_integer_var_value_map() 1467 for t in coeffs_map.items(): 1468 if not isinstance(t[0], IntVar): 1469 raise TypeError("Wrong argument" + str(t)) 1470 c = cmh.assert_is_int64(t[1]) 1471 model_ct.linear.vars.append(t[0].index) 1472 model_ct.linear.coeffs.append(c) 1473 model_ct.linear.domain.extend( 1474 [ 1475 cmh.capped_subtraction(x, constant) 1476 for x in domain.flattened_intervals() 1477 ] 1478 ) 1479 return ct 1480 if isinstance(linear_expr, numbers.Integral): 1481 if not domain.contains(int(linear_expr)): 1482 return self.add_bool_or([]) # Evaluate to false. 1483 else: 1484 return self.add_bool_and([]) # Evaluate to true. 1485 raise TypeError( 1486 "not supported: CpModel.add_linear_expression_in_domain(" 1487 + str(linear_expr) 1488 + " " 1489 + str(domain) 1490 + ")" 1491 )
Adds the constraint: linear_expr
in domain
.
1493 def add(self, ct: Union[BoundedLinearExpression, bool]) -> Constraint: 1494 """Adds a `BoundedLinearExpression` to the model. 1495 1496 Args: 1497 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 1498 1499 Returns: 1500 An instance of the `Constraint` class. 1501 """ 1502 if isinstance(ct, BoundedLinearExpression): 1503 return self.add_linear_expression_in_domain( 1504 ct.expression(), Domain.from_flat_intervals(ct.bounds()) 1505 ) 1506 if ct and cmh.is_boolean(ct): 1507 return self.add_bool_or([True]) 1508 if not ct and cmh.is_boolean(ct): 1509 return self.add_bool_or([]) # Evaluate to false. 1510 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.
1522 def add_all_different(self, *expressions): 1523 """Adds AllDifferent(expressions). 1524 1525 This constraint forces all expressions to have different values. 1526 1527 Args: 1528 *expressions: simple expressions of the form a * var + constant. 1529 1530 Returns: 1531 An instance of the `Constraint` class. 1532 """ 1533 ct = Constraint(self) 1534 model_ct = self.__model.constraints[ct.index] 1535 expanded = expand_generator_or_tuple(expressions) 1536 model_ct.all_diff.exprs.extend( 1537 self.parse_linear_expression(x) for x in expanded 1538 ) 1539 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.
1541 def add_element( 1542 self, index: VariableT, variables: Sequence[VariableT], target: VariableT 1543 ) -> Constraint: 1544 """Adds the element constraint: `variables[index] == target`. 1545 1546 Args: 1547 index: The index of the variable that's being constrained. 1548 variables: A list of variables. 1549 target: The value that the variable must be equal to. 1550 1551 Returns: 1552 An instance of the `Constraint` class. 1553 """ 1554 1555 if not variables: 1556 raise ValueError("add_element expects a non-empty variables array") 1557 1558 if isinstance(index, numbers.Integral): 1559 return self.add(list(variables)[int(index)] == target) 1560 1561 ct = Constraint(self) 1562 model_ct = self.__model.constraints[ct.index] 1563 model_ct.element.index = self.get_or_make_index(index) 1564 model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables]) 1565 model_ct.element.target = self.get_or_make_index(target) 1566 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.
1568 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1569 """Adds Circuit(arcs). 1570 1571 Adds a circuit constraint from a sparse list of arcs that encode the graph. 1572 1573 A circuit is a unique Hamiltonian path in a subgraph of the total 1574 graph. In case a node 'i' is not in the path, then there must be a 1575 loop arc 'i -> i' associated with a true literal. Otherwise 1576 this constraint will fail. 1577 1578 Args: 1579 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1580 literal). The arc is selected in the circuit if the literal is true. 1581 Both source_node and destination_node must be integers between 0 and the 1582 number of nodes - 1. 1583 1584 Returns: 1585 An instance of the `Constraint` class. 1586 1587 Raises: 1588 ValueError: If the list of arcs is empty. 1589 """ 1590 if not arcs: 1591 raise ValueError("add_circuit expects a non-empty array of arcs") 1592 ct = Constraint(self) 1593 model_ct = self.__model.constraints[ct.index] 1594 for arc in arcs: 1595 tail = cmh.assert_is_int32(arc[0]) 1596 head = cmh.assert_is_int32(arc[1]) 1597 lit = self.get_or_make_boolean_index(arc[2]) 1598 model_ct.circuit.tails.append(tail) 1599 model_ct.circuit.heads.append(head) 1600 model_ct.circuit.literals.append(lit) 1601 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.
1603 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1604 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1605 1606 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1607 literals[i] is true must satisfy this set of properties: 1608 - #incoming arcs == 1 except for node 0. 1609 - #outgoing arcs == 1 except for node 0. 1610 - for node zero, #incoming arcs == #outgoing arcs. 1611 - There are no duplicate arcs. 1612 - Self-arcs are allowed except for node 0. 1613 - There is no cycle in this graph, except through node 0. 1614 1615 Args: 1616 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1617 literal). The arc is selected in the circuit if the literal is true. 1618 Both source_node and destination_node must be integers between 0 and the 1619 number of nodes - 1. 1620 1621 Returns: 1622 An instance of the `Constraint` class. 1623 1624 Raises: 1625 ValueError: If the list of arcs is empty. 1626 """ 1627 if not arcs: 1628 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1629 ct = Constraint(self) 1630 model_ct = self.__model.constraints[ct.index] 1631 for arc in arcs: 1632 tail = cmh.assert_is_int32(arc[0]) 1633 head = cmh.assert_is_int32(arc[1]) 1634 lit = self.get_or_make_boolean_index(arc[2]) 1635 model_ct.routes.tails.append(tail) 1636 model_ct.routes.heads.append(head) 1637 model_ct.routes.literals.append(lit) 1638 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.
1640 def add_allowed_assignments( 1641 self, 1642 variables: Sequence[VariableT], 1643 tuples_list: Iterable[Sequence[IntegralT]], 1644 ) -> Constraint: 1645 """Adds AllowedAssignments(variables, tuples_list). 1646 1647 An AllowedAssignments constraint is a constraint on an array of variables, 1648 which requires that when all variables are assigned values, the resulting 1649 array equals one of the tuples in `tuple_list`. 1650 1651 Args: 1652 variables: A list of variables. 1653 tuples_list: A list of admissible tuples. Each tuple must have the same 1654 length as the variables, and the ith value of a tuple corresponds to the 1655 ith variable. 1656 1657 Returns: 1658 An instance of the `Constraint` class. 1659 1660 Raises: 1661 TypeError: If a tuple does not have the same size as the list of 1662 variables. 1663 ValueError: If the array of variables is empty. 1664 """ 1665 1666 if not variables: 1667 raise ValueError( 1668 "add_allowed_assignments expects a non-empty variables array" 1669 ) 1670 1671 ct = Constraint(self) 1672 model_ct = self.__model.constraints[ct.index] 1673 model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables]) 1674 arity = len(variables) 1675 for t in tuples_list: 1676 if len(t) != arity: 1677 raise TypeError("Tuple " + str(t) + " has the wrong arity") 1678 ar = [] 1679 for v in t: 1680 ar.append(cmh.assert_is_int64(v)) 1681 model_ct.table.values.extend(ar) 1682 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.
1684 def add_forbidden_assignments( 1685 self, 1686 variables: Sequence[VariableT], 1687 tuples_list: Iterable[Sequence[IntegralT]], 1688 ) -> Constraint: 1689 """Adds add_forbidden_assignments(variables, [tuples_list]). 1690 1691 A ForbiddenAssignments constraint is a constraint on an array of variables 1692 where the list of impossible combinations is provided in the tuples list. 1693 1694 Args: 1695 variables: A list of variables. 1696 tuples_list: A list of forbidden tuples. Each tuple must have the same 1697 length as the variables, and the *i*th value of a tuple corresponds to 1698 the *i*th variable. 1699 1700 Returns: 1701 An instance of the `Constraint` class. 1702 1703 Raises: 1704 TypeError: If a tuple does not have the same size as the list of 1705 variables. 1706 ValueError: If the array of variables is empty. 1707 """ 1708 1709 if not variables: 1710 raise ValueError( 1711 "add_forbidden_assignments expects a non-empty variables array" 1712 ) 1713 1714 index = len(self.__model.constraints) 1715 ct = self.add_allowed_assignments(variables, tuples_list) 1716 self.__model.constraints[index].table.negated = True 1717 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.
1719 def add_automaton( 1720 self, 1721 transition_variables: Sequence[VariableT], 1722 starting_state: IntegralT, 1723 final_states: Sequence[IntegralT], 1724 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1725 ) -> Constraint: 1726 """Adds an automaton constraint. 1727 1728 An automaton constraint takes a list of variables (of size *n*), an initial 1729 state, a set of final states, and a set of transitions. A transition is a 1730 triplet (*tail*, *transition*, *head*), where *tail* and *head* are states, 1731 and *transition* is the label of an arc from *head* to *tail*, 1732 corresponding to the value of one variable in the list of variables. 1733 1734 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1735 contains the possible states of the automaton. The first state contains the 1736 initial state. The last phase contains the final states. 1737 1738 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1739 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1740 an arc from the state *tail* of phase *i* and the state *head* of phase 1741 *i* + 1. This arc is labeled by the value *transition* of the variables 1742 `variables[i]`. That is, this arc can only be selected if `variables[i]` 1743 is assigned the value *transition*. 1744 1745 A feasible solution of this constraint is an assignment of variables such 1746 that, starting from the initial state in phase 0, there is a path labeled by 1747 the values of the variables that ends in one of the final states in the 1748 final phase. 1749 1750 Args: 1751 transition_variables: A non-empty list of variables whose values 1752 correspond to the labels of the arcs traversed by the automaton. 1753 starting_state: The initial state of the automaton. 1754 final_states: A non-empty list of admissible final states. 1755 transition_triples: A list of transitions for the automaton, in the 1756 following format (current_state, variable_value, next_state). 1757 1758 Returns: 1759 An instance of the `Constraint` class. 1760 1761 Raises: 1762 ValueError: if `transition_variables`, `final_states`, or 1763 `transition_triples` are empty. 1764 """ 1765 1766 if not transition_variables: 1767 raise ValueError( 1768 "add_automaton expects a non-empty transition_variables array" 1769 ) 1770 if not final_states: 1771 raise ValueError("add_automaton expects some final states") 1772 1773 if not transition_triples: 1774 raise ValueError("add_automaton expects some transition triples") 1775 1776 ct = Constraint(self) 1777 model_ct = self.__model.constraints[ct.index] 1778 model_ct.automaton.vars.extend( 1779 [self.get_or_make_index(x) for x in transition_variables] 1780 ) 1781 starting_state = cmh.assert_is_int64(starting_state) 1782 model_ct.automaton.starting_state = starting_state 1783 for v in final_states: 1784 v = cmh.assert_is_int64(v) 1785 model_ct.automaton.final_states.append(v) 1786 for t in transition_triples: 1787 if len(t) != 3: 1788 raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)") 1789 tail = cmh.assert_is_int64(t[0]) 1790 label = cmh.assert_is_int64(t[1]) 1791 head = cmh.assert_is_int64(t[2]) 1792 model_ct.automaton.transition_tail.append(tail) 1793 model_ct.automaton.transition_label.append(label) 1794 model_ct.automaton.transition_head.append(head) 1795 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.
1797 def add_inverse( 1798 self, 1799 variables: Sequence[VariableT], 1800 inverse_variables: Sequence[VariableT], 1801 ) -> Constraint: 1802 """Adds Inverse(variables, inverse_variables). 1803 1804 An inverse constraint enforces that if `variables[i]` is assigned a value 1805 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1806 1807 Args: 1808 variables: An array of integer variables. 1809 inverse_variables: An array of integer variables. 1810 1811 Returns: 1812 An instance of the `Constraint` class. 1813 1814 Raises: 1815 TypeError: if variables and inverse_variables have different lengths, or 1816 if they are empty. 1817 """ 1818 1819 if not variables or not inverse_variables: 1820 raise TypeError("The Inverse constraint does not accept empty arrays") 1821 if len(variables) != len(inverse_variables): 1822 raise TypeError( 1823 "In the inverse constraint, the two array variables and" 1824 " inverse_variables must have the same length." 1825 ) 1826 ct = Constraint(self) 1827 model_ct = self.__model.constraints[ct.index] 1828 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1829 model_ct.inverse.f_inverse.extend( 1830 [self.get_or_make_index(x) for x in inverse_variables] 1831 ) 1832 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.
1834 def add_reservoir_constraint( 1835 self, 1836 times: Iterable[LinearExprT], 1837 level_changes: Iterable[LinearExprT], 1838 min_level: int, 1839 max_level: int, 1840 ) -> Constraint: 1841 """Adds Reservoir(times, level_changes, min_level, max_level). 1842 1843 Maintains a reservoir level within bounds. The water level starts at 0, and 1844 at any time, it must be between min_level and max_level. 1845 1846 If the affine expression `times[i]` is assigned a value t, then the current 1847 level changes by `level_changes[i]`, which is constant, at time t. 1848 1849 Note that min level must be <= 0, and the max level must be >= 0. Please 1850 use fixed level_changes to simulate initial state. 1851 1852 Therefore, at any time: 1853 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1854 1855 Args: 1856 times: A list of 1-var affine expressions (a * x + b) which specify the 1857 time of the filling or emptying the reservoir. 1858 level_changes: A list of integer values that specifies the amount of the 1859 emptying or filling. Currently, variable demands are not supported. 1860 min_level: At any time, the level of the reservoir must be greater or 1861 equal than the min level. 1862 max_level: At any time, the level of the reservoir must be less or equal 1863 than the max level. 1864 1865 Returns: 1866 An instance of the `Constraint` class. 1867 1868 Raises: 1869 ValueError: if max_level < min_level. 1870 1871 ValueError: if max_level < 0. 1872 1873 ValueError: if min_level > 0 1874 """ 1875 1876 if max_level < min_level: 1877 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1878 1879 if max_level < 0: 1880 raise ValueError("Reservoir constraint must have a max_level >= 0") 1881 1882 if min_level > 0: 1883 raise ValueError("Reservoir constraint must have a min_level <= 0") 1884 1885 ct = Constraint(self) 1886 model_ct = self.__model.constraints[ct.index] 1887 model_ct.reservoir.time_exprs.extend( 1888 [self.parse_linear_expression(x) for x in times] 1889 ) 1890 model_ct.reservoir.level_changes.extend( 1891 [self.parse_linear_expression(x) for x in level_changes] 1892 ) 1893 model_ct.reservoir.min_level = min_level 1894 model_ct.reservoir.max_level = max_level 1895 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
1996 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1997 """Adds `a => b` (`a` implies `b`).""" 1998 ct = Constraint(self) 1999 model_ct = self.__model.constraints[ct.index] 2000 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 2001 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 2002 return ct
Adds a => b
(a
implies b
).
2012 def add_bool_or(self, *literals): 2013 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 2014 ct = Constraint(self) 2015 model_ct = self.__model.constraints[ct.index] 2016 model_ct.bool_or.literals.extend( 2017 [ 2018 self.get_or_make_boolean_index(x) 2019 for x in expand_generator_or_tuple(literals) 2020 ] 2021 ) 2022 return ct
Adds Or(literals) == true
: sum(literals) >= 1.
2032 def add_at_least_one(self, *literals): 2033 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 2034 return self.add_bool_or(*literals)
Same as add_bool_or
: sum(literals) >= 1
.
2044 def add_at_most_one(self, *literals): 2045 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 2046 ct = Constraint(self) 2047 model_ct = self.__model.constraints[ct.index] 2048 model_ct.at_most_one.literals.extend( 2049 [ 2050 self.get_or_make_boolean_index(x) 2051 for x in expand_generator_or_tuple(literals) 2052 ] 2053 ) 2054 return ct
Adds AtMostOne(literals)
: sum(literals) <= 1
.
2064 def add_exactly_one(self, *literals): 2065 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 2066 ct = Constraint(self) 2067 model_ct = self.__model.constraints[ct.index] 2068 model_ct.exactly_one.literals.extend( 2069 [ 2070 self.get_or_make_boolean_index(x) 2071 for x in expand_generator_or_tuple(literals) 2072 ] 2073 ) 2074 return ct
Adds ExactlyOne(literals)
: sum(literals) == 1
.
2084 def add_bool_and(self, *literals): 2085 """Adds `And(literals) == true`.""" 2086 ct = Constraint(self) 2087 model_ct = self.__model.constraints[ct.index] 2088 model_ct.bool_and.literals.extend( 2089 [ 2090 self.get_or_make_boolean_index(x) 2091 for x in expand_generator_or_tuple(literals) 2092 ] 2093 ) 2094 return ct
Adds And(literals) == true
.
2104 def add_bool_xor(self, *literals): 2105 """Adds `XOr(literals) == true`. 2106 2107 In contrast to add_bool_or and add_bool_and, it does not support 2108 .only_enforce_if(). 2109 2110 Args: 2111 *literals: the list of literals in the constraint. 2112 2113 Returns: 2114 An `Constraint` object. 2115 """ 2116 ct = Constraint(self) 2117 model_ct = self.__model.constraints[ct.index] 2118 model_ct.bool_xor.literals.extend( 2119 [ 2120 self.get_or_make_boolean_index(x) 2121 for x in expand_generator_or_tuple(literals) 2122 ] 2123 ) 2124 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.
2126 def add_min_equality( 2127 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2128 ) -> Constraint: 2129 """Adds `target == Min(exprs)`.""" 2130 ct = Constraint(self) 2131 model_ct = self.__model.constraints[ct.index] 2132 model_ct.lin_max.exprs.extend( 2133 [self.parse_linear_expression(x, True) for x in exprs] 2134 ) 2135 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 2136 return ct
Adds target == Min(exprs)
.
2138 def add_max_equality( 2139 self, target: LinearExprT, exprs: Iterable[LinearExprT] 2140 ) -> Constraint: 2141 """Adds `target == Max(exprs)`.""" 2142 ct = Constraint(self) 2143 model_ct = self.__model.constraints[ct.index] 2144 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 2145 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2146 return ct
Adds target == Max(exprs)
.
2148 def add_division_equality( 2149 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 2150 ) -> Constraint: 2151 """Adds `target == num // denom` (integer division rounded towards 0).""" 2152 ct = Constraint(self) 2153 model_ct = self.__model.constraints[ct.index] 2154 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 2155 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 2156 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 2157 return ct
Adds target == num // denom
(integer division rounded towards 0).
2159 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 2160 """Adds `target == Abs(expr)`.""" 2161 ct = Constraint(self) 2162 model_ct = self.__model.constraints[ct.index] 2163 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 2164 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 2165 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 2166 return ct
Adds target == Abs(expr)
.
2168 def add_modulo_equality( 2169 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 2170 ) -> Constraint: 2171 """Adds `target = expr % mod`.""" 2172 ct = Constraint(self) 2173 model_ct = self.__model.constraints[ct.index] 2174 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 2175 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 2176 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 2177 return ct
Adds target = expr % mod
.
2179 def add_multiplication_equality( 2180 self, 2181 target: LinearExprT, 2182 *expressions: Union[Iterable[LinearExprT], LinearExprT], 2183 ) -> Constraint: 2184 """Adds `target == expressions[0] * .. * expressions[n]`.""" 2185 ct = Constraint(self) 2186 model_ct = self.__model.constraints[ct.index] 2187 model_ct.int_prod.exprs.extend( 2188 [ 2189 self.parse_linear_expression(expr) 2190 for expr in expand_generator_or_tuple(expressions) 2191 ] 2192 ) 2193 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 2194 return ct
Adds target == expressions[0] * .. * expressions[n]
.
2198 def new_interval_var( 2199 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 2200 ) -> IntervalVar: 2201 """Creates an interval variable from start, size, and end. 2202 2203 An interval variable is a constraint, that is itself used in other 2204 constraints like NoOverlap. 2205 2206 Internally, it ensures that `start + size == end`. 2207 2208 Args: 2209 start: The start of the interval. It must be of the form a * var + b. 2210 size: The size of the interval. It must be of the form a * var + b. 2211 end: The end of the interval. It must be of the form a * var + b. 2212 name: The name of the interval variable. 2213 2214 Returns: 2215 An `IntervalVar` object. 2216 """ 2217 2218 lin = self.add(start + size == end) 2219 if name: 2220 lin.with_name("lin_" + name) 2221 2222 start_expr = self.parse_linear_expression(start) 2223 size_expr = self.parse_linear_expression(size) 2224 end_expr = self.parse_linear_expression(end) 2225 if len(start_expr.vars) > 1: 2226 raise TypeError( 2227 "cp_model.new_interval_var: start must be 1-var affine or constant." 2228 ) 2229 if len(size_expr.vars) > 1: 2230 raise TypeError( 2231 "cp_model.new_interval_var: size must be 1-var affine or constant." 2232 ) 2233 if len(end_expr.vars) > 1: 2234 raise TypeError( 2235 "cp_model.new_interval_var: end must be 1-var affine or constant." 2236 ) 2237 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.
2239 def new_interval_var_series( 2240 self, 2241 name: str, 2242 index: pd.Index, 2243 starts: Union[LinearExprT, pd.Series], 2244 sizes: Union[LinearExprT, pd.Series], 2245 ends: Union[LinearExprT, pd.Series], 2246 ) -> pd.Series: 2247 """Creates a series of interval variables with the given name. 2248 2249 Args: 2250 name (str): Required. The name of the variable set. 2251 index (pd.Index): Required. The index to use for the variable set. 2252 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2253 set. If a `pd.Series` is passed in, it will be based on the 2254 corresponding values of the pd.Series. 2255 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2256 set. If a `pd.Series` is passed in, it will be based on the 2257 corresponding values of the pd.Series. 2258 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2259 set. If a `pd.Series` is passed in, it will be based on the 2260 corresponding values of the pd.Series. 2261 2262 Returns: 2263 pd.Series: The interval variable set indexed by its corresponding 2264 dimensions. 2265 2266 Raises: 2267 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2268 ValueError: if the `name` is not a valid identifier or already exists. 2269 ValueError: if the all the indexes do not match. 2270 """ 2271 if not isinstance(index, pd.Index): 2272 raise TypeError("Non-index object is used as index") 2273 if not name.isidentifier(): 2274 raise ValueError("name={} is not a valid identifier".format(name)) 2275 2276 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2277 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2278 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2279 interval_array = [] 2280 for i in index: 2281 interval_array.append( 2282 self.new_interval_var( 2283 start=starts[i], 2284 size=sizes[i], 2285 end=ends[i], 2286 name=f"{name}[{i}]", 2287 ) 2288 ) 2289 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.
2291 def new_fixed_size_interval_var( 2292 self, start: LinearExprT, size: IntegralT, name: str 2293 ) -> IntervalVar: 2294 """Creates an interval variable from start, and a fixed size. 2295 2296 An interval variable is a constraint, that is itself used in other 2297 constraints like NoOverlap. 2298 2299 Args: 2300 start: The start of the interval. It must be of the form a * var + b. 2301 size: The size of the interval. It must be an integer value. 2302 name: The name of the interval variable. 2303 2304 Returns: 2305 An `IntervalVar` object. 2306 """ 2307 size = cmh.assert_is_int64(size) 2308 start_expr = self.parse_linear_expression(start) 2309 size_expr = self.parse_linear_expression(size) 2310 end_expr = self.parse_linear_expression(start + size) 2311 if len(start_expr.vars) > 1: 2312 raise TypeError( 2313 "cp_model.new_interval_var: start must be affine or constant." 2314 ) 2315 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.
2363 def new_optional_interval_var( 2364 self, 2365 start: LinearExprT, 2366 size: LinearExprT, 2367 end: LinearExprT, 2368 is_present: LiteralT, 2369 name: str, 2370 ) -> IntervalVar: 2371 """Creates an optional interval var from start, size, end, and is_present. 2372 2373 An optional interval variable is a constraint, that is itself used in other 2374 constraints like NoOverlap. This constraint is protected by a presence 2375 literal that indicates if it is active or not. 2376 2377 Internally, it ensures that `is_present` implies `start + size == 2378 end`. 2379 2380 Args: 2381 start: The start of the interval. It must be of the form a * var + b. 2382 size: The size of the interval. It must be of the form a * var + b. 2383 end: The end of the interval. It must be of the form a * var + b. 2384 is_present: A literal that indicates if the interval is active or not. A 2385 inactive interval is simply ignored by all constraints. 2386 name: The name of the interval variable. 2387 2388 Returns: 2389 An `IntervalVar` object. 2390 """ 2391 2392 # add the linear constraint. 2393 lin = self.add(start + size == end).only_enforce_if(is_present) 2394 if name: 2395 lin.with_name("lin_opt_" + name) 2396 2397 # Creates the IntervalConstraintProto object. 2398 is_present_index = self.get_or_make_boolean_index(is_present) 2399 start_expr = self.parse_linear_expression(start) 2400 size_expr = self.parse_linear_expression(size) 2401 end_expr = self.parse_linear_expression(end) 2402 if len(start_expr.vars) > 1: 2403 raise TypeError( 2404 "cp_model.new_interval_var: start must be affine or constant." 2405 ) 2406 if len(size_expr.vars) > 1: 2407 raise TypeError( 2408 "cp_model.new_interval_var: size must be affine or constant." 2409 ) 2410 if len(end_expr.vars) > 1: 2411 raise TypeError( 2412 "cp_model.new_interval_var: end must be affine or constant." 2413 ) 2414 return IntervalVar( 2415 self.__model, start_expr, size_expr, end_expr, is_present_index, name 2416 )
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.
2418 def new_optional_interval_var_series( 2419 self, 2420 name: str, 2421 index: pd.Index, 2422 starts: Union[LinearExprT, pd.Series], 2423 sizes: Union[LinearExprT, pd.Series], 2424 ends: Union[LinearExprT, pd.Series], 2425 are_present: Union[LiteralT, pd.Series], 2426 ) -> pd.Series: 2427 """Creates a series of interval variables with the given name. 2428 2429 Args: 2430 name (str): Required. The name of the variable set. 2431 index (pd.Index): Required. The index to use for the variable set. 2432 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2433 set. If a `pd.Series` is passed in, it will be based on the 2434 corresponding values of the pd.Series. 2435 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 2436 set. If a `pd.Series` is passed in, it will be based on the 2437 corresponding values of the pd.Series. 2438 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 2439 set. If a `pd.Series` is passed in, it will be based on the 2440 corresponding values of the pd.Series. 2441 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2442 interval in the set. If a `pd.Series` is passed in, it will be based on 2443 the corresponding values of the pd.Series. 2444 2445 Returns: 2446 pd.Series: The interval variable set indexed by its corresponding 2447 dimensions. 2448 2449 Raises: 2450 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2451 ValueError: if the `name` is not a valid identifier or already exists. 2452 ValueError: if the all the indexes do not match. 2453 """ 2454 if not isinstance(index, pd.Index): 2455 raise TypeError("Non-index object is used as index") 2456 if not name.isidentifier(): 2457 raise ValueError("name={} is not a valid identifier".format(name)) 2458 2459 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2460 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 2461 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 2462 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2463 2464 interval_array = [] 2465 for i in index: 2466 interval_array.append( 2467 self.new_optional_interval_var( 2468 start=starts[i], 2469 size=sizes[i], 2470 end=ends[i], 2471 is_present=are_present[i], 2472 name=f"{name}[{i}]", 2473 ) 2474 ) 2475 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.
2477 def new_optional_fixed_size_interval_var( 2478 self, 2479 start: LinearExprT, 2480 size: IntegralT, 2481 is_present: LiteralT, 2482 name: str, 2483 ) -> IntervalVar: 2484 """Creates an interval variable from start, and a fixed size. 2485 2486 An interval variable is a constraint, that is itself used in other 2487 constraints like NoOverlap. 2488 2489 Args: 2490 start: The start of the interval. It must be of the form a * var + b. 2491 size: The size of the interval. It must be an integer value. 2492 is_present: A literal that indicates if the interval is active or not. A 2493 inactive interval is simply ignored by all constraints. 2494 name: The name of the interval variable. 2495 2496 Returns: 2497 An `IntervalVar` object. 2498 """ 2499 size = cmh.assert_is_int64(size) 2500 start_expr = self.parse_linear_expression(start) 2501 size_expr = self.parse_linear_expression(size) 2502 end_expr = self.parse_linear_expression(start + size) 2503 if len(start_expr.vars) > 1: 2504 raise TypeError( 2505 "cp_model.new_interval_var: start must be affine or constant." 2506 ) 2507 is_present_index = self.get_or_make_boolean_index(is_present) 2508 return IntervalVar( 2509 self.__model, 2510 start_expr, 2511 size_expr, 2512 end_expr, 2513 is_present_index, 2514 name, 2515 )
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.
2517 def new_optional_fixed_size_interval_var_series( 2518 self, 2519 name: str, 2520 index: pd.Index, 2521 starts: Union[LinearExprT, pd.Series], 2522 sizes: Union[IntegralT, pd.Series], 2523 are_present: Union[LiteralT, pd.Series], 2524 ) -> pd.Series: 2525 """Creates a series of interval variables with the given name. 2526 2527 Args: 2528 name (str): Required. The name of the variable set. 2529 index (pd.Index): Required. The index to use for the variable set. 2530 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 2531 set. If a `pd.Series` is passed in, it will be based on the 2532 corresponding values of the pd.Series. 2533 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 2534 the set. If a `pd.Series` is passed in, it will be based on the 2535 corresponding values of the pd.Series. 2536 are_present (Union[LiteralT, pd.Series]): The performed literal of each 2537 interval in the set. If a `pd.Series` is passed in, it will be based on 2538 the corresponding values of the pd.Series. 2539 2540 Returns: 2541 pd.Series: The interval variable set indexed by its corresponding 2542 dimensions. 2543 2544 Raises: 2545 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 2546 ValueError: if the `name` is not a valid identifier or already exists. 2547 ValueError: if the all the indexes do not match. 2548 """ 2549 if not isinstance(index, pd.Index): 2550 raise TypeError("Non-index object is used as index") 2551 if not name.isidentifier(): 2552 raise ValueError("name={} is not a valid identifier".format(name)) 2553 2554 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 2555 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 2556 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 2557 interval_array = [] 2558 for i in index: 2559 interval_array.append( 2560 self.new_optional_fixed_size_interval_var( 2561 start=starts[i], 2562 size=sizes[i], 2563 is_present=are_present[i], 2564 name=f"{name}[{i}]", 2565 ) 2566 ) 2567 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.
2569 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 2570 """Adds NoOverlap(interval_vars). 2571 2572 A NoOverlap constraint ensures that all present intervals do not overlap 2573 in time. 2574 2575 Args: 2576 interval_vars: The list of interval variables to constrain. 2577 2578 Returns: 2579 An instance of the `Constraint` class. 2580 """ 2581 ct = Constraint(self) 2582 model_ct = self.__model.constraints[ct.index] 2583 model_ct.no_overlap.intervals.extend( 2584 [self.get_interval_index(x) for x in interval_vars] 2585 ) 2586 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.
2588 def add_no_overlap_2d( 2589 self, 2590 x_intervals: Iterable[IntervalVar], 2591 y_intervals: Iterable[IntervalVar], 2592 ) -> Constraint: 2593 """Adds NoOverlap2D(x_intervals, y_intervals). 2594 2595 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2596 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2597 by two intervals which represent its projection onto the X and Y axis. 2598 2599 Furthermore, one box is optional if at least one of the x or y interval is 2600 optional. 2601 2602 Args: 2603 x_intervals: The X coordinates of the rectangles. 2604 y_intervals: The Y coordinates of the rectangles. 2605 2606 Returns: 2607 An instance of the `Constraint` class. 2608 """ 2609 ct = Constraint(self) 2610 model_ct = self.__model.constraints[ct.index] 2611 model_ct.no_overlap_2d.x_intervals.extend( 2612 [self.get_interval_index(x) for x in x_intervals] 2613 ) 2614 model_ct.no_overlap_2d.y_intervals.extend( 2615 [self.get_interval_index(x) for x in y_intervals] 2616 ) 2617 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.
2619 def add_cumulative( 2620 self, 2621 intervals: Iterable[IntervalVar], 2622 demands: Iterable[LinearExprT], 2623 capacity: LinearExprT, 2624 ) -> Constraint: 2625 """Adds Cumulative(intervals, demands, capacity). 2626 2627 This constraint enforces that: 2628 2629 for all t: 2630 sum(demands[i] 2631 if (start(intervals[i]) <= t < end(intervals[i])) and 2632 (intervals[i] is present)) <= capacity 2633 2634 Args: 2635 intervals: The list of intervals. 2636 demands: The list of demands for each interval. Each demand must be >= 0. 2637 Each demand can be a 1-var affine expression (a * x + b). 2638 capacity: The maximum capacity of the cumulative constraint. It can be a 2639 1-var affine expression (a * x + b). 2640 2641 Returns: 2642 An instance of the `Constraint` class. 2643 """ 2644 cumulative = Constraint(self) 2645 model_ct = self.__model.constraints[cumulative.index] 2646 model_ct.cumulative.intervals.extend( 2647 [self.get_interval_index(x) for x in intervals] 2648 ) 2649 for d in demands: 2650 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2651 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2652 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.
2655 def clone(self) -> "CpModel": 2656 """Reset the model, and creates a new one from a CpModelProto instance.""" 2657 clone = CpModel() 2658 clone.proto.CopyFrom(self.proto) 2659 clone.rebuild_constant_map() 2660 return clone
Reset the model, and creates a new one from a CpModelProto instance.
2668 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2669 """Returns an already created Boolean variable from its index.""" 2670 if index < 0 or index >= len(self.__model.variables): 2671 raise ValueError( 2672 f"get_bool_var_from_proto_index: out of bound index {index}" 2673 ) 2674 var = self.__model.variables[index] 2675 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2676 raise ValueError( 2677 f"get_bool_var_from_proto_index: index {index} does not reference" 2678 + " a Boolean variable" 2679 ) 2680 2681 return IntVar(self.__model, index, None)
Returns an already created Boolean variable from its index.
2683 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2684 """Returns an already created integer variable from its index.""" 2685 if index < 0 or index >= len(self.__model.variables): 2686 raise ValueError( 2687 f"get_int_var_from_proto_index: out of bound index {index}" 2688 ) 2689 return IntVar(self.__model, index, None)
Returns an already created integer variable from its index.
2691 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2692 """Returns an already created interval variable from its index.""" 2693 if index < 0 or index >= len(self.__model.constraints): 2694 raise ValueError( 2695 f"get_interval_var_from_proto_index: out of bound index {index}" 2696 ) 2697 ct = self.__model.constraints[index] 2698 if not ct.HasField("interval"): 2699 raise ValueError( 2700 f"get_interval_var_from_proto_index: index {index} does not" 2701 " reference an" + " interval variable" 2702 ) 2703 2704 return IntervalVar(self.__model, index, None, None, None, None)
Returns an already created interval variable from its index.
2836 def minimize(self, obj: ObjLinearExprT): 2837 """Sets the objective of the model to minimize(obj).""" 2838 self._set_objective(obj, minimize=True)
Sets the objective of the model to minimize(obj).
2840 def maximize(self, obj: ObjLinearExprT): 2841 """Sets the objective of the model to maximize(obj).""" 2842 self._set_objective(obj, minimize=False)
Sets the objective of the model to maximize(obj).
2853 def add_decision_strategy( 2854 self, 2855 variables: Sequence[IntVar], 2856 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2857 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2858 ) -> None: 2859 """Adds a search strategy to the model. 2860 2861 Args: 2862 variables: a list of variables this strategy will assign. 2863 var_strategy: heuristic to choose the next variable to assign. 2864 domain_strategy: heuristic to reduce the domain of the selected variable. 2865 Currently, this is advanced code: the union of all strategies added to 2866 the model must be complete, i.e. instantiates all variables. Otherwise, 2867 solve() will fail. 2868 """ 2869 2870 strategy = self.__model.search_strategy.add() 2871 for v in variables: 2872 expr = strategy.exprs.add() 2873 if v.index >= 0: 2874 expr.vars.append(v.index) 2875 expr.coeffs.append(1) 2876 else: 2877 expr.vars.append(self.negated(v.index)) 2878 expr.coeffs.append(-1) 2879 expr.offset = 1 2880 2881 strategy.variable_selection_strategy = var_strategy 2882 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.
2884 def model_stats(self) -> str: 2885 """Returns a string containing some model statistics.""" 2886 return swig_helper.CpSatHelper.model_stats(self.__model)
Returns a string containing some model statistics.
2888 def validate(self) -> str: 2889 """Returns a string indicating that the model is invalid.""" 2890 return swig_helper.CpSatHelper.validate_model(self.__model)
Returns a string indicating that the model is invalid.
2892 def export_to_file(self, file: str) -> bool: 2893 """Write the model as a protocol buffer to 'file'. 2894 2895 Args: 2896 file: file to write the model to. If the filename ends with 'txt', the 2897 model will be written as a text file, otherwise, the binary format will 2898 be used. 2899 2900 Returns: 2901 True if the model was correctly written. 2902 """ 2903 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.
2905 def add_hint(self, var: IntVar, value: int) -> None: 2906 """Adds 'var == value' as a hint to the solver.""" 2907 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2908 self.__model.solution_hint.values.append(value)
Adds 'var == value' as a hint to the solver.
2910 def clear_hints(self): 2911 """Removes any solution hint from the model.""" 2912 self.__model.ClearField("solution_hint")
Removes any solution hint from the model.
2914 def add_assumption(self, lit: LiteralT) -> None: 2915 """Adds the literal to the model as assumptions.""" 2916 self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
Adds the literal to the model as assumptions.
2918 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2919 """Adds the literals to the model as assumptions.""" 2920 for lit in literals: 2921 self.add_assumption(lit)
Adds the literals to the model as assumptions.
3034def evaluate_linear_expr( 3035 expression: LinearExprT, solution: cp_model_pb2.CpSolverResponse 3036) -> int: 3037 """Evaluate a linear expression against a solution.""" 3038 if isinstance(expression, numbers.Integral): 3039 return int(expression) 3040 if not isinstance(expression, LinearExpr): 3041 raise TypeError("Cannot interpret %s as a linear expression." % expression) 3042 3043 value = 0 3044 to_process = [(expression, 1)] 3045 while to_process: 3046 expr, coeff = to_process.pop() 3047 if isinstance(expr, numbers.Integral): 3048 value += int(expr) * coeff 3049 elif isinstance(expr, _ProductCst): 3050 to_process.append((expr.expression(), coeff * expr.coefficient())) 3051 elif isinstance(expr, _Sum): 3052 to_process.append((expr.left(), coeff)) 3053 to_process.append((expr.right(), coeff)) 3054 elif isinstance(expr, _SumArray): 3055 for e in expr.expressions(): 3056 to_process.append((e, coeff)) 3057 value += expr.constant() * coeff 3058 elif isinstance(expr, _WeightedSum): 3059 for e, c in zip(expr.expressions(), expr.coefficients()): 3060 to_process.append((e, coeff * c)) 3061 value += expr.constant() * coeff 3062 elif isinstance(expr, IntVar): 3063 value += coeff * solution.solution[expr.index] 3064 elif isinstance(expr, _NotBooleanVariable): 3065 value += coeff * (1 - solution.solution[expr.negated().index]) 3066 else: 3067 raise TypeError(f"Cannot interpret {expr} as a linear expression.") 3068 3069 return value
Evaluate a linear expression against a solution.
3072def evaluate_boolean_expression( 3073 literal: LiteralT, solution: cp_model_pb2.CpSolverResponse 3074) -> bool: 3075 """Evaluate a boolean expression against a solution.""" 3076 if isinstance(literal, numbers.Integral): 3077 return bool(literal) 3078 elif isinstance(literal, IntVar) or isinstance(literal, _NotBooleanVariable): 3079 index: int = cast(Union[IntVar, _NotBooleanVariable], literal).index 3080 if index >= 0: 3081 return bool(solution.solution[index]) 3082 else: 3083 return not solution.solution[-index - 1] 3084 else: 3085 raise TypeError(f"Cannot interpret {literal} as a boolean expression.")
Evaluate a boolean expression against a solution.
3088class CpSolver: 3089 """Main solver class. 3090 3091 The purpose of this class is to search for a solution to the model provided 3092 to the solve() method. 3093 3094 Once solve() is called, this class allows inspecting the solution found 3095 with the value() and boolean_value() methods, as well as general statistics 3096 about the solve procedure. 3097 """ 3098 3099 def __init__(self): 3100 self.__solution: Optional[cp_model_pb2.CpSolverResponse] = None 3101 self.parameters: sat_parameters_pb2.SatParameters = ( 3102 sat_parameters_pb2.SatParameters() 3103 ) 3104 self.log_callback: Optional[swig_helper.LogCallback] = None 3105 self.__solve_wrapper: Optional[swig_helper.SolveWrapper] = None 3106 self.__lock: threading.Lock = threading.Lock() 3107 3108 def solve( 3109 self, 3110 model: CpModel, 3111 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3112 ) -> cp_model_pb2.CpSolverStatus: 3113 """Solves a problem and passes each solution to the callback if not null.""" 3114 with self.__lock: 3115 self.__solve_wrapper = swig_helper.SolveWrapper() 3116 3117 self.__solve_wrapper.set_parameters(self.parameters) 3118 if solution_callback is not None: 3119 self.__solve_wrapper.add_solution_callback(solution_callback) 3120 3121 if self.log_callback is not None: 3122 self.__solve_wrapper.add_log_callback(self.log_callback) 3123 3124 self.__solution = self.__solve_wrapper.solve(model.proto) 3125 3126 if solution_callback is not None: 3127 self.__solve_wrapper.clear_solution_callback(solution_callback) 3128 3129 with self.__lock: 3130 self.__solve_wrapper = None 3131 3132 return self.__solution.status 3133 3134 def stop_search(self) -> None: 3135 """Stops the current search asynchronously.""" 3136 with self.__lock: 3137 if self.__solve_wrapper: 3138 self.__solve_wrapper.stop_search() 3139 3140 def value(self, expression: LinearExprT) -> int: 3141 """Returns the value of a linear expression after solve.""" 3142 return evaluate_linear_expr(expression, self._solution) 3143 3144 def values(self, variables: _IndexOrSeries) -> pd.Series: 3145 """Returns the values of the input variables. 3146 3147 If `variables` is a `pd.Index`, then the output will be indexed by the 3148 variables. If `variables` is a `pd.Series` indexed by the underlying 3149 dimensions, then the output will be indexed by the same underlying 3150 dimensions. 3151 3152 Args: 3153 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3154 get the values. 3155 3156 Returns: 3157 pd.Series: The values of all variables in the set. 3158 """ 3159 solution = self._solution 3160 return _attribute_series( 3161 func=lambda v: solution.solution[v.index], 3162 values=variables, 3163 ) 3164 3165 def boolean_value(self, literal: LiteralT) -> bool: 3166 """Returns the boolean value of a literal after solve.""" 3167 return evaluate_boolean_expression(literal, self._solution) 3168 3169 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 3170 """Returns the values of the input variables. 3171 3172 If `variables` is a `pd.Index`, then the output will be indexed by the 3173 variables. If `variables` is a `pd.Series` indexed by the underlying 3174 dimensions, then the output will be indexed by the same underlying 3175 dimensions. 3176 3177 Args: 3178 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3179 get the values. 3180 3181 Returns: 3182 pd.Series: The values of all variables in the set. 3183 """ 3184 solution = self._solution 3185 return _attribute_series( 3186 func=lambda literal: evaluate_boolean_expression(literal, solution), 3187 values=variables, 3188 ) 3189 3190 @property 3191 def objective_value(self) -> float: 3192 """Returns the value of the objective after solve.""" 3193 return self._solution.objective_value 3194 3195 @property 3196 def best_objective_bound(self) -> float: 3197 """Returns the best lower (upper) bound found when min(max)imizing.""" 3198 return self._solution.best_objective_bound 3199 3200 @property 3201 def num_booleans(self) -> int: 3202 """Returns the number of boolean variables managed by the SAT solver.""" 3203 return self._solution.num_booleans 3204 3205 @property 3206 def num_conflicts(self) -> int: 3207 """Returns the number of conflicts since the creation of the solver.""" 3208 return self._solution.num_conflicts 3209 3210 @property 3211 def num_branches(self) -> int: 3212 """Returns the number of search branches explored by the solver.""" 3213 return self._solution.num_branches 3214 3215 @property 3216 def wall_time(self) -> float: 3217 """Returns the wall time in seconds since the creation of the solver.""" 3218 return self._solution.wall_time 3219 3220 @property 3221 def user_time(self) -> float: 3222 """Returns the user time in seconds since the creation of the solver.""" 3223 return self._solution.user_time 3224 3225 @property 3226 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3227 """Returns the response object.""" 3228 return self._solution 3229 3230 def response_stats(self) -> str: 3231 """Returns some statistics on the solution found as a string.""" 3232 return swig_helper.CpSatHelper.solver_response_stats(self._solution) 3233 3234 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 3235 """Returns the indices of the infeasible assumptions.""" 3236 return self._solution.sufficient_assumptions_for_infeasibility 3237 3238 def status_name(self, status: Optional[Any] = None) -> str: 3239 """Returns the name of the status returned by solve().""" 3240 if status is None: 3241 status = self._solution.status 3242 return cp_model_pb2.CpSolverStatus.Name(status) 3243 3244 def solution_info(self) -> str: 3245 """Returns some information on the solve process. 3246 3247 Returns some information on how the solution was found, or the reason 3248 why the model or the parameters are invalid. 3249 3250 Raises: 3251 RuntimeError: if solve() has not been called. 3252 """ 3253 return self._solution.solution_info 3254 3255 @property 3256 def _solution(self) -> cp_model_pb2.CpSolverResponse: 3257 """Checks solve() has been called, and returns the solution.""" 3258 if self.__solution is None: 3259 raise RuntimeError("solve() has not been called.") 3260 return self.__solution 3261 3262 # Compatibility with pre PEP8 3263 # pylint: disable=invalid-name 3264 3265 def BestObjectiveBound(self) -> float: 3266 return self.best_objective_bound 3267 3268 def BooleanValue(self, literal: LiteralT) -> bool: 3269 return self.boolean_value(literal) 3270 3271 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: 3272 return self.boolean_values(variables) 3273 3274 def NumBooleans(self) -> int: 3275 return self.num_booleans 3276 3277 def NumConflicts(self) -> int: 3278 return self.num_conflicts 3279 3280 def NumBranches(self) -> int: 3281 return self.num_branches 3282 3283 def ObjectiveValue(self) -> float: 3284 return self.objective_value 3285 3286 def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: 3287 return self.response_proto 3288 3289 def ResponseStats(self) -> str: 3290 return self.response_stats() 3291 3292 def Solve( 3293 self, 3294 model: CpModel, 3295 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3296 ) -> cp_model_pb2.CpSolverStatus: 3297 return self.solve(model, solution_callback) 3298 3299 def SolutionInfo(self) -> str: 3300 return self.solution_info() 3301 3302 def StatusName(self, status: Optional[Any] = None) -> str: 3303 return self.status_name(status) 3304 3305 def StopSearch(self) -> None: 3306 self.stop_search() 3307 3308 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: 3309 return self.sufficient_assumptions_for_infeasibility() 3310 3311 def UserTime(self) -> float: 3312 return self.user_time 3313 3314 def Value(self, expression: LinearExprT) -> int: 3315 return self.value(expression) 3316 3317 def Values(self, variables: _IndexOrSeries) -> pd.Series: 3318 return self.values(variables) 3319 3320 def WallTime(self) -> float: 3321 return self.wall_time 3322 3323 def SolveWithSolutionCallback( 3324 self, model: CpModel, callback: "CpSolverSolutionCallback" 3325 ) -> cp_model_pb2.CpSolverStatus: 3326 """DEPRECATED Use solve() with the callback argument.""" 3327 warnings.warn( 3328 "solve_with_solution_callback is deprecated; use solve() with" 3329 + "the callback argument.", 3330 DeprecationWarning, 3331 ) 3332 return self.solve(model, callback) 3333 3334 def SearchForAllSolutions( 3335 self, model: CpModel, callback: "CpSolverSolutionCallback" 3336 ) -> cp_model_pb2.CpSolverStatus: 3337 """DEPRECATED Use solve() with the right parameter. 3338 3339 Search for all solutions of a satisfiability problem. 3340 3341 This method searches for all feasible solutions of a given model. 3342 Then it feeds the solution to the callback. 3343 3344 Note that the model cannot contain an objective. 3345 3346 Args: 3347 model: The model to solve. 3348 callback: The callback that will be called at each solution. 3349 3350 Returns: 3351 The status of the solve: 3352 3353 * *FEASIBLE* if some solutions have been found 3354 * *INFEASIBLE* if the solver has proved there are no solution 3355 * *OPTIMAL* if all solutions have been found 3356 """ 3357 warnings.warn( 3358 "search_for_all_solutions is deprecated; use solve() with" 3359 + "enumerate_all_solutions = True.", 3360 DeprecationWarning, 3361 ) 3362 if model.has_objective(): 3363 raise TypeError( 3364 "Search for all solutions is only defined on satisfiability problems" 3365 ) 3366 # Store old parameter. 3367 enumerate_all = self.parameters.enumerate_all_solutions 3368 self.parameters.enumerate_all_solutions = True 3369 3370 self.solve(model, callback) 3371 3372 # Restore parameter. 3373 self.parameters.enumerate_all_solutions = enumerate_all 3374 return self.__solution.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.
3108 def solve( 3109 self, 3110 model: CpModel, 3111 solution_callback: Optional["CpSolverSolutionCallback"] = None, 3112 ) -> cp_model_pb2.CpSolverStatus: 3113 """Solves a problem and passes each solution to the callback if not null.""" 3114 with self.__lock: 3115 self.__solve_wrapper = swig_helper.SolveWrapper() 3116 3117 self.__solve_wrapper.set_parameters(self.parameters) 3118 if solution_callback is not None: 3119 self.__solve_wrapper.add_solution_callback(solution_callback) 3120 3121 if self.log_callback is not None: 3122 self.__solve_wrapper.add_log_callback(self.log_callback) 3123 3124 self.__solution = self.__solve_wrapper.solve(model.proto) 3125 3126 if solution_callback is not None: 3127 self.__solve_wrapper.clear_solution_callback(solution_callback) 3128 3129 with self.__lock: 3130 self.__solve_wrapper = None 3131 3132 return self.__solution.status
Solves a problem and passes each solution to the callback if not null.
3134 def stop_search(self) -> None: 3135 """Stops the current search asynchronously.""" 3136 with self.__lock: 3137 if self.__solve_wrapper: 3138 self.__solve_wrapper.stop_search()
Stops the current search asynchronously.
3140 def value(self, expression: LinearExprT) -> int: 3141 """Returns the value of a linear expression after solve.""" 3142 return evaluate_linear_expr(expression, self._solution)
Returns the value of a linear expression after solve.
3144 def values(self, variables: _IndexOrSeries) -> pd.Series: 3145 """Returns the values of the input variables. 3146 3147 If `variables` is a `pd.Index`, then the output will be indexed by the 3148 variables. If `variables` is a `pd.Series` indexed by the underlying 3149 dimensions, then the output will be indexed by the same underlying 3150 dimensions. 3151 3152 Args: 3153 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3154 get the values. 3155 3156 Returns: 3157 pd.Series: The values of all variables in the set. 3158 """ 3159 solution = self._solution 3160 return _attribute_series( 3161 func=lambda v: solution.solution[v.index], 3162 values=variables, 3163 )
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.
3165 def boolean_value(self, literal: LiteralT) -> bool: 3166 """Returns the boolean value of a literal after solve.""" 3167 return evaluate_boolean_expression(literal, self._solution)
Returns the boolean value of a literal after solve.
3169 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 3170 """Returns the values of the input variables. 3171 3172 If `variables` is a `pd.Index`, then the output will be indexed by the 3173 variables. If `variables` is a `pd.Series` indexed by the underlying 3174 dimensions, then the output will be indexed by the same underlying 3175 dimensions. 3176 3177 Args: 3178 variables (Union[pd.Index, pd.Series]): The set of variables from which to 3179 get the values. 3180 3181 Returns: 3182 pd.Series: The values of all variables in the set. 3183 """ 3184 solution = self._solution 3185 return _attribute_series( 3186 func=lambda literal: evaluate_boolean_expression(literal, solution), 3187 values=variables, 3188 )
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.
3190 @property 3191 def objective_value(self) -> float: 3192 """Returns the value of the objective after solve.""" 3193 return self._solution.objective_value
Returns the value of the objective after solve.
3195 @property 3196 def best_objective_bound(self) -> float: 3197 """Returns the best lower (upper) bound found when min(max)imizing.""" 3198 return self._solution.best_objective_bound
Returns the best lower (upper) bound found when min(max)imizing.
3200 @property 3201 def num_booleans(self) -> int: 3202 """Returns the number of boolean variables managed by the SAT solver.""" 3203 return self._solution.num_booleans
Returns the number of boolean variables managed by the SAT solver.
3205 @property 3206 def num_conflicts(self) -> int: 3207 """Returns the number of conflicts since the creation of the solver.""" 3208 return self._solution.num_conflicts
Returns the number of conflicts since the creation of the solver.
3210 @property 3211 def num_branches(self) -> int: 3212 """Returns the number of search branches explored by the solver.""" 3213 return self._solution.num_branches
Returns the number of search branches explored by the solver.
3215 @property 3216 def wall_time(self) -> float: 3217 """Returns the wall time in seconds since the creation of the solver.""" 3218 return self._solution.wall_time
Returns the wall time in seconds since the creation of the solver.
3220 @property 3221 def user_time(self) -> float: 3222 """Returns the user time in seconds since the creation of the solver.""" 3223 return self._solution.user_time
Returns the user time in seconds since the creation of the solver.
3225 @property 3226 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3227 """Returns the response object.""" 3228 return self._solution
Returns the response object.
3230 def response_stats(self) -> str: 3231 """Returns some statistics on the solution found as a string.""" 3232 return swig_helper.CpSatHelper.solver_response_stats(self._solution)
Returns some statistics on the solution found as a string.
3234 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 3235 """Returns the indices of the infeasible assumptions.""" 3236 return self._solution.sufficient_assumptions_for_infeasibility
Returns the indices of the infeasible assumptions.
3238 def status_name(self, status: Optional[Any] = None) -> str: 3239 """Returns the name of the status returned by solve().""" 3240 if status is None: 3241 status = self._solution.status 3242 return cp_model_pb2.CpSolverStatus.Name(status)
Returns the name of the status returned by solve().
3244 def solution_info(self) -> str: 3245 """Returns some information on the solve process. 3246 3247 Returns some information on how the solution was found, or the reason 3248 why the model or the parameters are invalid. 3249 3250 Raises: 3251 RuntimeError: if solve() has not been called. 3252 """ 3253 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.
3323 def SolveWithSolutionCallback( 3324 self, model: CpModel, callback: "CpSolverSolutionCallback" 3325 ) -> cp_model_pb2.CpSolverStatus: 3326 """DEPRECATED Use solve() with the callback argument.""" 3327 warnings.warn( 3328 "solve_with_solution_callback is deprecated; use solve() with" 3329 + "the callback argument.", 3330 DeprecationWarning, 3331 ) 3332 return self.solve(model, callback)
DEPRECATED Use solve() with the callback argument.
3334 def SearchForAllSolutions( 3335 self, model: CpModel, callback: "CpSolverSolutionCallback" 3336 ) -> cp_model_pb2.CpSolverStatus: 3337 """DEPRECATED Use solve() with the right parameter. 3338 3339 Search for all solutions of a satisfiability problem. 3340 3341 This method searches for all feasible solutions of a given model. 3342 Then it feeds the solution to the callback. 3343 3344 Note that the model cannot contain an objective. 3345 3346 Args: 3347 model: The model to solve. 3348 callback: The callback that will be called at each solution. 3349 3350 Returns: 3351 The status of the solve: 3352 3353 * *FEASIBLE* if some solutions have been found 3354 * *INFEASIBLE* if the solver has proved there are no solution 3355 * *OPTIMAL* if all solutions have been found 3356 """ 3357 warnings.warn( 3358 "search_for_all_solutions is deprecated; use solve() with" 3359 + "enumerate_all_solutions = True.", 3360 DeprecationWarning, 3361 ) 3362 if model.has_objective(): 3363 raise TypeError( 3364 "Search for all solutions is only defined on satisfiability problems" 3365 ) 3366 # Store old parameter. 3367 enumerate_all = self.parameters.enumerate_all_solutions 3368 self.parameters.enumerate_all_solutions = True 3369 3370 self.solve(model, callback) 3371 3372 # Restore parameter. 3373 self.parameters.enumerate_all_solutions = enumerate_all 3374 return self.__solution.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
3380class CpSolverSolutionCallback(swig_helper.SolutionCallback): 3381 """Solution callback. 3382 3383 This class implements a callback that will be called at each new solution 3384 found during search. 3385 3386 The method on_solution_callback() will be called by the solver, and must be 3387 implemented. The current solution can be queried using the boolean_value() 3388 and value() methods. 3389 3390 These methods returns the same information as their counterpart in the 3391 `CpSolver` class. 3392 """ 3393 3394 def __init__(self): 3395 swig_helper.SolutionCallback.__init__(self) 3396 3397 def OnSolutionCallback(self) -> None: 3398 """Proxy for the same method in snake case.""" 3399 self.on_solution_callback() 3400 3401 def boolean_value(self, lit: LiteralT) -> bool: 3402 """Returns the boolean value of a boolean literal. 3403 3404 Args: 3405 lit: A boolean variable or its negation. 3406 3407 Returns: 3408 The Boolean value of the literal in the solution. 3409 3410 Raises: 3411 RuntimeError: if `lit` is not a boolean variable or its negation. 3412 """ 3413 if not self.has_response(): 3414 raise RuntimeError("solve() has not been called.") 3415 if isinstance(lit, numbers.Integral): 3416 return bool(lit) 3417 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3418 return self.SolutionBooleanValue( 3419 cast(Union[IntVar, _NotBooleanVariable], lit).index 3420 ) 3421 if cmh.is_boolean(lit): 3422 return bool(lit) 3423 raise TypeError(f"Cannot interpret {lit} as a boolean expression.") 3424 3425 def value(self, expression: LinearExprT) -> int: 3426 """Evaluates an linear expression in the current solution. 3427 3428 Args: 3429 expression: a linear expression of the model. 3430 3431 Returns: 3432 An integer value equal to the evaluation of the linear expression 3433 against the current solution. 3434 3435 Raises: 3436 RuntimeError: if 'expression' is not a LinearExpr. 3437 """ 3438 if not self.has_response(): 3439 raise RuntimeError("solve() has not been called.") 3440 3441 value = 0 3442 to_process = [(expression, 1)] 3443 while to_process: 3444 expr, coeff = to_process.pop() 3445 if isinstance(expr, numbers.Integral): 3446 value += int(expr) * coeff 3447 elif isinstance(expr, _ProductCst): 3448 to_process.append((expr.expression(), coeff * expr.coefficient())) 3449 elif isinstance(expr, _Sum): 3450 to_process.append((expr.left(), coeff)) 3451 to_process.append((expr.right(), coeff)) 3452 elif isinstance(expr, _SumArray): 3453 for e in expr.expressions(): 3454 to_process.append((e, coeff)) 3455 value += expr.constant() * coeff 3456 elif isinstance(expr, _WeightedSum): 3457 for e, c in zip(expr.expressions(), expr.coefficients()): 3458 to_process.append((e, coeff * c)) 3459 value += expr.constant() * coeff 3460 elif isinstance(expr, IntVar): 3461 value += coeff * self.SolutionIntegerValue(expr.index) 3462 elif isinstance(expr, _NotBooleanVariable): 3463 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3464 else: 3465 raise TypeError( 3466 f"cannot interpret {expression} as a linear expression." 3467 ) 3468 3469 return value 3470 3471 def has_response(self) -> bool: 3472 return self.HasResponse() 3473 3474 def stop_search(self) -> None: 3475 """Stops the current search asynchronously.""" 3476 if not self.has_response(): 3477 raise RuntimeError("solve() has not been called.") 3478 self.StopSearch() 3479 3480 @property 3481 def objective_value(self) -> float: 3482 """Returns the value of the objective after solve.""" 3483 if not self.has_response(): 3484 raise RuntimeError("solve() has not been called.") 3485 return self.ObjectiveValue() 3486 3487 @property 3488 def best_objective_bound(self) -> float: 3489 """Returns the best lower (upper) bound found when min(max)imizing.""" 3490 if not self.has_response(): 3491 raise RuntimeError("solve() has not been called.") 3492 return self.BestObjectiveBound() 3493 3494 @property 3495 def num_booleans(self) -> int: 3496 """Returns the number of boolean variables managed by the SAT solver.""" 3497 if not self.has_response(): 3498 raise RuntimeError("solve() has not been called.") 3499 return self.NumBooleans() 3500 3501 @property 3502 def num_conflicts(self) -> int: 3503 """Returns the number of conflicts since the creation of the solver.""" 3504 if not self.has_response(): 3505 raise RuntimeError("solve() has not been called.") 3506 return self.NumConflicts() 3507 3508 @property 3509 def num_branches(self) -> int: 3510 """Returns the number of search branches explored by the solver.""" 3511 if not self.has_response(): 3512 raise RuntimeError("solve() has not been called.") 3513 return self.NumBranches() 3514 3515 @property 3516 def num_integer_propagations(self) -> int: 3517 """Returns the number of integer propagations done by the solver.""" 3518 if not self.has_response(): 3519 raise RuntimeError("solve() has not been called.") 3520 return self.NumIntegerPropagations() 3521 3522 @property 3523 def num_boolean_propagations(self) -> int: 3524 """Returns the number of Boolean propagations done by the solver.""" 3525 if not self.has_response(): 3526 raise RuntimeError("solve() has not been called.") 3527 return self.NumBooleanPropagations() 3528 3529 @property 3530 def deterministic_time(self) -> float: 3531 """Returns the determistic time in seconds since the creation of the solver.""" 3532 if not self.has_response(): 3533 raise RuntimeError("solve() has not been called.") 3534 return self.DeterministicTime() 3535 3536 @property 3537 def wall_time(self) -> float: 3538 """Returns the wall time in seconds since the creation of the solver.""" 3539 if not self.has_response(): 3540 raise RuntimeError("solve() has not been called.") 3541 return self.WallTime() 3542 3543 @property 3544 def user_time(self) -> float: 3545 """Returns the user time in seconds since the creation of the solver.""" 3546 if not self.has_response(): 3547 raise RuntimeError("solve() has not been called.") 3548 return self.UserTime() 3549 3550 @property 3551 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3552 """Returns the response object.""" 3553 if not self.has_response(): 3554 raise RuntimeError("solve() has not been called.") 3555 return self.Response() 3556 3557 # Compatibility with pre PEP8 3558 # pylint: disable=invalid-name 3559 Value = value 3560 BooleanValue = boolean_value 3561 # 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.
3397 def OnSolutionCallback(self) -> None: 3398 """Proxy for the same method in snake case.""" 3399 self.on_solution_callback()
Proxy for the same method in snake case.
3401 def boolean_value(self, lit: LiteralT) -> bool: 3402 """Returns the boolean value of a boolean literal. 3403 3404 Args: 3405 lit: A boolean variable or its negation. 3406 3407 Returns: 3408 The Boolean value of the literal in the solution. 3409 3410 Raises: 3411 RuntimeError: if `lit` is not a boolean variable or its negation. 3412 """ 3413 if not self.has_response(): 3414 raise RuntimeError("solve() has not been called.") 3415 if isinstance(lit, numbers.Integral): 3416 return bool(lit) 3417 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3418 return self.SolutionBooleanValue( 3419 cast(Union[IntVar, _NotBooleanVariable], lit).index 3420 ) 3421 if cmh.is_boolean(lit): 3422 return bool(lit) 3423 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.
3425 def value(self, expression: LinearExprT) -> int: 3426 """Evaluates an linear expression in the current solution. 3427 3428 Args: 3429 expression: a linear expression of the model. 3430 3431 Returns: 3432 An integer value equal to the evaluation of the linear expression 3433 against the current solution. 3434 3435 Raises: 3436 RuntimeError: if 'expression' is not a LinearExpr. 3437 """ 3438 if not self.has_response(): 3439 raise RuntimeError("solve() has not been called.") 3440 3441 value = 0 3442 to_process = [(expression, 1)] 3443 while to_process: 3444 expr, coeff = to_process.pop() 3445 if isinstance(expr, numbers.Integral): 3446 value += int(expr) * coeff 3447 elif isinstance(expr, _ProductCst): 3448 to_process.append((expr.expression(), coeff * expr.coefficient())) 3449 elif isinstance(expr, _Sum): 3450 to_process.append((expr.left(), coeff)) 3451 to_process.append((expr.right(), coeff)) 3452 elif isinstance(expr, _SumArray): 3453 for e in expr.expressions(): 3454 to_process.append((e, coeff)) 3455 value += expr.constant() * coeff 3456 elif isinstance(expr, _WeightedSum): 3457 for e, c in zip(expr.expressions(), expr.coefficients()): 3458 to_process.append((e, coeff * c)) 3459 value += expr.constant() * coeff 3460 elif isinstance(expr, IntVar): 3461 value += coeff * self.SolutionIntegerValue(expr.index) 3462 elif isinstance(expr, _NotBooleanVariable): 3463 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3464 else: 3465 raise TypeError( 3466 f"cannot interpret {expression} as a linear expression." 3467 ) 3468 3469 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.
3474 def stop_search(self) -> None: 3475 """Stops the current search asynchronously.""" 3476 if not self.has_response(): 3477 raise RuntimeError("solve() has not been called.") 3478 self.StopSearch()
Stops the current search asynchronously.
3480 @property 3481 def objective_value(self) -> float: 3482 """Returns the value of the objective after solve.""" 3483 if not self.has_response(): 3484 raise RuntimeError("solve() has not been called.") 3485 return self.ObjectiveValue()
Returns the value of the objective after solve.
3487 @property 3488 def best_objective_bound(self) -> float: 3489 """Returns the best lower (upper) bound found when min(max)imizing.""" 3490 if not self.has_response(): 3491 raise RuntimeError("solve() has not been called.") 3492 return self.BestObjectiveBound()
Returns the best lower (upper) bound found when min(max)imizing.
3494 @property 3495 def num_booleans(self) -> int: 3496 """Returns the number of boolean variables managed by the SAT solver.""" 3497 if not self.has_response(): 3498 raise RuntimeError("solve() has not been called.") 3499 return self.NumBooleans()
Returns the number of boolean variables managed by the SAT solver.
3501 @property 3502 def num_conflicts(self) -> int: 3503 """Returns the number of conflicts since the creation of the solver.""" 3504 if not self.has_response(): 3505 raise RuntimeError("solve() has not been called.") 3506 return self.NumConflicts()
Returns the number of conflicts since the creation of the solver.
3508 @property 3509 def num_branches(self) -> int: 3510 """Returns the number of search branches explored by the solver.""" 3511 if not self.has_response(): 3512 raise RuntimeError("solve() has not been called.") 3513 return self.NumBranches()
Returns the number of search branches explored by the solver.
3515 @property 3516 def num_integer_propagations(self) -> int: 3517 """Returns the number of integer propagations done by the solver.""" 3518 if not self.has_response(): 3519 raise RuntimeError("solve() has not been called.") 3520 return self.NumIntegerPropagations()
Returns the number of integer propagations done by the solver.
3522 @property 3523 def num_boolean_propagations(self) -> int: 3524 """Returns the number of Boolean propagations done by the solver.""" 3525 if not self.has_response(): 3526 raise RuntimeError("solve() has not been called.") 3527 return self.NumBooleanPropagations()
Returns the number of Boolean propagations done by the solver.
3529 @property 3530 def deterministic_time(self) -> float: 3531 """Returns the determistic time in seconds since the creation of the solver.""" 3532 if not self.has_response(): 3533 raise RuntimeError("solve() has not been called.") 3534 return self.DeterministicTime()
Returns the determistic time in seconds since the creation of the solver.
3536 @property 3537 def wall_time(self) -> float: 3538 """Returns the wall time in seconds since the creation of the solver.""" 3539 if not self.has_response(): 3540 raise RuntimeError("solve() has not been called.") 3541 return self.WallTime()
Returns the wall time in seconds since the creation of the solver.
3543 @property 3544 def user_time(self) -> float: 3545 """Returns the user time in seconds since the creation of the solver.""" 3546 if not self.has_response(): 3547 raise RuntimeError("solve() has not been called.") 3548 return self.UserTime()
Returns the user time in seconds since the creation of the solver.
3550 @property 3551 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 3552 """Returns the response object.""" 3553 if not self.has_response(): 3554 raise RuntimeError("solve() has not been called.") 3555 return self.Response()
Returns the response object.
3425 def value(self, expression: LinearExprT) -> int: 3426 """Evaluates an linear expression in the current solution. 3427 3428 Args: 3429 expression: a linear expression of the model. 3430 3431 Returns: 3432 An integer value equal to the evaluation of the linear expression 3433 against the current solution. 3434 3435 Raises: 3436 RuntimeError: if 'expression' is not a LinearExpr. 3437 """ 3438 if not self.has_response(): 3439 raise RuntimeError("solve() has not been called.") 3440 3441 value = 0 3442 to_process = [(expression, 1)] 3443 while to_process: 3444 expr, coeff = to_process.pop() 3445 if isinstance(expr, numbers.Integral): 3446 value += int(expr) * coeff 3447 elif isinstance(expr, _ProductCst): 3448 to_process.append((expr.expression(), coeff * expr.coefficient())) 3449 elif isinstance(expr, _Sum): 3450 to_process.append((expr.left(), coeff)) 3451 to_process.append((expr.right(), coeff)) 3452 elif isinstance(expr, _SumArray): 3453 for e in expr.expressions(): 3454 to_process.append((e, coeff)) 3455 value += expr.constant() * coeff 3456 elif isinstance(expr, _WeightedSum): 3457 for e, c in zip(expr.expressions(), expr.coefficients()): 3458 to_process.append((e, coeff * c)) 3459 value += expr.constant() * coeff 3460 elif isinstance(expr, IntVar): 3461 value += coeff * self.SolutionIntegerValue(expr.index) 3462 elif isinstance(expr, _NotBooleanVariable): 3463 value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index)) 3464 else: 3465 raise TypeError( 3466 f"cannot interpret {expression} as a linear expression." 3467 ) 3468 3469 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.
3401 def boolean_value(self, lit: LiteralT) -> bool: 3402 """Returns the boolean value of a boolean literal. 3403 3404 Args: 3405 lit: A boolean variable or its negation. 3406 3407 Returns: 3408 The Boolean value of the literal in the solution. 3409 3410 Raises: 3411 RuntimeError: if `lit` is not a boolean variable or its negation. 3412 """ 3413 if not self.has_response(): 3414 raise RuntimeError("solve() has not been called.") 3415 if isinstance(lit, numbers.Integral): 3416 return bool(lit) 3417 if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): 3418 return self.SolutionBooleanValue( 3419 cast(Union[IntVar, _NotBooleanVariable], lit).index 3420 ) 3421 if cmh.is_boolean(lit): 3422 return bool(lit) 3423 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.
3564class ObjectiveSolutionPrinter(CpSolverSolutionCallback): 3565 """Display the objective value and time of intermediate solutions.""" 3566 3567 def __init__(self): 3568 CpSolverSolutionCallback.__init__(self) 3569 self.__solution_count = 0 3570 self.__start_time = time.time() 3571 3572 def on_solution_callback(self) -> None: 3573 """Called on each new solution.""" 3574 current_time = time.time() 3575 obj = self.objective_value 3576 print( 3577 "Solution %i, time = %0.2f s, objective = %i" 3578 % (self.__solution_count, current_time - self.__start_time, obj) 3579 ) 3580 self.__solution_count += 1 3581 3582 def solution_count(self) -> int: 3583 """Returns the number of solutions found.""" 3584 return self.__solution_count
Display the objective value and time of intermediate solutions.
3567 def __init__(self): 3568 CpSolverSolutionCallback.__init__(self) 3569 self.__solution_count = 0 3570 self.__start_time = time.time()
__init__(self: ortools.sat.python.swig_helper.SolutionCallback) -> None
3572 def on_solution_callback(self) -> None: 3573 """Called on each new solution.""" 3574 current_time = time.time() 3575 obj = self.objective_value 3576 print( 3577 "Solution %i, time = %0.2f s, objective = %i" 3578 % (self.__solution_count, current_time - self.__start_time, obj) 3579 ) 3580 self.__solution_count += 1
Called on each new solution.
3582 def solution_count(self) -> int: 3583 """Returns the number of solutions found.""" 3584 return self.__solution_count
Returns the number of solutions found.
Inherited Members
3587class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): 3588 """Print intermediate solutions (objective, variable values, time).""" 3589 3590 def __init__(self, variables): 3591 CpSolverSolutionCallback.__init__(self) 3592 self.__variables: Sequence[IntVar] = variables 3593 self.__solution_count: int = 0 3594 self.__start_time: float = time.time() 3595 3596 def on_solution_callback(self) -> None: 3597 """Called on each new solution.""" 3598 current_time = time.time() 3599 obj = self.objective_value 3600 print( 3601 "Solution %i, time = %0.2f s, objective = %i" 3602 % (self.__solution_count, current_time - self.__start_time, obj) 3603 ) 3604 for v in self.__variables: 3605 print(" %s = %i" % (v, self.value(v)), end=" ") 3606 print() 3607 self.__solution_count += 1 3608 3609 @property 3610 def solution_count(self) -> int: 3611 """Returns the number of solutions found.""" 3612 return self.__solution_count
Print intermediate solutions (objective, variable values, time).
3590 def __init__(self, variables): 3591 CpSolverSolutionCallback.__init__(self) 3592 self.__variables: Sequence[IntVar] = variables 3593 self.__solution_count: int = 0 3594 self.__start_time: float = time.time()
__init__(self: ortools.sat.python.swig_helper.SolutionCallback) -> None
3596 def on_solution_callback(self) -> None: 3597 """Called on each new solution.""" 3598 current_time = time.time() 3599 obj = self.objective_value 3600 print( 3601 "Solution %i, time = %0.2f s, objective = %i" 3602 % (self.__solution_count, current_time - self.__start_time, obj) 3603 ) 3604 for v in self.__variables: 3605 print(" %s = %i" % (v, self.value(v)), end=" ") 3606 print() 3607 self.__solution_count += 1
Called on each new solution.
3609 @property 3610 def solution_count(self) -> int: 3611 """Returns the number of solutions found.""" 3612 return self.__solution_count
Returns the number of solutions found.
Inherited Members
3615class VarArraySolutionPrinter(CpSolverSolutionCallback): 3616 """Print intermediate solutions (variable values, time).""" 3617 3618 def __init__(self, variables: Sequence[IntVar]): 3619 CpSolverSolutionCallback.__init__(self) 3620 self.__variables: Sequence[IntVar] = variables 3621 self.__solution_count: int = 0 3622 self.__start_time: float = time.time() 3623 3624 def on_solution_callback(self) -> None: 3625 """Called on each new solution.""" 3626 current_time = time.time() 3627 print( 3628 "Solution %i, time = %0.2f s" 3629 % (self.__solution_count, current_time - self.__start_time) 3630 ) 3631 for v in self.__variables: 3632 print(" %s = %i" % (v, self.value(v)), end=" ") 3633 print() 3634 self.__solution_count += 1 3635 3636 @property 3637 def solution_count(self) -> int: 3638 """Returns the number of solutions found.""" 3639 return self.__solution_count
Print intermediate solutions (variable values, time).
3618 def __init__(self, variables: Sequence[IntVar]): 3619 CpSolverSolutionCallback.__init__(self) 3620 self.__variables: Sequence[IntVar] = variables 3621 self.__solution_count: int = 0 3622 self.__start_time: float = time.time()
__init__(self: ortools.sat.python.swig_helper.SolutionCallback) -> None
3624 def on_solution_callback(self) -> None: 3625 """Called on each new solution.""" 3626 current_time = time.time() 3627 print( 3628 "Solution %i, time = %0.2f s" 3629 % (self.__solution_count, current_time - self.__start_time) 3630 ) 3631 for v in self.__variables: 3632 print(" %s = %i" % (v, self.value(v)), end=" ") 3633 print() 3634 self.__solution_count += 1
Called on each new solution.
3636 @property 3637 def solution_count(self) -> int: 3638 """Returns the number of solutions found.""" 3639 return self.__solution_count
Returns the number of solutions found.